]> granicus.if.org Git - pdns/commitdiff
add remotebackend, by Aki Tuomi
authorPeter van Dijk <peter.van.dijk@netherlabs.nl>
Fri, 5 Oct 2012 08:04:59 +0000 (08:04 +0000)
committerPeter van Dijk <peter.van.dijk@netherlabs.nl>
Fri, 5 Oct 2012 08:04:59 +0000 (08:04 +0000)
git-svn-id: svn://svn.powerdns.com/pdns/trunk/pdns@2755 d19b8d6e-7fed-0310-83ef-9ca221ded41b

21 files changed:
configure.ac
modules/Makefile.am
modules/remotebackend/Makefile.am [new file with mode: 0644]
modules/remotebackend/OBJECTFILES [new file with mode: 0644]
modules/remotebackend/OBJECTLIBS [new file with mode: 0644]
modules/remotebackend/README [new file with mode: 0644]
modules/remotebackend/TODO [new file with mode: 0644]
modules/remotebackend/httpconnector.cc [new file with mode: 0644]
modules/remotebackend/pipeconnector.cc [new file with mode: 0644]
modules/remotebackend/regression-tests/Gemfile [new file with mode: 0644]
modules/remotebackend/regression-tests/Gemfile.lock [new file with mode: 0644]
modules/remotebackend/regression-tests/backend.rb [new file with mode: 0755]
modules/remotebackend/regression-tests/dnsbackend.rb [new file with mode: 0644]
modules/remotebackend/regression-tests/http-backend.rb [new file with mode: 0755]
modules/remotebackend/regression-tests/pipe-backend.rb [new file with mode: 0755]
modules/remotebackend/regression-tests/test-schema.sql [new file with mode: 0644]
modules/remotebackend/regression-tests/unix-backend.rb [new symlink]
modules/remotebackend/remotebackend.cc [new file with mode: 0644]
modules/remotebackend/remotebackend.hh [new file with mode: 0644]
modules/remotebackend/test_backend.rb [new file with mode: 0755]
modules/remotebackend/unixconnector.cc [new file with mode: 0644]

index c082c23024915584995ea195cfdd09dfa3d6a96e..2e48cdee8acbe57accc4095b79e7db0f946611b7 100644 (file)
@@ -666,4 +666,5 @@ modules/mongodbbackend/Makefile \
 modules/gpgsqlbackend/Makefile modules/ldapbackend/Makefile \
 modules/gsqlitebackend/Makefile modules/gsqlite3backend/Makefile \
 modules/goraclebackend/Makefile modules/mydnsbackend/Makefile \
-modules/luabackend/Makefile modules/tinydnsbackend/Makefile)
+modules/luabackend/Makefile modules/tinydnsbackend/Makefile \
+modules/remotebackend/Makefile)
index af5bccbd854899fc55e55fb5e8525ce1c725a21a..5aebbe1d904f6bc4fb3ff639207f2293af95dc3d 100644 (file)
@@ -1,2 +1,2 @@
 SUBDIRS=@moduledirs@
-DIST_SUBDIRS=db2backend geobackend gmysqlbackend godbcbackend goraclebackend gpgsqlbackend gsqlite3backend gsqlitebackend ldapbackend luabackend mongodbbackend mydnsbackend opendbxbackend oraclebackend pipebackend xdbbackend tinydnsbackend
+DIST_SUBDIRS=db2backend geobackend gmysqlbackend godbcbackend goraclebackend gpgsqlbackend gsqlite3backend gsqlitebackend ldapbackend luabackend mongodbbackend mydnsbackend opendbxbackend oraclebackend pipebackend xdbbackend tinydnsbackend remotebackend
diff --git a/modules/remotebackend/Makefile.am b/modules/remotebackend/Makefile.am
new file mode 100644 (file)
index 0000000..72d96c0
--- /dev/null
@@ -0,0 +1,13 @@
+AM_CPPFLAGS=@THREADFLAGS@ $(BOOST_CPPFLAGS)
+#if !ALLSTATIC
+#install-exec-local:
+#       install .lib/libremotebackend.so.0.0.0 @libdir@
+#endif
+
+EXTRA_DIST=OBJECTFILES OBJECTLIBS
+lib_LTLIBRARIES = libremotebackend.la
+
+libremotebackend_la_SOURCES=remotebackend.hh remotebackend.cc unixconnector.cc httpconnector.cc pipeconnector.cc 
+
+libremotebackend_la_LDFLAGS=-module -avoid-version
+libremotebackend_la_LIBS=-lboost_system -ljsoncpp -lcurl
diff --git a/modules/remotebackend/OBJECTFILES b/modules/remotebackend/OBJECTFILES
new file mode 100644 (file)
index 0000000..d46943f
--- /dev/null
@@ -0,0 +1 @@
+remotebackend.o unixconnector.o httpconnector.o pipeconnector.o
diff --git a/modules/remotebackend/OBJECTLIBS b/modules/remotebackend/OBJECTLIBS
new file mode 100644 (file)
index 0000000..3b2a6a0
--- /dev/null
@@ -0,0 +1 @@
+-lboost_system -ljsoncpp -lcurl
diff --git a/modules/remotebackend/README b/modules/remotebackend/README
new file mode 100644 (file)
index 0000000..e310344
--- /dev/null
@@ -0,0 +1,181 @@
+Remote backend for PowerDNS.
+
+This backend provides unix socket / pipe / http remoting for powerdns.
+
+NB! THIS BACKEND IS EXPERIMENTAL - DO NOT USE IN PRODUCTION ENVIRONMENT
+
+This backend is provided under the same licensing terms as PowerDNS itself. 
+
+1. Compiling
+
+Install following libraries for dependencies: libjsoncpp, libcurl 
+
+To compile this backend, you need to configure --with-modules="remote pipe". 
+
+Also, you need to apply the patch 0001-Added-remotebackend-for-compile.patch
+to enable compiling the backend. Then you need to run ./bootstrap and
+configure.
+
+2. Usage
+
+The only configuration option for this backend is remote-connection-string.
+
+It comprises of two elements: type of backend, and parameters
+
+remote-connection-string=<type>:<param>=<value>,<param>=<value>...
+
+You can pass as many parameters as you want, for unix and pipe backends, these
+are passed along to the remote end as initialization. See API.
+
+2.1. Unix backend
+
+parameters: path 
+
+remote-connection-string=unix:path=/path/to/socket
+
+2.2. Pipe backend
+
+parameters: command
+
+remote-connection-string=unix:command=/path/to/executable
+
+2.3. HTTP backend
+
+parameters: url, url-suffix
+
+HTTP backend tries to do RESTful requests to your server. See examples.
+
+URL should not end with /, and url-suffix is optional, but if you define it, it's
+up to you to write the ".php" or ".json". Lack of dot causes lack of dot in
+URL. 
+
+3. API
+
+3.1. Queries
+
+Unix and Pipe backend sends JSON formatted string to the remote end. Each 
+JSON query has two sections, 'method' and 'parameters'. 
+
+HTTP backend calls methods based on URL and has parameters in the query string.
+The calls are always GET calls.
+
+3.2. Replies
+
+You *must* always reply with JSON hash with at least one key, 'result'. This 
+must be false if the query failed. Otherwise it must conform to the expected
+result.
+
+You can optionally add 'log' array, each line in this array will be logged in
+PowerDNS.
+
+3.3. Methods
+
+The following methods are used:
+
+Method: lookup
+Parameters: qtype, domain, remote, local, real-remote, zone_id
+Reply: array of <qtype,qname,content,ttl,domaind_id,priority,scopeMask>
+Optional values: domain_id and scopeMask
+
+Method: list
+Parameters: zonename, domain_id
+Reply: array of <qtype,qname,content,ttl,domaind_id,priority,scopeMask>
+Optional values: domain_id and scopeMask
+
+Method: getBeforeAndAfterNamesAbsolute
+Parameters: id, qname
+Reply: unhashed, before, after
+
+Method: getBeforeAndAfterNames
+Parameters: id, zonename, qname
+Reply: before, after
+
+Method: getDomainMetadata
+Parameters: name, kind
+Reply: array of strings
+
+Method: getDomainKeys
+Parameters: name, kind
+Reply: array of domain keys <id,flags,active,content>
+
+Method: getTSIGKey
+Parameters: name
+Reply: algorithm, content
+
+Method: setDomainMetadata
+Parameters: name, kind, value
+Reply: true or false
+
+Method: addDomainKey
+Parameters: flags, active, content
+Reply: id-of-key
+
+Method: remoteDomainKey
+Parameters: name, id
+Reply: true or false
+
+Method: activateDomainKey
+Parameters: name, id
+Reply: true or false
+
+Method: deactivateDomainKey
+Parameters: name, id
+Reply: true or false
+
+4. EXAMPLES
+
+Scenario: SOA lookup via pipe or unix
+
+Query: 
+
+{ 
+  "method": "lookup",
+  "parameters": {
+     "qname": "example.com", 
+     "qtype": "SOA",
+     "zone_id": "-1"
+  }
+}
+
+Reply:
+
+{
+  "result": 
+   [ 
+     { "qtype": "SOA",
+       "qname": "example.com", 
+       "content": "dns1.icann.org. hostmaster.icann.org. 2012080849 7200 3600 1209600 3600",
+       "ttl": 3600,
+       "priority": 0,
+       "domain_id": -1
+     }
+   ]
+}
+
+
+Scenario: SOA lookup via HTTP backend
+
+Query:
+
+/dns/lookup/example.com/SOA
+
+Reply:
+
+{
+  "result":
+   [
+     { "qtype": "SOA",
+       "qname": "example.com",
+       "content": "dns1.icann.org. hostmaster.icann.org. 2012080849 7200 3600 1209600 3600",
+       "ttl": 3600,
+       "priority": 0,
+       "domain_id": -1
+     }
+   ]
+}
+
+5. TODO
+
+ - Improve error handling and reply validation
+ - Code coverage
+
diff --git a/modules/remotebackend/TODO b/modules/remotebackend/TODO
new file mode 100644 (file)
index 0000000..02a1c74
--- /dev/null
@@ -0,0 +1,3 @@
+- detect jsoncpp and libcurl in configure
+- move coprocess from pipebackend to core so remotebackend can run without pipebackend
+- make ruby scripts compatible with 1.8
diff --git a/modules/remotebackend/httpconnector.cc b/modules/remotebackend/httpconnector.cc
new file mode 100644 (file)
index 0000000..2433efb
--- /dev/null
@@ -0,0 +1,206 @@
+#include "remotebackend.hh"
+#include <sys/socket.h>
+#include <unistd.h>
+#include <sys/select.h>
+#include <fcntl.h>
+#include <curl/curl.h>
+#include <boost/foreach.hpp>
+#include <sstream>
+#ifndef UNIX_PATH_MAX 
+#define UNIX_PATH_MAX 108
+#endif
+
+HTTPConnector::HTTPConnector(std::map<std::string,std::string> options) {
+    this->d_url = options.find("url")->second;
+    if (options.find("url-suffix") != options.end()) {
+      this->d_url_suffix = options.find("url-suffix")->second;
+    } else {
+      this->d_url_suffix = "";
+    }
+}
+
+HTTPConnector::~HTTPConnector() {
+    this->d_c = NULL;
+}
+
+// friend method for writing data into our buffer
+size_t httpconnector_write_data(void *buffer, size_t size, size_t nmemb, void *userp) {
+    HTTPConnector *tc = reinterpret_cast<HTTPConnector*>(userp);
+    std::string tmp(reinterpret_cast<char *>(buffer), size*nmemb);
+    tc->d_data += tmp;
+    return nmemb;
+}
+
+// converts json value into string
+void HTTPConnector::json2string(const Json::Value &input, std::string &output) {
+   if (input.isString()) output = input.asString();
+   else if (input.isNull()) output = "";
+   else if (input.isUInt()) output = lexical_cast<std::string>(input.asUInt());
+   else if (input.isInt()) output = lexical_cast<std::string>(input.asInt());
+   else output = "inconvertible value";
+}
+
+// builds our request
+void HTTPConnector::requestbuilder(const std::string &method, const Json::Value &parameters, struct curl_slist **slist)
+{
+    std::stringstream ss;
+    Json::Value param;
+    std::string sparam;
+    char *tmpstr;
+
+    // check for certain elements
+    std::vector<std::string> members = parameters.getMemberNames();
+
+    // special names are qname, name, zonename, kind, others go to headers
+
+    ss << d_url;
+
+    ss << "/" << method;
+
+    // add the url components, if found, in following order 
+
+    if ((param = parameters.get("zonename", Json::Value())).isNull() == false) {
+       json2string(param, sparam);
+       ss << "/" << sparam;
+    }
+
+    if ((param = parameters.get("qname", Json::Value())).isNull() == false) {
+       json2string(param, sparam);
+       ss << "/" << sparam;
+    }
+    
+    if ((param = parameters.get("name", Json::Value())).isNull() == false) {
+       json2string(param, sparam);
+       ss << "/" << sparam;
+    }
+
+    if ((param = parameters.get("kind", Json::Value())).isNull() == false) {
+       json2string(param, sparam);
+       ss << "/" << sparam;
+    }
+    if ((param = parameters.get("qtype", Json::Value())).isNull() == false) {
+       json2string(param, sparam);
+       ss << "/" << sparam;
+    }
+
+    if ((param = parameters.get("id", Json::Value())).isNull() == false) {
+       json2string(param, sparam);
+       ss << "/" << sparam;
+    }
+
+    // finally add suffix
+    ss << d_url_suffix;
+    curl_easy_setopt(d_c, CURLOPT_URL, ss.str().c_str());
+
+    (*slist) = NULL;
+    // set the correct type of request based on method
+    if (method == "activateDomainKey" || method == "deactivateDomainKey") { 
+        // create an empty post
+        curl_easy_setopt(d_c, CURLOPT_POST, 1);
+        curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, 0);
+    } else if (method == "addDomainKey") {
+        // create post with keydata
+        std::stringstream ss2;
+        param = parameters["key"]; 
+        ss2 << "flags" << param["flags"].asUInt() << "&active" << (param["active"].asBool() ? 1 : 0) << "&content=";
+        tmpstr = curl_easy_escape(d_c, param["content"].asCString(), 0);
+        ss2 << tmpstr;
+        sparam = ss2.str();
+        curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, sparam.size());
+        curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, sparam.c_str());
+        curl_free(tmpstr);
+    } else if (method == "setDomainMetadata") {
+        // copy all metadata values into post
+        std::stringstream ss2;
+        param = parameters["value"];
+        curl_easy_setopt(d_c, CURLOPT_POST, 1);
+        // this one has values too
+        if (param.isArray()) {
+           for(Json::ValueIterator i = param.begin(); i != param.end(); i++) {
+              ss2 << "value[]=" << (*i).asString() << "&";
+           }
+        }
+        sparam = ss2.str();
+        curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, sparam.size());
+        curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, sparam.c_str());
+    } else if (method == "removeDomainKey") {
+        // this one is delete
+        curl_easy_setopt(d_c, CURLOPT_CUSTOMREQUEST, "DELETE");
+    } else {
+        // perform normal get
+        curl_easy_setopt(d_c, CURLOPT_HTTPGET, 1);
+    }
+
+    // put everything else into headers
+    BOOST_FOREACH(std::string member, members) {
+      char header[1024];
+      // these are not put into headers for obvious reasons
+      if (member == "zonename" || member == "qname" ||
+          member == "name" || member == "kind" ||
+          member == "qtype" || member == "id" ||
+          member == "key" ) continue;
+      json2string(parameters[member], sparam);
+      snprintf(header, sizeof header, "X-RemoteBackend-%s: %s", member.c_str(), sparam.c_str());
+      (*slist) = curl_slist_append((*slist), header);
+    };
+
+    // store headers into request
+    curl_easy_setopt(d_c, CURLOPT_HTTPHEADER, *slist); 
+}
+
+int HTTPConnector::send_message(const Json::Value &input) {
+    int rv;
+    long rcode;
+    struct curl_slist *slist;
+
+    std::vector<std::string> members;
+    std::string method;
+
+    // initialize curl
+    d_c = curl_easy_init();
+    d_data = "";
+    curl_easy_setopt(d_c, CURLOPT_NOSIGNAL, 1);
+    curl_easy_setopt(d_c, CURLOPT_TIMEOUT, 2);
+    slist = NULL;
+
+    // build request
+    requestbuilder(input["method"].asString(), input["parameters"], &slist);
+
+    // setup write function helper
+    curl_easy_setopt(d_c, CURLOPT_WRITEFUNCTION, &(httpconnector_write_data));
+    curl_easy_setopt(d_c, CURLOPT_WRITEDATA, this);
+
+    // then we actually do it
+    if (curl_easy_perform(d_c) != CURLE_OK) {
+      // boo, it failed
+      rv = -1;
+    } else {
+      // ensure the result was OK
+      if (curl_easy_getinfo(d_c, CURLINFO_RESPONSE_CODE, &rcode) != CURLE_OK || rcode < 200 || rcode > 299) {
+         rv = -1;
+      } else {
+         // ok. if d_data == 0 but rcode is 2xx then result:true
+         if (this->d_data.size() == 0) 
+            this->d_data = "{\"result\": true}";
+         rv = this->d_data.size();
+      }
+    }
+
+    // clean up resources
+    curl_slist_free_all(slist);
+    curl_easy_cleanup(d_c);
+
+    return rv;
+}
+
+int HTTPConnector::recv_message(Json::Value &output) {
+    Json::Reader r;
+    int rv = -1;
+
+    // offer whatever we read in send_message
+    if (r.parse(d_data, output) == true)
+       rv = d_data.size();
+
+    d_data = ""; // cleanup here
+    return rv;
+}
diff --git a/modules/remotebackend/pipeconnector.cc b/modules/remotebackend/pipeconnector.cc
new file mode 100644 (file)
index 0000000..13a97c5
--- /dev/null
@@ -0,0 +1,71 @@
+#include "remotebackend.hh"
+
+PipeConnector::PipeConnector(std::map<std::string,std::string> options) {
+  this->command = options.find("command")->second;
+  this->options = options;
+  this->coproc = NULL;
+  launch();
+}
+
+PipeConnector::~PipeConnector(){
+  if (this->coproc != NULL) 
+    delete coproc; 
+}
+
+void PipeConnector::launch() {
+  if (coproc != NULL) return;
+
+  Json::Value init,res;
+  coproc = new CoProcess(this->command, 2);
+  init["method"] = "initialize";
+  init["parameters"] = Json::Value();
+
+  for(std::map<std::string,std::string>::iterator i = options.begin(); i != options.end(); i++)
+    init["parameters"][i->first] = i->second;
+
+  this->send(init);
+  if (this->recv(res)==false) {
+    L<<Logger::Error<<"Failed to initialize coprocess"<<std::endl;
+  }
+}
+
+int PipeConnector::send_message(const Json::Value &input)
+{
+   std::string data;
+   Json::FastWriter writer;
+   data = writer.write(input);
+
+   launch();
+   try {
+      coproc->send(data);
+      return 1;
+   }
+   catch(AhuException &ae) {
+      delete coproc;
+      coproc=NULL;
+      throw;
+   } 
+}
+
+int PipeConnector::recv_message(Json::Value &output) 
+{
+   Json::Reader r;
+   std::string tmp;
+   std::string data;
+   launch();
+   try {
+      std::string line;
+      while(1) {
+        coproc->receive(tmp);
+        data.append(tmp);
+        if (r.parse(data,output) == true) 
+          return data.size();
+      }
+   }
+   catch(AhuException &ae) {
+      L<<Logger::Warning<<"[pipeconnector] "<<" unable to receive data from coprocess. "<<ae.reason<<endl;
+      delete coproc;
+      coproc = NULL;
+      throw;
+   }
+}
diff --git a/modules/remotebackend/regression-tests/Gemfile b/modules/remotebackend/regression-tests/Gemfile
new file mode 100644 (file)
index 0000000..5d4c4ab
--- /dev/null
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+gem 'webrick'
+gem 'sqlite3'
diff --git a/modules/remotebackend/regression-tests/Gemfile.lock b/modules/remotebackend/regression-tests/Gemfile.lock
new file mode 100644 (file)
index 0000000..6c21f3d
--- /dev/null
@@ -0,0 +1,12 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    sqlite3 (1.3.6)
+    webrick (1.3.1)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  sqlite3
+  webrick
diff --git a/modules/remotebackend/regression-tests/backend.rb b/modules/remotebackend/regression-tests/backend.rb
new file mode 100755 (executable)
index 0000000..c7c882c
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/ruby1.9.1
+
+require 'json'
+require 'sqlite3'
+
+def rr(qname, qtype, content, ttl, priority = 0, auth = 1, domain_id = -1)
+   {:qname => qname, :qtype => qtype, :content => content, :ttl => ttl.to_i, :priority => priority.to_i, :auth => auth.to_i, :domain_id => domain_id.to_i}
+end
+
+class Handler
+   attr :db
+   def initialize(dbpath)
+     @db = SQLite3::Database.new dbpath
+   end
+
+   def do_initialize(*args)
+     return true, "Test bench initialized"
+   end
+
+   def do_getbeforeandafternamesabsolute(args)
+        before = @db.get_first_value("SELECT ordername FROM records WHERE ordername < ? AND domain_id = ?", args["qname"], args["id"])
+        after = @db.get_first_value("SELECT ordername FROM records WHERE ordername > ? AND domain_id = ?", args["qname"], args["id"])
+        return [{:before => before, :after => after, :unhashed => args["qname"]}, nil]
+   end
+
+   def do_getbeforeandafternames(args)
+        before = @db.get_first_value("SELECT ordername FROM records WHERE ordername < ? AND domain_id = ?", args["qname"], args["id"])
+        after = @db.get_first_value("SELECT ordername FROM records WHERE ordername > ? AND domain_id = ?", args["qname"], args["id"])
+        return [{:before => before, :after => after, :unhashed => args["qname"]}, nil]
+   end
+
+   def do_getdomainkeys(args)
+       ret = []
+       @db.execute("SELECT flags,active,content FROM domains JOIN cryptokeys ON domains.id = cryptokeys.domain_id WHERE domains.name = ?", args["name"]) do |row|
+          ret << {:flags => row[0].to_i, :active => !(row[1].to_i.zero?), :content => row[2]}
+       end 
+
+       return false if ret.empty?
+       return [ret,nil]
+   end
+
+   def do_lookup(args)
+     ret = []
+     loop do
+        begin
+          sargs = {}
+          if (args["qtype"] == "ANY")
+             sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname"
+             sargs["qname"] = args["qname"]
+          else
+             sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname AND type = :qtype"
+             sargs["qname"] = args["qname"]
+             sargs["qtype"] = args["qtype"]
+          end
+          db.execute(sql, sargs) do |row|
+            ret << rr(row[1], row[2], row[3], row[4], row[5], row[6], row[0])
+          end
+        rescue Exception => e
+          e.backtrace
+        end
+        break
+     end
+     return false unless ret.size > 0
+     return [ret,nil]
+   end
+   def do_list(args)
+     target = args["target"]
+     ret = []
+     loop do
+        begin
+          d_id = db.get_first_value("SELECT id FROM domains WHERE name = ?", target)
+          return false if d_id.nil?
+          db.execute("SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE domain_id = ?", d_id) do |row|
+            ret << rr(row[1], row[2], row[3], row[4], row[5], row[6], row[0])
+          end
+        rescue Exception => e
+          e.backtrace
+        end
+        break
+     end
+     return false unless ret.size > 0
+     return [ret,nil]
+   end
+
+   def do_getdomainmetadata(args) 
+       return false
+   end
+
+   def do_setdomainmetadata(args)
+       return false
+   end
+end
diff --git a/modules/remotebackend/regression-tests/dnsbackend.rb b/modules/remotebackend/regression-tests/dnsbackend.rb
new file mode 100644 (file)
index 0000000..427446f
--- /dev/null
@@ -0,0 +1,55 @@
+require 'json'
+
+class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet
+   def initialize(server, dnsbackend)
+     @dnsbackend = dnsbackend
+   end
+
+   def do_GET(req,res)
+     tmp = req.path[/dns\/(.*)/,1]
+     return 400, "Bad request" if (tmp.nil?)
+     tmp = tmp.split("/")
+     method = "do_#{tmp.shift}".downcase
+     args = {}
+
+     if tmp.size > 0 
+       args["qname"] = tmp[0]
+       args["name"] = tmp[0]
+       args["target"] = tmp.shift
+     end
+     if tmp.size > 0
+       args["kind"] = tmp[0]
+       args["qtype"] = tmp[0]
+       args["id"] = tmp.shift
+     end
+
+     # get more arguments
+     req.each do |k,v|
+        attr = k[/X-RemoteBackend-(.*)/,1]
+        if attr 
+          args[attr] = v
+        end
+     end
+
+     if @dnsbackend.respond_to?(method.to_sym)
+       result, log = @dnsbackend.send(method.to_sym, args)
+        body = {:result => result, :log => log}
+        res.status = 200
+        res["Content-Type"] = "application/javascript; charset=utf-8"
+        res.body = body.to_json
+     else
+        res.status = 404
+        res["Content-Type"] = "application/javascript; charset=utf-8"
+        res.body = ({:result => false, :log => ["Method not found"]}).to_json
+     end
+   end
+
+   def do_DELETE(req,res)
+   end
+   
+   def do_POST(req,res)
+     req.continue
+
+     # get method name and args
+   end 
+end
diff --git a/modules/remotebackend/regression-tests/http-backend.rb b/modules/remotebackend/regression-tests/http-backend.rb
new file mode 100755 (executable)
index 0000000..a0e0529
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/ruby1.9.1
+require "rubygems"
+#require "bundler/setup"
+require "webrick"
+require "../modules/remotebackend/regression-tests/dnsbackend"
+require "../modules/remotebackend/regression-tests/backend"
+
+server = WEBrick::HTTPServer.new :Port => 62434
+
+be = Handler.new("../modules/remotebackend/regression-tests/remote.sqlite3") 
+
+server.mount "/dns", DNSBackendHandler, be
+trap('INT') { server.stop }
+server.start
diff --git a/modules/remotebackend/regression-tests/pipe-backend.rb b/modules/remotebackend/regression-tests/pipe-backend.rb
new file mode 100755 (executable)
index 0000000..0e4125f
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/ruby1.9.1
+
+require 'json'
+require '../modules/remotebackend/regression-tests/backend'
+
+h = Handler.new("../modules/remotebackend/regression-tests/remote.sqlite3")
+
+STDOUT.sync = true
+begin 
+  STDIN.each_line do |line|
+    # expect json
+    input = {}
+    line = line.strip
+    next if line.empty?
+    begin
+      input = JSON.parse(line)
+      method = "do_#{input["method"].downcase}"
+      args = input["parameters"]
+
+      if h.respond_to?(method.to_sym) == false
+         res = false
+      elsif args.size > 0
+         res, log = h.send(method,args)
+      else
+         res, log = h.send(method)
+      end
+      puts ({:result => res, :log => log}).to_json
+    rescue JSON::ParserError
+      send_failure "Cannot parse input #{line}"
+      next
+    end
+  end
+rescue SystemExit, Interrupt
+end
diff --git a/modules/remotebackend/regression-tests/test-schema.sql b/modules/remotebackend/regression-tests/test-schema.sql
new file mode 100644 (file)
index 0000000..ba64010
--- /dev/null
@@ -0,0 +1,74 @@
+begin transaction;
+
+create table domains (
+  id                INTEGER PRIMARY KEY,
+  name              VARCHAR(255) NOT NULL COLLATE NOCASE,
+  master            VARCHAR(128) DEFAULT NULL,
+  last_check        INTEGER DEFAULT NULL,
+  type              VARCHAR(6) NOT NULL,
+  notified_serial   INTEGER DEFAULT NULL, 
+  account           VARCHAR(40) DEFAULT NULL
+);
+
+CREATE UNIQUE INDEX name_index ON domains(name);
+
+CREATE TABLE records (
+  id              INTEGER PRIMARY KEY,
+  domain_id       INTEGER DEFAULT NULL,
+  name            VARCHAR(255) DEFAULT NULL, 
+  type            VARCHAR(10) DEFAULT NULL,
+  content         VARCHAR(65535) DEFAULT NULL,
+  ttl             INTEGER DEFAULT NULL,
+  prio            INTEGER DEFAULT NULL,
+  change_date     INTEGER DEFAULT NULL
+);
+              
+CREATE INDEX rec_name_index ON records(name);
+CREATE INDEX nametype_index ON records(name,type);
+CREATE INDEX domain_id ON records(domain_id);
+
+create table supermasters (
+  ip          VARCHAR(25) NOT NULL, 
+  nameserver  VARCHAR(255) NOT NULL COLLATE NOCASE, 
+  account     VARCHAR(40) DEFAULT NULL
+);
+
+alter table records add ordername      VARCHAR(255);
+alter table records add auth bool;
+create index orderindex on records(ordername);
+
+create table domainmetadata (
+ id         INTEGER PRIMARY KEY,
+ domain_id       INT NOT NULL,
+ kind         VARCHAR(16) COLLATE NOCASE,
+ content    TEXT
+);
+
+create index domainmetaidindex on domainmetadata(domain_id);
+
+create table cryptokeys (
+ id        INTEGER PRIMARY KEY,
+ domain_id      INT NOT NULL,
+ flags        INT NOT NULL,
+ active        BOOL,
+ content    TEXT
+);         
+
+create index domainidindex on cryptokeys(domain_id);           
+
+create table tsigkeys (
+ id        INTEGER PRIMARY KEY,
+ name        VARCHAR(255) COLLATE NOCASE,
+ algorithm    VARCHAR(50) COLLATE NOCASE,
+ secret        VARCHAR(255)
+);
+
+create unique index namealgoindex on tsigkeys(name, algorithm);
+insert into domains (name,type) VALUES('delegated.dnssec-parent.com','NATIVE');
+insert into domains (name,type) VALUES('dnssec-parent.com','NATIVE');
+insert into domains (name,type) VALUES('example.com','NATIVE');
+insert into domains (name,type) VALUES('minimal.com','NATIVE');
+insert into domains (name,type) VALUES('test.com','NATIVE');
+insert into domains (name,type) VALUES('wtest.com','NATIVE');
+
+commit;
diff --git a/modules/remotebackend/regression-tests/unix-backend.rb b/modules/remotebackend/regression-tests/unix-backend.rb
new file mode 120000 (symlink)
index 0000000..ebb5e0c
--- /dev/null
@@ -0,0 +1 @@
+pipe-backend.rb
\ No newline at end of file
diff --git a/modules/remotebackend/remotebackend.cc b/modules/remotebackend/remotebackend.cc
new file mode 100644 (file)
index 0000000..29d7a50
--- /dev/null
@@ -0,0 +1,411 @@
+#include "remotebackend.hh"
+#include <boost/foreach.hpp>
+
+static const char *kBackendId = "[RemoteBackend]";
+
+/**
+ * Forwarder for value. This is just in case
+ * we need to do some treatment to the value before
+ * sending it downwards.
+ */
+bool Connector::send(Json::Value &value) {
+    return send_message(value);
+}
+
+/** 
+ * Helper for handling receiving of data.
+ * Basically what happens here is that we check 
+ * that the receiving happened ok, and extract
+ * result. Logging is performed here, too. 
+ */
+bool Connector::recv(Json::Value &value) {
+    Json::Value input;
+    if (recv_message(input)>0) {
+       bool rv = true;
+       // check for error
+       value = input.get("result",Json::Value());
+       if (value.isNull() || (value.isBool() && value.asBool() == false)) {
+           rv = false;
+          value = Json::Value(false);
+        } 
+        Json::Value messages = input.get("log", Json::Value());
+        if (messages.isArray()) {
+           // log em all
+           for(Json::ValueIterator iter = messages.begin(); iter != messages.end(); iter++) {
+              L<<Logger::Info<<"[remotebackend]:"<< (*iter).asString()<<std::endl;
+           }
+        }
+        return rv;
+    }
+    return false;
+}
+
+RemoteBackend::RemoteBackend(const std::string &suffix)
+{
+      setArgPrefix("remote"+suffix);
+      build(getArg("connection-string"));
+      this->d_dnssec = mustDo("dnssec");
+      this->d_index = -1;
+}
+
+RemoteBackend::~RemoteBackend() {
+     if (connector != NULL) {
+       delete connector;
+     }
+}
+
+/** 
+ * Builds connector based on options
+ * Currently supports unix,pipe and http
+ */
+int RemoteBackend::build(const std::string &connstr) {
+      std::vector<std::string> parts;
+      std::string type;
+      std::string opts;
+      std::map<std::string, std::string> options;
+
+      // connstr is of format "type:options"
+      size_t pos;
+      pos = connstr.find_first_of(":");
+      if (pos == std::string::npos)
+         throw new AhuException("Invalid connection string: malformed");
+
+      type = connstr.substr(0, pos);
+      opts = connstr.substr(pos+1);
+
+      // tokenize the string on comma
+      stringtok(parts, opts, ",");
+
+      // find out some options and parse them while we're at it
+      BOOST_FOREACH(std::string opt, parts) {
+          std::string key,val;
+          // make sure there is something else than air in the option...
+          if (opt.find_first_not_of(" ") == std::string::npos) continue;
+
+          // split it on '='. if not found, we treat it as "yes"
+          pos = opt.find_first_of("=");
+
+          if (pos == std::string::npos) {
+             key = opt;
+             val = "yes";
+          } else {
+             key = opt.substr(0,pos);
+             val = opt.substr(pos+1);
+          }
+          options[key] = val;
+      }
+
+      // connectors know what they are doing
+      if (type == "http") {
+        this->connector = new HTTPConnector(options);
+      } else if (type == "unix") {
+        this->connector = new UnixsocketConnector(options);
+      } else if (type == "pipe") {
+        this->connector = new PipeConnector(options);
+      } else {
+        throw new AhuException("Invalid connection string: unknown connector");
+      }
+
+      return -1;
+}
+
+/** 
+ * The functions here are just remote json stubs that send and receive the method call
+ * data is mainly left alone, some defaults are assumed. 
+ */
+void RemoteBackend::lookup(const QType &qtype, const std::string &qdomain, DNSPacket *pkt_p, int zoneId) {
+   Json::Value query,args;
+
+   if (d_index != -1) 
+      throw AhuException("Attempt to lookup while one running");
+   args["qtype"] = qtype.getName();
+   args["qname"] = qdomain;
+   if (pkt_p != NULL) {
+     args["remote"] = pkt_p->getRemote();
+     args["local"] = pkt_p->getLocal();
+     args["real-remote"] = pkt_p->getRealRemote().toString();
+   }
+   args["zone-id"] = zoneId;
+   query["method"] = "lookup";
+   query["parameters"] = args;
+
+   if (connector->send(query) == false || connector->recv(d_result) == false)  return;
+
+   // OK. we have result values in result
+   if (d_result.isArray() == false) return;
+   d_index = 0;
+}
+
+bool RemoteBackend::get(DNSResourceRecord &rr) {
+   if (d_index == -1) return false;
+   
+   Json::Value empty("");
+   Json::Value emptyint(-1);
+
+   rr.qtype = d_result[d_index].get("qtype",empty).asString();
+   rr.qname = d_result[d_index].get("qname",empty).asString();
+   rr.qclass = QClass::IN;
+   rr.content = d_result[d_index].get("content",empty).asString();
+   rr.ttl = d_result[d_index].get("ttl",emptyint).asInt();
+   rr.domain_id = d_result[d_index].get("domain_id",emptyint).asInt();
+   rr.priority = d_result[d_index].get("priority",emptyint).asInt();
+   if (d_dnssec) 
+     rr.auth = d_result[d_index].get("auth", Json::Value(1)).asInt();
+   else
+     rr.auth = 1;
+   rr.scopeMask = d_result[d_index].get("scopeMask",Json::Value(0)).asInt();
+
+   d_index++;
+   
+   // id index is out of bounds, we know the results end here. 
+   if (d_index == static_cast<int>(d_result.size())) {
+     d_result = Json::Value();
+     d_index = -1;
+   }
+   return true;
+}
+
+bool RemoteBackend::list(const std::string &target, int domain_id) {
+   Json::Value query;
+
+   if (d_index != -1) 
+      throw AhuException("Attempt to lookup while one running");
+
+   query["method"] = "list";
+   query["parameters"] = Json::Value();
+   query["parameters"]["zonename"] = target;
+   query["parameters"]["domain-id"] = domain_id;
+
+   if (connector->send(query) == false || connector->recv(d_result) == false) 
+     return false;
+   d_index = 0;
+
+   return true;
+}
+
+bool RemoteBackend::getBeforeAndAfterNamesAbsolute(uint32_t id, const std::string& qname, std::string& unhashed, std::string& before, std::string& after) {
+   Json::Value query,answer;
+   // no point doing dnssec if it's not supported
+   if (d_dnssec == false) return false;
+
+   query["method"] = "getBeforeAndAfterNamesAbsolute";
+   query["parameters"] = Json::Value();
+   query["parameters"]["id"] = id;
+   query["parameters"]["qname"] = qname;
+
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+   
+   unhashed = answer["unhashed"].asString();
+   before = answer["before"].asString();
+   after = answer["after"].asString();
+  
+   return true;
+}
+
+bool RemoteBackend::getBeforeAndAfterNames(uint32_t id, const std::string& zonename, const std::string& qname, std::string& before, std::string& after) {
+   Json::Value query,answer;
+
+   // no point doing dnssec if it's not supported
+   if (d_dnssec == false) return false;
+
+   query["method"] = "getBeforeAndAfterNames";
+   query["parameters"] = Json::Value();
+   query["parameters"]["id"] = id;
+   query["parameters"]["zonename"] = zonename;
+   query["parameters"]["qname"] = qname;
+
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   before = answer["before"].asString();
+   after = answer["after"].asString();
+
+   return true;
+}
+
+bool RemoteBackend::getDomainMetadata(const std::string& name, const std::string& kind, std::vector<std::string>& meta) {
+   Json::Value query,answer;
+
+   query["method"] = "getDomainMetadata";
+   query["parameters"] = Json::Value();
+   query["parameters"]["name"] = name;
+   query["parameters"]["kind"] = kind;
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   meta.clear();
+
+   for(Json::ValueIterator iter = answer.begin(); iter != answer.end(); iter++) {
+          meta.push_back((*iter).asString());
+   }
+
+   return true;
+}
+
+bool RemoteBackend::setDomainMetadata(const string& name, const std::string& kind, const std::vector<std::string>& meta) {
+   Json::Value query,answer;
+   query["method"] = "setDomainMetadata";
+   query["parameters"] = Json::Value();
+   query["parameters"]["name"] = name;
+   query["parameters"]["kind"] = kind;
+   query["parameters"]["value"] = Json::Value();
+   BOOST_FOREACH(std::string value, meta) {
+     query["parameters"]["value"].append(value);
+   }
+
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   return answer.asBool();
+}
+
+
+bool RemoteBackend::getDomainKeys(const std::string& name, unsigned int kind, std::vector<DNSBackend::KeyData>& keys) {
+   Json::Value query,answer;
+   // no point doing dnssec if it's not supported
+   if (d_dnssec == false) return false;
+
+   query["method"] = "getDomainKeys";
+   query["parameters"] = Json::Value();
+   query["parameters"]["name"] = name;
+   query["parameters"]["kind"] = kind;
+
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   keys.clear();
+
+   for(Json::ValueIterator iter = answer.begin(); iter != answer.end(); iter++) {
+      DNSBackend::KeyData key;
+      key.id = (*iter)["id"].asUInt();
+      key.flags = (*iter)["flags"].asUInt();
+      key.active = (*iter)["active"].asBool();
+      key.content = (*iter)["content"].asString();
+      keys.push_back(key);
+   }
+
+   return true;
+}
+
+bool RemoteBackend::removeDomainKey(const string& name, unsigned int id) { 
+   Json::Value query,answer;
+   // no point doing dnssec if it's not supported
+   if (d_dnssec == false) return false;
+   query["method"] = "remoteDomainKey";
+   query["parameters"] = Json::Value();
+   query["parameters"]["name"] = name;
+   query["parameters"]["id"] = id;
+
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   return answer.asBool();
+}
+
+int RemoteBackend::addDomainKey(const string& name, const KeyData& key) {
+   Json::Value query,answer;
+
+   // no point doing dnssec if it's not supported
+   if (d_dnssec == false) return false;
+   query["method"] = "addDomainKey";
+   query["parameters"] = Json::Value();
+   query["parameters"]["name"] = name;
+   query["parameters"]["key"] = Json::Value();
+   query["parameters"]["key"]["flags"] = key.flags;
+   query["parameters"]["key"]["active"] = key.active;
+   query["parameters"]["key"]["content"] = key.content;
+
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   return answer.asInt();
+}
+
+bool RemoteBackend::activateDomainKey(const string& name, unsigned int id) {
+   Json::Value query,answer;
+   // no point doing dnssec if it's not supported
+   if (d_dnssec == false) return false;
+   query["method"] = "activateDomainKey";
+   query["parameters"] = Json::Value();
+   query["parameters"]["name"] = name;
+   query["parameters"]["id"] = id;
+
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   return answer.asBool();
+}
+
+bool RemoteBackend::deactivateDomainKey(const string& name, unsigned int id) {
+   Json::Value query,answer;
+   // no point doing dnssec if it's not supported
+   if (d_dnssec == false) return false;
+   query["method"] = "deactivateDomainKey";
+   query["parameters"] = Json::Value();
+   query["parameters"]["name"] = name;
+   query["parameters"]["id"] = id;
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   return answer.asBool();
+}
+
+bool RemoteBackend::getTSIGKey(const std::string& name, std::string* algorithm, std::string* content) {
+   Json::Value query,answer;
+   query["method"] = "getTSIGKey";
+   query["parameters"] = Json::Value();
+   query["parameters"]["name"] = name;
+
+   if (connector->send(query) == false || connector->recv(answer) == false)
+     return false;
+
+   if (algorithm != NULL)
+     algorithm->assign(answer["algorithm"].asString());
+   if (content != NULL)
+     content->assign(answer["content"].asString());
+   
+   return true;
+}
+
+DNSBackend *RemoteBackend::maker()
+{
+   try {
+      return new RemoteBackend();
+   }
+   catch(...) {
+      L<<Logger::Error<<kBackendId<<" Unable to instantiate a remotebackend!"<<endl;
+      return 0;
+   };
+}
+
+class RemoteBackendFactory : public BackendFactory
+{
+  public:
+      RemoteBackendFactory() : BackendFactory("remote") {}
+
+      void declareArguments(const std::string &suffix="")
+      {
+          declare(suffix,"dnssec","Enable dnssec support","no");
+          declare(suffix,"connection-string","Connection string","");
+      }
+
+      DNSBackend *make(const std::string &suffix="")
+      {
+         return new RemoteBackend(suffix);
+      }
+};
+
+class RemoteLoader
+{
+   public:
+      RemoteLoader()
+      {
+         curl_global_init(CURL_GLOBAL_ALL);
+         BackendMakers().report(new RemoteBackendFactory);
+         L<<Logger::Notice<<kBackendId<<" This is the remotebackend version "VERSION" ("__DATE__", "__TIME__") reporting"<<endl;
+      }
+};
+
+static RemoteLoader remoteloader;
diff --git a/modules/remotebackend/remotebackend.hh b/modules/remotebackend/remotebackend.hh
new file mode 100644 (file)
index 0000000..a2ed9b8
--- /dev/null
@@ -0,0 +1,101 @@
+#ifndef REMOTEBACKEND_REMOTEBACKEND_HH
+
+#include <string>
+#include "pdns/namespaces.hh"
+#include <pdns/dns.hh>
+#include <pdns/dnsbackend.hh>
+#include <pdns/dnspacket.hh>
+#include <pdns/ueberbackend.hh>
+#include <pdns/ahuexception.hh>
+#include <pdns/logger.hh>
+#include <pdns/arguments.hh>
+#include <boost/lexical_cast.hpp>
+#include <jsoncpp/json/json.h>
+#include "../pipebackend/coprocess.hh"
+#include <curl/curl.h>
+
+class Connector {
+   public:
+    virtual ~Connector() {};
+    bool send(Json::Value &value);
+    bool recv(Json::Value &value);
+    virtual int send_message(const Json::Value &input) = 0;
+    virtual int recv_message(Json::Value &output) = 0;
+};
+
+// fwd declarations
+class UnixsocketConnector: public Connector {
+  public:
+    UnixsocketConnector(std::map<std::string,std::string> options);
+    virtual ~UnixsocketConnector();
+    virtual int send_message(const Json::Value &input);
+    virtual int recv_message(Json::Value &output);
+};
+
+class HTTPConnector: public Connector {
+  public:
+
+  HTTPConnector(std::map<std::string,std::string> options);
+  ~HTTPConnector();
+
+  virtual int send_message(const Json::Value &input);
+  virtual int recv_message(Json::Value &output);
+  friend size_t ::httpconnector_write_data(void*, size_t, size_t, void*);
+
+  private:
+    std::string d_url;
+    std::string d_url_suffix;
+    CURL *d_c;
+    std::string d_data;
+    void json2string(const Json::Value &input, std::string &output);
+    void requestbuilder(const std::string &method, const Json::Value &parameters, struct curl_slist **slist);
+};
+
+class PipeConnector: public Connector {
+  public:
+
+  PipeConnector(std::map<std::string,std::string> options);
+  ~PipeConnector();
+
+  virtual int send_message(const Json::Value &input);
+  virtual int recv_message(Json::Value &output);
+
+  private:
+
+  void launch();
+  CoProcess *coproc;
+  std::string command;
+  std::map<std::string,std::string> options;
+};
+
+class RemoteBackend : public DNSBackend
+{
+  public:
+  RemoteBackend(const std::string &suffix="");
+  ~RemoteBackend();
+
+  void lookup(const QType &qtype, const std::string &qdomain, DNSPacket *pkt_p=0, int zoneId=-1);
+  bool get(DNSResourceRecord &rr);
+  bool list(const std::string &target, int domain_id);
+
+  virtual bool getDomainMetadata(const std::string& name, const std::string& kind, std::vector<std::string>& meta);
+  virtual bool getDomainKeys(const std::string& name, unsigned int kind, std::vector<DNSBackend::KeyData>& keys);
+  virtual bool getTSIGKey(const std::string& name, std::string* algorithm, std::string* content);
+  virtual bool getBeforeAndAfterNamesAbsolute(uint32_t id, const std::string& qname, std::string& unhashed, std::string& before, std::string& after);
+  virtual bool getBeforeAndAfterNames(uint32_t id, const std::string& zonename, const std::string& qname, std::string& before, std::string& after);
+  virtual bool setDomainMetadata(const string& name, const string& kind, const std::vector<std::basic_string<char> >& meta);
+  virtual bool removeDomainKey(const string& name, unsigned int id);
+  virtual int addDomainKey(const string& name, const KeyData& key);
+  virtual bool activateDomainKey(const string& name, unsigned int id);
+  virtual bool deactivateDomainKey(const string& name, unsigned int id);
+
+  static DNSBackend *maker();
+
+  private:
+    int build(const std::string &connstr);
+    Connector *connector;
+    bool d_dnssec;
+    Json::Value d_result;
+    int d_index; 
+};
+#endif
diff --git a/modules/remotebackend/test_backend.rb b/modules/remotebackend/test_backend.rb
new file mode 100755 (executable)
index 0000000..424c765
--- /dev/null
@@ -0,0 +1,199 @@
+#!/usr/bin/ruby
+
+require 'json'
+
+prefix = "2001:06e8:0500:0000"
+
+# domain keys
+
+$domain_keys = [
+ {
+  :id => 8,
+  :flags => 257,
+  :active => true,
+  :content => "Private-key-format: v1.2
+Algorithm: 8 (RSASHA256)
+Modulus: nFfl3jgFE+KET9PFPVmRCVmGZ+o+Stnqq7AZPAmhSYf96BPqK3t22vKHJhmVla+td94if2XUSggRUDz8YSeIybVNDTCmhF4xNyhAYAeh+HxJQU7CL8jQLs0b86Hd/Ua/s2pmSFSX4vzoAg+1lucLT3AiGYWjrvciFFpWnMyYftqcfTy3Vu/TMA9Kr7tr75CsJ2mzu1rywHsvOErCY8xH6dGd2LCY9ozuOwsgAlou+rOF5W71UPIuomM9QcIdVD2nq8+aHSsKeWk8gabLEsKU0SFFutO/R41lYXiUaFv+zY7r7NmP+d0Wmr0rgnzq2vlV0USQhlDPAsPSVstAuMrjgw==
+PublicExponent: AQAB
+PrivateExponent: kSdnrMbKe7dqfcpDG9WMgYp6BS88A5/a2F2ysv+Ds+Q9cAENXTpPn2yNDd4+sd3pgcmYsjsS6deTr9/YFRXw9bKBKLI80JFdZhKng5hMvYqnBGQmt/Dz4/RE87PVLNIC4/mrFE4Bn2vStnRTu3WY/wURXf3YkgI+IdXsfQTsBNloLiRoVV1QmB4kkHtRXsr02dg4kMhaXA13kT7lsrquCvGoxP++keVJ88AaQA8si30/E+CSgrbCHThUXeL6ErYz6myI5O2RJqodlcEQeXYi2cNkzQJnO3yjXcaSJy2u3tVNE2wXCXf1pwHXyTA4YT1qMbMIABq9TiVW8Q2Kg4sN
+Prime1: wrNo1cyyL/LpO+Das2EQzMbX0ctdy0OPvg3kWZqt2ZEwJjcVe1Vav5ZzSWKQWuT0IytUst7qdTE7lKpWWKDAbxJ4IMcMiMN2iQqE9V/dhIJluIrshupQzdhnXR46MfbazkDuvMTM1Fk4ZOgm3lrJoRvl6EOxl7e9veTwCjKqrA8=
+Prime2: zZDvTPq/Ea7aKa+CNmP7oOYxpvE1fpPV8MRjQ+IMi8VIDHpE72NoSSVoVuSL7fgl7qAFlbJMEtQGIlqUFQkC0sbI6ddb1/Ane+xhH4aPaljtIJQs0GlM/ECz30qZCreyHqaVzXlk9ZrN7HlnIOQ//DV3lCsS8EE1djeQxFThrU0=
+Exponent1: S9uW1uX/7sqXsKq0yvrgjshSQf0YOB/Em2nSNE8duQzmU51Wk0z4JHk7xbXPRHq73A//2gkcFDjwW8XaCoHnN99cSnkDGy38uvwMPYXySrR7aWFHMnGMtgbAjvk990WUjpOh8I5Et99jJ32D11JMCKdT9iCZyuDd3mSaWX7QHGU=
+Exponent2: hiaslG8a3B5gz01zS62KHCG9i3XkdDtkJeDz6uwNRfW0JDhy3krgVsPryLETxHPpxUV2/49A6BSoACledC/SQN1rZnedv1lBWzUS2PEGjN+FuHoamNPvYruS5wiWwZDJ1AjgwBwVz9Z7xnQf4i4yt5Po+q/1hwb3LbPrbMT8Fg0=
+Coefficient: zZDvTPq/Ea7aKa+CNmP7oOYxpvE1fpPV8MRjQ+IMi8VIDHpE72NoSSVoVuSL7fgl7qAFlbJMEtQGIlqUFQkC0sbI6ddb1/Ane+xhH4aPaljtIJQs0GlM/ECz30qZCreyHqaVzXlk9ZrN7HlnIOQ//DV3lCsS8EE1djeQxFThrU0="
+ },
+ {
+   :id => 9,
+   :flags => 256,
+   :active => true,
+   :content => "Private-key-format: v1.2
+Algorithm: 8 (RSASHA256)
+Modulus: 0fcOKzIHcE5K3B3zwu0iAm4Z/cRBH9JX0mMhl9h58i0C8QEK3fK+425MEI/q4P7qGkwB9hJ9TefC5ny3b+F2iSIDTqcrMoRr6/lJO2YH7MyeuAv1T5HMsC7DoCkdaq0xuoOkOtKaVz5/FwSFmCIlCNeboFX3Fq4kCoWsa1m63AE=
+PublicExponent: AQAB
+PrivateExponent: BWc0q6LlcxvorEJvD/CXQ/W+YHvo6x84GFdpuWUeOj+zSC1tMKn7BJJFjdWOR0z4DEYxdLokFFmm99R0ygHE0ZWh7WS1OX5AzqoFczeC0BDLvoAYu6XMvwlYp78ffqlI2qv4ohfew5TYzWCugxIufFsC55i2FoworGBLSFUloaM=
+Prime1: +aIMyvDUAzL/JZDHsPLnawLhy7sfZB4BbtARAYxZ6HSE9MAnFKbQhXQ9yRdGLSZEhe8g5b9tXBvxT8TPIGhwcw==
+Prime2: 11H/gYNtYkloQuRcL2xd3ElxosgncgFvXKY0y4sGyJuEhHJfttaxXCmNfVN9fg5lX18kfxCv4s8rqI+M+rZouw==
+Exponent1: SmrNp34NpfqA52D2tsBizproVwSsgfsT8EXkm/KMJuj9bb0OqXBlPzN868Kdb/41dTvpMbRUVJ4b3OzN1lpsEw==
+Exponent2: zPRiTzd48SuKsNGJ5iIynbLTFe2LjntLM1eJvY2SYXWXCDOOZA2sOVvcMEU+mLS/Ta7UoJaTtUMZ/ZLW0Pa8bQ==
+Coefficient: 11H/gYNtYkloQuRcL2xd3ElxosgncgFvXKY0y4sGyJuEhHJfttaxXCmNfVN9fg5lX18kfxCv4s8rqI+M+rZouw=="
+ },
+ {
+   :id => 10,
+   :flags => 256,
+   :active => false,
+   :content => "Private-key-format: v1.2
+Algorithm: 8 (RSASHA256)
+Modulus: rzJLZNmV+xQLkYWT3PytSYDeE8SovXi9+WCp9MVkWcSj1BgrBbugregqsSQLvz+rGsgc2nK3kqygDhypRMExmd2TRHeIrtcLHZ3nq+mmL/GfE0K6KWS2FJx06ol7v9xG8esLe+TUSAENo2xFR0BFmkiJN+R5hq06Rb0Y7OMSBs8=
+PublicExponent: AQAB
+PrivateExponent: FwH6jMABG6bCQ4DQrlDbS6/felEf/TdXcOHqRU7houMEG4fLqUZpZO1Rzfmh4U0x4fktxjJn5pyXrcLDKAMHHv+DjtPSC4B+49HFOZEHl/522VYw6NyJ+Firk41hbFxIStSFYh47qxKyAwbI/LuyTJE0Au8D+4dl8oFwfh0x+aE=
+Prime1: 0sDZukxVtL4zI9JPvEdBUAXLu4I98GS7xhxiLNeb+iF2+Vvw06igV7+CccMldz/r3gqr9y+NdAloErFvkieLRQ==
+Prime2: 1M822AT25uYwdKiiYuBPQlUIKVuP1paa8y4rh+uDuoHpm9E29hwHr2uLXESOO5YxB0oc8yauNOdH+HGTg8GhAw==
+Exponent1: IOq2Fv7tM/mxCxtCEOogLVt6YqMJAY76NQsh2lciqYKojnHpv2VLBemHejU8mM+HC3snOMhYk5MUijbkcjNy8Q==
+Exponent2: V0LUkUWP3GQ9MEjJtVOXDHMDkrnZxDsjNF4VOXmoHT0SBnOGXupleFfX4DC4RdSzK/MG5elRe53ulAA2Zctq8w==
+Coefficient: 1M822AT25uYwdKiiYuBPQlUIKVuP1paa8y4rh+uDuoHpm9E29hwHr2uLXESOO5YxB0oc8yauNOdH+HGTg8GhAw=="
+ }
+]
+
+def to32(number) 
+   number.to_s(32).tr "0123456789abcdefghijklmnopqrstuv", "ybndrfg8ejkmcpqxot1uwisza345h769"
+end
+
+def from32(str)
+   str.tr("0123456789abcdefghijklmnopqrstuv", "ybndrfg8ejkmcpqxot1uwisza345h769").to_i(32)
+end
+
+def rr(qname, qtype, content, ttl, priority = 0, auth = 1)
+   {:qname => qname, :qtype => qtype, :content => content, :ttl => ttl, :priority => priority, :auth => auth}
+end
+
+def send_result(result, log = nil)
+  out = {:result => result}
+  if log.class != Array
+    log = [log]
+  end
+  out[:log] = log unless log.nil?
+  print out.to_json
+  print "\n"
+end
+
+def send_failure(msg)
+  send_result false, [msg]
+end
+
+class Handler
+   attr :prefix, :plen, :suffix, :domain
+   
+   def initialize(prefix)
+     @prefix = prefix
+   end
+
+   def do_initialize(*args)
+     # precalculate some things
+     tmp = self.prefix.gsub(/[^0-9a-f]/,'')
+     @plen = tmp.size
+     @suffix = tmp.split(//).reverse.join('.')+".ip6.arpa"
+     @domain = "dyn.example.com"
+     send_result true, "Autorev v6 backend initialized"
+   end
+
+   def do_getbeforeandafternamesabsolute(args)
+     qname = args["qname"]
+     # assume prefix
+     name = qname.gsub(/ /,'')
+     nlen = 32-self.plen
+
+     name_a = nil
+     name_b = nil
+     unhashed = nil
+
+     if name == ''
+         name_a = sprintf("%0#{nlen}x",0).split(//).join(' ')
+     elsif name.size < nlen
+         name_a = sprintf("%0#{nlen}x",0).split(//).join(' ')
+     else 
+        unhashed = sprintf("%0#{nlen}x",name.to_i(16)).split(//).join(' ')
+        if (name.to_i(16) > 0)
+           name_b = sprintf("%0#{nlen}x",(name.to_i(16)-1)).split(//).join ' '
+        end
+        unless (name[/[^f]/].nil?)
+           name_a = sprintf("%0#{nlen}x",(name.to_i(16)+1)).split(//).join ' '
+        end
+     end
+     send_result ({:before => name_b, :after => name_a, :unhashed => unhashed})
+   end
+
+   def do_getbeforeandafternames(args)
+      self.do_getbeforeandafternamesabsolute(args)
+   end
+
+   def do_getdomainkeys(args)
+      if args["name"] == self.suffix
+         send_result $domain_keys
+      else
+         send_result false
+      end
+   end
+
+   def do_lookup(args)
+     qtype = args["qtype"]
+     qname = args["qname"]
+
+     if qname[/.ip6.arpa$/]
+      if qname == self.suffix
+        ret = []
+        if (qtype != "SOA")
+           ret << rr(qname, "NS", "ns1.example.com",300)
+           ret << rr(qname, "NS", "ns2.example.com",300)
+           ret << rr(qname, "NS", "ns3.example.com",300)      
+        end
+        ret << rr(qname,"SOA","ns1.example.com hostmaster.example.com #{Time.now.strftime("%Y%m%d%H")} 28800 7200 1209600 300",300)
+        return send_result ret
+      elsif qtype == "ANY" or qtype == "PTR"
+        # assume suffix
+        name = qname.gsub(self.suffix,"").split(".").reverse.join("")
+        if name.empty? or name.size != 32-plen
+          return send_result false
+        end
+       name = to32(name.to_i(16))
+        return send_result [rr(qname, "PTR", "node-#{name}.#{self.domain}", 300)]
+      end
+     end
+     send_result false
+   end
+   def do_getdomainmetadata(args) 
+      name = args["name"]
+      kind = args["kind"]
+      send_result false
+   end
+end
+
+h = Handler.new(prefix)
+
+STDOUT.sync = true
+begin 
+  STDIN.each_line do |line|
+    # expect json
+    input = {}
+    line = line.strip
+    next if line.empty?
+    begin
+      input = JSON.parse(line)
+      method = "do_#{input["method"].downcase}"
+      args = input["parameters"]
+
+      if h.respond_to?(method.to_sym) == false
+         res = false
+         send_result res, nil
+      elsif args.size > 0
+         h.send(method,args)
+      else
+         h.send(method)
+      end
+    rescue JSON::ParserError
+      send_failure "Cannot parse input #{line}"
+      next
+    end
+  end
+rescue SystemExit, Interrupt
+end
diff --git a/modules/remotebackend/unixconnector.cc b/modules/remotebackend/unixconnector.cc
new file mode 100644 (file)
index 0000000..a1216e0
--- /dev/null
@@ -0,0 +1,181 @@
+#include "remotebackend.hh"
+#include <sys/socket.h>
+#include <pdns/lock.hh> 
+#include <unistd.h>
+#include <sys/select.h>
+#include <fcntl.h>
+
+#ifndef UNIX_PATH_MAX 
+#define UNIX_PATH_MAX 108
+#endif
+
+
+// Singleton class to maintain client connection
+// via single unix socket connection
+static int n_unix_socket_connection;
+static pthread_mutex_t unix_build_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_mutex_t unix_mutex = PTHREAD_MUTEX_INITIALIZER;
+class UnixsocketConnection {
+  public:
+    UnixsocketConnection(const std::string &path)
+    {
+      this->path = path;
+      connected = false;
+    };
+
+    ~UnixsocketConnection() {
+      L<<Logger::Info<<"closing socket connection"<<endl;
+      close(fd);
+    };
+
+    ssize_t read(std::string &data) { 
+        ssize_t nread;
+        char buf[1500] = {0};
+        Lock scoped_lock(&unix_mutex); 
+
+        reconnect();
+        if (!connected) return -1;
+        nread = ::read(this->fd, buf, sizeof buf); 
+        // just try again later...
+        if (nread==-1 && errno == EAGAIN) return 0;
+
+        if (nread==-1) {
+           connected = false;
+           close(fd);
+           return -1;
+        }
+
+        data.append(buf, nread);
+        return nread;
+    };
+
+    ssize_t write(const std::string &data) { 
+        ssize_t nwrite, nbuf;
+        char buf[1500];
+        Lock scoped_lock(&unix_mutex); 
+
+        reconnect();
+        if (!connected) return -1;
+        nbuf = data.copy(buf, sizeof buf); // copy data and write
+        nwrite = ::write(fd, buf, nbuf);
+        if (nwrite == -1) {
+          connected = false;
+          close(fd);
+          return -1;
+        }
+        return nwrite;
+    };
+
+  private:
+    int fd;
+    bool connected;
+    std::string path;
+
+    void reconnect() {
+       struct sockaddr_un sock;
+       struct timeval tv;
+       fd_set rd;
+
+       if (connected) return; // no point reconnecting if connected...
+       connected = true;
+
+       L<<Logger::Info<<"Reconnecting to backend" << std::endl;
+       fd = socket(AF_UNIX, SOCK_STREAM, 0);
+       if (fd < 0) {
+          connected = false;
+          L<<Logger::Error<<"Cannot create socket: " << strerror(errno) << std::endl;;
+          return;
+       }
+       sock.sun_family = AF_UNIX;
+       memset(sock.sun_path, 0, UNIX_PATH_MAX);
+       path.copy(sock.sun_path, UNIX_PATH_MAX, 0);
+       fcntl(fd, F_SETFL, O_NONBLOCK, &fd);
+       
+       while(connect(fd, reinterpret_cast<struct sockaddr*>(&sock), sizeof sock)==-1 && (errno == EINPROGRESS)) {
+        tv.tv_sec = 0;
+         tv.tv_usec = 500;
+         FD_ZERO(&rd);
+         FD_SET(fd, &rd);
+         select(fd+1,&rd,NULL,NULL,&tv); // wait a moment
+       };
+       if (errno != EISCONN && errno != 0) {
+          L<<Logger::Error<<"Cannot connect to socket: " << strerror(errno) << std::endl;
+          close(fd);
+          connected = false;
+          return;
+       }
+    };
+};
+
+static UnixsocketConnection *unix_socket_connection;
+
+UnixsocketConnector::UnixsocketConnector(std::map<std::string,std::string> options) {
+     Lock scoped_lock(&unix_build_mutex);
+
+     if (unix_socket_connection == NULL) {
+       Json::Value init,res;
+       unix_socket_connection = new UnixsocketConnection(options.find("path")->second);
+       n_unix_socket_connection = 1;
+       init["method"] = "initialize";
+       init["parameters"] = Json::Value();
+       for(std::map<std::string,std::string>::iterator i = options.begin(); i != options.end(); i++)
+         init["parameters"][i->first] = i->second;
+       this->send(init);
+       if (this->recv(res) == false)
+          L<<Logger::Warning << "Failed to initialize backend" << std::endl;
+     } else {
+       n_unix_socket_connection++;
+     }
+}
+
+UnixsocketConnector::~UnixsocketConnector() {
+     Lock scoped_lock(&unix_build_mutex);
+     n_unix_socket_connection--;
+     if (n_unix_socket_connection == 0) {
+       delete unix_socket_connection;
+       unix_socket_connection = NULL;
+     }
+}
+
+int UnixsocketConnector::send_message(const Json::Value &input) {
+        std::string data;
+        Json::FastWriter writer;
+        int rv;
+        data = writer.write(input);
+        //i make sure we got nothing waiting there.
+        std::string temp;
+        while(unix_socket_connection->read(temp)>0) { temp = ""; }
+        rv = unix_socket_connection->write(data);
+        if (rv == -1)
+            throw AhuException("Failed to write to socket");
+        return rv;
+}
+
+int UnixsocketConnector::recv_message(Json::Value &output) {
+        int rv,nread;
+        std::string s_output;
+        Json::Reader r;
+        time_t t0;
+
+        nread = 0;
+        t0 = time(NULL);
+        s_output = "";       
+        while(time(NULL) - t0 < 2) { // 2 second timeout 
+          std::string temp;
+          rv = unix_socket_connection->read(temp);
+          if (rv == -1) 
+            throw AhuException("Failed to read from socket");
+
+          if (rv>0) {
+            nread += rv;
+            s_output.append(temp);
+            if (r.parse(s_output,output)==true) {
+               return nread;
+            }
+          }
+        }
+
+        return -1;
+}