Antoin
anycast
api
+apidocfiles
apikey
APIv
AQAB
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
+ # Python and virtualenv are required for building the Authoritative server
+ - uses: actions/setup-python@v2
+ with:
+ python-version: '2.7'
+ - run: pip install virtualenv
+
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
This chapter describes the PowerDNS Authoritative API.
When creating an API wrapper (for instance when fronting multiple API's), it is recommended to stick to this API specification.
-The API is described in the `OpenAPI format <https://www.openapis.org/>`_, also known as "Swagger", and this description is `available <https://raw.githubusercontent.com/PowerDNS/pdns/master/docs/http-api/swagger/authoritative-api-swagger.yaml>`_.
+The API is described in the `OpenAPI format <https://www.openapis.org/>`_, also known as "Swagger", and this description is `available <https://raw.githubusercontent.com/PowerDNS/pdns/master/docs/http-api/swagger/authoritative-api-swagger.yaml>`_. It can also be obtained from a running server if the administrator of that server has enabled the API; it
+is available at the `/api/docs` endpoint in both YAML and JSON formats (the 'Accept' header can be used to indicate the
+desired format).
Authentication
~~~~~~~~~~~~~~
title: PowerDNS Authoritative HTTP API
license:
name: MIT
-host: localhost:8081
basePath: /api/v1
-schemes:
- - http
consumes:
- application/json
produces:
schema:
type: array
items:
- - $ref: '#/definitions/StatisticItem'
- - $ref: '#/definitions/MapStatisticItem'
- - $ref: '#/definitions/RingStatisticItem'
+ - $ref: '#/definitions/StatisticItem'
+ - $ref: '#/definitions/MapStatisticItem'
+ - $ref: '#/definitions/RingStatisticItem'
'422':
description: 'Returned when a non-existing statistic name has been requested. Contains an error message'
yahttp
EXTRA_DIST = \
- luawrapper/include/LuaContext.hpp
+ luawrapper/include/LuaContext.hpp \
+ incbin/incbin.h
- "sed -i 's/codedocs docs//' Makefile.am"
- "autoreconf -vi"
- "./configure --with-modules='bind gsqlite3 gmysql gpgsql ldap'"
+ - "pushd pdns"
+ - "touch .venv api-swagger.yaml api-swagger.json"
+ - "popd"
index:
build_command:
- "pushd pdns/dnsdistdist"
])
AM_CONDITIONAL([HAVE_VIRTUALENV], [test "x$VIRTUALENV" != "xno"])
AM_CONDITIONAL([HAVE_MANPAGES], [test -e "$srcdir/docs/pdns_server.1"])
+ AM_CONDITIONAL([HAVE_API_SWAGGER_JSON], [test -e "$srcdir/pdns/api-swagger.json"])
])
-
/pubsuffix.cc
/calidns
/dumresp
-/htmlfiles.h
+/apidocfiles.h
+/api-swagger.yaml
+/api-swagger.json
+/.venv
effective_tld_names.dat
/dnsmessage.pb.cc
/dnsmessage.pb.h
ixfrdist.example.yml \
lua-record.cc \
minicurl.cc \
- minicurl.hh
+ minicurl.hh \
+ api-swagger.yaml \
+ api-swagger.json \
+ requirements.txt \
+ incfiles
BUILT_SOURCES = \
bind-dnssec.schema.sqlite3.sql.h \
bindparser.h \
- dnslabeltext.cc
+ dnslabeltext.cc \
+ apidocfiles.h
CLEANFILES = \
*.gcda \
backends/gsql/gsqlbackend.gcno \
backends/gsql/gsqlbackend.gcov \
dnsmessage.pb.cc dnsmessage.pb.h \
- pdns.conf-dist
+ pdns.conf-dist \
+ apidocfiles.h \
+ api-swagger.yaml \
+ api-swagger.json
+
+# use a $(wildcard) wrapper here to allow build to proceed if output
+# file is present but input file is not (e.g. in a dist tarball)
+api-swagger.yaml: $(wildcard ../docs/http-api/swagger/authoritative-api-swagger.yaml)
+ cp $< $@
+
+if HAVE_VIRTUALENV
+.venv: requirements.txt
+ virtualenv .venv
+ .venv/bin/pip install -U pip setuptools setuptools-git
+ .venv/bin/pip install -r requirements.txt
+
+api-swagger.json: api-swagger.yaml .venv
+ .venv/bin/python -c "import sys, json, yaml; y = yaml.safe_load(sys.stdin.read()); json.dump(y, sys.stdout, indent=2, separators=(',', ': '))" < $< > $@
+else # if HAVE_VIRTUALENV
+if !HAVE_API_SWAGGER_JSON
+api-swagger.json:
+ echo "You need virtualenv to generate the JSON API document"
+ exit 1
+endif
+endif
+
+apidocfiles.h: api-swagger.yaml api-swagger.json
+ ./incfiles $^ > $@
noinst_SCRIPTS = pdns.init
sysconf_DATA = pdns.conf-dist
--- /dev/null
+#!/bin/sh
+
+export LC_ALL=C.UTF-8
+export LANG=C.UTF-8
+
+for a in $@
+do
+ c=$(echo $a | tr "/.-" "___")
+ echo "INCBIN(${c}, \"${a}\");"
+ echo "static const string g_${c}{(const char*)g${c}Data, g${c}Size};"
+done
return (0==strcmp(header->second.c_str(), expected_value.c_str()));
}
+void HttpResponse::setPlainBody(const string& document)
+{
+ this->headers["Content-Type"] = "text/plain; charset=utf-8";
+
+ this->body = document;
+}
-void HttpResponse::setBody(const json11::Json& document)
+void HttpResponse::setYamlBody(const string& document)
{
+ this->headers["Content-Type"] = "application/x-yaml";
+
+ this->body = document;
+}
+
+void HttpResponse::setJsonBody(const string& document)
+{
+ this->headers["Content-Type"] = "application/json";
+
+ this->body = document;
+}
+
+void HttpResponse::setJsonBody(const json11::Json& document)
+{
+ this->headers["Content-Type"] = "application/json";
+
document.dump(this->body);
}
void HttpResponse::setErrorResult(const std::string& message, const int status_)
{
- setBody(json11::Json::object { { "error", message } });
+ setJsonBody(json11::Json::object { { "error", message } });
this->status = status_;
}
void HttpResponse::setSuccessResult(const std::string& message, const int status_)
{
- setBody(json11::Json::object { { "result", message } });
+ setJsonBody(json11::Json::object { { "result", message } });
this->status = status_;
}
throw HttpUnauthorizedException("X-API-Key");
}
- resp->headers["Content-Type"] = "application/json";
-
// security headers
resp->headers["X-Content-Type-Options"] = "nosniff";
resp->headers["X-Frame-Options"] = "deny";
YaHTTP::strstr_map_t::iterator header;
if ((header = req.headers.find("accept")) != req.headers.end()) {
- // json wins over html
- if (header->second.find("application/json") != std::string::npos) {
+ // yaml wins over json, json wins over html
+ if (header->second.find("application/x-yaml") != std::string::npos) {
+ req.accept_yaml = true;
+ } else if (header->second.find("text/x-yaml") != std::string::npos) {
+ req.accept_yaml = true;
+ } else if (header->second.find("application/json") != std::string::npos) {
req.accept_json = true;
} else if (header->second.find("text/html") != std::string::npos) {
req.accept_html = true;
// TODO rm this logline?
g_log<<Logger::Debug<<req.logprefix<<"Error result for \"" << req.url.path << "\": " << resp.status << endl;
string what = YaHTTP::Utility::status2text(resp.status);
- if(req.accept_html) {
- resp.headers["Content-Type"] = "text/html; charset=utf-8";
- resp.body = "<!html><title>" + what + "</title><h1>" + what + "</h1>";
- } else if (req.accept_json) {
+ if (req.accept_json) {
resp.headers["Content-Type"] = "application/json";
if (resp.body.empty()) {
resp.setErrorResult(what, resp.status);
}
+ } else if (req.accept_html) {
+ resp.headers["Content-Type"] = "text/html; charset=utf-8";
+ resp.body = "<!html><title>" + what + "</title><h1>" + what + "</h1>";
} else {
resp.headers["Content-Type"] = "text/plain; charset=utf-8";
resp.body = what;
public:
HttpRequest(const string& logprefix_="") : YaHTTP::Request(), accept_json(false), accept_html(false), complete(false), logprefix(logprefix_) { };
+ bool accept_yaml;
bool accept_json;
bool accept_html;
bool complete;
HttpResponse() : YaHTTP::Response() { };
HttpResponse(const YaHTTP::Response &resp) : YaHTTP::Response(resp) { };
- void setBody(const json11::Json& document);
+ void setPlainBody(const string& document);
+ void setYamlBody(const string& document);
+ void setJsonBody(const string& document);
+ void setJsonBody(const json11::Json& document);
void setErrorResult(const std::string& message, const int status);
void setSuccessResult(const std::string& message, const int status = 200);
};
};
Json doc = Json::array { version1 };
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
void apiServer(HttpRequest* req, HttpResponse* resp) {
throw HttpMethodNotAllowedException();
Json doc = Json::array {getServerDetail()};
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
void apiServerDetail(HttpRequest* req, HttpResponse* resp) {
if(req->method != "GET")
throw HttpMethodNotAllowedException();
- resp->setBody(getServerDetail());
+ resp->setJsonBody(getServerDetail());
}
void apiServerConfig(HttpRequest* req, HttpResponse* resp) {
{ "value", value },
});
}
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
void apiServerStatistics(HttpRequest* req, HttpResponse* resp) {
{ "value", std::to_string(*stat) },
});
- resp->setBody(doc);
+ resp->setJsonBody(doc);
return;
}
}
#endif
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
DNSName apiNameToDNSName(const string& name) {
#include "auth-caches.hh"
#include "threadname.hh"
#include "tsigutils.hh"
+#include "ext/incbin/incbin.h"
using json11::Json;
doc["rrsets"] = rrsets;
}
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
void productServerStatisticsFetch(map<string,string>& out)
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"]);
document.push_back(key);
}
- resp->setBody(document);
+ resp->setJsonBody(document);
} else if (req->method == "POST") {
auto document = req->json();
string kind;
};
resp->status = 201;
- resp->setBody(key);
+ resp->setJsonBody(key);
} else
throw HttpMethodNotAllowedException();
}
entries.push_back(i);
document["metadata"] = entries;
- resp->setBody(document);
+ resp->setJsonBody(document);
} else if (req->method == "PUT") {
auto document = req->json();
{ "metadata", metadata }
};
- resp->setBody(key);
+ resp->setJsonBody(key);
} else if (req->method == "DELETE") {
if (!isValidMetadataKind(kind, false))
throw ApiException("Unsupported metadata kind '" + kind + "'");
if (inquireSingleKey) {
key["privatekey"] = value.first.getKey()->convertToISC();
- resp->setBody(key);
+ resp->setJsonBody(key);
return;
}
doc.push_back(key);
// we came here because we couldn't find the requested key.
throw HttpNotFoundException();
}
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
for(const auto &key : keys) {
doc.push_back(makeJSONTSIGKey(key, false));
}
- resp->setBody(doc);
+ resp->setJsonBody(doc);
} else if (req->method == "POST") {
auto document = req->json();
DNSName keyname(stringFromJson(document, "name"));
}
resp->status = 201;
- resp->setBody(makeJSONTSIGKey(keyname, algo, content));
+ resp->setJsonBody(makeJSONTSIGKey(keyname, algo, content));
} else {
throw HttpMethodNotAllowedException();
}
tsk.key = content;
if (req->method == "GET") {
- resp->setBody(makeJSONTSIGKey(tsk));
+ resp->setJsonBody(makeJSONTSIGKey(tsk));
} else if (req->method == "PUT") {
json11::Json document;
if (!req->body.empty()) {
throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
}
}
- resp->setBody(makeJSONTSIGKey(tsk));
+ resp->setJsonBody(makeJSONTSIGKey(tsk));
} else if (req->method == "DELETE") {
if (!B.deleteTSIGKey(keyname)) {
throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
for(const DomainInfo& di : domains) {
doc.push_back(getZoneInfo(di, with_dnssec ? &dk : nullptr));
}
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
}
if (req->accept_json) {
- resp->setBody(Json::object { { "zone", ss.str() } });
+ resp->setJsonBody(Json::object { { "zone", ss.str() } });
} else {
resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
resp->body = ss.str();
}
}
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
DNSName canon = apiNameToDNSName(req->getvars["domain"]);
uint64_t count = purgeAuthCachesExact(canon);
- resp->setBody(Json::object {
+ resp->setJsonBody(Json::object {
{ "count", (int) count },
{ "result", "Flushed cache." }
});
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/docs", &apiDocs);
d_ws->registerApiHandler("/api", &apiDiscovery);
}
if (::arg().mustDo("webserver")) {
Ewma d_qcachehits, d_qcachemisses;
WebServer *d_ws{nullptr};
};
+
+void apiDocs(HttpRequest* req, HttpResponse* resp);
vector<string> entries;
t_allowFrom->toStringVector(&entries);
- resp->setBody(Json::object {
+ resp->setJsonBody(Json::object {
{ "name", "allow-from" },
{ "value", entries },
});
{ "records", records }
};
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
static void doCreateZone(const Json document)
{ "recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward }
});
}
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
});
}
}
- resp->setBody(doc);
+ resp->setJsonBody(doc);
}
static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
int count = g_recCache->doWipeCache(canon, subtree, qtype);
count += broadcastAccFunction<uint64_t>([=]{return pleaseWipePacketCache(canon, subtree, qtype);});
count += g_negCache->wipe(canon, subtree);
- resp->setBody(Json::object {
+ resp->setJsonBody(Json::object {
{ "count", count },
{ "result", "Flushed cache." }
});
};
ret[name] = zoneInfo;
}
- resp->setBody(ret);
+ resp->setJsonBody(ret);
}
(int)(queries.size() - totIncluded), "", ""
});
}
- resp->setBody(Json::object { { "entries", entries } });
+ resp->setJsonBody(Json::object { { "entries", entries } });
return;
}
else if(command == "get-remote-ring") {
});
}
- resp->setBody(Json::object { { "entries", entries } });
+ resp->setJsonBody(Json::object { { "entries", entries } });
return;
} else {
resp->setErrorResult("Command '"+command+"' not found", 404);