/***************************************************************************
                          secondary.cpp  -  secondary dns
                             -------------------
    begin                : wo mei 14 2003
    copyright            : (C) 2003 by Meilof
    email                : meilof@users.sourceforge.net
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include <poslib/poslib.h>
#include <poslib/server/server.h>
#include <poslib/server/configuration.h>
#include "secondary.h"
#include "resolver.h"
#include "updates.h"
#include "masterfile.h"
#include "query.h"

void *secondary_transfer_thread(void *zn);
 
void start_secondaryzone_update(domainname znroot, SecondaryZone *zn) {
  _addr a;
  txt_to_addr(&a, "0.0.0.0");
  zn->pending_updates.push_front(a);
  start_secondaryzone_update_thread(znroot, zn);
}

class SecZoneData { public: domainname znroot; SecondaryZone *zn; };

void start_secondaryzone_update_thread(domainname znroot, SecondaryZone *zn) {
  pthread_t tr;
  SecZoneData *dat = new SecZoneData();
  dat->znroot = znroot;
  dat->zn = zn;
  posthread_create(&tr, secondary_transfer_thread, dat);
}

SecondaryZone::SecondaryZone() {
  update_busy = false;
  type = Z_SECONDARY;
}

SecondaryZone::~SecondaryZone() {

}

void SecondaryZone::feed_setting(const char *name, const char *val) {
  _addr a;
  addrrange rng;
  if (strcmpi(name, "reset") == 0) {
    masters.clear();
    allow_notify_from.clear();
    auth_feed_setting("reset", "");
    return;
  } else if (strcmpi(name, "reload") == 0) {
    if (!update_busy) {
      update_list_unregister(this);
      start_secondaryzone_update(getfqdn(), this);
    }
  } 
  if (auth_feed_setting(name, val)) return;

  if (strcmpi(name, "masters") == 0) {
    txt_to_addr(&a, val);
    masters.push_front(a);
  } else if (strcmpi(name, "allow_notify_from") == 0) {
    txt_to_addrrange(rng.range, val);
    allow_notify_from.push_front(rng);
  } else {
    throw PException(true, "Unknown options %s", name);
  }
}

void SecondaryZone::end_setting() {
  AuthMemZone::end_setting();
  start_secondaryzone_update(getfqdn(), this);
}

/* load bin file */
void load_bin_file(SecondaryZone *zn, domainname &znroot, const char *fname) {
  char len[2];
  char *buff = NULL;
  message_buff mbuf;
  int x, y;
  FILE *f = try_fopen(fname, "rb");
  DnsRR rr;
  ZoneDomain *zonedom;
  RrSet *set;
  if (!f) throw PException(true, "Cannot open %s", fname);
  delete zn->rootdomain; zn->rootdomain = new ZoneDomain(NULL, "");
  while (!feof(f)) {
    try {
      if (buff) free(buff); buff = NULL;
      y = fread(len, 1, 2, f);
      if (y == 0) break;
      if (y != 2) throw PException(true, "Cannot read from data file: returned %d", y);
      buff = (char *)malloc(uint16_value(len));
      if (fread(buff, 1, uint16_value(len), f) < uint16_value(len)) throw PException("Cannot read from data file: unexpected EOF");
      mbuf = message_buff(buff, uint16_value(len));
      rr.NAME = domainname(mbuf, 0);
      if (!(rr.NAME >= znroot)) {
        pos_log(context_zonedata, log_error, "Ignoring domain %s outside of %s zone", rr.NAME.tocstr(), znroot.tocstr());
        continue;
      }
      x = dom_comprlen(mbuf, 0);
      if (x + 10 > mbuf.len) throw PException("Cannot read from data file: unexpected EOF");
      rr.TYPE = uint16_value(mbuf.msg + x);
      rr.CLASS = uint16_value(mbuf.msg + x + 2);
      rr.TTL = uint32_value(mbuf.msg + x + 4);
      rr.RDLENGTH = uint16_value(mbuf.msg + x + 8);
      if (x + 10 + rr.RDLENGTH > mbuf.len) throw PException("Cannot read from data file: unexpected EOF");
      if (rr.RDATA) free(rr.RDATA);
      rr.RDATA = (char *)malloc(rr.RDLENGTH);
      memcpy(rr.RDATA, mbuf.msg + x + 10, rr.RDLENGTH);
      if (rr.NAME == znroot && rr.TYPE == DNS_TYPE_SOA)
        /* set serial for zone */
        zn->serial = rr_getlong(rr.RDATA, DNS_TYPE_SOA, 2);
      /* put it into the tree */          
      zonedom = create_domainname(zn->rootdomain, znroot, rr.NAME);
      if (zonedom) {
        set = zonedom->create_rrset(rr.TYPE);
        set->add_rr(rr.TTL, rr.RDLENGTH, rr.RDATA);
      } else {
        pos_log(context_zonedata, log_error, "%s: Could not create zone node %s: ignored", znroot.tocstr(), rr.NAME.tocstr());
      }
    } catch (PException p) {
      if (buff) free(buff);
      throw p;
    }
  }
  zn->zone_is_loaded = true;
}

uint32_t getserial(pos_cliresolver &res, domainname &znroot, int sockid) {
  DnsMessage *q = NULL, *a = NULL;
  uint32_t serial;
  try {
    q = create_query(znroot, DNS_TYPE_SOA, false);
    res.tcpquery(q, a, sockid);
    if (a->RCODE != RCODE_NOERROR || a->answers.size() != 1 ||
        a->answers.begin()->NAME != znroot || a->answers.begin()->TYPE != DNS_TYPE_SOA) {
        throw PException("incorrect answer");
    }
    serial = rr_getlong(a->answers.begin()->RDATA, DNS_TYPE_SOA, 2);
  } catch (PException p) {
    if (q) delete q;
    if (a) delete a;
    throw PException("Could not retrieve SOA from master: ", p);
  }
  if (q) delete q;
  if (a) delete a;
  return serial;
}

bool zone_is_valid(domainname &znroot, Zone *zn) {
  domainname znroot2;
  Zone *z = lookup_authoritative_zone(znroot, &znroot2, false);
  if (!z || znroot != znroot2 || zn != z || zn->type != z->type) {
    pthread_mutex_unlock(&m_auth_zones);
    return false;
  }
  return true;
}

void queue_new_update(domainname &znroot, SecondaryZone *zn, bool succeeded, bool is_general) {
  RrSet *set;
  _addr nil;
  postime_t time = getcurtime();
  txt_to_addr(&nil, "0.0.0.0");
  if (!is_general && !succeeded) {
    pthread_mutex_unlock(&m_auth_zones);
    return;
  }
  if (!succeeded && zn->zone_is_loaded && zn->rootdomain &&
      (set = zn->rootdomain->get_rrset(DNS_TYPE_SOA)) != NULL &&
      set->rrs.count == 1) {
    /* check whether we should discard the zone */
    uint32_t expire = rr_getlong(set->rrs[0]->rdata, DNS_TYPE_SOA, 5) * 1000,
             retry = rr_getlong(set->rrs[0]->rdata, DNS_TYPE_SOA, 4) * 1000;
    if (zn->loadtime + expire - retry > getcurtime()) {
      /* next update won't come in time */
      pos_log(context_server, log_error, "The zone %s has just expired because no masters are reachable!",
              znroot.tocstr());      
      delete zn->rootdomain; zn->rootdomain = NULL;
      zn->zone_is_loaded = false;
    }
  } 
  if (zn->zone_is_loaded && zn->rootdomain &&
      (set = zn->rootdomain->get_rrset(DNS_TYPE_SOA)) != NULL &&
      set->rrs.count == 1) {
    /* zone loaded */
    if (succeeded)
      time = time + (rr_getlong(set->rrs[0]->rdata, DNS_TYPE_SOA, 3) * 1000);
    else
      time  = time + (rr_getlong(set->rrs[0]->rdata, DNS_TYPE_SOA, 4) * 1000);
  } else {
    time = time + 1800000; /* default: try again in half an hour */
  }
  zn->pending_updates.push_back(nil);
  enqueue_update(time, znroot, zn, (update_function)start_secondaryzone_update_thread);
  pthread_mutex_unlock(&m_auth_zones);
}

/* rfc1982 serial arithmetic, taken from posadis 0.50.x */
inline int soa_firstsmaller(unsigned int i1, unsigned int i2) {
	return (i1 != i2 &&
		   ((i1 < i2 && i2 - i1 < 2147483647) ||
	        (i1 > i2 && i1 - i2 > 2147483647)));
}

void *secondary_transfer_thread(void *_dat) {
  SecZoneData *dat = (SecZoneData *)_dat;
  SecondaryZone *zn = dat->zn;
  domainname znroot = dat->znroot;
  _addr cur;
  char *ptr;
  stl_slist(_addr) addrlist;
  stl_slist(_addr)::iterator it;
  bool zn_loaded = false;
  uint32_t curserial, newserial;
  pos_cliresolver res;
  int sockid;
  FILE *out;
  int ilen, pos, n, t, l;
  char *msg = NULL;
  _domain retdom = NULL;
  char len[2];
  char *rdata = NULL;
  uint16_t rdlen;
  int soa = 0;
  DnsMessage *q;
  message_buff buff;
  char *fname_ptr;
  char fname[PATH_MAX];
  bool has_generic = false, is_generic, result = true;
  delete dat;
  bool first = true;
start:
  try {
    pthread_mutex_lock(&m_auth_zones);
    if (first && zn->update_busy) { pthread_mutex_unlock(&m_auth_zones); return NULL; }
    if (!zone_is_valid(znroot, zn)) return NULL;
    if (!datadir[0]) {
      pos_log(context_server, log_error, "Since there is no data directory, secondary zone transfers will fail (%s being an example)!", znroot.tocstr());
      pthread_mutex_unlock(&m_auth_zones);
      return NULL;
    }
    first = false;
    zn->update_busy = true;
    if (zn->pending_updates.size() == 0) {
      /* done! */
      zn->update_busy = false;
      /* schedule new update */
      queue_new_update(znroot, zn, result, has_generic);
      return NULL;
    }
    cur = *zn->pending_updates.begin();
    zn->pending_updates.pop_front();
    ptr = get_ipv4_ptr(&cur);
    if (ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 0 && ptr[3] == 0) {
      /* no particular address */
      addrlist = zn->masters;
      has_generic = true;
      is_generic = true;
    } else {
      addrlist.push_front(cur);
      is_generic = false;
    }
    if (zn->zone_is_loaded) {
      zn_loaded = true;
      curserial = zn->serial;
    }
    pthread_mutex_unlock(&m_auth_zones);
    /* allright, make tcp connect */
    it = addrlist.begin();
    while (true) {
next_server:
      if (it == addrlist.end()) {
        pos_log(context_server, log_error, "No servers could be reached for %s: delaying zone transfer", znroot.tocstr());
        if (is_generic) result = false;
        goto start;
        break;
      }
      try {
        sockid = res.tcpconnect(&*it);
      } catch (PException p) {
        pos_log(context_server, log_error, "Could not contact %s for %s zone transfer!",
                addr_to_string(&*it).c_str(), znroot.tocstr());
        sockid = -1;
        it++;
        continue;
      }
      if (!sockid) {
        pos_log(context_server, log_error, "No servers could be reached for %s: delaying zone transfer", znroot.tocstr());
        if (is_generic) result = false;
        goto start;
      }
      try {
        /* do zone transfer (code from getzone.cpp) */
        if (zn_loaded) {
          /* see for secondary */
          newserial = getserial(res, znroot, sockid);
          if (!soa_firstsmaller(curserial, newserial)) {
            /* no update nessecary */
            result = true;
            goto start;
          }
        }
        /* we're needing an update here */
        out = NULL;
        q = create_query(znroot, QTYPE_AXFR);
        buff = q->compile(TCP_MSG_SIZE);
        delete q;
        len[0] = buff.len / 256; len[1] = buff.len;
        msg = NULL;
        retdom = NULL;
        rdata = NULL;
        soa = 0;
        rdlen = 0;
        tcpsendall(sockid, len, 2, conf_get_tcp_io_timeout() / 4);
        tcpsendall(sockid, buff.msg, buff.len, conf_get_tcp_io_timeout() / 4);
        try {
          while (soa != 2) {
            /* read message */
            if (msg) { free(msg); msg = NULL; }
            tcpreadall(sockid, len, 2, conf_get_tcp_io_timeout() / 4);
            ilen = len[0] * 256 + len[1];
            if (ilen < 12) throw PException("Incomplete answer message");
            msg = (char *)malloc(ilen);
            tcpreadall(sockid, msg, ilen, conf_get_tcp_io_timeout() / 4);
            buff = message_buff(msg, ilen);
            /* interpret it */
            if ((msg[3] & 15) != RCODE_NOERROR) throw PException(true, "Erroneous answer: %s", str_rcode(msg[3]&15).c_str());
            n = msg[6] * 256 + msg[7];
            pos = 12;
            if (msg[4] * 256 + msg[5] >= 1) {
              /* some DNS server is telling is what we already know (yes I'm looking at you BIND9) */
              pos += dom_comprlen(buff, pos);
              pos += 4;
            }
            for (t = 0; t < n && pos < ilen; t++) {
              /* read this RR */
              if (retdom) { free(retdom); retdom = NULL; }
              if (rdata) { free(rdata); rdata = NULL; }
              retdom = dom_uncompress(buff, pos);
              pos += dom_comprlen(buff, pos);
              if (pos + 10 > ilen) throw PException(true, "Truncated answer message");
              l = buff.msg[pos+8] * 256 + buff.msg[pos+9];
              if (pos + 10 + l > ilen) throw PException(true, "Truncated answer message");
              if (buff.msg[pos] * 256 + buff.msg[pos+1] == DNS_TYPE_SOA) {
                rr_read(buff.msg[pos] * 256 + buff.msg[pos+1], rdata, rdlen, buff, pos + 10, l);
                if (soa == 0) {
                  fname_ptr = build_filename(znroot, rr_getlong(rdata, DNS_TYPE_SOA, 2));
                  if (!fname_ptr)
                    throw PException(true, "Cannot determine data file name for secondary zone transfer for %s!", znroot.tocstr());
                  strcpy(fname, fname_ptr);
                  if ((out = try_fopen(fname, "wb")) == NULL)
                    throw PException(true, "Cannot open data file for secondary zone transfer for %s!", znroot.tocstr());
                }                  
                soa++;
                if (soa == 2) {
                  /* done! */
                  break;
                }
              } else {
                if (!out) throw PException("Zone transfer doesn't start with SOA record!");
                rr_read(buff.msg[pos] * 256 + buff.msg[pos+1], rdata, rdlen, buff, pos + 10, l);
              }
              fwrite(uint16_buff(domlen(retdom) + rdlen + 10), 1, 2, out);
              fwrite(retdom, 1, domlen(retdom), out);
              fwrite(buff.msg + pos, 1, 8, out);
              fwrite(uint16_buff(rdlen), 1, 2, out);
              fwrite(rdata, 1, rdlen, out);
              pos += 10 + l;
            }
          }
        } catch (PException p) {
          pos_log(context_server, log_error, "Error in zone transfer from %s for %s: %s!",
                addr_to_string(&*it).c_str(), znroot.tocstr(), p.message);
          if (msg) free(msg);
          if (retdom) free(retdom);
          if (rdata) free(rdata);
          if (out) fclose(out);
          if (is_generic) result = false;
          throw p;
        }
        if (msg) free(msg);
        if (retdom) free(retdom);
        if (rdata) free(rdata);
        fclose(out);
        pthread_mutex_lock(&m_auth_zones);
        if (!zone_is_valid(znroot, zn)) goto start;
        try {
          load_bin_file(zn, znroot, fname);
          zn->loadtime = getcurtime();
          pos_log(context_zonedata, log_info, "secondary %s (re)loaded, serial %d", znroot.tocstr(), zn->serial);
          zn->start_notification();
          pthread_mutex_unlock(&m_auth_zones);
        } catch (PException p) {
          delete zn->rootdomain; zn->rootdomain = new ZoneDomain(NULL, "");
          zn->zone_is_loaded = false;
          if (is_generic) result = false;
          pthread_mutex_unlock(&m_auth_zones);
          pos_log(context_server, log_error, "Error in retrieved zone %s: %s. Zone discarded.", znroot.tocstr(), p.message);
          goto start;
        }
        result = true;
        goto start;
      } catch (PException p) { it++; goto next_server; }
    }
  } catch (PException p) { }
  goto start;
}

DnsMessage *answer_notify(pending_query *q) {
  bool update = true;
  bool is_from_master;
  if (q->message->questions.size() != 1 ||
      q->message->questions.begin()->QTYPE != DNS_TYPE_SOA)
    return new_srvfail(q->message, RCODE_QUERYERR);
  domainname dom = q->message->questions.begin()->QNAME;
  domainname dom2;
  pthread_mutex_lock(&m_auth_zones);
  SecondaryZone *z = (SecondaryZone*)lookup_authoritative_zone(dom, &dom2, false);
  if (dom != dom2 || z->type != Z_SECONDARY) {
    pthread_mutex_unlock(&m_auth_zones);
    return new_srvfail(q->message, RCODE_NOTIMP);
  }
  if (!in_addr_list(z->masters, &q->querier)) {
    if (!in_addrrange_list(z->allow_notify_from, &q->querier)) {
      pthread_mutex_unlock(&m_auth_zones);
      return new_srvfail(q->message, RCODE_REFUSED);
    } else {
      is_from_master = false;
    }
  } else {
    is_from_master = true;
  }
  if (q->message->answers.size() &&
      q->message->answers.begin()->TYPE == DNS_TYPE_SOA) {
    uint32_t ser = rr_getlong(q->message->answers.begin()->RDATA, DNS_TYPE_SOA, 2);
    if (z->zone_is_loaded && ser == z->serial) update = false;
  }
  if (update) {
    _addr ad;
    if (is_from_master) {
      /* use master for zone transfer */
      ad = q->querier;
      addr_setport(&ad, DNS_PORT);
    } else {
      /* don't explicitly use this server */
      txt_to_addr(&ad, "0.0.0.0");
    }      
    z->pending_updates.push_back(ad);
    if (!z->update_busy) start_secondaryzone_update_thread(dom, z);
  }
  pthread_mutex_unlock(&m_auth_zones);
  DnsMessage *a = new DnsMessage();
  a->ID = q->message->ID;
  a->RCODE = RCODE_NOERROR;
  a->OPCODE = OPCODE_NOTIFY;
  a->AA = true;
  a->questions.push_front(*q->message->questions.begin());
  return a;      
}



syntax highlighted by Code2HTML, v. 0.9.1