Skip to content

Commit

Permalink
Merge pull request #4658 from rgacogne/dnsdist-set-acl
Browse files Browse the repository at this point in the history
dnsdist: Allow editing the ACL via the API
  • Loading branch information
rgacogne committed Nov 18, 2016
2 parents 523c71f + 56d68fa commit 7448bae
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 27 deletions.
1 change: 1 addition & 0 deletions pdns/README-dnsdist.md
Expand Up @@ -1224,6 +1224,7 @@ Here are all functions:
* quit or ^D: exit the console
* `webserver(address:port, password [, apiKey [, customHeaders ]])`: launch a webserver with stats on that address with that password
* `includeDirectory(dir)`: all files ending in `.conf` in the directory `dir` are loaded into the configuration
* `setAPIWritable(bool, [dir])`: allow modifications via the API. If `dir` is set, it must be a valid directory where the configuration files will be written by the API. Otherwise the modifications done via the API will not be written to the configuration and will not persist after a reload
* ACL related:
* `addACL(netmask)`: add to the ACL set who can use this server
* `setACL({netmask, netmask})`: replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us
Expand Down
1 change: 1 addition & 0 deletions pdns/dnsdist-console.cc
Expand Up @@ -324,6 +324,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
{ "QTypeRule", true, "qtype", "matches queries with the specified qtype" },
{ "RCodeRule", true, "rcode", "matches responses with the specified rcode" },
{ "setACL", true, "{netmask, netmask}", "replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us" },
{ "setAPIWritable", true, "bool, dir", "allow modifications via the API. if `dir` is set, it must be a valid directory where the configuration files will be written by the API" },
{ "setDNSSECPool", true, "pool name", "move queries requesting DNSSEC processing to this pool" },
{ "setECSOverride", true, "bool", "whether to override an existing EDNS Client Subnet value in the query" },
{ "setECSSourcePrefixV4", true, "prefix-length", "the EDNS Client Subnet prefix-length used for IPv4 queries" },
Expand Down
14 changes: 14 additions & 0 deletions pdns/dnsdist-lua2.cc
Expand Up @@ -1103,4 +1103,18 @@ void moreLua(bool client)

g_included = false;
});

g_lua.writeFunction("setAPIWritable", [](bool writable, boost::optional<std::string> apiConfigDir) {
setLuaSideEffect();
g_apiReadWrite = writable;
if (apiConfigDir) {
if (!(*apiConfigDir).empty()) {
g_apiConfigDirectory = *apiConfigDir;
}
else {
errlog("The API configuration directory value cannot be empty!");
g_outputBuffer="The API configuration directory value cannot be empty!";
}
}
});
}
168 changes: 143 additions & 25 deletions pdns/dnsdist-web.cc
Expand Up @@ -35,6 +35,49 @@
#include "base64.hh"
#include "gettime.hh"

bool g_apiReadWrite{false};
std::string g_apiConfigDirectory;

static bool apiWriteConfigFile(const string& filebasename, const string& content)
{
if (!g_apiReadWrite) {
errlog("Not writing content to %s since the API is read-only", filebasename);
return false;
}

if (g_apiConfigDirectory.empty()) {
vinfolog("Not writing content to %s since the API configuration directory is not set", filebasename);
return false;
}

string filename = g_apiConfigDirectory + "/" + filebasename + ".conf";
ofstream ofconf(filename.c_str());
if (!ofconf) {
errlog("Could not open configuration fragment file '%s' for writing: %s", filename, stringerror());
return false;
}
ofconf << "-- Generated by the REST API, DO NOT EDIT" << endl;
ofconf << content << endl;
ofconf.close();
return true;
}

static void apiSaveACL(const NetmaskGroup& nmg)
{
vector<string> vec;
g_ACL.getCopy().toStringVector(&vec);

string acl;
for(const auto& s : vec) {
if (!acl.empty()) {
acl += ", ";
}
acl += "\"" + s + "\"";
}

string content = "setACL({" + acl + "})";
apiWriteConfigFile("acl", content);
}

static bool compareAuthorization(YaHTTP::Request& req, const string &expected_password, const string& expectedApiKey)
{
Expand All @@ -59,6 +102,7 @@ static bool compareAuthorization(YaHTTP::Request& req, const string &expected_pa
if (req.url.path=="/jsonstat" ||
req.url.path=="/api/v1/servers/localhost" ||
req.url.path=="/api/v1/servers/localhost/config" ||
req.url.path=="/api/v1/servers/localhost/config/allow-from" ||
req.url.path=="/api/v1/servers/localhost/statistics") {
header = req.headers.find("x-api-key");
if (header != req.headers.end()) {
Expand All @@ -69,13 +113,31 @@ static bool compareAuthorization(YaHTTP::Request& req, const string &expected_pa
return auth_ok;
}

static bool isMethodAllowed(const YaHTTP::Request& req)
{
if (req.method == "GET") {
return true;
}
if (req.method == "PUT" && g_apiReadWrite) {
if (req.url.path == "/api/v1/servers/localhost/config/allow-from") {
return true;
}
}
return false;
}

static void handleCORS(YaHTTP::Request& req, YaHTTP::Response& resp)
{
YaHTTP::strstr_map_t::iterator origin = req.headers.find("Origin");
if (origin != req.headers.end()) {
if (req.method == "OPTIONS") {
/* Pre-flight request */
resp.headers["Access-Control-Allow-Methods"] = "GET";
if (g_apiReadWrite) {
resp.headers["Access-Control-Allow-Methods"] = "GET, PUT";
}
else {
resp.headers["Access-Control-Allow-Methods"] = "GET";
}
resp.headers["Access-Control-Allow-Headers"] = "Authorization, X-API-Key";
}

Expand Down Expand Up @@ -121,22 +183,26 @@ static void connectionThread(int sock, ComboAddress remote, string password, str
{
using namespace json11;
vinfolog("Webserver handling connection from %s", remote.toStringWithPort());
FILE* fp=0;
fp=fdopen(sock, "r");

try {
string line;
string request;
while(stringfgets(fp, line)) {
request+=line;
trim(line);

if(line.empty())
break;
}

std::istringstream ifs(request);
YaHTTP::AsyncRequestLoader yarl;
YaHTTP::Request req;
ifs >> req;
bool finished = false;

yarl.initialize(&req);
while(!finished) {
int bytes;
char buf[1024];
bytes = read(sock, buf, sizeof(buf));
if (bytes > 0) {
string data = string(buf, bytes);
finished = yarl.feed(data);
} else {
// read error OR EOF
break;
}
}
yarl.finalize();

string command=req.getvars["command"];

Expand Down Expand Up @@ -166,7 +232,7 @@ static void connectionThread(int sock, ComboAddress remote, string password, str
resp.headers["WWW-Authenticate"] = "basic realm=\"PowerDNS\"";

}
else if(req.method != "GET") {
else if(!isMethodAllowed(req)) {
resp.status=405;
}
else if(req.url.path=="/jsonstat") {
Expand Down Expand Up @@ -336,7 +402,6 @@ static void connectionThread(int sock, ComboAddress remote, string password, str
string acl;

vector<string> vec;

g_ACL.getCopy().toStringVector(&vec);

for(const auto& s : vec) {
Expand Down Expand Up @@ -443,6 +508,62 @@ static void connectionThread(int sock, ComboAddress remote, string password, str
resp.body=my_json.dump();
resp.headers["Content-Type"] = "application/json";
}
else if(req.url.path=="/api/v1/servers/localhost/config/allow-from") {
handleCORS(req, resp);

resp.headers["Content-Type"] = "application/json";
resp.status=200;

if (req.method == "PUT") {
std::string err;
Json doc = Json::parse(req.body, err);

if (!doc.is_null()) {
NetmaskGroup nmg;
auto aclList = doc["value"];
if (aclList.is_array()) {

for (auto value : aclList.array_items()) {
try {
nmg.addMask(value.string_value());
} catch (NetmaskException &e) {
resp.status = 400;
break;
}
}

if (resp.status == 200) {
infolog("Updating the ACL via the API to %s", nmg.toString());
g_ACL.setState(nmg);
apiSaveACL(nmg);
}
}
else {
resp.status = 400;
}
}
else {
resp.status = 400;
}
}
if (resp.status == 200) {
Json::array acl;
vector<string> vec;
g_ACL.getCopy().toStringVector(&vec);

for(const auto& s : vec) {
acl.push_back(s);
}

Json::object obj{
{ "type", "ConfigSetting" },
{ "name", "allow-from" },
{ "value", acl }
};
Json my_json = obj;
resp.body=my_json.dump();
}
}
else if(!req.url.path.empty() && g_urlmap.count(req.url.path.c_str()+1)) {
resp.body.assign(g_urlmap[req.url.path.c_str()+1]);
vector<string> parts;
Expand Down Expand Up @@ -473,23 +594,20 @@ static void connectionThread(int sock, ComboAddress remote, string password, str
done=ofs.str();
writen2(sock, done.c_str(), done.size());

fclose(fp);
fp=0;
close(sock);
sock = -1;
}
catch(const YaHTTP::ParseError& e) {
vinfolog("Webserver thread died with parse error exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
if(fp)
fclose(fp);
close(sock);
}
catch(const std::exception& e) {
errlog("Webserver thread died with exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
if(fp)
fclose(fp);
close(sock);
}
catch(...) {
errlog("Webserver thread died with exception while processing a request from %s", remote.toStringWithPort());
if(fp)
fclose(fp);
close(sock);
}
}
void dnsdistWebserverThread(int sock, const ComboAddress& local, const std::string& password, const std::string& apiKey, const boost::optional<std::map<std::string, std::string> >& customHeaders)
Expand Down
2 changes: 2 additions & 0 deletions pdns/dnsdist.hh
Expand Up @@ -689,6 +689,8 @@ extern uint64_t g_maxTCPQueuedConnections;
extern std::atomic<uint16_t> g_cacheCleaningDelay;
extern bool g_verboseHealthChecks;
extern uint32_t g_staleCacheEntriesTTL;
extern bool g_apiReadWrite;
extern std::string g_apiConfigDirectory;

struct ConsoleKeyword {
std::string name;
Expand Down

0 comments on commit 7448bae

Please sign in to comment.