/*
 * This file is part of PowerDNS or dnsdist.
 * Copyright -- PowerDNS.COM B.V. and its contributors
 *
 * 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.
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "utility.hh"
#include "dynlistener.hh"
#include "ws-auth.hh"
#include "json.hh"
#include "logger.hh"
#include "statbag.hh"
#include "misc.hh"
#include "base64.hh"
#include "arguments.hh"
#include "dns.hh"
#include "comment.hh"
#include "ueberbackend.hh"
#include <boost/format.hpp>

#include "namespaces.hh"
#include "ws-api.hh"
#include "version.hh"
#include "dnsseckeeper.hh"
#include <iomanip>
#include "zoneparser-tng.hh"
#include "auth-main.hh"
#include "auth-caches.hh"
#include "auth-zonecache.hh"
#include "threadname.hh"
#include "tsigutils.hh"

using json11::Json;

extern StatBag S;

static void patchZone(UeberBackend& B, HttpRequest* req, HttpResponse* resp);

// QTypes that MUST NOT have multiple records of the same type in a given RRset.
static const std::set<uint16_t> onlyOneEntryTypes = { QType::CNAME, QType::DNAME, QType::SOA };
// QTypes that MUST NOT be used with any other QType on the same name.
static const std::set<uint16_t> exclusiveEntryTypes = { QType::CNAME };
// QTypes that MUST be at apex.
static const std::set<uint16_t> atApexTypes = {QType::SOA, QType::DNSKEY};
// QTypes that are NOT allowed at apex.
static const std::set<uint16_t> nonApexTypes = {QType::DS};

AuthWebServer::AuthWebServer() :
  d_start(time(nullptr)),
  d_min10(0),
  d_min5(0),
  d_min1(0)
{
  if (arg().mustDo("webserver") || arg().mustDo("api")) {
    d_ws = std::make_unique<WebServer>(arg()["webserver-address"], arg().asNum("webserver-port"));
    d_ws->setApiKey(arg()["api-key"], arg().mustDo("webserver-hash-plaintext-credentials"));
    d_ws->setPassword(arg()["webserver-password"], arg().mustDo("webserver-hash-plaintext-credentials"));
    d_ws->setLogLevel(arg()["webserver-loglevel"]);

    NetmaskGroup acl;
    acl.toMasks(::arg()["webserver-allow-from"]);
    d_ws->setACL(acl);

    d_ws->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));

    d_ws->bind();
  }
}

void AuthWebServer::go()
{
  S.doRings();
  std::thread webT([this](){webThread();});
  webT.detach();
  std::thread statT([this](){statThread();});
  statT.detach();
}

void AuthWebServer::statThread()
{
  try {
    setThreadName("pdns/statHelper");
    for(;;) {
      d_queries.submit(S.read("udp-queries"));
      d_cachehits.submit(S.read("packetcache-hit"));
      d_cachemisses.submit(S.read("packetcache-miss"));
      d_qcachehits.submit(S.read("query-cache-hit"));
      d_qcachemisses.submit(S.read("query-cache-miss"));
      Utility::sleep(1);
    }
  }
  catch(...) {
    g_log<<Logger::Error<<"Webserver statThread caught an exception, dying"<<endl;
    _exit(1);
  }
}

static string htmlescape(const string &s) {
  string result;
  for(char it : s) {
    switch (it) {
    case '&':
      result += "&amp;";
      break;
    case '<':
      result += "&lt;";
      break;
    case '>':
      result += "&gt;";
      break;
    case '"':
      result += "&quot;";
      break;
    default:
      result += it;
    }
  }
  return result;
}

static void printtable(ostringstream &ret, const string &ringname, const string &title, int limit=10)
{
  int tot=0;
  int entries=0;
  vector<pair <string,unsigned int> >ring=S.getRing(ringname);

  for(const auto & i : ring) {
    tot+=i.second;
    entries++;
  }

  ret<<"<div class=\"panel\">";
  ret<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname)<<"\">Reset</a></span>"<<endl;
  ret<<"<h2>"<<title<<"</h2>"<<endl;
  ret<<"<div class=ringmeta>";
  ret<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname)<<"\">Showing: Top "<<limit<<" of "<<entries<<"</a>"<<endl;
  ret<<"<span class=resizering>Resize: ";
  unsigned int sizes[]={10,100,500,1000,10000,500000,0};
  for(int i=0;sizes[i];++i) {
    if(S.getRingSize(ringname)!=sizes[i])
      ret<<"<a href=\"?resizering="<<htmlescape(ringname)<<"&amp;size="<<sizes[i]<<"\">"<<sizes[i]<<"</a> ";
    else
      ret<<"("<<sizes[i]<<") ";
  }
  ret<<"</span></div>";

  ret<<"<table class=\"data\">";
  int printed=0;
  int total=max(1,tot);
  for(vector<pair<string,unsigned int> >::const_iterator i=ring.begin();limit && i!=ring.end();++i,--limit) {
    ret<<"<tr><td>"<<htmlescape(i->first)<<"</td><td>"<<i->second<<"</td><td align=right>"<< AuthWebServer::makePercentage(i->second*100.0/total)<<"</td>"<<endl;
    printed+=i->second;
  }
  ret<<"<tr><td colspan=3></td></tr>"<<endl;
  if(printed!=tot)
    ret<<"<tr><td><b>Rest:</b></td><td><b>"<<tot-printed<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot-printed)*100.0/total)<<"</b></td>"<<endl;

  ret<<"<tr><td><b>Total:</b></td><td><b>"<<tot<<"</b></td><td align=right><b>100%</b></td>";
  ret<<"</table></div>"<<endl;
}

void AuthWebServer::printvars(ostringstream &ret)
{
  ret<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl;

  vector<string>entries=S.getEntries();
  for(const auto & entry : entries) {
    ret<<"<tr><td>"<<entry<<"</td><td>"<<S.read(entry)<<"</td><td>"<<S.getDescrip(entry)<<"</td>"<<endl;
  }

  ret<<"</table></div>"<<endl;
}

void AuthWebServer::printargs(ostringstream &ret)
{
  ret<<R"(<table border=1><tr><td colspan=3 bgcolor="#0000ff"><font color="#ffffff">Arguments</font></td>)"<<endl;

  vector<string>entries=arg().list();
  for(const auto & entry : entries) {
    ret<<"<tr><td>"<<entry<<"</td><td>"<<arg()[entry]<<"</td><td>"<<arg().getHelp(entry)<<"</td>"<<endl;
  }
}

string AuthWebServer::makePercentage(const double& val)
{
  return (boost::format("%.01f%%") % val).str();
}

void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
{
  if(!req->getvars["resetring"].empty()) {
    if (S.ringExists(req->getvars["resetring"]))
      S.resetRing(req->getvars["resetring"]);
    resp->status = 302;
    resp->headers["Location"] = req->url.path;
    return;
  }
  if(!req->getvars["resizering"].empty()){
    int size=std::stoi(req->getvars["size"]);
    if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000)
      S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
    resp->status = 302;
    resp->headers["Location"] = req->url.path;
    return;
  }

  ostringstream ret;

  ret<<"<!DOCTYPE html>"<<endl;
  ret<<"<html><head>"<<endl;
  ret<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl;
  ret<<R"(<link rel="stylesheet" href="style.css"/>)"<<endl;
  ret<<"</head><body>"<<endl;

  ret<<"<div class=\"row\">"<<endl;
  ret<<"<div class=\"headl columns\">";
  ret<<R"(<a href="/" id="appname">PowerDNS )"<<htmlescape(VERSION);
  if(!arg()["config-name"].empty()) {
    ret<<" ["<<htmlescape(arg()["config-name"])<<"]";
  }
  ret<<"</a></div>"<<endl;
  ret<<"<div class=\"header columns\"></div></div>";
  ret<<R"(<div class="row"><div class="all columns">)";

  time_t passed=time(nullptr)-g_starttime;

  ret<<"<p>Uptime: "<<
    humanDuration(passed)<<
    "<br>"<<endl;

  ret<<"Queries/second, 1, 5, 10 minute averages:  "<<std::setprecision(3)<<
    (int)d_queries.get1()<<", "<<
    (int)d_queries.get5()<<", "<<
    (int)d_queries.get10()<<". Max queries/second: "<<(int)d_queries.getMax()<<
    "<br>"<<endl;

  if(d_cachemisses.get10()+d_cachehits.get10()>0)
    ret<<"Cache hitrate, 1, 5, 10 minute averages: "<<
      makePercentage((d_cachehits.get1()*100.0)/((d_cachehits.get1())+(d_cachemisses.get1())))<<", "<<
      makePercentage((d_cachehits.get5()*100.0)/((d_cachehits.get5())+(d_cachemisses.get5())))<<", "<<
      makePercentage((d_cachehits.get10()*100.0)/((d_cachehits.get10())+(d_cachemisses.get10())))<<
      "<br>"<<endl;

  if(d_qcachemisses.get10()+d_qcachehits.get10()>0)
    ret<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
      makePercentage((d_qcachehits.get1()*100.0)/((d_qcachehits.get1())+(d_qcachemisses.get1())))<<", "<<
      makePercentage((d_qcachehits.get5()*100.0)/((d_qcachehits.get5())+(d_qcachemisses.get5())))<<", "<<
      makePercentage((d_qcachehits.get10()*100.0)/((d_qcachehits.get10())+(d_qcachemisses.get10())))<<
      "<br>"<<endl;

  ret<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
    (int)d_qcachemisses.get1()<<", "<<
    (int)d_qcachemisses.get5()<<", "<<
    (int)d_qcachemisses.get10()<<". Max queries/second: "<<(int)d_qcachemisses.getMax()<<
    "<br>"<<endl;

  ret<<"Total queries: "<<S.read("udp-queries")<<". Question/answer latency: "<<S.read("latency")/1000.0<<"ms</p><br>"<<endl;
  if(req->getvars["ring"].empty()) {
    auto entries = S.listRings();
    for(const auto &i: entries) {
      printtable(ret, i, S.getRingTitle(i));
    }

    printvars(ret);
    if(arg().mustDo("webserver-print-arguments"))
      printargs(ret);
  }
  else if(S.ringExists(req->getvars["ring"]))
    printtable(ret,req->getvars["ring"],S.getRingTitle(req->getvars["ring"]),100);

  ret<<"</div></div>"<<endl;
  ret<<"<footer class=\"row\">"<<fullVersionString()<<"<br>&copy; 2013 - 2022 <a href=\"https://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl;
  ret<<"</body></html>"<<endl;

  resp->body = ret.str();
  resp->status = 200;
}

/** Helper to build a record content as needed. */
static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot) {
  // noDot: for backend storage, pass true. for API users, pass false.
  auto drc = DNSRecordContent::mastermake(qtype.getCode(), QClass::IN, content);
  return drc->getZoneRepresentation(noDot);
}

/** "Normalize" record content for API consumers. */
static inline string makeApiRecordContent(const QType& qtype, const string& content) {
  return makeRecordContent(qtype, content, false);
}

/** "Normalize" record content for backend storage. */
static inline string makeBackendRecordContent(const QType& qtype, const string& content) {
  return makeRecordContent(qtype, content, true);
}

static Json::object getZoneInfo(const DomainInfo& di, DNSSECKeeper* dk) {
  string zoneId = apiZoneNameToId(di.zone);
  vector<string> masters;
  masters.reserve(di.masters.size());
  for(const auto& m : di.masters) {
    masters.push_back(m.toStringWithPortExcept(53));
  }

  auto obj = Json::object{
    // id is the canonical lookup key, which doesn't actually match the name (in some cases)
    {"id", zoneId},
    {"url", "/api/v1/servers/localhost/zones/" + zoneId},
    {"name", di.zone.toString()},
    {"kind", di.getKindString()},
    {"catalog", (!di.catalog.empty() ? di.catalog.toString() : "")},
    {"account", di.account},
    {"masters", std::move(masters)},
    {"serial", (double)di.serial},
    {"notified_serial", (double)di.notified_serial},
    {"last_check", (double)di.last_check}};
  if (dk) {
    obj["dnssec"] = dk->isSecuredZone(di.zone);
    string soa_edit;
    dk->getSoaEdit(di.zone, soa_edit, false);
    obj["edited_serial"] = (double)calculateEditSOA(di.serial, soa_edit, di.zone);
  }
  return obj;
}

static bool shouldDoRRSets(HttpRequest* req) {
  if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true")
    return true;
  if (req->getvars["rrsets"] == "false")
    return false;
  throw ApiException("'rrsets' request parameter value '"+req->getvars["rrsets"]+"' is not supported");
}

static void fillZone(UeberBackend& B, const DNSName& zonename, HttpResponse* resp, HttpRequest* req) {
  DomainInfo di;
  if(!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  DNSSECKeeper dk(&B);
  Json::object doc = getZoneInfo(di, &dk);
  // extra stuff getZoneInfo doesn't do for us (more expensive)
  string soa_edit_api;
  di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
  doc["soa_edit_api"] = soa_edit_api;
  string soa_edit;
  di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
  doc["soa_edit"] = soa_edit;

  string nsec3param;
  bool nsec3narrowbool = false;
  bool is_secured = dk.isSecuredZone(zonename);
  if (is_secured) { // ignore NSEC3PARAM and NSEC3NARROW metadata present in the db for unsigned zones
    di.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param);
    string nsec3narrow;
    di.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow);
    if (nsec3narrow == "1") {
      nsec3narrowbool = true;
    }
  }
  doc["nsec3param"] = nsec3param;
  doc["nsec3narrow"] = nsec3narrowbool;
  doc["dnssec"] = is_secured;

  string api_rectify;
  di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
  doc["api_rectify"] = (api_rectify == "1");

  // TSIG
  vector<string> tsig_master, tsig_slave;
  di.backend->getDomainMetadata(zonename, "TSIG-ALLOW-AXFR", tsig_master);
  di.backend->getDomainMetadata(zonename, "AXFR-MASTER-TSIG", tsig_slave);

  Json::array tsig_master_keys;
  for (const auto& keyname : tsig_master) {
    tsig_master_keys.push_back(apiZoneNameToId(DNSName(keyname)));
  }
  doc["master_tsig_key_ids"] = tsig_master_keys;

  Json::array tsig_slave_keys;
  for (const auto& keyname : tsig_slave) {
    tsig_slave_keys.push_back(apiZoneNameToId(DNSName(keyname)));
  }
  doc["slave_tsig_key_ids"] = tsig_slave_keys;

  if (shouldDoRRSets(req)) {
    vector<DNSResourceRecord> records;
    vector<Comment> comments;

    // load all records + sort
    {
      DNSResourceRecord rr;
      if (req->getvars.count("rrset_name") == 0) {
        di.backend->list(zonename, di.id, true); // incl. disabled
      } else {
        QType qt;
        if (req->getvars.count("rrset_type") == 0) {
          qt = QType::ANY;
        } else {
          qt = req->getvars["rrset_type"];
        }
        di.backend->lookup(qt, DNSName(req->getvars["rrset_name"]), di.id);
      }
      while(di.backend->get(rr)) {
        if (!rr.qtype.getCode())
          continue; // skip empty non-terminals
        records.push_back(rr);
      }
      sort(records.begin(), records.end(), [](const DNSResourceRecord& a, const DNSResourceRecord& b) {
              /* if you ever want to update this comparison function,
                 please be aware that you will also need to update the conditions in the code merging
                 the records and comments below */
              if (a.qname == b.qname) {
                  return b.qtype < a.qtype;
              }
              return b.qname < a.qname;
          });
    }

    // load all comments + sort
    {
      Comment comment;
      di.backend->listComments(di.id);
      while(di.backend->getComment(comment)) {
        comments.push_back(comment);
      }
      sort(comments.begin(), comments.end(), [](const Comment& a, const Comment& b) {
              /* if you ever want to update this comparison function,
                 please be aware that you will also need to update the conditions in the code merging
                 the records and comments below */
              if (a.qname == b.qname) {
                  return b.qtype < a.qtype;
              }
              return b.qname < a.qname;
          });
    }

    Json::array rrsets;
    Json::object rrset;
    Json::array rrset_records;
    Json::array rrset_comments;
    DNSName current_qname;
    QType current_qtype;
    uint32_t ttl;
    auto rit = records.begin();
    auto cit = comments.begin();

    while (rit != records.end() || cit != comments.end()) {
      // if you think this should be rit < cit instead of cit < rit, note the b < a instead of a < b in the sort comparison functions above
      if (cit == comments.end() || (rit != records.end() && (rit->qname == cit->qname ? (cit->qtype < rit->qtype || cit->qtype == rit->qtype) : cit->qname < rit->qname))) {
        current_qname = rit->qname;
        current_qtype = rit->qtype;
        ttl = rit->ttl;
      } else {
        current_qname = cit->qname;
        current_qtype = cit->qtype;
        ttl = 0;
      }

      while(rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
        ttl = min(ttl, rit->ttl);
        rrset_records.push_back(Json::object {
          { "disabled", rit->disabled },
          { "content", makeApiRecordContent(rit->qtype, rit->content) }
        });
        rit++;
      }
      while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
        rrset_comments.push_back(Json::object {
          { "modified_at", (double)cit->modified_at },
          { "account", cit->account },
          { "content", cit->content }
        });
        cit++;
      }

      rrset["name"] = current_qname.toString();
      rrset["type"] = current_qtype.toString();
      rrset["records"] = rrset_records;
      rrset["comments"] = rrset_comments;
      rrset["ttl"] = (double)ttl;
      rrsets.push_back(rrset);
      rrset.clear();
      rrset_records.clear();
      rrset_comments.clear();
    }

    doc["rrsets"] = rrsets;
  }

  resp->setJsonBody(doc);
}

void productServerStatisticsFetch(map<string,string>& out)
{
  vector<string> items = S.getEntries();
  for(const string& item :  items) {
    out[item] = std::to_string(S.read(item));
  }

  // add uptime
  out["uptime"] = std::to_string(time(nullptr) - g_starttime);
}

std::optional<uint64_t> productServerStatisticsFetch(const std::string& name)
{
  try {
    // ::read() calls ::exists() which throws a PDNSException when the key does not exist
    return S.read(name);
  }
  catch (...) {
    return std::nullopt;
  }
}

static void validateGatheredRRType(const DNSResourceRecord& rr) {
  if (rr.qtype.getCode() == QType::OPT || rr.qtype.getCode() == QType::TSIG) {
    throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.toString()+": invalid type given");
  }
}

static void gatherRecords(UeberBackend& B, const string& logprefix, const Json& container, const DNSName& qname, const QType& qtype, const int ttl, vector<DNSResourceRecord>& new_records) {
  DNSResourceRecord rr;
  rr.qname = qname;
  rr.qtype = qtype;
  rr.auth = true;
  rr.ttl = ttl;

  validateGatheredRRType(rr);
  const auto& items = container["records"].array_items();
  for(const auto& record : items) {
    string content = stringFromJson(record, "content");
    rr.disabled = false;
    if(!record["disabled"].is_null()) {
      rr.disabled = boolFromJson(record, "disabled");
    }

    // validate that the client sent something we can actually parse, and require that data to be dotted.
    try {
      if (rr.qtype.getCode() != QType::AAAA) {
        string tmp = makeApiRecordContent(rr.qtype, content);
        if (!pdns_iequals(tmp, content)) {
          throw std::runtime_error("Not in expected format (parsed as '"+tmp+"')");
        }
      } else {
        struct in6_addr tmpbuf;
        if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
          throw std::runtime_error("Invalid IPv6 address");
        }
      }
      rr.content = makeBackendRecordContent(rr.qtype, content);
    }
    catch(std::exception& e)
    {
      throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.toString()+" '"+content+"': "+e.what());
    }

    new_records.push_back(rr);
  }
}

static void gatherComments(const Json& container, const DNSName& qname, const QType& qtype, vector<Comment>& new_comments) {
  Comment c;
  c.qname = qname;
  c.qtype = qtype;

  time_t now = time(nullptr);
  for (const auto& comment : container["comments"].array_items()) {
    c.modified_at = intFromJson(comment, "modified_at", now);
    c.content = stringFromJson(comment, "content");
    c.account = stringFromJson(comment, "account");
    new_comments.push_back(c);
  }
}

static void checkDefaultDNSSECAlgos() {
  int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
  int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
  int k_size = arg().asNum("default-ksk-size");
  int z_size = arg().asNum("default-zsk-size");

  // Sanity check DNSSEC parameters
  if (::arg()["default-zsk-algorithm"] != "") {
    if (k_algo == -1)
      throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
    else if (k_algo <= 10 && k_size == 0)
      throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
  }

  if (::arg()["default-zsk-algorithm"] != "") {
    if (z_algo == -1)
      throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
    else if (z_algo <= 10 && z_size == 0)
      throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
  }
}

static void throwUnableToSecure(const DNSName& zonename) {
  throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
      + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
}

static void extractDomainInfoFromDocument(const Json& document, boost::optional<DomainInfo::DomainKind>& kind, boost::optional<vector<ComboAddress>>& masters, boost::optional<DNSName>& catalog, boost::optional<string>& account)
{
  if (document["kind"].is_string()) {
    kind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
  } else {
    kind = boost::none;
  }

  if (document["masters"].is_array()) {
    masters = vector<ComboAddress>();
    for(const auto& value : document["masters"].array_items()) {
      string master = value.string_value();
      if (master.empty())
        throw ApiException("Master can not be an empty string");
      try {
        masters->emplace_back(master, 53);
      } catch (const PDNSException &e) {
        throw ApiException("Master (" + master + ") is not an IP address: " + e.reason);
      }
    }
  } else {
    masters = boost::none;
  }

  if (document["catalog"].is_string()) {
    string catstring = document["catalog"].string_value();
    catalog = (!catstring.empty() ? DNSName(catstring) : DNSName());
  }
  else {
    catalog = boost::none;
  }

  if (document["account"].is_string()) {
    account = document["account"].string_value();
  } else {
    account = boost::none;
  }
}

static void updateDomainSettingsFromDocument(UeberBackend& B, const DomainInfo& di, const DNSName& zonename, const Json& document, bool rectifyTransaction=true) {
  boost::optional<DomainInfo::DomainKind> kind;
  boost::optional<vector<ComboAddress>> masters;
  boost::optional<DNSName> catalog;
  boost::optional<string> account;

  extractDomainInfoFromDocument(document, kind, masters, catalog, account);

  if (kind) {
    di.backend->setKind(zonename, *kind);
  }
  if (masters) {
    di.backend->setMasters(zonename, *masters);
  }
  if (catalog) {
    di.backend->setCatalog(zonename, *catalog);
  }
  if (account) {
    di.backend->setAccount(zonename, *account);
  }

  if (document["soa_edit_api"].is_string()) {
    di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
  }
  if (document["soa_edit"].is_string()) {
    di.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
  }
  try {
    bool api_rectify = boolFromJson(document, "api_rectify");
    di.backend->setDomainMetadataOne(zonename, "API-RECTIFY", api_rectify ? "1" : "0");
  }
  catch (const JsonException&) {}


  DNSSECKeeper dk(&B);
  bool shouldRectify = false;
  bool dnssecInJSON = false;
  bool dnssecDocVal = false;
  bool nsec3paramInJSON = false;
  bool updateNsec3Param = false;
  string nsec3paramDocVal;

  try {
    dnssecDocVal = boolFromJson(document, "dnssec");
    dnssecInJSON = true;
  }
  catch (const JsonException&) {}

  try {
    nsec3paramDocVal = stringFromJson(document, "nsec3param");
    nsec3paramInJSON = true;
  }
  catch (const JsonException&) {}


  bool isDNSSECZone = dk.isSecuredZone(zonename);
  bool isPresigned = dk.isPresigned(zonename);

  if (dnssecInJSON) {
    if (dnssecDocVal) {
      if (!isDNSSECZone) {
        checkDefaultDNSSECAlgos();

        int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
        int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
        int k_size = arg().asNum("default-ksk-size");
        int z_size = arg().asNum("default-zsk-size");

        if (k_algo != -1) {
          int64_t id;
          if (!dk.addKey(zonename, true, k_algo, id, k_size)) {
            throwUnableToSecure(zonename);
          }
        }

        if (z_algo != -1) {
          int64_t id;
          if (!dk.addKey(zonename, false, z_algo, id, z_size)) {
            throwUnableToSecure(zonename);
          }
        }

        // Used later for NSEC3PARAM
        isDNSSECZone = dk.isSecuredZone(zonename);

        if (!isDNSSECZone) {
          throwUnableToSecure(zonename);
        }
        shouldRectify = true;
        updateNsec3Param = true;
      }
    } else {
      // "dnssec": false in json
      if (isDNSSECZone) {
        string info, error;
        if (!dk.unSecureZone(zonename, error, info)) {
          throw ApiException("Error while un-securing zone '"+ zonename.toString()+"': " + error);
        }
        isDNSSECZone = dk.isSecuredZone(zonename, false);
        if (isDNSSECZone) {
          throw ApiException("Unable to un-secure zone '"+ zonename.toString()+"'");
        }
        shouldRectify = true;
        updateNsec3Param = true;
      }
    }
  }

  if (nsec3paramInJSON || updateNsec3Param) {
    shouldRectify = true;
    if (!isDNSSECZone && !nsec3paramDocVal.empty()) {
      throw ApiException("NSEC3PARAM value provided for zone '" + zonename.toString() + "', but zone is not DNSSEC secured.");
    }

    if (nsec3paramDocVal.empty()) {
      // Switch to NSEC
      if (!dk.unsetNSEC3PARAM(zonename)) {
        throw ApiException("Unable to remove NSEC3PARAMs from zone '" + zonename.toString());
      }
    }
    else {
      // Set the NSEC3PARAMs
      NSEC3PARAMRecordContent ns3pr(nsec3paramDocVal);
      string error_msg = "";
      if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
        throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
      }
      if (!dk.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) {
        throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() +
            "' passed our basic sanity checks, but cannot be used with the current backend.");
      }
    }
  }

  if (shouldRectify && !isPresigned) {
    // Rectify
    string api_rectify;
    di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
    if (api_rectify.empty()) {
      if (::arg().mustDo("default-api-rectify")) {
        api_rectify = "1";
      }
    }
    if (api_rectify == "1") {
      string info;
      string error_msg;
      if (!dk.rectifyZone(zonename, error_msg, info, rectifyTransaction)) {
        throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
      }
    }

    // Increase serial
    string soa_edit_api_kind;
    di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
    if (!soa_edit_api_kind.empty()) {
      SOAData sd;
      if (!B.getSOAUncached(zonename, sd))
        return;

      string soa_edit_kind;
      di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);

      DNSResourceRecord rr;
      if (makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, rr)) {
        if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
          throw ApiException("Hosting backend does not support editing records.");
        }
      }
    }
  }

  if (!document["master_tsig_key_ids"].is_null()) {
    vector<string> metadata;
    for(const auto& value : document["master_tsig_key_ids"].array_items()) {
      auto keyname(apiZoneIdToName(value.string_value()));
      DNSName keyAlgo;
      string keyContent;
      if (!B.getTSIGKey(keyname, keyAlgo, keyContent)) {
        throw ApiException("A TSIG key with the name '"+keyname.toLogString()+"' does not exist");
      }
      metadata.push_back(keyname.toString());
    }
    if (!di.backend->setDomainMetadata(zonename, "TSIG-ALLOW-AXFR", metadata)) {
      throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename.toLogString() + "'");
    }
  }
  if (!document["slave_tsig_key_ids"].is_null()) {
    vector<string> metadata;
    for(const auto& value : document["slave_tsig_key_ids"].array_items()) {
      auto keyname(apiZoneIdToName(value.string_value()));
      DNSName keyAlgo;
      string keyContent;
      if (!B.getTSIGKey(keyname, keyAlgo, keyContent)) {
        throw ApiException("A TSIG key with the name '"+keyname.toLogString()+"' does not exist");
      }
      metadata.push_back(keyname.toString());
    }
    if (!di.backend->setDomainMetadata(zonename, "AXFR-MASTER-TSIG", metadata)) {
      throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename.toLogString() + "'");
    }
  }
}

static bool isValidMetadataKind(const string& kind, bool readonly) {
  static vector<string> builtinOptions {
    "ALLOW-AXFR-FROM",
    "AXFR-SOURCE",
    "ALLOW-DNSUPDATE-FROM",
    "TSIG-ALLOW-DNSUPDATE",
    "FORWARD-DNSUPDATE",
    "SOA-EDIT-DNSUPDATE",
    "NOTIFY-DNSUPDATE",
    "ALSO-NOTIFY",
    "AXFR-MASTER-TSIG",
    "GSS-ALLOW-AXFR-PRINCIPAL",
    "GSS-ACCEPTOR-PRINCIPAL",
    "IXFR",
    "LUA-AXFR-SCRIPT",
    "NSEC3NARROW",
    "NSEC3PARAM",
    "PRESIGNED",
    "PUBLISH-CDNSKEY",
    "PUBLISH-CDS",
    "SLAVE-RENOTIFY",
    "SOA-EDIT",
    "TSIG-ALLOW-AXFR",
    "TSIG-ALLOW-DNSUPDATE"
  };

  // the following options do not allow modifications via API
  static vector<string> protectedOptions {
    "API-RECTIFY",
    "AXFR-MASTER-TSIG",
    "NSEC3NARROW",
    "NSEC3PARAM",
    "PRESIGNED",
    "LUA-AXFR-SCRIPT",
    "TSIG-ALLOW-AXFR"
  };

  if (kind.find("X-") == 0)
    return true;

  bool found = false;

  for (const string& s : builtinOptions) {
    if (kind == s) {
      for (const string& s2 : protectedOptions) {
        if (!readonly && s == s2)
          return false;
      }
      found = true;
      break;
    }
  }

  return found;
}

/* Return OpenAPI document describing the supported API.
 */
#include "apidocfiles.h"

void apiDocs(HttpRequest* req, HttpResponse* resp) {
  if(req->method != "GET")
    throw HttpMethodNotAllowedException();

  if (req->accept_yaml) {
    resp->setYamlBody(g_api_swagger_yaml);
  } else if (req->accept_json) {
    resp->setJsonBody(g_api_swagger_json);
  } else {
    resp->setPlainBody(g_api_swagger_yaml);
  }
}

static void apiZoneMetadata(HttpRequest* req, HttpResponse *resp) {
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);

  UeberBackend B;
  DomainInfo di;
  if (!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  if (req->method == "GET") {
    map<string, vector<string> > md;
    Json::array document;

    if (!B.getAllDomainMetadata(zonename, md))
      throw HttpNotFoundException();

    for (const auto& i : md) {
      Json::array entries;
      for (const string& j : i.second)
        entries.push_back(j);

      Json::object key {
        { "type", "Metadata" },
        { "kind", i.first },
        { "metadata", entries }
      };

      document.push_back(key);
    }

    resp->setJsonBody(document);
  } else if (req->method == "POST") {
    auto document = req->json();
    string kind;
    vector<string> entries;

    try {
      kind = stringFromJson(document, "kind");
    } catch (const JsonException&) {
      throw ApiException("kind is not specified or not a string");
    }

    if (!isValidMetadataKind(kind, false))
      throw ApiException("Unsupported metadata kind '" + kind + "'");

    vector<string> vecMetadata;

    if (!B.getDomainMetadata(zonename, kind, vecMetadata))
      throw ApiException("Could not retrieve metadata entries for domain '" +
        zonename.toString() + "'");

    auto& metadata = document["metadata"];
    if (!metadata.is_array())
      throw ApiException("metadata is not specified or not an array");

    for (const auto& i : metadata.array_items()) {
      if (!i.is_string())
        throw ApiException("metadata must be strings");
      else if (std::find(vecMetadata.cbegin(),
                         vecMetadata.cend(),
                         i.string_value()) == vecMetadata.cend()) {
        vecMetadata.push_back(i.string_value());
      }
    }

    if (!B.setDomainMetadata(zonename, kind, vecMetadata))
      throw ApiException("Could not update metadata entries for domain '" +
        zonename.toString() + "'");

    DNSSECKeeper::clearMetaCache(zonename);

    Json::array respMetadata;
    for (const string& s : vecMetadata)
      respMetadata.push_back(s);

    Json::object key {
      { "type", "Metadata" },
      { "kind", document["kind"] },
      { "metadata", respMetadata }
    };

    resp->status = 201;
    resp->setJsonBody(key);
  } else
    throw HttpMethodNotAllowedException();
}

static void apiZoneMetadataKind(HttpRequest* req, HttpResponse* resp) {
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);

  UeberBackend B;
  DomainInfo di;
  if (!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  string kind = req->parameters["kind"];

  if (req->method == "GET") {
    vector<string> metadata;
    Json::object document;
    Json::array entries;

    if (!B.getDomainMetadata(zonename, kind, metadata))
      throw HttpNotFoundException();
    else if (!isValidMetadataKind(kind, true))
      throw ApiException("Unsupported metadata kind '" + kind + "'");

    document["type"] = "Metadata";
    document["kind"] = kind;

    for (const string& i : metadata)
      entries.push_back(i);

    document["metadata"] = entries;
    resp->setJsonBody(document);
  } else if (req->method == "PUT") {
    auto document = req->json();

    if (!isValidMetadataKind(kind, false))
      throw ApiException("Unsupported metadata kind '" + kind + "'");

    vector<string> vecMetadata;
    auto& metadata = document["metadata"];
    if (!metadata.is_array())
      throw ApiException("metadata is not specified or not an array");

    for (const auto& i : metadata.array_items()) {
      if (!i.is_string())
        throw ApiException("metadata must be strings");
      vecMetadata.push_back(i.string_value());
    }

    if (!B.setDomainMetadata(zonename, kind, vecMetadata))
      throw ApiException("Could not update metadata entries for domain '" + zonename.toString() + "'");

    DNSSECKeeper::clearMetaCache(zonename);

    Json::object key {
      { "type", "Metadata" },
      { "kind", kind },
      { "metadata", metadata }
    };

    resp->setJsonBody(key);
  } else if (req->method == "DELETE") {
    if (!isValidMetadataKind(kind, false))
      throw ApiException("Unsupported metadata kind '" + kind + "'");

    vector<string> md;  // an empty vector will do it
    if (!B.setDomainMetadata(zonename, kind, md))
      throw ApiException("Could not delete metadata for domain '" + zonename.toString() + "' (" + kind + ")");

    DNSSECKeeper::clearMetaCache(zonename);
  } else
    throw HttpMethodNotAllowedException();
}

// Throws 404 if the key with inquireKeyId does not exist
static void apiZoneCryptoKeysCheckKeyExists(const DNSName& zonename, int inquireKeyId, DNSSECKeeper *dk) {
  DNSSECKeeper::keyset_t keyset=dk->getKeys(zonename, false);
  bool found = false;
  for(const auto& value : keyset) {
    if (value.second.id == (unsigned) inquireKeyId) {
      found = true;
      break;
    }
  }
  if (!found) {
    throw HttpNotFoundException();
  }
}

static void apiZoneCryptokeysGET(const DNSName& zonename, int inquireKeyId, HttpResponse *resp, DNSSECKeeper *dk) {
  DNSSECKeeper::keyset_t keyset=dk->getKeys(zonename, false);

  bool inquireSingleKey = inquireKeyId >= 0;

  Json::array doc;
  for(const auto& value : keyset) {
    if (inquireSingleKey && (unsigned)inquireKeyId != value.second.id) {
      continue;
    }

    string keyType;
    switch (value.second.keyType) {
      case DNSSECKeeper::KSK: keyType="ksk"; break;
      case DNSSECKeeper::ZSK: keyType="zsk"; break;
      case DNSSECKeeper::CSK: keyType="csk"; break;
    }

    Json::object key {
        { "type", "Cryptokey" },
        { "id", (int)value.second.id },
        { "active", value.second.active },
        { "published", value.second.published },
        { "keytype", keyType },
        { "flags", (uint16_t)value.first.d_flags },
        { "dnskey", value.first.getDNSKEY().getZoneRepresentation() },
        { "algorithm", DNSSECKeeper::algorithm2name(value.first.d_algorithm) },
        { "bits", value.first.getKey()->getBits() }
    };

    string publishCDS;
    dk->getPublishCDS(zonename, publishCDS);

    vector<string> digestAlgos;
    stringtok(digestAlgos, publishCDS, ", ");

    std::set<unsigned int> CDSalgos;
    for(auto const &digestAlgo : digestAlgos) {
      CDSalgos.insert(pdns::checked_stoi<unsigned int>(digestAlgo));
    }

    if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
      Json::array cdses;
      Json::array dses;
      for(const uint8_t keyid : { DNSSECKeeper::DIGEST_SHA1, DNSSECKeeper::DIGEST_SHA256, DNSSECKeeper::DIGEST_GOST, DNSSECKeeper::DIGEST_SHA384 })
        try {
          string ds = makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation();

          dses.push_back(ds);

          if (CDSalgos.count(keyid)) { cdses.push_back(ds); }
        } catch (...) {}

      key["ds"] = dses;

      if (cdses.size()) {
        key["cds"] = cdses;
      }
    }

    if (inquireSingleKey) {
      key["privatekey"] = value.first.getKey()->convertToISC();
      resp->setJsonBody(key);
      return;
    }
    doc.push_back(key);
  }

  if (inquireSingleKey) {
    // we came here because we couldn't find the requested key.
    throw HttpNotFoundException();
  }
  resp->setJsonBody(doc);

}

/*
 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
 * It deletes a key from :zone_name specified by :cryptokey_id.
 * Server Answers:
 * Case 1: the backend returns true on removal. This means the key is gone.
 *      The server returns 204 No Content, no body.
 * Case 2: the backend returns false on removal. An error occurred.
 *      The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
 * Case 3: the key or zone does not exist.
 *      The server returns 404 Not Found
 * */
static void apiZoneCryptokeysDELETE(const DNSName& zonename, int inquireKeyId, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
  if (dk->removeKey(zonename, inquireKeyId)) {
    resp->body = "";
    resp->status = 204;
  } else {
    resp->setErrorResult("Could not DELETE " + req->parameters["key_id"], 422);
  }
}

/*
 * This method adds a key to a zone by generate it or content parameter.
 * Parameter:
 *  {
 *  "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
 *  "keytype" : "ksk|zsk" <string>
 *  "active"  : "true|false" <value>
 *  "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
 *  "bits" : number of bits <int>
 *  }
 *
 * Response:
 *  Case 1: keytype isn't ksk|zsk
 *    The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
 *  Case 2: 'bits' must be a positive integer value.
 *    The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
 *  Case 3: The "algorithm" isn't supported
 *    The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
 *  Case 4: Algorithm <= 10 and no bits were passed
 *    The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
 *  Case 5: The wrong keysize was passed
 *    The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
 *  Case 6: If the server cant guess the keysize
 *    The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
 *  Case 7: The key-creation failed
 *    The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
 *  Case 8: The key in content has the wrong format
 *    The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
 *  Case 9: The wrong combination of fields is submitted
 *    The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
 *  Case 10: No content and everything was fine
 *    The server returns 201 Created and all public data about the new cryptokey
 *  Case 11: With specified content
 *    The server returns 201 Created and all public data about the added cryptokey
 */

static void apiZoneCryptokeysPOST(const DNSName& zonename, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
  auto document = req->json();
  string privatekey_fieldname = "privatekey";
  auto privatekey = document["privatekey"];
  if (privatekey.is_null()) {
    // Fallback to the old "content" behaviour
    privatekey = document["content"];
    privatekey_fieldname = "content";
  }
  bool active = boolFromJson(document, "active", false);
  bool published = boolFromJson(document, "published", true);
  bool keyOrZone;

  if (stringFromJson(document, "keytype") == "ksk" || stringFromJson(document, "keytype") == "csk") {
    keyOrZone = true;
  } else if (stringFromJson(document, "keytype") == "zsk") {
    keyOrZone = false;
  } else {
    throw ApiException("Invalid keytype " + stringFromJson(document, "keytype"));
  }

  int64_t insertedId = -1;

  if (privatekey.is_null()) {
    int bits = keyOrZone ? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
    auto docbits = document["bits"];
    if (!docbits.is_null()) {
      if (!docbits.is_number() || (fmod(docbits.number_value(), 1.0) != 0) || docbits.int_value() < 0) {
        throw ApiException("'bits' must be a positive integer value");
      } else {
        bits = docbits.int_value();
      }
    }
    int algorithm = DNSSECKeeper::shorthand2algorithm(keyOrZone ? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
    auto providedAlgo = document["algorithm"];
    if (providedAlgo.is_string()) {
      algorithm = DNSSECKeeper::shorthand2algorithm(providedAlgo.string_value());
      if (algorithm == -1)
        throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
    } else if (providedAlgo.is_number()) {
      algorithm = providedAlgo.int_value();
    } else if (!providedAlgo.is_null()) {
      throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
    }

    try {
      if (!dk->addKey(zonename, keyOrZone, algorithm, insertedId, bits, active, published)) {
        throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
      }
    } catch (std::runtime_error& error) {
      throw ApiException(error.what());
    }
    if (insertedId < 0)
      throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
  } else if (document["bits"].is_null() && document["algorithm"].is_null()) {
    auto keyData = stringFromJson(document, privatekey_fieldname);
    DNSKEYRecordContent dkrc;
    DNSSECPrivateKey dpk;
    try {
      shared_ptr<DNSCryptoKeyEngine> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc, keyData));
      dpk.d_algorithm = dkrc.d_algorithm;
      // TODO remove in 4.2.0
      if(dpk.d_algorithm == DNSSECKeeper::RSASHA1NSEC3SHA1)
        dpk.d_algorithm = DNSSECKeeper::RSASHA1;

      if (keyOrZone)
        dpk.d_flags = 257;
      else
        dpk.d_flags = 256;

      dpk.setKey(dke);
    }
    catch (std::runtime_error& error) {
      throw ApiException("Key could not be parsed. Make sure your key format is correct.");
    } try {
      if (!dk->addKey(zonename, dpk,insertedId, active, published)) {
        throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
      }
    } catch (std::runtime_error& error) {
      throw ApiException(error.what());
    }
    if (insertedId < 0)
      throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
  } else {
    throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
  }
  apiZoneCryptokeysGET(zonename, insertedId, resp, dk);
  resp->status = 201;
}

/*
 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
 * It de/activates a key from :zone_name specified by :cryptokey_id.
 * Server Answers:
 * Case 1: invalid JSON data
 *      The server returns 400 Bad Request
 * Case 2: the backend returns true on de/activation. This means the key is de/active.
 *      The server returns 204 No Content
 * Case 3: the backend returns false on de/activation. An error occurred.
 *      The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
 * */
static void apiZoneCryptokeysPUT(const DNSName& zonename, int inquireKeyId, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
  //throws an exception if the Body is empty
  auto document = req->json();
  //throws an exception if the key does not exist or is not a bool
  bool active = boolFromJson(document, "active");
  bool published = boolFromJson(document, "published", true);
  if (active) {
    if (!dk->activateKey(zonename, inquireKeyId)) {
      resp->setErrorResult("Could not activate Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
      return;
    }
  } else {
    if (!dk->deactivateKey(zonename, inquireKeyId)) {
      resp->setErrorResult("Could not deactivate Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
      return;
    }
  }

  if (published) {
    if (!dk->publishKey(zonename, inquireKeyId)) {
      resp->setErrorResult("Could not publish Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
      return;
    }
  } else {
    if (!dk->unpublishKey(zonename, inquireKeyId)) {
      resp->setErrorResult("Could not unpublish Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
      return;
    }
  }

  resp->body = "";
  resp->status = 204;
  return;
}

/*
 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
 * */
static void apiZoneCryptokeys(HttpRequest *req, HttpResponse *resp) {
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);

  UeberBackend B;
  DNSSECKeeper dk(&B);
  DomainInfo di;
  if (!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  int inquireKeyId = -1;
  if (req->parameters.count("key_id")) {
    inquireKeyId = std::stoi(req->parameters["key_id"]);
    apiZoneCryptoKeysCheckKeyExists(zonename, inquireKeyId, &dk);
  }

  if (req->method == "GET") {
    apiZoneCryptokeysGET(zonename, inquireKeyId, resp, &dk);
  } else if (req->method == "DELETE") {
    if (inquireKeyId == -1)
      throw HttpBadRequestException();
    apiZoneCryptokeysDELETE(zonename, inquireKeyId, req, resp, &dk);
  } else if (req->method == "POST") {
    apiZoneCryptokeysPOST(zonename, req, resp, &dk);
  } else if (req->method == "PUT") {
    if (inquireKeyId == -1)
      throw HttpBadRequestException();
    apiZoneCryptokeysPUT(zonename, inquireKeyId, req, resp, &dk);
  } else {
    throw HttpMethodNotAllowedException(); //Returns method not allowed
  }
}

static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, const DNSName& zonename) {
  DNSResourceRecord rr;
  vector<string> zonedata;
  stringtok(zonedata, zonestring, "\r\n");

  ZoneParserTNG zpt(zonedata, zonename);
  zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
  zpt.setMaxIncludes(::arg().asNum("max-include-depth"));

  bool seenSOA=false;

  string comment = "Imported via the API";

  try {
    while(zpt.get(rr, &comment)) {
      if(seenSOA && rr.qtype.getCode() == QType::SOA)
        continue;
      if(rr.qtype.getCode() == QType::SOA)
        seenSOA=true;
      validateGatheredRRType(rr);

      new_records.push_back(rr);
    }
  }
  catch(std::exception& ae) {
    throw ApiException("An error occurred while parsing the zonedata: "+string(ae.what()));
  }
}

/** Throws ApiException if records which violate RRset constraints are present.
 *  NOTE: sorts records in-place.
 *
 *  Constraints being checked:
 *   *) no exact duplicates
 *   *) no duplicates for QTypes that can only be present once per RRset
 *   *) hostnames are hostnames
 */
static void checkNewRecords(vector<DNSResourceRecord>& records, const DNSName& zone)
{
  sort(records.begin(), records.end(),
    [](const DNSResourceRecord& rec_a, const DNSResourceRecord& rec_b) -> bool {
      /* we need _strict_ weak ordering */
      return std::tie(rec_a.qname, rec_a.qtype, rec_a.content) < std::tie(rec_b.qname, rec_b.qtype, rec_b.content);
    }
  );

  DNSResourceRecord previous;
  for(const auto& rec : records) {
    if (previous.qname == rec.qname) {
      if (previous.qtype == rec.qtype) {
        if (onlyOneEntryTypes.count(rec.qtype.getCode()) != 0) {
          throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.toString()+" has more than one record");
        }
        if (previous.content == rec.content) {
          throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " with content \"" + rec.content + "\"");
        }
      } else if (exclusiveEntryTypes.count(rec.qtype.getCode()) != 0 || exclusiveEntryTypes.count(previous.qtype.getCode()) != 0) {
        throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.toString()+": Conflicts with another RRset");
      }
    }

    if (rec.qname == zone) {
      if (nonApexTypes.count(rec.qtype.getCode()) != 0) {
        throw ApiException("Record " + rec.qname.toString() + " IN " + rec.qtype.toString() + " is not allowed at apex");
      }
    }
    else if (atApexTypes.count(rec.qtype.getCode()) != 0) {
      throw ApiException("Record " + rec.qname.toString() + " IN " + rec.qtype.toString() + " is only allowed at apex");
    }

    // Check if the DNSNames that should be hostnames, are hostnames
    try {
      checkHostnameCorrectness(rec);
    } catch (const std::exception& e) {
      throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.toString() + " " + e.what());
    }

    previous = rec;
  }
}

static void checkTSIGKey(UeberBackend& B, const DNSName& keyname, const DNSName& algo, const string& content) {
  DNSName algoFromDB;
  string contentFromDB;
  if (B.getTSIGKey(keyname, algoFromDB, contentFromDB)) {
    throw HttpConflictException("A TSIG key with the name '"+keyname.toLogString()+"' already exists");
  }

  TSIGHashEnum the;
  if (!getTSIGHashEnum(algo, the)) {
    throw ApiException("Unknown TSIG algorithm: " + algo.toLogString());
  }

  string b64out;
  if (B64Decode(content, b64out) == -1) {
    throw ApiException("TSIG content '" + content + "' cannot be base64-decoded");
  }
}

static Json::object makeJSONTSIGKey(const DNSName& keyname, const DNSName& algo, const string& content) {
  Json::object tsigkey = {
    { "name", keyname.toStringNoDot() },
    { "id", apiZoneNameToId(keyname) },
    { "algorithm", algo.toStringNoDot() },
    { "key", content },
    { "type", "TSIGKey" }
  };
  return tsigkey;
}

static Json::object makeJSONTSIGKey(const struct TSIGKey& key, bool doContent=true) {
  return makeJSONTSIGKey(key.name, key.algorithm, doContent ? key.key : "");
}

static void apiServerTSIGKeys(HttpRequest* req, HttpResponse* resp) {
  UeberBackend B;
  if (req->method == "GET") {
    vector<struct TSIGKey> keys;

    if (!B.getTSIGKeys(keys)) {
      throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
    }

    Json::array doc;

    for(const auto &key : keys) {
      doc.push_back(makeJSONTSIGKey(key, false));
    }
    resp->setJsonBody(doc);
  } else if (req->method == "POST") {
    auto document = req->json();
    DNSName keyname(stringFromJson(document, "name"));
    DNSName algo(stringFromJson(document, "algorithm"));
    string content = document["key"].string_value();

    if (content.empty()) {
      try {
        content = makeTSIGKey(algo);
      } catch (const PDNSException& e) {
        throw HttpBadRequestException(e.reason);
      }
    }

    // Will throw an ApiException or HttpConflictException on error
    checkTSIGKey(B, keyname, algo, content);

    if(!B.setTSIGKey(keyname, algo, content)) {
      throw HttpInternalServerErrorException("Unable to add TSIG key");
    }

    resp->status = 201;
    resp->setJsonBody(makeJSONTSIGKey(keyname, algo, content));
  } else {
    throw HttpMethodNotAllowedException();
  }
}

static void apiServerTSIGKeyDetail(HttpRequest* req, HttpResponse* resp) {
  UeberBackend B;
  DNSName keyname = apiZoneIdToName(req->parameters["id"]);
  DNSName algo;
  string content;

  if (!B.getTSIGKey(keyname, algo, content)) {
    throw HttpNotFoundException("TSIG key with name '"+keyname.toLogString()+"' not found");
  }

  struct TSIGKey tsk;
  tsk.name = keyname;
  tsk.algorithm = algo;
  tsk.key = content;

  if (req->method == "GET") {
    resp->setJsonBody(makeJSONTSIGKey(tsk));
  } else if (req->method == "PUT") {
    json11::Json document;
    if (!req->body.empty()) {
      document = req->json();
    }
    if (document["name"].is_string()) {
      tsk.name = DNSName(document["name"].string_value());
    }
    if (document["algorithm"].is_string()) {
      tsk.algorithm = DNSName(document["algorithm"].string_value());

      TSIGHashEnum the;
      if (!getTSIGHashEnum(tsk.algorithm, the)) {
        throw ApiException("Unknown TSIG algorithm: " + tsk.algorithm.toLogString());
      }
    }
    if (document["key"].is_string()) {
      string new_content = document["key"].string_value();
      string decoded;
      if (B64Decode(new_content, decoded) == -1) {
        throw ApiException("Can not base64 decode key content '" + new_content + "'");
      }
      tsk.key = new_content;
    }
    if (!B.setTSIGKey(tsk.name, tsk.algorithm, tsk.key)) {
      throw HttpInternalServerErrorException("Unable to save TSIG Key");
    }
    if (tsk.name != keyname) {
      // Remove the old key
      if (!B.deleteTSIGKey(keyname)) {
        throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
      }
    }
    resp->setJsonBody(makeJSONTSIGKey(tsk));
  } else if (req->method == "DELETE") {
    if (!B.deleteTSIGKey(keyname)) {
      throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
    } else {
      resp->body = "";
      resp->status = 204;
    }
  } else {
    throw HttpMethodNotAllowedException();
  }
}

static void apiServerAutoprimaryDetail(HttpRequest* req, HttpResponse* resp) {
  UeberBackend B;
  if (req->method == "DELETE") {
    const AutoPrimary primary(req->parameters["ip"], req->parameters["nameserver"], "");
    if (!B.autoPrimaryRemove(primary))
       throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
    resp->body = "";
    resp->status = 204;
  } else {
    throw HttpMethodNotAllowedException();
  }
}

static void apiServerAutoprimaries(HttpRequest* req, HttpResponse* resp) {
  UeberBackend B;

  if (req->method == "GET") {
    std::vector<AutoPrimary> primaries;
    if (!B.autoPrimariesList(primaries))
      throw HttpInternalServerErrorException("Unable to retrieve autoprimaries");
    Json::array doc;
    for (const auto& primary: primaries) {
      Json::object obj = {
        { "ip", primary.ip },
        { "nameserver", primary.nameserver },
        { "account", primary.account }
      };
      doc.push_back(obj);
    }
    resp->setJsonBody(doc);
  } else if (req->method == "POST") {
    auto document = req->json();
    AutoPrimary primary(stringFromJson(document, "ip"), stringFromJson(document, "nameserver"), "");

    if (document["account"].is_string()) {
      primary.account = document["account"].string_value();
    }

    if (primary.ip=="" or primary.nameserver=="") {
      throw ApiException("ip and nameserver fields must be filled");
    }
    if (!B.superMasterAdd(primary))
      throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
    resp->body = "";
    resp->status = 201;
  } else {
    throw HttpMethodNotAllowedException();
  }
}

static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
  UeberBackend B;
  DNSSECKeeper dk(&B);
  if (req->method == "POST") {
    DomainInfo di;
    auto document = req->json();
    DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
    apiCheckNameAllowedCharacters(zonename.toString());
    zonename.makeUsLowerCase();

    bool exists = B.getDomainInfo(zonename, di);
    if(exists)
      throw HttpConflictException();

    // validate 'kind' is set
    DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));

    string zonestring = document["zone"].string_value();
    auto rrsets = document["rrsets"];
    if (rrsets.is_array() && zonestring != "")
      throw ApiException("You cannot give rrsets AND zone data as text");

    auto nameservers = document["nameservers"];
    if (!nameservers.is_null() && !nameservers.is_array() && zonekind != DomainInfo::Slave)
      throw ApiException("Nameservers is not a list");

    string soa_edit_api_kind;
    if (document["soa_edit_api"].is_string()) {
      soa_edit_api_kind = document["soa_edit_api"].string_value();
    }
    else {
      soa_edit_api_kind = "DEFAULT";
    }
    string soa_edit_kind = document["soa_edit"].string_value();

    // if records/comments are given, load and check them
    bool have_soa = false;
    bool have_zone_ns = false;
    vector<DNSResourceRecord> new_records;
    vector<Comment> new_comments;

    if (rrsets.is_array()) {
      for (const auto& rrset : rrsets.array_items()) {
        DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
        apiCheckQNameAllowedCharacters(qname.toString());
        QType qtype;
        qtype = stringFromJson(rrset, "type");
        if (qtype.getCode() == 0) {
          throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
        }
        if (rrset["records"].is_array()) {
          int ttl = intFromJson(rrset, "ttl");
          gatherRecords(B, req->logprefix, rrset, qname, qtype, ttl, new_records);
        }
        if (rrset["comments"].is_array()) {
          gatherComments(rrset, qname, qtype, new_comments);
        }
      }
    } else if (zonestring != "") {
      gatherRecordsFromZone(zonestring, new_records, zonename);
    }

    for(auto& rr : new_records) {
      rr.qname.makeUsLowerCase();
      if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
        throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.toString()+": Name is out of zone");
      apiCheckQNameAllowedCharacters(rr.qname.toString());

      if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
        have_soa = true;
        increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
      }
      if (rr.qtype.getCode() == QType::NS && rr.qname==zonename) {
        have_zone_ns = true;
      }
    }

    // synthesize RRs as needed
    DNSResourceRecord autorr;
    autorr.qname = zonename;
    autorr.auth = true;
    autorr.ttl = ::arg().asNum("default-ttl");

    if (!have_soa && zonekind != DomainInfo::Slave) {
      // synthesize a SOA record so the zone "really" exists
      string soa = ::arg()["default-soa-content"];
      boost::replace_all(soa, "@", zonename.toStringNoDot());
      SOAData sd;
      fillSOAData(soa, sd);
      sd.serial=document["serial"].int_value();
      autorr.qtype = QType::SOA;
      autorr.content = makeSOAContent(sd)->getZoneRepresentation(true);
      increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
      new_records.push_back(autorr);
    }

    // create NS records if nameservers are given
    for (const auto& value : nameservers.array_items()) {
      const string& nameserver = value.string_value();
      if (nameserver.empty())
        throw ApiException("Nameservers must be non-empty strings");
      if (!isCanonical(nameserver))
        throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
      try {
        // ensure the name parses
        autorr.content = DNSName(nameserver).toStringRootDot();
      } catch (...) {
        throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
      }
      autorr.qtype = QType::NS;
      new_records.push_back(autorr);
      if (have_zone_ns) {
        throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
      }
    }

    checkNewRecords(new_records, zonename);

    if (boolFromJson(document, "dnssec", false)) {
      checkDefaultDNSSECAlgos();

      if(document["nsec3param"].string_value().length() > 0) {
        NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
        string error_msg = "";
        if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
          throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
        }
      }
    }

    boost::optional<DomainInfo::DomainKind> kind;
    boost::optional<vector<ComboAddress>> masters;
    boost::optional<DNSName> catalog;
    boost::optional<string> account;
    extractDomainInfoFromDocument(document, kind, masters, catalog, account);

    // no going back after this
    if(!B.createDomain(zonename, kind.get_value_or(DomainInfo::Native), masters.get_value_or(vector<ComboAddress>()), account.get_value_or("")))
      throw ApiException("Creating domain '"+zonename.toString()+"' failed");

    if(!B.getDomainInfo(zonename, di))
      throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");

    di.backend->startTransaction(zonename, di.id);

    // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
    if (!soa_edit_api_kind.empty()) {
      di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
    }

    for(auto rr : new_records) {
      rr.domain_id = di.id;
      di.backend->feedRecord(rr, DNSName());
    }
    for(Comment& c : new_comments) {
      c.domain_id = di.id;
      di.backend->feedComment(c);
    }

    updateDomainSettingsFromDocument(B, di, zonename, document, false);

    di.backend->commitTransaction();

    g_zoneCache.add(zonename, di.id); // make new zone visible

    fillZone(B, zonename, resp, req);
    resp->status = 201;
    return;
  }

  if(req->method != "GET")
    throw HttpMethodNotAllowedException();

  vector<DomainInfo> domains;

  if (req->getvars.count("zone")) {
    string zone = req->getvars["zone"];
    apiCheckNameAllowedCharacters(zone);
    DNSName zonename = apiNameToDNSName(zone);
    zonename.makeUsLowerCase();
    DomainInfo di;
    if (B.getDomainInfo(zonename, di)) {
      domains.push_back(di);
    }
  } else {
    try {
      B.getAllDomains(&domains, true, true); // incl. serial and disabled
    } catch(const PDNSException &e) {
      throw HttpInternalServerErrorException("Could not retrieve all domain information: " + e.reason);
    }
  }

  bool with_dnssec = true;
  if (req->getvars.count("dnssec")) {
    // can send ?dnssec=false to improve performance.
    string dnssec_flag = req->getvars["dnssec"];
    if (dnssec_flag == "false") {
      with_dnssec = false;
    }
  }

  Json::array doc;
  doc.reserve(domains.size());
  for(const DomainInfo& di : domains) {
    doc.push_back(getZoneInfo(di, with_dnssec ? &dk : nullptr));
  }
  resp->setJsonBody(doc);
}

static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);

  UeberBackend B;
  DomainInfo di;
  try {
    if (!B.getDomainInfo(zonename, di)) {
      throw HttpNotFoundException();
    }
  } catch(const PDNSException &e) {
    throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e.reason);
  }

  if(req->method == "PUT") {
    // update domain settings

    di.backend->startTransaction(zonename, -1);
    updateDomainSettingsFromDocument(B, di, zonename, req->json(), false);
    di.backend->commitTransaction();

    resp->body = "";
    resp->status = 204; // No Content, but indicate success
    return;
  }
  else if(req->method == "DELETE") {
    // delete domain

    di.backend->startTransaction(zonename, -1);
    try {
      if(!di.backend->deleteDomain(zonename))
        throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");

      di.backend->commitTransaction();

      g_zoneCache.remove(zonename);
    } catch (...) {
      di.backend->abortTransaction();
      throw;
    }

    // clear caches
    DNSSECKeeper::clearCaches(zonename);
    purgeAuthCaches(zonename.toString() + "$");

    // empty body on success
    resp->body = "";
    resp->status = 204; // No Content: declare that the zone is gone now
    return;
  } else if (req->method == "PATCH") {
    patchZone(B, req, resp);
    return;
  } else if (req->method == "GET") {
    fillZone(B, zonename, resp, req);
    return;
  }
  throw HttpMethodNotAllowedException();
}

static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);

  if(req->method != "GET")
    throw HttpMethodNotAllowedException();

  ostringstream ss;

  UeberBackend B;
  DomainInfo di;
  if (!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  DNSResourceRecord rr;
  SOAData sd;
  di.backend->list(zonename, di.id);
  while(di.backend->get(rr)) {
    if (!rr.qtype.getCode())
      continue; // skip empty non-terminals

    ss <<
      rr.qname.toString() << "\t" <<
      rr.ttl << "\t" <<
      "IN" << "\t" <<
      rr.qtype.toString() << "\t" <<
      makeApiRecordContent(rr.qtype, rr.content) <<
      endl;
  }

  if (req->accept_json) {
    resp->setJsonBody(Json::object { { "zone", ss.str() } });
  } else {
    resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
    resp->body = ss.str();
  }
}

static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);

  if(req->method != "PUT")
    throw HttpMethodNotAllowedException();

  UeberBackend B;
  DomainInfo di;
  if (!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  if(di.masters.empty())
    throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");

  shuffle(di.masters.begin(), di.masters.end(), pdns::dns_random_engine());
  Communicator.addSuckRequest(zonename, di.masters.front(), SuckRequest::Api);
  resp->setSuccessResult("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front().toLogString());
}

static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);

  if(req->method != "PUT")
    throw HttpMethodNotAllowedException();

  UeberBackend B;
  DomainInfo di;
  if (!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  if(!Communicator.notifyDomain(zonename, &B))
    throw ApiException("Failed to add to the queue - see server log");

  resp->setSuccessResult("Notification queued");
}

static void apiServerZoneRectify(HttpRequest* req, HttpResponse* resp) {
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);

  if(req->method != "PUT")
    throw HttpMethodNotAllowedException();

  UeberBackend B;
  DomainInfo di;
  if (!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  DNSSECKeeper dk(&B);

  if (dk.isPresigned(zonename))
    throw ApiException("Zone '" + zonename.toString() + "' is pre-signed, not rectifying.");

  string error_msg = "";
  string info;
  if (!dk.rectifyZone(zonename, error_msg, info, true))
    throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);

  resp->setSuccessResult("Rectified");
}

static void patchZone(UeberBackend& B, HttpRequest* req, HttpResponse* resp) {
  bool zone_disabled;
  SOAData sd;
  DomainInfo di;
  DNSName zonename = apiZoneIdToName(req->parameters["id"]);
  if (!B.getDomainInfo(zonename, di)) {
    throw HttpNotFoundException();
  }

  vector<DNSResourceRecord> new_records;
  vector<Comment> new_comments;
  vector<DNSResourceRecord> new_ptrs;

  Json document = req->json();

  auto rrsets = document["rrsets"];
  if (!rrsets.is_array())
    throw ApiException("No rrsets given in update request");

  di.backend->startTransaction(zonename);

  try {
    string soa_edit_api_kind;
    string soa_edit_kind;
    di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
    di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
    bool soa_edit_done = false;

    set<std::tuple<DNSName, QType, string>> seen;

    for (const auto& rrset : rrsets.array_items()) {
      string changetype = toUpper(stringFromJson(rrset, "changetype"));
      DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
      apiCheckQNameAllowedCharacters(qname.toString());
      QType qtype;
      qtype = stringFromJson(rrset, "type");
      if (qtype.getCode() == 0) {
        throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
      }

      if(seen.count({qname, qtype, changetype}))
      {
        throw ApiException("Duplicate RRset "+qname.toString()+" IN "+qtype.toString()+" with changetype: "+changetype);
      }
      seen.insert({qname, qtype, changetype});

      if (changetype == "DELETE") {
        // delete all matching qname/qtype RRs (and, implicitly comments).
        if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
          throw ApiException("Hosting backend does not support editing records.");
        }
      }
      else if (changetype == "REPLACE") {
        // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
        if (!qname.isPartOf(zonename) && qname != zonename)
          throw ApiException("RRset "+qname.toString()+" IN "+qtype.toString()+": Name is out of zone");

        bool replace_records = rrset["records"].is_array();
        bool replace_comments = rrset["comments"].is_array();

        if (!replace_records && !replace_comments) {
          throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.toString());
        }

        new_records.clear();
        new_comments.clear();

        if (replace_records) {
          // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
          int ttl = intFromJson(rrset, "ttl");

          gatherRecords(B, req->logprefix, rrset, qname, qtype, ttl, new_records);

          for(DNSResourceRecord& rr : new_records) {
            rr.domain_id = di.id;
            if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
              soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
            }
          }
          checkNewRecords(new_records, zonename);
        }

        if (replace_comments) {
          gatherComments(rrset, qname, qtype, new_comments);

          for(Comment& c : new_comments) {
            c.domain_id = di.id;
          }
        }

        if (replace_records) {
          bool ent_present = false;
          bool dname_seen = false, ns_seen = false;

          di.backend->lookup(QType(QType::ANY), qname, di.id);
          DNSResourceRecord rr;
          while (di.backend->get(rr)) {
            if (rr.qtype.getCode() == QType::ENT) {
              ent_present = true;
              /* that's fine, we will override it */
              continue;
            }
            if (qtype == QType::DNAME || rr.qtype == QType::DNAME)
              dname_seen = true;
            if (qtype == QType::NS || rr.qtype == QType::NS)
              ns_seen = true;
            if (qtype.getCode() != rr.qtype.getCode()
              && (exclusiveEntryTypes.count(qtype.getCode()) != 0
                || exclusiveEntryTypes.count(rr.qtype.getCode()) != 0)) {

              // leave database handle in a consistent state
              while (di.backend->get(rr))
                ;

              throw ApiException("RRset "+qname.toString()+" IN "+qtype.toString()+": Conflicts with pre-existing RRset");
            }
          }

          if (dname_seen && ns_seen && qname != zonename) {
            throw ApiException("RRset "+qname.toString()+" IN "+qtype.toString()+": Cannot have both NS and DNAME except in zone apex");
          }
          if (!new_records.empty() && ent_present) {
            QType qt_ent{0};
            if (!di.backend->replaceRRSet(di.id, qname, qt_ent, new_records)) {
              throw ApiException("Hosting backend does not support editing records.");
            }
          }
          if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
            throw ApiException("Hosting backend does not support editing records.");
          }
        }
        if (replace_comments) {
          if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
            throw ApiException("Hosting backend does not support editing comments.");
          }
        }
      }
      else
        throw ApiException("Changetype not understood");
    }

    zone_disabled = (!B.getSOAUncached(zonename, sd));

    // edit SOA (if needed)
    if (!zone_disabled && !soa_edit_api_kind.empty() && !soa_edit_done) {
      DNSResourceRecord rr;
      if (makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, rr)) {
        if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
          throw ApiException("Hosting backend does not support editing records.");
        }
      }

      // return old and new serials in headers
      resp->headers["X-PDNS-Old-Serial"] = std::to_string(sd.serial);
      fillSOAData(rr.content, sd);
      resp->headers["X-PDNS-New-Serial"] = std::to_string(sd.serial);
    }

  } catch(...) {
    di.backend->abortTransaction();
    throw;
  }

  // Rectify
  DNSSECKeeper dk(&B);
  if (!zone_disabled && !dk.isPresigned(zonename)) {
    string api_rectify;
    if (!di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify) && ::arg().mustDo("default-api-rectify")) {
      api_rectify = "1";
    }
    if (api_rectify == "1") {
      string info;
      string error_msg;
      if (!dk.rectifyZone(zonename, error_msg, info, false)) {
        throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
      }
    }
  }

  di.backend->commitTransaction();

  purgeAuthCaches(zonename.toString() + "$");

  resp->body = "";
  resp->status = 204; // No Content, but indicate success
  return;
}

static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
  if(req->method != "GET")
    throw HttpMethodNotAllowedException();

  string q = req->getvars["q"];
  string sMax = req->getvars["max"];
  string sObjectType = req->getvars["object_type"];

  int maxEnts = 100;
  int ents = 0;

  // the following types of data can be searched for using the api
  enum class ObjectType
  {
    ALL,
    ZONE,
    RECORD,
    COMMENT
  } objectType;

  if (q.empty())
    throw ApiException("Query q can't be blank");
  if (!sMax.empty())
    maxEnts = std::stoi(sMax);
  if (maxEnts < 1)
    throw ApiException("Maximum entries must be larger than 0");

  if (sObjectType.empty())
    objectType = ObjectType::ALL;
  else if (sObjectType == "all")
    objectType = ObjectType::ALL;
  else if (sObjectType == "zone")
    objectType = ObjectType::ZONE;
  else if (sObjectType == "record")
    objectType = ObjectType::RECORD;
  else if (sObjectType == "comment")
    objectType = ObjectType::COMMENT;
  else
    throw ApiException("object_type must be one of the following options: all, zone, record, comment");

  SimpleMatch sm(q,true);
  UeberBackend B;
  vector<DomainInfo> domains;
  vector<DNSResourceRecord> result_rr;
  vector<Comment> result_c;
  map<int,DomainInfo> zoneIdZone;
  map<int,DomainInfo>::iterator val;
  Json::array doc;

  B.getAllDomains(&domains, false, true);

  for(const DomainInfo& di: domains)
  {
    if ((objectType == ObjectType::ALL || objectType == ObjectType::ZONE) && ents < maxEnts && sm.match(di.zone)) {
      doc.push_back(Json::object {
        { "object_type", "zone" },
        { "zone_id", apiZoneNameToId(di.zone) },
        { "name", di.zone.toString() }
      });
      ents++;
    }
    zoneIdZone[di.id] = di; // populate cache
  }

  if ((objectType == ObjectType::ALL || objectType == ObjectType::RECORD) && B.searchRecords(q, maxEnts, result_rr))
  {
    for(const DNSResourceRecord& rr: result_rr)
    {
      if (!rr.qtype.getCode())
        continue; // skip empty non-terminals

      auto object = Json::object {
        { "object_type", "record" },
        { "name", rr.qname.toString() },
        { "type", rr.qtype.toString() },
        { "ttl", (double)rr.ttl },
        { "disabled", rr.disabled },
        { "content", makeApiRecordContent(rr.qtype, rr.content) }
      };
      if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
        object["zone_id"] = apiZoneNameToId(val->second.zone);
        object["zone"] = val->second.zone.toString();
      }
      doc.push_back(object);
    }
  }

  if ((objectType == ObjectType::ALL || objectType == ObjectType::COMMENT) && B.searchComments(q, maxEnts, result_c))
  {
    for(const Comment &c: result_c)
    {
      auto object = Json::object {
        { "object_type", "comment" },
        { "name", c.qname.toString() },
        { "type", c.qtype.toString() },
        { "content", c.content }
      };
      if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
        object["zone_id"] = apiZoneNameToId(val->second.zone);
        object["zone"] = val->second.zone.toString();
      }
      doc.push_back(object);
    }
  }

  resp->setJsonBody(doc);
}

static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
  if(req->method != "PUT")
    throw HttpMethodNotAllowedException();

  DNSName canon = apiNameToDNSName(req->getvars["domain"]);

  if (g_zoneCache.isEnabled()) {
    DomainInfo di;
    UeberBackend B;
    if (B.getDomainInfo(canon, di, false)) {
      // zone exists (uncached), add/update it in the zone cache.
      // Handle this first, to avoid concurrent queries re-populating the other caches.
      g_zoneCache.add(di.zone, di.id);
    }
    else {
      g_zoneCache.remove(di.zone);
    }
  }

  // purge entire zone from cache, not just zone-level records.
  uint64_t count = purgeAuthCaches(canon.toString() + "$");
  resp->setJsonBody(Json::object {
      { "count", (int) count },
      { "result", "Flushed cache." }
  });
}

static std::ostream& operator<<(std::ostream& os, StatType statType)
{
  switch (statType)
  {
    case StatType::counter: return os << "counter";
    case StatType::gauge: return os << "gauge";
  };
  return os << static_cast<uint16_t>(statType);
}

static void prometheusMetrics(HttpRequest* req, HttpResponse* resp) {
  if (req->method != "GET")
    throw HttpMethodNotAllowedException();

  std::ostringstream output;
  for (const auto &metricName : S.getEntries()) {
    // Prometheus suggest using '_' instead of '-'
    std::string prometheusMetricName = "pdns_auth_" + boost::replace_all_copy(metricName, "-", "_");

    output << "# HELP " << prometheusMetricName << " " << S.getDescrip(metricName) << "\n";
    output << "# TYPE " << prometheusMetricName << " " << S.getStatType(metricName) << "\n";
    output << prometheusMetricName << " " << S.read(metricName) << "\n";
  }

  output << "# HELP pdns_auth_info " << "Info from PowerDNS, value is always 1" << "\n";
  output << "# TYPE pdns_auth_info " << "gauge" << "\n";
  output << "pdns_auth_info{version=\"" << VERSION << "\"} " << "1" << "\n";

  resp->body = output.str();
  resp->headers["Content-Type"] = "text/plain";
  resp->status = 200;
}

void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
{
  resp->headers["Cache-Control"] = "max-age=86400";
  resp->headers["Content-Type"] = "text/css";

  ostringstream ret;
  ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
  ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
  ret<<"a { color: #0959c2; }"<<endl;
  ret<<"a:hover { color: #3B8EC8; }"<<endl;
  ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
  ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
  ret<<".row:after { clear: both; }"<<endl;
  ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
  ret<<".all { width: 100%; }"<<endl;
  ret<<".headl { width: 60%; }"<<endl;
  ret<<".header { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
  ret<<"background-image: url(data:image/png;base64, );";
  ret<<" width: 154px; height: 20px; }"<<endl;
  ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
  ret<<"footer { border-top:  1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
  ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
  ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
  ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
  ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
  ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
  ret<<"table.data tr:hover { background: white; }"<<endl;
  ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
  ret<<".resetring {float: right; }"<<endl;
  ret<<".resetring i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA/klEQVQY01XPP04UUBgE8N/33vd2XZUWEuzYuMZEG4KFCQn2NhA4AIewAOMBPIG2xhNYeAcKGqkNCdmYlVBZGBIT4FHsbuE0U8xk/kAbqm9TOfI/nicfhmwgDNhvylUT58kxCp4l31L8SfH9IetJ2ev6PwyIwyZWsdb11/gbTK55Co+r8rmJaRPTFJcpZil+pTit7C5awMpA+Zpi1sRFE9MqflYOloYCjY2uP8EdYiGU4CVGUBubxKfOOLjrtOBmzvEilbVb/aQWvhRl0unBZVXe4XdnK+bprwqnhoyTsyZ+JG8Wk0apfExxlcp7PFruXH8gdxamWB4cyW2sIO4BG3czIp78jUIAAAAASUVORK5CYII=); width: 10px; height: 10px; margin-right: 2px; display: inline-block; background-repeat: no-repeat; }"<<endl;
  ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
  ret<<".resizering {float: right;}"<<endl;
  resp->body = ret.str();
  resp->status = 200;
}

void AuthWebServer::webThread()
{
  try {
    setThreadName("pdns/webserver");
    if(::arg().mustDo("api")) {
      d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", apiServerCacheFlush);
      d_ws->registerApiHandler("/api/v1/servers/localhost/config", apiServerConfig);
      d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", apiServerSearchData);
      d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", apiServerStatistics);
      d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries/<ip>/<nameserver>", &apiServerAutoprimaryDetail);
      d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimaries);
      d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetail);
      d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeys);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", apiServerZoneAxfrRetrieve);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeys);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", apiZoneCryptokeys);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", apiServerZoneExport);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKind);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", apiZoneMetadata);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", apiServerZoneNotify);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", apiServerZoneRectify);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetail);
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZones);
      d_ws->registerApiHandler("/api/v1/servers/localhost", apiServerDetail);
      d_ws->registerApiHandler("/api/v1/servers", apiServer);
      d_ws->registerApiHandler("/api/v1", apiDiscoveryV1);
      d_ws->registerApiHandler("/api/docs", apiDocs);
      d_ws->registerApiHandler("/api", apiDiscovery);
    }
    if (::arg().mustDo("webserver")) {
      d_ws->registerWebHandler("/style.css", [this](HttpRequest *req, HttpResponse *resp){cssfunction(req, resp);});
      d_ws->registerWebHandler("/", [this](HttpRequest *req, HttpResponse *resp){indexfunction(req, resp);});
      d_ws->registerWebHandler("/metrics", prometheusMetrics);
    }
    d_ws->go();
  }
  catch(...) {
    g_log<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
    _exit(1);
  }
}
