/*
 * This file is part of PowerDNS or dnsdist.
 * Copyright -- PowerDNS.COM B.V. and its contributors
 * originally authored by Norbert Sendetzky
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU General Public License as
 * published by the Free Software Foundation.
 *
 * In addition, for the avoidance of any doubt, permission is granted to
 * link this program with OpenSSL and to (re)distribute the binaries
 * produced as the result of such linking.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
#include "exceptions.hh"
#include "ldapbackend.hh"
#include <cstdlib>

/*
 *  Known DNS RR types
 *  Types which aren't active are currently not supported by PDNS
 */

static const char* ldap_attrany[] = { // NOLINT(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
  "associatedDomain", // needs to be first, code below depends on this
  "dNSTTL",
  "ALIASRecord",
  "aRecord",
  "nSRecord",
  "cNAMERecord",
  "sOARecord",
  "pTRRecord",
  "hInfoRecord",
  "mXRecord",
  "tXTRecord",
  "rPRecord",
  "aFSDBRecord",
  //  "SigRecord",
  "KeyRecord",
  //  "gPosRecord",
  "aAAARecord",
  "lOCRecord",
  "sRVRecord",
  "nAPTRRecord",
  "kXRecord",
  "certRecord",
  //  "a6Record",
  "dNameRecord",
  //  "aPLRecord",
  "dSRecord",
  "sSHFPRecord",
  "iPSecKeyRecord",
  "rRSIGRecord",
  "nSECRecord",
  "dNSKeyRecord",
  "dHCIDRecord",
  "nSEC3Record",
  "nSEC3PARAMRecord",
  "tLSARecord",
  "cDSRecord",
  "cDNSKeyRecord",
  "openPGPKeyRecord",
  "SVCBRecord",
  "HTTPSRecord",
  "sPFRecord",
  "EUI48Record",
  "EUI64Record",
  "tKeyRecord",
  "uRIRecord",
  "cAARecord",
  "TYPE65226Record",
  "TYPE65534Record",
  "modifyTimestamp",
  "PdnsRecordTTL",
  "PdnsRecordAuth",
  "PdnsRecordOrdername",
  nullptr};

bool LdapBackend::list(const ZoneName& target, domainid_t domain_id, bool /* include_disabled */)
{
  try {
    d_in_list = true;
    d_qname = target.operator const DNSName&();
    d_qtype = QType::ANY;
    d_results_cache.clear();

    return (this->*d_list_fcnt)(target, domain_id);
  }
  catch (LDAPTimeout& lt) {
    g_log << Logger::Warning << d_myname << " Unable to get zone " << target << " from LDAP directory: " << lt.what() << endl;
    throw DBException("LDAP server timeout");
  }
  catch (LDAPNoConnection& lnc) {
    g_log << Logger::Warning << d_myname << " Connection to LDAP lost, trying to reconnect" << endl;
    if (reconnect())
      this->list(target, domain_id);
    else
      throw PDNSException("Failed to reconnect to LDAP server");
  }
  catch (LDAPException& le) {
    g_log << Logger::Error << d_myname << " Unable to get zone " << target << " from LDAP directory: " << le.what() << endl;
    throw PDNSException("LDAP server unreachable"); // try to reconnect to another server
  }
  catch (std::exception& e) {
    g_log << Logger::Error << d_myname << " Caught STL exception for target " << target << ": " << e.what() << endl;
    throw DBException("STL exception");
  }

  return false;
}

bool LdapBackend::list_simple(const ZoneName& target, domainid_t /* domain_id */)
{
  string dn;
  string filter;
  string qesc;

  dn = getArg("basedn");
  qesc = toLower(d_pldap->escape(target.toStringRootDot()));

  // search for SOARecord of target
  filter = strbind(":target:", "&(associatedDomain=" + qesc + ")(sOARecord=*)", getArg("filter-axfr"));
  PowerLDAP::SearchResult::Ptr search = d_pldap->search(dn, LDAP_SCOPE_SUBTREE, filter, (const char**)ldap_attrany);
  if (!search->getNext(d_result, true))
    return false;

  if (d_result.count("dn") && !d_result["dn"].empty()) {
    if (!mustDo("basedn-axfr-override")) {
      dn = d_result["dn"][0];
    }
  }

  // If we have any records associated with this entry let's parse them here
  DNSResult soa_result;
  soa_result.ttl = d_default_ttl;
  soa_result.lastmod = 0;
  this->extract_common_attributes(soa_result);
  this->extract_entry_results(d_qname, soa_result, QType(uint16_t(QType::ANY)));

  filter = strbind(":target:", "associatedDomain=*." + qesc, getArg("filter-axfr"));
  g_log << Logger::Debug << d_myname << " Search = basedn: " << dn << ", filter: " << filter << endl;
  d_search = d_pldap->search(dn, LDAP_SCOPE_SUBTREE, filter, (const char**)ldap_attrany);

  return true;
}

bool LdapBackend::list_strict(const ZoneName& target, domainid_t domain_id)
{
  if (target.isPartOf(DNSName("in-addr.arpa")) || target.isPartOf(DNSName("ip6.arpa"))) {
    g_log << Logger::Warning << d_myname << " Request for reverse zone AXFR, but this is not supported in strict mode" << endl;
    return false; // AXFR isn't supported in strict mode. Use simple mode and additional PTR records
  }

  return list_simple(target, domain_id);
}

void LdapBackend::lookup(const QType& qtype, const DNSName& qname, domainid_t zoneid, DNSPacket* dnspkt)
{
  try {
    d_in_list = false;
    d_qname = qname;
    d_qtype = qtype;
    d_results_cache.clear();

    if (d_qlog) {
      g_log.log("Query: '" + qname.toStringRootDot() + "|" + qtype.toString() + "'", Logger::Error);
    }
    (this->*d_lookup_fcnt)(qtype, qname, dnspkt, zoneid);
  }
  catch (LDAPTimeout& lt) {
    g_log << Logger::Warning << d_myname << " Unable to search LDAP directory: " << lt.what() << endl;
    throw DBException("LDAP server timeout");
  }
  catch (LDAPNoConnection& lnc) {
    g_log << Logger::Warning << d_myname << " Connection to LDAP lost, trying to reconnect" << endl;
    if (reconnect())
      this->lookup(qtype, qname, zoneid, dnspkt);
    else
      throw PDNSException("Failed to reconnect to LDAP server");
  }
  catch (LDAPException& le) {
    g_log << Logger::Error << d_myname << " Unable to search LDAP directory: " << le.what() << endl;
    throw PDNSException("LDAP server unreachable"); // try to reconnect to another server
  }
  catch (std::exception& e) {
    g_log << Logger::Error << d_myname << " Caught STL exception for qname " << qname << ": " << e.what() << endl;
    throw DBException("STL exception");
  }
}

void LdapBackend::lookup_simple(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
{
  string filter, attr, qesc;
  const char** attributes = ldap_attrany + 1; // skip associatedDomain
  const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};

  qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
  filter = "associatedDomain=" + qesc;

  if (qtype.getCode() != QType::ANY) {
    attr = qtype.toString() + "Record";
    filter = "&(" + filter + ")(" + attr + "=*)";
    attronly[0] = attr.c_str();
    attributes = attronly;
  }

  filter = strbind(":target:", filter, getArg("filter-lookup"));

  g_log << Logger::Debug << d_myname << " Search = basedn: " << getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
  d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attributes);
}

void LdapBackend::lookup_strict(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
{
  int len;
  vector<string> parts;
  string filter, attr, qesc;
  const char** attributes = ldap_attrany + 1; // skip associatedDomain
  const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};

  qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
  stringtok(parts, qesc, ".");
  len = qesc.length();

  if (parts.size() == 6 && len > 13 && qesc.substr(len - 13, 13) == ".in-addr.arpa") // IPv4 reverse lookups
  {
    filter = "aRecord=" + ptr2ip4(parts);
    attronly[0] = "associatedDomain";
    attributes = attronly;
  }
  else if (parts.size() == 34 && len > 9 && (qesc.substr(len - 9, 9) == ".ip6.arpa")) // IPv6 reverse lookups
  {
    filter = "aAAARecord=" + ptr2ip6(parts);
    attronly[0] = "associatedDomain";
    attributes = attronly;
  }
  else // IPv4 and IPv6 lookups
  {
    filter = "associatedDomain=" + qesc;
  }

  if (qtype.getCode() != QType::ANY) {
    attr = qtype.toString() + "Record";
    filter = "&(" + filter + ")(" + attr + "=*)";
    attronly[0] = attr.c_str();
    attributes = attronly;
  }

  filter = strbind(":target:", filter, getArg("filter-lookup"));

  g_log << Logger::Debug << d_myname << " Search = basedn: " << getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
  d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attributes);
}

void LdapBackend::lookup_tree(const QType& qtype, const DNSName& qname, DNSPacket* /* dnspkt */, domainid_t /* zoneid */)
{
  string filter, attr, qesc, dn;
  const char** attributes = ldap_attrany + 1; // skip associatedDomain
  const char* attronly[] = {NULL, "dNSTTL", "modifyTimestamp", "PdnsRecordTTL", "PdnsRecordAuth", "PdnsRecordOrdername", NULL};
  vector<string> parts;

  qesc = toLower(d_pldap->escape(qname.toStringRootDot()));
  filter = "associatedDomain=" + qesc;

  if (qtype.getCode() != QType::ANY) {
    attr = qtype.toString() + "Record";
    filter = "&(" + filter + ")(" + attr + "=*)";
    attronly[0] = attr.c_str();
    attributes = attronly;
  }

  filter = strbind(":target:", filter, getArg("filter-lookup"));

  stringtok(parts, toLower(qname.toString()), ".");
  for (auto i = parts.crbegin(); i != parts.crend(); i++) {
    dn = "dc=" + d_pldap->escape(*i) + "," + dn;
  }

  g_log << Logger::Debug << d_myname << " Search = basedn: " << dn + getArg("basedn") << ", filter: " << filter << ", qtype: " << qtype.toString() << endl;
  d_search = d_pldap->search(dn + getArg("basedn"), LDAP_SCOPE_BASE, filter, attributes);
}

bool LdapBackend::get(DNSResourceRecord& rr)
{
  if (d_results_cache.empty()) {
    while (d_results_cache.empty()) {
      bool exhausted = false;
      bool valid_entry_found = false;

      while (!valid_entry_found && !exhausted) {
        try {
          exhausted = !d_search->getNext(d_result, true);
        }
        catch (LDAPException& le) {
          g_log << Logger::Error << d_myname << " Failed to get next result: " << le.what() << endl;
          throw PDNSException("Get next result impossible");
        }

        if (!exhausted) {
          if (!d_in_list) {
            // All entries are valid here
            valid_entry_found = true;
          }
          else {
            // If we're called after list() then the entry *must* contain
            // associatedDomain, otherwise let's just skip it
            if (d_result.count("associatedDomain"))
              valid_entry_found = true;
          }
        }
      }

      if (exhausted) {
        break;
      }

      DNSResult result_template;
      result_template.ttl = d_default_ttl;
      result_template.lastmod = 0;
      this->extract_common_attributes(result_template);

      std::vector<std::string> associatedDomains;

      if (d_result.count("associatedDomain")) {
        if (d_in_list) {
          // We can have more than one associatedDomain in the entry, so for each of them we have to check
          // that they are indeed under the domain we've been asked to list (nothing enforces this, so you
          // can have one associatedDomain set to "host.first-domain.com" and another one set to
          // "host.second-domain.com"). Better not return the latter I guess :)
          // We also have to generate one DNSResult per DNS-relevant attribute. As we've asked only for them
          // and the others above we've already cleaned it's just a matter of iterating over them.

          unsigned int axfrqlen = d_qname.toStringRootDot().length();
          for (auto i = d_result["associatedDomain"].begin(); i != d_result["associatedDomain"].end(); ++i) {
            // Sanity checks: is this associatedDomain attribute under the requested domain?
            if (i->size() >= axfrqlen && i->substr(i->size() - axfrqlen, axfrqlen) == d_qname.toStringRootDot())
              associatedDomains.push_back(*i);
          }
        }
        else {
          // This was a lookup in strict mode, so we add the reverse lookup
          // information manually.
          d_result["pTRRecord"] = d_result["associatedDomain"];
        }
      }

      if (d_in_list) {
        for (const auto& domain : associatedDomains)
          this->extract_entry_results(DNSName(domain), result_template, QType(uint16_t(QType::ANY)));
      }
      else {
        this->extract_entry_results(d_qname, result_template, QType(uint16_t(QType::ANY)));
      }
    }

    if (d_results_cache.empty())
      return false;
  }

  DNSResult result = d_results_cache.back();
  d_results_cache.pop_back();
  rr.qtype = result.qtype;
  rr.qname = result.qname;
  rr.ttl = result.ttl;
  rr.last_modified = 0;
  rr.content = result.value;
  rr.auth = result.auth;

  g_log << Logger::Debug << d_myname << " Record = qname: " << rr.qname << ", qtype: " << (rr.qtype).toString() << ", ttl: " << rr.ttl << ", content: " << rr.content << endl;
  return true;
}

bool LdapBackend::getDomainInfo(const ZoneName& domain, DomainInfo& info, bool /* getSerial */)
{
  string filter;
  SOAData sd;
  PowerLDAP::sentry_t result;
  const char* attronly[] = {
    "sOARecord",
    "PdnsDomainId",
    "PdnsDomainNotifiedSerial",
    "PdnsDomainLastCheck",
    "PdnsDomainMaster",
    "PdnsDomainType",
    NULL};

  try {
    // search for SOARecord of domain
    filter = "(&(associatedDomain=" + toLower(d_pldap->escape(domain.toStringRootDot())) + ")(SOARecord=*))";
    d_search = d_pldap->search(getArg("basedn"), LDAP_SCOPE_SUBTREE, filter, attronly);
    if (!d_search->getNext(result)) {
      return false;
    }
  }
  catch (LDAPTimeout& lt) {
    g_log << Logger::Warning << d_myname << " Unable to search LDAP directory: " << lt.what() << endl;
    throw DBException("LDAP server timeout");
  }
  catch (LDAPNoConnection& lnc) {
    g_log << Logger::Warning << d_myname << " Connection to LDAP lost, trying to reconnect" << endl;
    if (reconnect())
      this->getDomainInfo(domain, info);
    else
      throw PDNSException("Failed to reconnect to LDAP server");
  }
  catch (LDAPException& le) {
    g_log << Logger::Error << d_myname << " Unable to search LDAP directory: " << le.what() << endl;
    throw PDNSException("LDAP server unreachable"); // try to reconnect to another server
  }
  catch (std::exception& e) {
    throw DBException("STL exception");
  }

  if (result.count("sOARecord") && !result["sOARecord"].empty()) {
    sd.serial = 0;
    fillSOAData(result["sOARecord"][0], sd);

    if (result.count("PdnsDomainId") && !result["PdnsDomainId"].empty())
      info.id = static_cast<domainid_t>(std::stoll(result["PdnsDomainId"][0]));
    else
      info.id = UnknownDomainID;

    info.serial = sd.serial;
    info.zone = domain;

    if (result.count("PdnsDomainLastCheck") && !result["PdnsDomainLastCheck"].empty())
      pdns::checked_stoi_into(info.last_check, result["PdnsDomainLastCheck"][0]);
    else
      info.last_check = 0;

    if (result.count("PdnsDomainNotifiedSerial") && !result["PdnsDomainNotifiedSerial"].empty())
      pdns::checked_stoi_into(info.notified_serial, result["PdnsDomainNotifiedSerial"][0]);
    else
      info.notified_serial = 0;

    if (result.count("PdnsDomainMaster") && !result["PdnsDomainMaster"].empty()) {
      for (const auto& m : result["PdnsDomainMaster"])
        info.primaries.emplace_back(m, 53);
    }

    if (result.count("PdnsDomainType") && !result["PdnsDomainType"].empty()) {
      string kind = result["PdnsDomainType"][0];
      if (kind == "master")
        info.kind = DomainInfo::Primary;
      else if (kind == "slave")
        info.kind = DomainInfo::Secondary;
      else
        info.kind = DomainInfo::Native;
    }
    else {
      info.kind = DomainInfo::Native;
    }

    info.backend = this;
    return true;
  }

  return false;
}
