From 1a5c5f9a865f364b3f0d4091820026dcf32d0a04 Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Fri, 7 Jun 2013 13:53:42 +0200 Subject: [PATCH] cherry pick squash of e09ff8b07703...ae03af280d89 from master - various remotebackend improvements by Aki Tuomi --- configure.ac | 2 + modules/remotebackend/.gitignore | 5 + modules/remotebackend/Makefile.am | 48 +- modules/remotebackend/httpconnector.cc | 209 +++++- .../remotebackend/regression-tests/backend.rb | 2 +- .../regression-tests/dnsbackend.rb | 31 +- .../regression-tests/http-backend.rb | 2 +- .../regression-tests/pipe-backend.rb | 2 +- modules/remotebackend/remotebackend.cc | 280 ++++++- modules/remotebackend/remotebackend.hh | 22 +- .../remotebackend/test-remotebackend-http.cc | 73 ++ .../remotebackend/test-remotebackend-json.cc | 73 ++ .../remotebackend/test-remotebackend-keys.hh | 3 + .../remotebackend/test-remotebackend-pipe.cc | 65 ++ .../remotebackend/test-remotebackend-post.cc | 73 ++ modules/remotebackend/test-remotebackend.cc | 247 +++++++ modules/remotebackend/testrunner.sh | 57 ++ modules/remotebackend/unittest.rb | 224 ++++++ modules/remotebackend/unittest_http.rb | 179 +++++ modules/remotebackend/unittest_json.rb | 58 ++ modules/remotebackend/unittest_pipe.rb | 38 + modules/remotebackend/unittest_post.rb | 59 ++ pdns/docs/pdns.xml | 684 +++++++++++++++++- 23 files changed, 2391 insertions(+), 45 deletions(-) create mode 100644 modules/remotebackend/.gitignore create mode 100644 modules/remotebackend/test-remotebackend-http.cc create mode 100644 modules/remotebackend/test-remotebackend-json.cc create mode 100644 modules/remotebackend/test-remotebackend-keys.hh create mode 100644 modules/remotebackend/test-remotebackend-pipe.cc create mode 100644 modules/remotebackend/test-remotebackend-post.cc create mode 100644 modules/remotebackend/test-remotebackend.cc create mode 100644 modules/remotebackend/testrunner.sh create mode 100644 modules/remotebackend/unittest.rb create mode 100644 modules/remotebackend/unittest_http.rb create mode 100644 modules/remotebackend/unittest_json.rb create mode 100644 modules/remotebackend/unittest_pipe.rb create mode 100644 modules/remotebackend/unittest_post.rb diff --git a/configure.ac b/configure.ac index 04e0d70d98..d8c37ccf15 100644 --- a/configure.ac +++ b/configure.ac @@ -255,10 +255,12 @@ AM_CONDITIONAL(REMOTEBACKEND_HTTP,test x"$enable_remotebackend_http" = "xyes") if test "x$enable_remotebackend_http" = "xyes" then PKG_CHECK_MODULES(LIBCURL, libcurl, HAVE_LIBCURL=yes, AC_MSG_ERROR([Could not find libcurl])) + REMOTEBACKEND_HTTP=yes AC_SUBST(LIBCURL_LIBS) AC_SUBST(LIBCURL_CFLAGS) AC_DEFINE(HAVE_LIBCURL,1,[If we have libcurl]) AC_DEFINE(REMOTEBACKEND_HTTP,1,[If we want HTTP connector]) + AC_SUBST(REMOTEBACKEND_HTTP) fi AC_MSG_CHECKING(whether we should build static binaries) diff --git a/modules/remotebackend/.gitignore b/modules/remotebackend/.gitignore new file mode 100644 index 0000000000..c14f877c6a --- /dev/null +++ b/modules/remotebackend/.gitignore @@ -0,0 +1,5 @@ +remotebackend-access.log +test_remotebackend_http +test_remotebackend_pipe +test_remotebackend_json +test_remotebackend_post diff --git a/modules/remotebackend/Makefile.am b/modules/remotebackend/Makefile.am index a47f8bbae6..c4bf57930f 100644 --- a/modules/remotebackend/Makefile.am +++ b/modules/remotebackend/Makefile.am @@ -5,9 +5,55 @@ AM_CPPFLAGS=@THREADFLAGS@ $(BOOST_CPPFLAGS) $(LIBCURL_CFLAGS) -I../../pdns/ext/r #endif EXTRA_DIST=OBJECTFILES OBJECTLIBS -lib_LTLIBRARIES = libremotebackend.la +EXTRA_PROGRAMS=test_remotebackend_pipe test_remotebackend_http test_remotebackend_post test_remotebackend_json +EXTRA_LTLIBRARIES=libtestremotebackend.la + +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=$(LIBCURL_LIBS) + +TESTS_ENVIRONMENT = env BOOST_TEST_LOG_LEVEL=message REMOTEBACKEND_HTTP=$(REMOTEBACKEND_HTTP) ./testrunner.sh +TESTS=test_remotebackend_pipe test_remotebackend_http test_remotebackend_post test_remotebackend_json + +BUILT_SOURCES=../../pdns/dnslabeltext.cc + +../../pdns/dnslabeltext.cc: ../../pdns/dnslabeltext.rl + make -C ../../pdns dnslabeltext.cc + +libtestremotebackend_la_SOURCES=../../pdns/dnsbackend.hh ../../pdns/dnsbackend.cc ../../pdns/ueberbackend.hh ../../pdns/ueberbackend.cc \ + ../../pdns/nameserver.cc ../../pdns/misc.cc ../../pdns/arguments.hh \ + ../../pdns/unix_utility.cc ../../pdns/logger.cc ../../pdns/statbag.cc ../../pdns/arguments.hh ../../pdns/arguments.cc ../../pdns/qtype.cc ../../pdns/dnspacket.cc \ + ../../pdns/dnswriter.cc ../../pdns/base64.cc ../../pdns/base32.cc ../../pdns/dnsrecords.cc ../../pdns/dnslabeltext.cc ../../pdns/dnsparser.cc \ + ../../pdns/rcpgenerator.cc ../../pdns/ednssubnet.cc ../../pdns/nsecrecords.cc ../../pdns/sillyrecords.cc ../../pdns/dnssecinfra.cc \ + ../../pdns/aes/dns_random.cc ../../pdns/packetcache.hh ../../pdns/packetcache.cc \ + ../../pdns/aes/aescpp.h ../../pdns/dns.hh ../../pdns/dns.cc ../../pdns/json.hh ../../pdns/json.cc \ + ../../pdns/aes/aescrypt.c ../../pdns/aes/aes.h ../../pdns/aes/aeskey.c ../../pdns/aes/aes_modes.c ../../pdns/aes/aesopt.h \ + ../../pdns/aes/aestab.c ../../pdns/aes/aestab.h ../../pdns/aes/brg_endian.h ../../pdns/aes/brg_types.h ../pipebackend/coprocess.cc \ + remotebackend.hh remotebackend.cc unixconnector.cc httpconnector.cc pipeconnector.cc + +libtestremotebackend_la_CFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +libtestremotebackend_la_CXXFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns + +test_remotebackend_pipe_SOURCES=test-remotebackend.cc test-remotebackend-pipe.cc +test_remotebackend_http_SOURCES=test-remotebackend.cc test-remotebackend-http.cc ../../config.h +test_remotebackend_post_SOURCES=test-remotebackend.cc test-remotebackend-post.cc ../../config.h +test_remotebackend_json_SOURCES=test-remotebackend.cc test-remotebackend-json.cc ../../config.h + +test_remotebackend_pipe_CFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +test_remotebackend_pipe_CXXFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +test_remotebackend_pipe_LDADD=libtestremotebackend.la @DYNLINKFLAGS@ @THREADFLAGS@ $(BOOST_UNIT_TEST_FRAMEWORK_LDFLAGS) ../../pdns/ext/polarssl-1.1.2/library/libpolarssl.a $(BOOST_UNIT_TEST_FRAMEWORK_LIBS) $(BOOST_SERIALIZATION_LIBS) $(BOOST_PROGRAM_OPTIONS_LIBS) @LIBDL@ $(LIBCURL_LIBS) + +test_remotebackend_http_CFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +test_remotebackend_http_CXXFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +test_remotebackend_http_LDADD=libtestremotebackend.la @DYNLINKFLAGS@ @THREADFLAGS@ $(BOOST_UNIT_TEST_FRAMEWORK_LDFLAGS) ../../pdns/ext/polarssl-1.1.2/library/libpolarssl.a $(BOOST_UNIT_TEST_FRAMEWORK_LIBS) $(BOOST_SERIALIZATION_LIBS) $(BOOST_PROGRAM_OPTIONS_LIBS) @LIBDL@ $(LIBCURL_LIBS) + +test_remotebackend_post_CFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +test_remotebackend_post_CXXFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +test_remotebackend_post_LDADD=libtestremotebackend.la @DYNLINKFLAGS@ @THREADFLAGS@ $(BOOST_UNIT_TEST_FRAMEWORK_LDFLAGS) ../../pdns/ext/polarssl-1.1.2/library/libpolarssl.a $(BOOST_UNIT_TEST_FRAMEWORK_LIBS) $(BOOST_SERIALIZATION_LIBS) $(BOOST_PROGRAM_OPTIONS_LIBS) @LIBDL@ $(LIBCURL_LIBS) + +test_remotebackend_json_CFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +test_remotebackend_json_CXXFLAGS=$(BOOST_CPPFLAGS) @THREADFLAGS@ $(LIBCURL_CFLAGS) -g -O0 -I../../pdns +test_remotebackend_json_LDADD=libtestremotebackend.la @DYNLINKFLAGS@ @THREADFLAGS@ $(BOOST_UNIT_TEST_FRAMEWORK_LDFLAGS) ../../pdns/ext/polarssl-1.1.2/library/libpolarssl.a $(BOOST_UNIT_TEST_FRAMEWORK_LIBS) $(BOOST_SERIALIZATION_LIBS) $(BOOST_PROGRAM_OPTIONS_LIBS) @LIBDL@ $(LIBCURL_LIBS) diff --git a/modules/remotebackend/httpconnector.cc b/modules/remotebackend/httpconnector.cc index af49cd4c67..fe6a4cda47 100644 --- a/modules/remotebackend/httpconnector.cc +++ b/modules/remotebackend/httpconnector.cc @@ -4,6 +4,8 @@ #include #include #include +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" #ifdef REMOTEBACKEND_HTTP #include @@ -22,9 +24,26 @@ HTTPConnector::HTTPConnector(std::map options) { this->d_url_suffix = ""; } this->timeout = 2; + this->d_post = false; + this->d_post_json = false; + if (options.find("timeout") != options.end()) { this->timeout = boost::lexical_cast(options.find("timeout")->second)/1000; } + if (options.find("post") != options.end()) { + std::string val = options.find("post")->second; + if (val == "yes" || val == "true" || val == "on" || val == "1") { + this->d_post = true; + } + } + if (options.find("post_json") != options.end()) { + std::string val = options.find("post_json")->second; + if (val == "yes" || val == "true" || val == "on" || val == "1") { + this->d_post_json = true; + } + } + if (options.find("capath") != options.end()) this->d_capath = options.find("capath")->second; + if (options.find("cafile") != options.end()) this->d_cafile = options.find("cafile")->second; } HTTPConnector::~HTTPConnector() { @@ -40,12 +59,15 @@ size_t httpconnector_write_data(void *buffer, size_t size, size_t nmemb, void *u } // converts json value into string -void HTTPConnector::json2string(const rapidjson::Value &input, std::string &output) { +bool HTTPConnector::json2string(const rapidjson::Value &input, std::string &output) { if (input.IsString()) output = input.GetString(); else if (input.IsNull()) output = ""; + else if (input.IsUint64()) output = lexical_cast(input.GetUint64()); + else if (input.IsInt64()) output = lexical_cast(input.GetInt64()); else if (input.IsUint()) output = lexical_cast(input.GetUint()); else if (input.IsInt()) output = lexical_cast(input.GetInt()); - else output = "inconvertible value"; + else return false; + return true; } void HTTPConnector::addUrlComponent(const rapidjson::Value ¶meters, const char *element, std::stringstream& ss) { @@ -59,9 +81,36 @@ void HTTPConnector::addUrlComponent(const rapidjson::Value ¶meters, const ch } } +template std::string buildMemberListArgs(std::string prefix, const T* value, CURL* curlContext) { + std::stringstream stream; + + for (rapidjson::Value::ConstMemberIterator itr = value->MemberBegin(); itr != value->MemberEnd(); itr++) { + stream << prefix << "[" << itr->name.GetString() << "]="; -// builds our request -void HTTPConnector::requestbuilder(const std::string &method, const rapidjson::Value ¶meters, struct curl_slist **slist) + if (itr->value.IsUint64()) { + stream << itr->value.GetUint64(); + } else if (itr->value.IsInt64()) { + stream << itr->value.GetInt64(); + } else if (itr->value.IsUint()) { + stream << itr->value.GetUint(); + } else if (itr->value.IsInt()) { + stream << itr->value.GetInt(); + } else if (itr->value.IsBool()) { + stream << (itr->value.GetBool() ? 1 : 0); + } else if (itr->value.IsString()) { + char *tmpstr = curl_easy_escape(curlContext, itr->value.GetString(), 0); + stream << tmpstr; + curl_free(tmpstr); + } + + stream << "&"; + } + + return stream.str(); +} + +// builds our request (near-restful) +void HTTPConnector::restful_requestbuilder(const std::string &method, const rapidjson::Value ¶meters, struct curl_slist **slist) { std::stringstream ss; std::string sparam; @@ -77,16 +126,13 @@ void HTTPConnector::requestbuilder(const std::string &method, const rapidjson::V // id must be first due to the fact that the qname/name can be empty addUrlComponent(parameters, "id", ss); + addUrlComponent(parameters, "domain_id", ss); addUrlComponent(parameters, "zonename", ss); addUrlComponent(parameters, "qname", ss); addUrlComponent(parameters, "name", ss); addUrlComponent(parameters, "kind", ss); addUrlComponent(parameters, "qtype", ss); - // 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") { @@ -106,8 +152,86 @@ void HTTPConnector::requestbuilder(const std::string &method, const rapidjson::V curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, postfields); curl_free(tmpstr); delete postfields; + } else if (method == "superMasterBackend") { + std::stringstream ss2; + addUrlComponent(parameters, "ip", ss); + addUrlComponent(parameters, "domain", ss); + // then we need to serialize rrset payload into POST + size_t index = 0; + for(rapidjson::Value::ConstValueIterator itr = parameters["nsset"].Begin(); itr != parameters["nsset"].End(); itr++) { + index++; + ss2 << buildMemberListArgs("nsset[" + boost::lexical_cast(index) + "]", itr, d_c); + } + // then give it to curl + std::string out = ss2.str(); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, out.size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, out.c_str()); + } else if (method == "createSlaveDomain") { + addUrlComponent(parameters, "ip", ss); + addUrlComponent(parameters, "domain", ss); + if (parameters.HasMember("account")) { + std::string out = parameters["account"].GetString(); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, out.size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, out.c_str()); + } else { + curl_easy_setopt(d_c, CURLOPT_POST, 1); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, 0); + } + } else if (method == "replaceRRSet") { + std::stringstream ss2; + size_t index = 0; + for(rapidjson::Value::ConstValueIterator itr = parameters["rrset"].Begin(); itr != parameters["rrset"].End(); itr++) { + index++; + ss2 << buildMemberListArgs("rrset[" + boost::lexical_cast(index) + "]", itr, d_c); + } + // then give it to curl + std::string out = ss2.str(); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, out.size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, out.c_str()); + } else if (method == "feedRecord") { + std::string out = buildMemberListArgs("rr", ¶meters["rr"], d_c); + addUrlComponent(parameters, "trxid", ss); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, out.size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, out.c_str()); + } else if (method == "feedEnts") { + std::stringstream ss2; + addUrlComponent(parameters, "trxid", ss); + for(rapidjson::Value::ConstValueIterator itr = parameters["nonterm"].Begin(); itr != parameters["nonterm"].End(); itr++) { + tmpstr = curl_easy_escape(d_c, itr->GetString(), 0); + ss2 << "nonterm[]=" << tmpstr << "&"; + curl_free(tmpstr); + } + std::string out = ss2.str(); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, out.size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, out.c_str()); + } else if (method == "feedEnts3") { + std::stringstream ss2; + addUrlComponent(parameters, "domain", ss); + addUrlComponent(parameters, "trxid", ss); + ss2 << "times=" << parameters["times"].GetInt() << "&salt=" << parameters["salt"].GetString() << "&narrow=" << (parameters["narrow"].GetBool() ? 1 : 0) << "&"; + for(rapidjson::Value::ConstValueIterator itr = parameters["nonterm"].Begin(); itr != parameters["nonterm"].End(); itr++) { + tmpstr = curl_easy_escape(d_c, itr->GetString(), 0); + ss2 << "nonterm[]=" << tmpstr << "&"; + curl_free(tmpstr); + } + std::string out = ss2.str(); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, out.size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, out.c_str()); + } else if (method == "startTransaction") { + addUrlComponent(parameters, "domain", ss); + addUrlComponent(parameters, "trxid", ss); + curl_easy_setopt(d_c, CURLOPT_POST, 1); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, 0); + } else if (method == "commitTransaction" || method == "abortTransaction") { + addUrlComponent(parameters, "trxid", ss); + curl_easy_setopt(d_c, CURLOPT_POST, 1); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, 0); + } else if (method == "calculateSOASerial") { + addUrlComponent(parameters, "domain", ss); + std::string out = buildMemberListArgs("sd", ¶meters["sd"], d_c); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, out.size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, out.c_str()); } else if (method == "setDomainMetadata") { - int n=0; // copy all metadata values into post std::stringstream ss2; const rapidjson::Value& param = parameters["value"]; @@ -115,7 +239,7 @@ void HTTPConnector::requestbuilder(const std::string &method, const rapidjson::V // this one has values too if (param.IsArray()) { for(rapidjson::Value::ConstValueIterator i = param.Begin(); i != param.End(); i++) { - ss2 << "value" << (++n) << "=" << i->GetString() << "&"; + ss2 << "value[]=" << i->GetString() << "&"; } } sparam = ss2.str(); @@ -124,6 +248,12 @@ void HTTPConnector::requestbuilder(const std::string &method, const rapidjson::V } else if (method == "removeDomainKey") { // this one is delete curl_easy_setopt(d_c, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (method == "setNotified") { + tmpstr = (char*)malloc(128); + snprintf(tmpstr, 128, "serial=%u", parameters["serial"].GetInt()); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, strlen(tmpstr)); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, tmpstr); + free(tmpstr); } else { // perform normal get curl_easy_setopt(d_c, CURLOPT_HTTPGET, 1); @@ -138,15 +268,49 @@ void HTTPConnector::requestbuilder(const std::string &method, const rapidjson::V !strncmp(member,"name",4) || !strncmp(member,"kind",4) || !strncmp(member,"qtype",5) || !strncmp(member,"id",2) || !strncmp(member,"key",3)) continue; - json2string(parameters[member], sparam); - snprintf(header, sizeof header, "X-RemoteBackend-%s: %s", iter->name.GetString(), sparam.c_str()); - (*slist) = curl_slist_append((*slist), header); + if (json2string(parameters[member], sparam)) { + snprintf(header, sizeof header, "X-RemoteBackend-%s: %s", iter->name.GetString(), sparam.c_str()); + (*slist) = curl_slist_append((*slist), header); + } }; + // finally add suffix and store url + ss << d_url_suffix; + curl_easy_setopt(d_c, CURLOPT_URL, ss.str().c_str()); + // store headers into request curl_easy_setopt(d_c, CURLOPT_HTTPHEADER, *slist); } +void HTTPConnector::post_requestbuilder(const rapidjson::Document &input, struct curl_slist **slist) { + if (this->d_post_json) { + // simple case, POST JSON into url. nothing fancy. + std::string out = makeStringFromDocument(input); + (*slist) = curl_slist_append((*slist), "Content-Type: text/javascript; charset=utf-8"); + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, out.size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, out.c_str()); + curl_easy_setopt(d_c, CURLOPT_URL, d_url.c_str()); + curl_easy_setopt(d_c, CURLOPT_HTTPHEADER, *slist); + } else { + std::stringstream url,content; + char *tmpstr; + // call url/method.suffix + rapidjson::StringBuffer output; + rapidjson::Writer w(output); + input["parameters"].Accept(w); + url << d_url << "/" << input["method"].GetString() << d_url_suffix; + // then build content + tmpstr = curl_easy_escape(d_c, output.GetString(), 0); + content << "parameters=" << tmpstr; + // convert into parameters=urlencoded + curl_easy_setopt(d_c, CURLOPT_POSTFIELDSIZE, content.str().size()); + curl_easy_setopt(d_c, CURLOPT_COPYPOSTFIELDS, content.str().c_str()); + free(tmpstr); + curl_easy_setopt(d_c, CURLOPT_URL, d_url.c_str()); + curl_easy_setopt(d_c, CURLOPT_URL, url.str().c_str()); + } +} + int HTTPConnector::send_message(const rapidjson::Document &input) { int rv; long rcode; @@ -160,11 +324,26 @@ int HTTPConnector::send_message(const rapidjson::Document &input) { d_data = ""; curl_easy_setopt(d_c, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(d_c, CURLOPT_TIMEOUT, this->timeout); + + // turn off peer verification or set verification roots + if (d_capath.empty()) { + if (d_cafile.empty()) { + curl_easy_setopt(d_c, CURLOPT_SSL_VERIFYPEER, 0); + } else { + curl_easy_setopt(d_c, CURLOPT_CAINFO, d_cafile.c_str()); + } + } else { + curl_easy_setopt(d_c, CURLOPT_CAPATH, d_capath.c_str()); + } slist = NULL; - // build request - requestbuilder(input["method"].GetString(), input["parameters"], &slist); + // build request based on mode + + if (d_post) + post_requestbuilder(input, &slist); + else + restful_requestbuilder(input["method"].GetString(), input["parameters"], &slist); // setup write function helper curl_easy_setopt(d_c, CURLOPT_WRITEFUNCTION, &(httpconnector_write_data)); diff --git a/modules/remotebackend/regression-tests/backend.rb b/modules/remotebackend/regression-tests/backend.rb index 444e402182..8f85941809 100755 --- a/modules/remotebackend/regression-tests/backend.rb +++ b/modules/remotebackend/regression-tests/backend.rb @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.9.1 require 'json' require 'sqlite3' diff --git a/modules/remotebackend/regression-tests/dnsbackend.rb b/modules/remotebackend/regression-tests/dnsbackend.rb index dbf9e95835..aaac5a5b7f 100644 --- a/modules/remotebackend/regression-tests/dnsbackend.rb +++ b/modules/remotebackend/regression-tests/dnsbackend.rb @@ -8,6 +8,28 @@ class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet @f = File.open("/tmp/tmp.txt","a") end + def parse_arrays(params) + newparams = {} + params.each do |key,val| + if key=~/^(.*)\[(.*)\]\[(.*)\]/ + newparams[$1] = {} unless newparams.has_key? $1 + newparams[$1][$2] = {} unless newparams[$1].has_key? $2 + newparams[$1][$2][$3] = val + params.delete key + elsif key=~/^(.*)\[(.*)\]/ + if $2 == "" + newparams[$1] = [] unless newparams.has_key? $1 + newparams[$1] << val + else + newparams[$1] = {} unless newparams.has_key? $1 + newparams[$1][$2] = val + end + params.delete key + end + end + params.merge newparams + end + def parse_url(url) url = url.split('/') method = url.shift.downcase @@ -75,13 +97,8 @@ class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet } end - if method == "do_setdomainmetadata" - args["value"] = [] - args.each do |k,a| - args["value"] << a if k[/^value/] - end - end - + args = parse_arrays args + @f.puts method @f.puts args diff --git a/modules/remotebackend/regression-tests/http-backend.rb b/modules/remotebackend/regression-tests/http-backend.rb index 6bf6be373b..8be678a877 100755 --- a/modules/remotebackend/regression-tests/http-backend.rb +++ b/modules/remotebackend/regression-tests/http-backend.rb @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.9.1 require "rubygems" #require "bundler/setup" require "webrick" diff --git a/modules/remotebackend/regression-tests/pipe-backend.rb b/modules/remotebackend/regression-tests/pipe-backend.rb index 271c782128..27f1213d37 100755 --- a/modules/remotebackend/regression-tests/pipe-backend.rb +++ b/modules/remotebackend/regression-tests/pipe-backend.rb @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.9.1 require 'json' require '../modules/remotebackend/regression-tests/backend' diff --git a/modules/remotebackend/remotebackend.cc b/modules/remotebackend/remotebackend.cc index 0e9d132dce..e81a6f3c85 100644 --- a/modules/remotebackend/remotebackend.cc +++ b/modules/remotebackend/remotebackend.cc @@ -43,12 +43,16 @@ bool Connector::recv(rapidjson::Document &value) { return false; } +/** + * Standard ctor and dtor + */ RemoteBackend::RemoteBackend(const std::string &suffix) { setArgPrefix("remote"+suffix); build(getArg("connection-string")); this->d_dnssec = mustDo("dnssec"); this->d_index = -1; + this->d_trxid = 0; } RemoteBackend::~RemoteBackend() { @@ -327,7 +331,10 @@ bool RemoteBackend::getDomainKeys(const std::string& name, unsigned int kind, st DNSBackend::KeyData key; key.id = (*iter)["id"].GetUint(); key.flags = (*iter)["flags"].GetUint(); - key.active = (*iter)["active"].GetBool(); + if ((*iter)["active"].IsBool()) + key.active = (*iter)["active"].GetBool(); + else + key.active = ((*iter)["active"].GetInt() != 0 ? true : false ); // case where it's returned as non-boolean key.content = (*iter)["content"].GetString(); keys.push_back(key); } @@ -342,7 +349,7 @@ bool RemoteBackend::removeDomainKey(const string& name, unsigned int id) { if (d_dnssec == false) return false; query.SetObject(); - JSON_ADD_MEMBER(query, "method", "getDomainKeys", query.GetAllocator()); + JSON_ADD_MEMBER(query, "method", "removeDomainKey", query.GetAllocator()); parameters.SetObject(); JSON_ADD_MEMBER(parameters, "name", name.c_str(), query.GetAllocator()); JSON_ADD_MEMBER(parameters, "id", id, query.GetAllocator()); @@ -351,7 +358,7 @@ bool RemoteBackend::removeDomainKey(const string& name, unsigned int id) { if (connector->send(query) == false || connector->recv(answer) == false) return false; - return answer["result"].GetBool(); + return true; } int RemoteBackend::addDomainKey(const string& name, const KeyData& key) { @@ -394,7 +401,7 @@ bool RemoteBackend::activateDomainKey(const string& name, unsigned int id) { if (connector->send(query) == false || connector->recv(answer) == false) return false; - return answer["result"].GetBool(); + return true; } bool RemoteBackend::deactivateDomainKey(const string& name, unsigned int id) { @@ -414,7 +421,7 @@ bool RemoteBackend::deactivateDomainKey(const string& name, unsigned int id) { if (connector->send(query) == false || connector->recv(answer) == false) return false; - return answer["result"].GetBool(); + return true; } bool RemoteBackend::doesDNSSEC() { @@ -499,13 +506,272 @@ void RemoteBackend::setNotified(uint32_t id, uint32_t serial) { JSON_ADD_MEMBER(query, "method", "setNotified", query.GetAllocator()); parameters.SetObject(); JSON_ADD_MEMBER(parameters, "id", id, query.GetAllocator()); - JSON_ADD_MEMBER(parameters, "serial", id, query.GetAllocator()); - + JSON_ADD_MEMBER(parameters, "serial", serial, query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + if (connector->send(query) == false || connector->recv(answer) == false) { L<&nsset, string *account, DNSBackend **ddb) +{ + rapidjson::Document query,answer; + rapidjson::Value parameters; + rapidjson::Value rrset; + + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "superMasterBackend", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "ip", ip.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "domain", domain.c_str(), query.GetAllocator()); + rrset.SetArray(); + rrset.Reserve(nsset.size(), query.GetAllocator()); + for(rapidjson::SizeType i = 0; i < nsset.size(); i++) { + rapidjson::Value rr; + rr.SetObject(); + JSON_ADD_MEMBER(rr, "qtype", nsset[i].qtype.getName().c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rr, "qname", nsset[i].qname.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rr, "qclass", QClass::IN, query.GetAllocator()); + JSON_ADD_MEMBER(rr, "content", nsset[i].content.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rr, "ttl", nsset[i].ttl, query.GetAllocator()); + JSON_ADD_MEMBER(rr, "priority", nsset[i].priority, query.GetAllocator()); + JSON_ADD_MEMBER(rr, "auth", nsset[i].auth, query.GetAllocator()); + rrset.PushBack(rr, query.GetAllocator()); + } + parameters.AddMember("nsset", rrset, query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + + *ddb = 0; + + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + + // we are the backend + *ddb = this; + + // we allow simple true as well... + if (answer["result"].IsObject() && answer["result"].HasMember("account")) + *account = answer["result"]["account"].GetString(); + + return true; +} + +bool RemoteBackend::createSlaveDomain(const string &ip, const string &domain, const string &account) { + rapidjson::Document query,answer; + rapidjson::Value parameters; + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "createSlaveDomain", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "ip", ip.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "domain", domain.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "account", account.c_str(), query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + return true; +} + +bool RemoteBackend::replaceRRSet(uint32_t domain_id, const string& qname, const QType& qtype, const vector& rrset) { + rapidjson::Document query,answer; + rapidjson::Value parameters; + rapidjson::Value rj_rrset; + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "replaceRRSet", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "domain_id", domain_id, query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "qname", qname.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "qtype", qtype.getName().c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "trxid", d_trxid, query.GetAllocator()); + + rj_rrset.SetArray(); + rj_rrset.Reserve(rrset.size(), query.GetAllocator()); + + for(rapidjson::SizeType i = 0; i < rrset.size(); i++) { + rapidjson::Value rr; + rr.SetObject(); + JSON_ADD_MEMBER(rr, "qtype", rrset[i].qtype.getName().c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rr, "qname", rrset[i].qname.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rr, "qclass", QClass::IN, query.GetAllocator()); + JSON_ADD_MEMBER(rr, "content", rrset[i].content.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rr, "ttl", rrset[i].ttl, query.GetAllocator()); + JSON_ADD_MEMBER(rr, "priority", rrset[i].priority, query.GetAllocator()); + JSON_ADD_MEMBER(rr, "auth", rrset[i].auth, query.GetAllocator()); + rj_rrset.PushBack(rr, query.GetAllocator()); + } + parameters.AddMember("rrset", rj_rrset, query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + + return true; +} + +bool RemoteBackend::feedRecord(const DNSResourceRecord &rr, string *ordername) { + rapidjson::Document query,answer; + rapidjson::Value parameters,rj_rr; + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "feedRecord", query.GetAllocator()); + parameters.SetObject(); + rj_rr.SetObject(); + JSON_ADD_MEMBER(rj_rr, "qtype", rr.qtype.getName().c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rj_rr, "qname", rr.qname.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rj_rr, "qclass", QClass::IN, query.GetAllocator()); + JSON_ADD_MEMBER(rj_rr, "content", rr.content.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(rj_rr, "ttl", rr.ttl, query.GetAllocator()); + JSON_ADD_MEMBER(rj_rr, "priority", rr.priority, query.GetAllocator()); + JSON_ADD_MEMBER(rj_rr, "auth", rr.auth, query.GetAllocator()); + parameters.AddMember("rr", rj_rr, query.GetAllocator()); + + JSON_ADD_MEMBER(parameters, "trxid", d_trxid, query.GetAllocator()); + + if (ordername) { + JSON_ADD_MEMBER(parameters, "ordername", ordername->c_str(), query.GetAllocator()); + } + + query.AddMember("parameters", parameters, query.GetAllocator()); + + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + return true; // XXX FIXME this API should not return 'true' I think -ahu +} + +bool RemoteBackend::feedEnts(int domain_id, set& nonterm) { + rapidjson::Document query,answer; + rapidjson::Value parameters; + rapidjson::Value nts; + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "feedEnts", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "domain_id", domain_id, query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "trxid", d_trxid, query.GetAllocator()); + nts.SetArray(); + BOOST_FOREACH(const string &t, nonterm) { + nts.PushBack(t.c_str(), query.GetAllocator()); + } + parameters.AddMember("nonterm", nts, query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + return true; +} + +bool RemoteBackend::feedEnts3(int domain_id, const string &domain, set &nonterm, unsigned int times, const string &salt, bool narrow) { + rapidjson::Document query,answer; + rapidjson::Value parameters; + rapidjson::Value nts; + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "feedEnts3", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "domain_id", domain_id, query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "domain", domain.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "times", times, query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "salt", salt.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "narrow", narrow, query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "trxid", d_trxid, query.GetAllocator()); + + nts.SetArray(); + BOOST_FOREACH(const string &t, nonterm) { + nts.PushBack(t.c_str(), query.GetAllocator()); + } + parameters.AddMember("nonterm", nts, query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + return true; +} + +bool RemoteBackend::startTransaction(const string &domain, int domain_id) { + rapidjson::Document query,answer; + rapidjson::Value parameters; + this->d_trxid = time((time_t*)NULL); + + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "startTransaction", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "domain", domain.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "domain_id", domain_id, query.GetAllocator()); + JSON_ADD_MEMBER(parameters, "trxid", d_trxid, query.GetAllocator()); + + query.AddMember("parameters", parameters, query.GetAllocator()); + + if (connector->send(query) == false || connector->recv(answer) == false) { + d_trxid = -1; + return false; + } + return true; + +} +bool RemoteBackend::commitTransaction() { + rapidjson::Document query,answer; + rapidjson::Value parameters; + + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "abortTransaction", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "trxid", d_trxid, query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + + d_trxid = -1; + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + return true; +} + +bool RemoteBackend::abortTransaction() { + rapidjson::Document query,answer; + rapidjson::Value parameters; + + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "commitTransaction", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "trxid", d_trxid, query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + + d_trxid = -1; + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + return true; +} + +bool RemoteBackend::calculateSOASerial(const string& domain, const SOAData& sd, time_t& serial) { + rapidjson::Document query,answer; + rapidjson::Value parameters; + rapidjson::Value soadata; + + query.SetObject(); + JSON_ADD_MEMBER(query, "method", "calculateSOASerial", query.GetAllocator()); + parameters.SetObject(); + JSON_ADD_MEMBER(parameters, "domain", domain.c_str(), query.GetAllocator()); + soadata.SetObject(); + JSON_ADD_MEMBER(soadata, "qname", sd.qname.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "nameserver", sd.nameserver.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "hostmaster", sd.hostmaster.c_str(), query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "ttl", sd.ttl, query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "serial", sd.serial, query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "refresh", sd.refresh, query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "retry", sd.retry, query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "expire", sd.expire, query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "default_ttl", sd.default_ttl, query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "domain_id", sd.domain_id, query.GetAllocator()); + JSON_ADD_MEMBER(soadata, "scopeMask", sd.scopeMask, query.GetAllocator()); + parameters.AddMember("sd", soadata, query.GetAllocator()); + query.AddMember("parameters", parameters, query.GetAllocator()); + + if (connector->send(query) == false || connector->recv(answer) == false) + return false; + + if (answer["result"].IsInt64() == false) + return false; + + serial = answer["result"].GetInt64(); + return true; +} + DNSBackend *RemoteBackend::maker() { try { diff --git a/modules/remotebackend/remotebackend.hh b/modules/remotebackend/remotebackend.hh index 7bc02b71e3..c1758d8a4e 100644 --- a/modules/remotebackend/remotebackend.hh +++ b/modules/remotebackend/remotebackend.hh @@ -66,8 +66,13 @@ class HTTPConnector: public Connector { CURL *d_c; std::string d_data; int timeout; - void json2string(const rapidjson::Value &input, std::string &output); - void requestbuilder(const std::string &method, const rapidjson::Value ¶meters, struct curl_slist **slist); + bool d_post; + bool d_post_json; + std::string d_capath; + std::string d_cafile; + bool json2string(const rapidjson::Value &input, std::string &output); + void restful_requestbuilder(const std::string &method, const rapidjson::Value ¶meters, struct curl_slist **slist); + void post_requestbuilder(const rapidjson::Document &input, struct curl_slist **slist); void addUrlComponent(const rapidjson::Value ¶meters, const char *element, std::stringstream& ss); }; #endif @@ -111,6 +116,16 @@ class RemoteBackend : public DNSBackend virtual bool getDomainInfo(const string&, DomainInfo&); virtual void setNotified(uint32_t id, uint32_t serial); virtual bool doesDNSSEC(); + virtual bool superMasterBackend(const string &ip, const string &domain, const vector&nsset, string *account, DNSBackend **ddb); + virtual bool createSlaveDomain(const string &ip, const string &domain, const string &account); + virtual bool replaceRRSet(uint32_t domain_id, const string& qname, const QType& qt, const vector& rrset); + virtual bool feedRecord(const DNSResourceRecord &r, string *ordername); + virtual bool feedEnts(int domain_id, set& nonterm); + virtual bool feedEnts3(int domain_id, const string &domain, set &nonterm, unsigned int times, const string &salt, bool narrow); + virtual bool startTransaction(const string &domain, int domain_id); + virtual bool commitTransaction(); + virtual bool abortTransaction(); + virtual bool calculateSOASerial(const string& domain, const SOAData& sd, time_t& serial); static DNSBackend *maker(); @@ -119,6 +134,7 @@ class RemoteBackend : public DNSBackend Connector *connector; bool d_dnssec; rapidjson::Document *d_result; - int d_index; + int d_index; + int64_t d_trxid; }; #endif diff --git a/modules/remotebackend/test-remotebackend-http.cc b/modules/remotebackend/test-remotebackend-http.cc new file mode 100644 index 0000000000..f91d05ad9f --- /dev/null +++ b/modules/remotebackend/test-remotebackend-http.cc @@ -0,0 +1,73 @@ +#include "pdns/namespaces.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pdns/json.hh" +#include "pdns/statbag.hh" +#include "pdns/packetcache.hh" + +StatBag S; +PacketCache PC; +ArgvMap &arg() +{ + static ArgvMap arg; + return arg; +}; + +class RemoteLoader +{ + public: + RemoteLoader(); +}; + +DNSBackend *be; + +#ifdef REMOTEBACKEND_HTTP + +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_MAIN +#define BOOST_TEST_MODULE unit + +#include +#include +#include +#include + +struct RemotebackendSetup { + RemotebackendSetup() { + be = 0; + try { + // setup minimum arguments + ::arg().set("module-dir")=""; + new RemoteLoader(); + BackendMakers().launch("remote"); + // then get us a instance of it + ::arg().set("remote-connection-string")="http:url=http://localhost:62434/dns"; + ::arg().set("remote-dnssec")="yes"; + be = BackendMakers().all()[0]; + } catch (AhuException &ex) { + BOOST_TEST_MESSAGE("Cannot start remotebackend: " << ex.reason ); + }; + } + ~RemotebackendSetup() { } +}; + +BOOST_GLOBAL_FIXTURE( RemotebackendSetup ); + +#else + +#include + +int main(void) { + std::cout << "No HTTP support in remotebackend - skipping test" << std::endl; + return 0; +} + +#endif diff --git a/modules/remotebackend/test-remotebackend-json.cc b/modules/remotebackend/test-remotebackend-json.cc new file mode 100644 index 0000000000..1522d52ab7 --- /dev/null +++ b/modules/remotebackend/test-remotebackend-json.cc @@ -0,0 +1,73 @@ +#include "pdns/namespaces.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pdns/json.hh" +#include "pdns/statbag.hh" +#include "pdns/packetcache.hh" + +StatBag S; +PacketCache PC; +ArgvMap &arg() +{ + static ArgvMap arg; + return arg; +}; + +class RemoteLoader +{ + public: + RemoteLoader(); +}; + +DNSBackend *be; + +#ifdef REMOTEBACKEND_HTTP + +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_MAIN +#define BOOST_TEST_MODULE unit + +#include +#include +#include +#include + +struct RemotebackendSetup { + RemotebackendSetup() { + be = 0; + try { + // setup minimum arguments + ::arg().set("module-dir")=""; + new RemoteLoader(); + BackendMakers().launch("remote"); + // then get us a instance of it + ::arg().set("remote-connection-string")="http:url=http://localhost:62434/dns/endpoint.json,post=1,post_json=1"; + ::arg().set("remote-dnssec")="yes"; + be = BackendMakers().all()[0]; + } catch (AhuException &ex) { + BOOST_TEST_MESSAGE("Cannot start remotebackend: " << ex.reason ); + }; + } + ~RemotebackendSetup() { } +}; + +BOOST_GLOBAL_FIXTURE( RemotebackendSetup ); + +#else + +#include + +int main(void) { + std::cout << "No HTTP support in remotebackend - skipping test" << std::endl; + return 0; +} + +#endif diff --git a/modules/remotebackend/test-remotebackend-keys.hh b/modules/remotebackend/test-remotebackend-keys.hh new file mode 100644 index 0000000000..2ccc9b42ba --- /dev/null +++ b/modules/remotebackend/test-remotebackend-keys.hh @@ -0,0 +1,3 @@ +DNSBackend::KeyData k1 = {1, 257, true, std::string("Private-key-format: v1.2\nAlgorithm: 5 (RSASHA1)\nModulus: qpe9fxlN4dBT38cLPWtqljZhcJjbqRprj9XsYmf2/uFu4kA5sHYrlQY7H9lpzGJPRfOAfxShBpKs1AVaVInfJQ==\nPublicExponent: AQAB\nPrivateExponent: Ad3YogzXvVDLsWuAfioY571QlolbdTbzVlhLEMLD6dSRx+xcZgw6c27ak2HAH00iSKTvqK3AyeaK8Eqy/oJ5QQ==\nPrime1: wo8LZrdU2y0xLGCeLhwziQDDtTMi18NEIwlx8tUPnhs=\nPrime2: 4HcuFqgo7NOiXFvN+V2PT+QaIt2+oi6D2m8/qtTDS78=\nExponent1: GUdCoPbi9JM7l1t6Ud1iKMPLqchaF5SMTs0UXAuous8=\nExponent2: nzgKqimX9f1corTAEw0pddrwKyEtcu8ZuhzFhZCsAxM=\nCoefficient: YGNxbulf5GTNiIu0oNKmAF0khNtx9layjOPEI0R4/RY=") }; + +DNSBackend::KeyData k2 = {2, 256, true, std::string("Private-key-format: v1.2\nAlgorithm: 5 (RSASHA1)\nModulus: tY2TAMgL/whZdSbn2aci4wcMqohO24KQAaq5RlTRwQ33M8FYdW5fZ3DMdMsSLQUkjGnKJPKEdN3Qd4Z5b18f+w==\nPublicExponent: AQAB\nPrivateExponent: BB6xibPNPrBV0PUp3CQq0OdFpk9v9EZ2NiBFrA7osG5mGIZICqgOx/zlHiHKmX4OLmL28oU7jPKgogeuONXJQQ==\nPrime1: yjxe/iHQ4IBWpvCmuGqhxApWF+DY9LADIP7bM3Ejf3M=\nPrime2: 5dGWTyYEQRBVK74q1a64iXgaNuYm1pbClvvZ6ccCq1k=\nExponent1: TwM5RebmWeAqerzJFoIqw5IaQugJO8hM4KZR9A4/BTs=\nExponent2: bpV2HSmu3Fvuj7jWxbFoDIXlH0uJnrI2eg4/4hSnvSk=\nCoefficient: e2uDDWN2zXwYa2P6VQBWQ4mR1ZZjFEtO/+YqOJZun1Y=") }; diff --git a/modules/remotebackend/test-remotebackend-pipe.cc b/modules/remotebackend/test-remotebackend-pipe.cc new file mode 100644 index 0000000000..9a4eb15547 --- /dev/null +++ b/modules/remotebackend/test-remotebackend-pipe.cc @@ -0,0 +1,65 @@ +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_MAIN +#define BOOST_TEST_MODULE unit + +#include +#include +#include +#include +#include "pdns/namespaces.hh" +#include +#include +#include +#include +#include +#include +#include +#include "pdns/dnsrecords.hh" +#include +#include +#include +#include "pdns/json.hh" +#include "pdns/statbag.hh" +#include "pdns/packetcache.hh" + +StatBag S; +PacketCache PC; +ArgvMap &arg() +{ + static ArgvMap arg; + return arg; +}; + +class RemoteLoader +{ + public: + RemoteLoader(); +}; + +DNSBackend *be; + +struct RemotebackendSetup { + RemotebackendSetup() { + be = 0; + try { + // setup minimum arguments + ::arg().set("module-dir")=""; + new RemoteLoader(); + BackendMakers().launch("remote"); + // then get us a instance of it + ::arg().set("remote-connection-string")="pipe:command=unittest_pipe.rb"; + ::arg().set("remote-dnssec")="yes"; + be = BackendMakers().all()[0]; + // load few record types to help out + SOARecordContent::report(); + NSRecordContent::report(); + ARecordContent::report(); + } catch (AhuException &ex) { + BOOST_TEST_MESSAGE("Cannot start remotebackend: " << ex.reason ); + }; + } + ~RemotebackendSetup() { } +}; + +BOOST_GLOBAL_FIXTURE( RemotebackendSetup ); + diff --git a/modules/remotebackend/test-remotebackend-post.cc b/modules/remotebackend/test-remotebackend-post.cc new file mode 100644 index 0000000000..754e7a9afd --- /dev/null +++ b/modules/remotebackend/test-remotebackend-post.cc @@ -0,0 +1,73 @@ +#include "pdns/namespaces.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pdns/json.hh" +#include "pdns/statbag.hh" +#include "pdns/packetcache.hh" + +StatBag S; +PacketCache PC; +ArgvMap &arg() +{ + static ArgvMap arg; + return arg; +}; + +class RemoteLoader +{ + public: + RemoteLoader(); +}; + +DNSBackend *be; + +#ifdef REMOTEBACKEND_HTTP + +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_MAIN +#define BOOST_TEST_MODULE unit + +#include +#include +#include +#include + +struct RemotebackendSetup { + RemotebackendSetup() { + be = 0; + try { + // setup minimum arguments + ::arg().set("module-dir")=""; + new RemoteLoader(); + BackendMakers().launch("remote"); + // then get us a instance of it + ::arg().set("remote-connection-string")="http:url=http://localhost:62434/dns,post=1"; + ::arg().set("remote-dnssec")="yes"; + be = BackendMakers().all()[0]; + } catch (AhuException &ex) { + BOOST_TEST_MESSAGE("Cannot start remotebackend: " << ex.reason ); + }; + } + ~RemotebackendSetup() { } +}; + +BOOST_GLOBAL_FIXTURE( RemotebackendSetup ); + +#else + +#include + +int main(void) { + std::cout << "No HTTP support in remotebackend - skipping test" << std::endl; + return 0; +} + +#endif diff --git a/modules/remotebackend/test-remotebackend.cc b/modules/remotebackend/test-remotebackend.cc new file mode 100644 index 0000000000..28427c8a6f --- /dev/null +++ b/modules/remotebackend/test-remotebackend.cc @@ -0,0 +1,247 @@ +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN + +#include +#include +#include +#include +#include "pdns/namespaces.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pdns/json.hh" +#include "pdns/statbag.hh" +#include "pdns/packetcache.hh" + +#include "test-remotebackend-keys.hh" + +extern DNSBackend *be; + +BOOST_AUTO_TEST_SUITE(test_remotebackend_so) + +BOOST_AUTO_TEST_CASE(test_method_lookup) { + BOOST_TEST_MESSAGE("Testing lookup method"); + DNSResourceRecord rr; + be->lookup(QType(QType::SOA), "unit.test"); + // then try to get() + BOOST_CHECK(be->get(rr)); // and this should be TRUE. + // then we check rr contains what we expect + BOOST_CHECK_EQUAL(rr.qname, "unit.test"); + BOOST_CHECK_MESSAGE(rr.qtype == QType::SOA, "returned qtype was not SOA"); + BOOST_CHECK_EQUAL(rr.content, "ns.unit.test hostmaster.unit.test 1 2 3 4 5 6"); + BOOST_CHECK_EQUAL(rr.ttl, 300); +} + +BOOST_AUTO_TEST_CASE(test_method_list) { + int record_count = 0; + DNSResourceRecord rr; + + BOOST_TEST_MESSAGE("Testing list method"); + be->list("unit.test", -1); + while(be->get(rr)) record_count++; + + BOOST_CHECK_EQUAL(record_count, 5); // number of records our test domain has +} + +BOOST_AUTO_TEST_CASE(test_method_doesDNSSEC) { + BOOST_TEST_MESSAGE("Testing doesDNSSEC method"); + BOOST_CHECK(be->doesDNSSEC()); // should be true +} + +BOOST_AUTO_TEST_CASE(test_method_setDomainMetadata) { + std::vector meta; + meta.push_back("VALUE"); + BOOST_TEST_MESSAGE("Testing setDomainMetadata method"); + BOOST_CHECK(be->setDomainMetadata("unit.test","TEST", meta)); +} + +BOOST_AUTO_TEST_CASE(test_method_getDomainMetadata) { + std::vector meta; + BOOST_TEST_MESSAGE("Testing getDomainMetadata method"); + be->getDomainMetadata("unit.test","TEST", meta); + BOOST_CHECK_EQUAL(meta.size(), 1); + // in case we got more than one value, which would be unexpected + // but not fatal + if (meta.size() > 0) + BOOST_CHECK_EQUAL(meta[0], "VALUE"); +} + +BOOST_AUTO_TEST_CASE(test_method_addDomainKey) { + BOOST_TEST_MESSAGE("Testing addDomainKey method"); + BOOST_CHECK_EQUAL(be->addDomainKey("unit.test",k1), 1); + BOOST_CHECK_EQUAL(be->addDomainKey("unit.test",k2), 2); +} + +BOOST_AUTO_TEST_CASE(test_method_getDomainKeys) { + std::vector keys; + BOOST_TEST_MESSAGE("Testing getDomainKeys method"); + // we expect to get two keys + be->getDomainKeys("unit.test",0,keys); + BOOST_CHECK_EQUAL(keys.size(), 2); + // in case we got more than 2 keys, which would be unexpected + // but not fatal + if (keys.size() > 1) { + // check that we have two keys + BOOST_FOREACH(DNSBackend::KeyData &kd, keys) { + BOOST_CHECK(kd.id > 0); + BOOST_CHECK(kd.flags == 256 || kd.flags == 257); + BOOST_CHECK(kd.active == true); + BOOST_CHECK(kd.content.size() > 500); + } + } +} + +BOOST_AUTO_TEST_CASE(test_method_deactivateDomainKey) { + BOOST_TEST_MESSAGE("Testing deactivateDomainKey method"); + BOOST_CHECK(be->deactivateDomainKey("unit.test",1)); +} + +BOOST_AUTO_TEST_CASE(test_method_activateDomainKey) { + BOOST_TEST_MESSAGE("Testing activateDomainKey method"); + BOOST_CHECK(be->activateDomainKey("unit.test",1)); +} + +BOOST_AUTO_TEST_CASE(test_method_removeDomainKey) { + BOOST_CHECK(be->removeDomainKey("unit.test",2)); + BOOST_CHECK(be->removeDomainKey("unit.test",1)); +} + +BOOST_AUTO_TEST_CASE(test_method_getBeforeAndAfterNamesAbsolute) { + std::string unhashed,before,after; + BOOST_TEST_MESSAGE("Testing getBeforeAndAfterNamesAbsolute method"); + + be->getBeforeAndAfterNamesAbsolute(-1, "middle.unit.test", unhashed, before, after); + BOOST_CHECK_EQUAL(unhashed, "middle"); + BOOST_CHECK_EQUAL(before, "begin"); + BOOST_CHECK_EQUAL(after, "stop"); +} + +BOOST_AUTO_TEST_CASE(test_method_getTSIGKey) { + std::string algorithm, content; + BOOST_TEST_MESSAGE("Testing getTSIGKey method"); + be->getTSIGKey("unit.test",&algorithm,&content); + BOOST_CHECK_EQUAL(algorithm, "NULL"); + BOOST_CHECK_EQUAL(content, "NULL"); +} + +BOOST_AUTO_TEST_CASE(test_method_setNotified) { + BOOST_TEST_MESSAGE("Testing setNotified method"); + be->setNotified(1, 2); + BOOST_CHECK(true); // we check this on next step +} + +BOOST_AUTO_TEST_CASE(test_method_getDomainInfo) { + DomainInfo di; + BOOST_TEST_MESSAGE("Testing getDomainInfo method"); + be->getDomainInfo("unit.test", di); + BOOST_CHECK_EQUAL(di.zone, "unit.test"); + BOOST_CHECK_EQUAL(di.serial, 2); + BOOST_CHECK_EQUAL(di.notified_serial, 2); + BOOST_CHECK_EQUAL(di.kind, DomainInfo::Native); + BOOST_CHECK_EQUAL(di.backend, be); +} + +BOOST_AUTO_TEST_CASE(test_method_superMasterBackend) { + DNSResourceRecord rr; + std::vector nsset; + DNSBackend *dbd; + BOOST_TEST_MESSAGE("Testing superMasterBackend method"); + + rr.qname = "example.com"; + rr.qtype = QType::NS; + rr.qclass = QClass::IN; + rr.ttl = 300; + rr.content = "ns1.example.com"; + nsset.push_back(rr); + rr.qname = "example.com"; + rr.qtype = QType::NS; + rr.qclass = QClass::IN; + rr.ttl = 300; + rr.content = "ns2.example.com"; + nsset.push_back(rr); + + BOOST_CHECK(be->superMasterBackend("10.0.0.1", "example.com", nsset, NULL, &dbd)); + + // let's see what we got + BOOST_CHECK_EQUAL(dbd, be); +} + +BOOST_AUTO_TEST_CASE(test_method_createSlaveDomain) { + BOOST_TEST_MESSAGE("Testing createSlaveDomain method"); + BOOST_CHECK(be->createSlaveDomain("10.0.0.1", "pirate.unit.test", "")); +} + +BOOST_AUTO_TEST_CASE(test_method_feedRecord) { + DNSResourceRecord rr; + BOOST_TEST_MESSAGE("Testing feedRecord method"); + be->startTransaction("example.com",2); + rr.qname = "example.com"; + rr.qtype = QType::SOA; + rr.qclass = QClass::IN; + rr.ttl = 300; + rr.content = "ns1.example.com hostmaster.example.com 2013013441 7200 3600 1209600 300"; + BOOST_CHECK(be->feedRecord(rr, NULL)); + rr.qname = "replace.example.com"; + rr.qtype = QType::A; + rr.qclass = QClass::IN; + rr.ttl = 300; + rr.content = "127.0.0.1"; + BOOST_CHECK(be->feedRecord(rr, NULL)); + be->commitTransaction(); +} + +BOOST_AUTO_TEST_CASE(test_method_replaceRRSet) { + be->startTransaction("example.com",2); + DNSResourceRecord rr; + std::vector rrset; + BOOST_TEST_MESSAGE("Testing replaceRRSet method"); + rr.qname = "replace.example.com"; + rr.qtype = QType::A; + rr.qclass = QClass::IN; + rr.ttl = 300; + rr.content = "1.1.1.1"; + rrset.push_back(rr); + BOOST_CHECK(be->replaceRRSet(2, "replace.example.com", QType(QType::A), rrset)); + be->commitTransaction(); +} + +BOOST_AUTO_TEST_CASE(test_method_feedEnts) { + BOOST_TEST_MESSAGE("Testing feedEnts method"); + be->startTransaction("example.com",2); + set nonterm = boost::assign::list_of("_udp")("_sip._udp"); + BOOST_CHECK(be->feedEnts(2, nonterm)); + be->commitTransaction(); +} + +BOOST_AUTO_TEST_CASE(test_method_feedEnts3) { + BOOST_TEST_MESSAGE("Testing feedEnts3 method"); + be->startTransaction("example.com",2); + set nonterm = boost::assign::list_of("_udp")("_sip._udp"); + BOOST_CHECK(be->feedEnts3(2, "example.com", nonterm, 1, "\xaa\xbb\xcc\xdd", 0)); + be->commitTransaction(); +} + +BOOST_AUTO_TEST_CASE(test_method_abortTransaction) { + BOOST_TEST_MESSAGE("Testing abortTransaction method"); + be->startTransaction("example.com",2); + BOOST_CHECK(be->abortTransaction()); +} + +BOOST_AUTO_TEST_CASE(test_method_calculateSOASerial) { + SOAData sd; + time_t serial; + + be->getSOA("unit.test",sd); + BOOST_CHECK(be->calculateSOASerial("unit.test",sd,serial)); + + BOOST_CHECK_EQUAL(serial, 2013060300); +} + +BOOST_AUTO_TEST_SUITE_END(); diff --git a/modules/remotebackend/testrunner.sh b/modules/remotebackend/testrunner.sh new file mode 100644 index 0000000000..7f687a5caf --- /dev/null +++ b/modules/remotebackend/testrunner.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +webrick_pid="" + +function start_web() { + if [ x"$REMOTEBACKEND_HTTP" == "xyes" ]; then + ./unittest_$1.rb & + webrick_pid=$! + sleep 1 + fi +} + +function stop_web() { + if [ ! -z "$webrick_pid" ]; then + kill -TERM $webrick_pid + # wait a moment for it to die + i=0 + while [ $i -lt 5 ]; do + sleep 1 + kill -0 $webrick_pid 2>/dev/null + if [ $? -ne 0 ]; then break; fi + let i=i+1 + done + fi +} + +mode=`basename "$1"` + +case "$mode" in + test_remotebackend_pipe) + ./test_remotebackend_pipe + rv=$? + ;; + test_remotebackend_http) + start_web "http" + ./test_remotebackend_http + rv=$? + stop_web + ;; + test_remotebackend_post) + start_web "post" + ./test_remotebackend_post + rv=$? + stop_web + ;; + test_remotebackend_json) + start_web "json" + ./test_remotebackend_json + rv=$? + stop_web + ;; + *) + echo "Usage: $0 test_remotebackend_(pipe|http|post)" + ;; +esac + +exit $rv diff --git a/modules/remotebackend/unittest.rb b/modules/remotebackend/unittest.rb new file mode 100644 index 0000000000..69ae27fe83 --- /dev/null +++ b/modules/remotebackend/unittest.rb @@ -0,0 +1,224 @@ +require 'rubygems' +require 'json' + +# define a simple $domain + +$ttl = 300 +$notified_serial = 1 + +$domain = { + "unit.test" => { + "SOA" => ["ns.unit.test hostmaster.unit.test 1 2 3 4 5 6"], + "NS" => ["ns1.unit.test", "ns2.unit.test"], + }, + "ns1.unit.test" => { + "A" => ["10.0.0.1"] + }, + "ns2.unit.test" => { + "A" => ["10.0.0.2"] + } +} + +$meta = {} + +$keys = {} + +class Handler + def initialize + end + + 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 + + def do_initialize(*args) + return true, "Test bench initialized" + end + + def do_lookup(args) + ret = [] + if $domain.has_key?(args["qname"]) + if $domain[args["qname"]].has_key?(args["qtype"]) + $domain[args["qname"]][args["qtype"]].each do |rd| + ret << rr(args["qname"], args["qtype"], rd, $ttl) + end + elsif args["qtype"] == 'ANY' + $domain[args["qname"]].each do |qt,qr| + qr.each do |rd| + ret << rr(args["qname"], qt, rd, $ttl) + end + end + end + end + [false] unless ret.size>0 + [ret] + end + + def do_list(args) + ret = [] + if args["zonename"] == "unit.test" + $domain.each do |qname,rdata| + rdata.each do |rtype,rc| + rc.each do |rd| + ret << rr(qname,rtype,rd,$ttl) + end + end + end + end + [false] unless ret.size>0 + [ret] + end + + def do_getdomainmetadata(args) + return [ $meta[args["name"]][args["kind"]] ] if $meta.has_key?(args["name"]) and $meta[args["name"]].has_key?(args["kind"]) + return [false] + end + + def do_setdomainmetadata(args) + $meta[args["name"].to_s] = {} unless $meta.has_key? args["name"] + $meta[args["name"].to_s][args["kind"].to_s] = args["value"].to_a + [true] + end + + def do_adddomainkey(args) + $keys[args["name"]] = [] unless $keys.has_key? args["name"] + id=$keys[args["name"]].size + 1 + args["key"]["id"] = id + $keys[args["name"]] << args["key"] + [id] + end + + def do_getdomainkeys(args) + if $keys.has_key? args["name"] + return [ $keys[args["name"]] ] + end + [false] + end + + def do_activatedomainkey(args) + args["id"] = args["id"].to_i + if $keys.has_key? args["name"] + if $keys[args["name"]][args["id"]-1] + $keys[args["name"]][args["id"]-1]["active"] = true + return [true] + end + end + [false] + end + + def do_deactivatedomainkey(args) + args["id"] = args["id"].to_i + if $keys.has_key? args["name"] + if $keys[args["name"]][args["id"]-1] + $keys[args["name"]][args["id"]-1]["active"] = false + return [true] + end + end + [false] + end + + def do_removedomainkey(args) + args["id"] = args["id"].to_i + if $keys.has_key? args["name"] + if $keys[args["name"]][args["id"]-1] + $keys[args["name"]].delete_at args["id"]-1 + return [true] + end + end + [false] + end + + def do_getbeforeandafternamesabsolute(args) + return [ { :unhashed => "middle", :before => "begin", :after => "stop" } ] if args["qname"] == 'middle.unit.test' + [false] + end + + def do_gettsigkey(args) + if args["name"] == "unit.test" + return [{:algorithm => "NULL", :content => "NULL"}] + end + [false] + end + + def do_setnotified(args) + if args["id"].to_i == 1 + $notified_serial = args["serial"].to_i + return [true] + end + [false] + end + + def do_getdomaininfo(args) + if args["name"] == "unit.test" + return [{ + :id => 1, + :zone => "unit.test", + :masters => ["10.0.0.1"], + :notified_serial => $notified_serial, + :serial => $notified_serial, + :last_check => Time.now.to_i, + :kind => 'native' + }] + end + [false] + end + + def do_supermasterbackend(args) + $domain[args["domain"]] = { + "NS" => args["nsset"] + } + [true] + end + + def do_createslavedomain(args) + $domain[args["domain"]] = { + } + [true] + end + + def do_feedrecord(args) + args.delete "trxid" + rr = args["rr"] + name = rr["qname"] + qtype = rr["qtype"] + $domain[name] = {} unless $domain.has_key? name + $domain[name][qtype] = [] unless $domain[name].has_key? qtype + $domain[name][qtype] << rr["content"] + [true] + end + + def do_replacerrset(args) + $domain[args["qname"]].delete args["qtype"] if $domain.has_key?(args["qname"]) and $domain[args["qname"]].has_key?(args["qtype"]) + args["rrset"] = args["rrset"].values if args["rrset"].is_a?(Hash) + args["rrset"].each do |rr| + self.do_feedrecord({"trxid" => args["trxid"], "rr" => rr}) + end + [true] + end + + def do_feedents(args) + [true] + end + + def do_feedents3(args) + [true] + end + + def do_starttransaction(args) + [true] + end + + def do_committransaction(args) + [true] + end + + def do_aborttransaction(args) + [true] + end + + def do_calculatesoaserial(args) + return [2013060300] if args["sd"]["qname"] == "unit.test" + [false] + end +end + diff --git a/modules/remotebackend/unittest_http.rb b/modules/remotebackend/unittest_http.rb new file mode 100644 index 0000000000..da10670da2 --- /dev/null +++ b/modules/remotebackend/unittest_http.rb @@ -0,0 +1,179 @@ +#!/usr/bin/ruby1.9.1 + +require 'json' +require 'thread' +require "rubygems" +require "webrick" +require "./unittest" + +class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet + def initialize(server, dnsbackend) + @dnsbackend = dnsbackend + @semaphore = Mutex.new + @f = File.open("/tmp/tmp.txt","a") + end + + def parse_arrays(params) + newparams = {} + params.each do |key,val| + if key=~/^(.*)\[(.*)\]\[(.*)\]/ + newparams[$1] = {} unless newparams.has_key? $1 + newparams[$1][$2] = {} unless newparams[$1].has_key? $2 + newparams[$1][$2][$3] = val + params.delete key + elsif key=~/^(.*)\[(.*)\]/ + if $2 == "" + newparams[$1] = [] unless newparams.has_key? $1 + newparams[$1] << val + else + newparams[$1] = {} unless newparams.has_key? $1 + newparams[$1][$2] = val + end + params.delete key + end + end + params.merge newparams + end + + def parse_url(url) + url = url.split('/') + method = url.shift.downcase + + # do some determining based on method names + args = case method + when "lookup" + { + "qname" => url.shift, + "qtype" => url.shift + } + when "list" + { + "zonename" => url.shift + } + when "getbeforeandafternamesabsolute", "getbeforeandafternames" + { + "id" => url.shift.to_i, + "qname" => url.shift + } + when "getdomainmetadata", "setdomainmetadata", "getdomainkeys" + { + "name" => url.shift, + "kind" => url.shift + } + when "removedomainkey", "activatedomainkey", "deactivatedomainkey" + { + "id" => url.shift.to_i, + "name" => url.shift + } + when "adddomainkey", "gettsigkey", "getdomaininfo" + { + "name" => url.shift + } + when "setnotified", "feedents" + { + "id" => url.shift.to_i + } + when "supermasterbackend", "createslavedomain" + { + "ip" => url.shift, + "domain" => url.shift + } + when "feedents3" + { + "id" => url.shift.to_i, + "domain" => url.shift + } + when "starttransaction" + { + "id" => url.shift.to_i, + "domain" => url.shift, + "trxid" => url.shift.to_i + } + when "committransaction", "aborttransaction" + { + "trxid" => url.shift.to_i + } + when "replacerrset" + { + "id" => url.shift.to_i, + "qname" => url.shift, + "qtype" => url.shift + } + else + {} + end + + [method, args] + end + + def do_GET(req,res) + req.continue + + tmp = req.path[/dns\/(.*)/,1] + return 400, "Bad request" if (tmp.nil?) + + method, args = parse_url(tmp) + + method = "do_#{method}" + + # get more arguments + req.each do |k,v| + attr = k[/X-RemoteBackend-(.*)/,1] + if attr + args[attr] = v + end + end + + args = args.merge req.query + + if method == "do_adddomainkey" + args["key"] = { + "flags" => args.delete("flags").to_i, + "active" => args.delete("active").to_i, + "content" => args.delete("content") + } + end + + args = parse_arrays args + + @f.puts method + @f.puts args + + @semaphore.synchronize do + 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 + end + + def do_DELETE(req,res) + do_GET(req,res) + end + + def do_POST(req,res) + do_GET(req,res) + end +end + +server = WEBrick::HTTPServer.new( + :Port=>62434, + :BindAddress=>"localhost", +# Logger: WEBrick::Log.new("remotebackend-server.log"), + :AccessLog=>[ [ File.open("remotebackend-access.log", "w"), WEBrick::AccessLog::COMBINED_LOG_FORMAT ] ] +) + +be = Handler.new +server.mount "/dns", DNSBackendHandler, be + +trap('INT') { server.stop } +trap('TERM') { server.stop } + +server.start diff --git a/modules/remotebackend/unittest_json.rb b/modules/remotebackend/unittest_json.rb new file mode 100644 index 0000000000..1496bd6f10 --- /dev/null +++ b/modules/remotebackend/unittest_json.rb @@ -0,0 +1,58 @@ +#!/usr/bin/ruby1.9.1 + +require 'json' +require 'thread' +require "rubygems" +require "webrick" +require "./unittest" + +class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet + def initialize(server, dnsbackend) + @dnsbackend = dnsbackend + @semaphore = Mutex.new + @f = File.open("/tmp/tmp.txt","a") + end + + def do_POST(req,res) + req.continue + + return 400, "Bad request" unless req.path == "/dns/endpoint.json" + + tmp = JSON::parse(req.body) + method = tmp["method"].downcase + method = "do_#{method}" + args = tmp["parameters"] + + @f.puts method + @f.puts args + + @semaphore.synchronize do + 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 + end +end + +server = WEBrick::HTTPServer.new( + :Port=>62434, + :BindAddress=>"localhost", +# Logger: WEBrick::Log.new("remotebackend-server.log"), + :AccessLog=>[ [ File.open("remotebackend-access.log", "w"), WEBrick::AccessLog::COMBINED_LOG_FORMAT ] ] +) + +be = Handler.new +server.mount "/dns", DNSBackendHandler, be + +trap('INT') { server.stop } +trap('TERM') { server.stop } + +server.start diff --git a/modules/remotebackend/unittest_pipe.rb b/modules/remotebackend/unittest_pipe.rb new file mode 100644 index 0000000000..f3c3c51ede --- /dev/null +++ b/modules/remotebackend/unittest_pipe.rb @@ -0,0 +1,38 @@ +#!/usr/bin/ruby1.9.1 + +require 'rubygems' +require 'json' +require './unittest' + +h = Handler.new() +f = File.open "/tmp/tmp.txt","a" + +STDOUT.sync = true +begin + STDIN.each_line do |line| + f.puts 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 + f.puts({:result => res, :log => log}).to_json + rescue JSON::ParserError + puts ({:result => false, :log => "Cannot parse input #{line}"}).to_json + next + end + end +rescue SystemExit, Interrupt +end diff --git a/modules/remotebackend/unittest_post.rb b/modules/remotebackend/unittest_post.rb new file mode 100644 index 0000000000..079d7ad967 --- /dev/null +++ b/modules/remotebackend/unittest_post.rb @@ -0,0 +1,59 @@ +#!/usr/bin/ruby1.9.1 + +require 'json' +require 'thread' +require "rubygems" +require "webrick" +require "./unittest" + +class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet + def initialize(server, dnsbackend) + @dnsbackend = dnsbackend + @semaphore = Mutex.new + @f = File.open("/tmp/tmp.txt","a") + end + + def do_POST(req,res) + req.continue + + tmp = req.path[/dns\/(.*)/,1] + return 400, "Bad request" if (tmp.nil?) + + url = tmp.split('/') + method = url.shift.downcase + method = "do_#{method}" + args = JSON::parse(req.query["parameters"]) + + @f.puts method + @f.puts args + + @semaphore.synchronize do + 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 + end +end + +server = WEBrick::HTTPServer.new( + :Port=>62434, + :BindAddress=>"localhost", +# Logger: WEBrick::Log.new("remotebackend-server.log"), + :AccessLog=>[ [ File.open("remotebackend-access.log", "w"), WEBrick::AccessLog::COMBINED_LOG_FORMAT ] ] +) + +be = Handler.new +server.mount "/dns", DNSBackendHandler, be + +trap('INT') { server.stop } +trap('TERM') { server.stop } + +server.start diff --git a/pdns/docs/pdns.xml b/pdns/docs/pdns.xml index 6ebe4f83b0..ab42a2df2c 100644 --- a/pdns/docs/pdns.xml +++ b/pdns/docs/pdns.xml @@ -19041,8 +19041,8 @@ record building scripts on his This backend provides unix socket / pipe / http remoting for powerdns. + Important notices + Please do not use remotebackend shipped before version 3.3. This version has severe bug that can crash the entire process. + Compiling To compile this backend, you need to configure --with-modules="remote pipe", for @@ -19063,7 +19066,7 @@ record building scripts on his Usage - The only configuration option for this backend is remote-connection-string. It comprises of two elements: type of backend, and parameters. + The only configuration optionss for backend are remote-connection-string and remote-dnssec. @@ -19099,12 +19102,21 @@ remote-connection-string=unix:command=/path/to/executable,timeout=2000 - HTTP backend + HTTP backend - parameters: url, url-suffix, timeout (default 2000) + parameters: url, url-suffix, post, post_json, cafile, capath, timeout (default 2000) - HTTP backend tries to do RESTful requests to your server. See examples. + +remote-connection-string=http:url=http://localhost:63636/dns,url-suffix=.php + + + + HTTP backend tries to do RESTful requests to your server. See examples. You can also + use post to change behaviour so that it will send POST request to url/method + url_suffix + with parameters=json-formatted-parameters. If you use post and post_json, it will POST + url with text/javascript containing JSON formatted RPC request, just like for pipe and unix. + You can use '1', 'yes', 'on' or 'true' to turn these features on. URL should not end with /, and url-suffix is optional, but if you define it, it's @@ -19112,6 +19124,10 @@ remote-connection-string=unix:command=/path/to/executable,timeout=2000 URL. Timeout is divided by 1000 because libcurl only supports seconds, but this is given in milliseconds for consistency with other backends. + + You can use HTTPS requests. If cafile and capath is left empty, remote SSL certificate is not checked. + HTTP Authentication is not supported. SSL support requires that your cURL is compiled with it. + @@ -19308,7 +19324,7 @@ to appropriate value, otherwise things can go wrong. Query: -GET /dnsapi/list/example.com HTTP/1.1 +GET /dnsapi/list/-1/example.com HTTP/1.1 X-RemoteBackend-domain-id: -1 @@ -19344,7 +19360,7 @@ Content-Type: text/javascript; charset=utf-8 Description Asks the names before and after qname. qname is given without dots or domain part. The query -can also be hashed. Care must be taken to handle wrap-around when qname is first or last in +will be hashed when using NSEC3. Care must be taken to handle wrap-around when qname is first or last in the ordered list. Do not return nil for either one. @@ -19496,7 +19512,7 @@ POST /dnsapi/setdomainmetadata/example.com/PRESIGNED HTTP/1.1 Content-Type: application/x-www-form-urlencoded Content-Length: 12 -value1=YES& +value[]=YES& @@ -20041,6 +20057,656 @@ Content-Type: text/javascript; charset=utf-8 + +Method: superMasterBackend + + + Mandatory: + No + + + Parameters: + ip,domain,nsset,account + + + Reply: + true for success, false for failure. can also return account=>name of account + + + Description + +Creates new domain with given record(s) as master servers. IP address is the address where notify is received from. nsset is array of NS resource records. + + + + Example JSON/RPC: + + + Query: + +{"method":"superMasterBackend","parameters":{"ip":"10.0.0.1","domain":"example.com","nsset":[{"qtype":"NS","qname":"example.com","qclass":1,"content":"ns1.example.com","ttl":300,"priority":0,"auth":true},{"qtype":"NS","qname":"example.com","qclass":1,"content":"ns2.example.com","ttl":300,"priority":0,"auth":true}]}} + + + + Response: + +{"result":true} + + Alternative response: + +{"result":{"account":"my account"}} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/supermasterbackend/10.0.0.1/example.com +Content-Type: application/x-www-form-urlencoded +Content-Length: 317 + +nsset[1][qtype]=NS&nsset[1][qname]=example.com&nsset[1][qclass]=1&nsset[1][content]=ns1.example.com&nsset[1][ttl]=300&nsset[1][priority]=0&nsset[1][auth]=true&nsset[2][qtype]=NS&nsset[2][qname]=example.com&nsset[2][qclass]=1&nsset[2][content]=ns2.example.com&nsset[2][ttl]=300&nsset[2][priority]=0&nsset[2][auth]=true + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + Alternative response + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":{"account":"my account}} + + + + + + + + +Method: createSlaveDomain + + + Mandatory: + No + + + Parameters: + ip, domain + + + Optional parameters: + account + + + Reply: + true for success, false for failure + + + Description + +Creates new domain. This method is called when NOTIFY is received and you are superslaving. + + + + Example JSON/RPC: + + + Query: + +{"method":"createSlaveDomain","parameters":{"ip":"10.0.0.1","domain":"pirate.unit.test"}} + + + + Response: + +{"result":true} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/createslavedomain/10.0.0.1/pirate.unit.test +Content-Type: application/x-www-form-urlencoded +Content-Length: 0 + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + + + + + + +Method: replaceRRSet + + + Mandatory: + No + + + Parameters: + domain_id, qname, qtype, rrset + + + Reply: + true for success, false for failure + + + Description + +This method replaces a given resource record with new set. The new qtype can be different from the old. + + + + Example JSON/RPC: + + + Query: + +{"method":"replaceRRSet","parameters":{"domain_id":2,"qname":"replace.example.com","qtype":"A","trxid":1370416133,"rrset":[{"qtype":"A","qname":"replace.example.com","qclass":1,"content":"1.1.1.1","ttl":300,"priority":0,"auth":true}]}} + + + + Response: + +{"result":true} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/replacerrset/2/replace.example.com/A +Content-Type: application/x-www-form-urlencoded +Content-Length: 135 + +trxid=1370416133&rrset[qtype]=A&rrset[qname]=replace.example.com&rrset[qclass]=1&rrset[content]=1.1.1.1&rrset[priority]=0&rrset[auth]=1 + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + + + + + + +Method: feedRecord + + + Mandatory: + No + + + Parameters: + rr, trxid + + + Reply: + true for success, false for failure + + + Description + +Asks to feed new record into system. If startTransaction was called, trxId identifies a transaction. It is not always called by PowerDNS. + + + + Example JSON/RPC: + + + Query: + +{"method":"feedRecord","parameters":{"rr":{"qtype":"A","qname":"replace.example.com","qclass":1,"content":"127.0.0.1","ttl":300,"priority":0,"auth":true},"trxid":1370416133}} + + + + Response: + +{"result":true} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/feedrecord/1370416133 +Content-Type: application/x-www-form-urlencoded +Content-Length: 117 + +rr[qtype]=A&rr[qname]=replace.example.com&rr[qclass]=1&rr[content]=127.0.0.1&rr[ttl]=300&rr[priority]=0&rr[auth]=true + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + + + + + + +Method: feedEnts + + + Mandatory: + No + + + Parameters: + nonterm, trxid + + + Reply: + true for success, false for failure + + + Description + +This method is used by pdnssec rectify-zone to populate missing non-terminals. This is used when you have, say, record like _sip._upd.example.com, but no _udp.example.com. PowerDNS requires that there exists a non-terminal in between, and this instructs you to add one. If startTransaction is called, trxid identifies a transaction. + + + + Example JSON/RPC: + + + Query: + +{"method":"feedEnts","parameters":{"domain_id":2,"trxid":1370416133,"nonterm":["_sip._udp","_udp"]}} + + + + Response: + +{"result":true} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/feedents/2 +Content-Type: application/x-www-form-urlencoded +Content-Length: 50 + +trxid=1370416133&nonterm[]=_udp&nonterm[]=_sip.udp + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + + + + + + +Method: feedEnts3 + + + Mandatory: + No + + + Parameters: + trxid, domain_id, domain, times, salt, narrow, nonterm + + + Reply: + true for success, false for failure + + + Description + +Same as , but provides NSEC3 hashing parameters. Note that salt is BYTE value, and can be non-readable text. + + + + Example JSON/RPC: + + + Query: + +{"method":"feedEnts3","parameters":{"domain_id":2,"domain":"example.com","times":1,"salt":"9642","narrow":false,"trxid":1370416356,"nonterm":["_sip._udp","_udp"]}} + + + + Response: + +{"result":true} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/2/example.com +Content-Type: application/x-www-form-urlencoded +Content-Length: 78 + +trxid=1370416356&times=1&salt=9642&narrow=0&nonterm[]=_sip._udp&nonterm[]=_udp + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + + + + + + +Method: startTransaction + + + Mandatory: + No + + + Parameters: + domain_id, domain, trxid + + + Reply: + true for success, false for failure + + + Description + +Starts a new transaction. Transaction ID is chosen for you. Used to identify f.ex. AXFR transfer. + + + + Example JSON/RPC: + + + Query: + +{"method":"startTransaction","parameters":{"trxid":1234,"domain_id":1,"domain":"example.com"}} + + + + Response: + +{"result":true} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/starttransaction/1/example.com +Content-Type: application/x-www-form-urlencoded +Content-Length: 10 + +trxid=1234 + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + + + + + + +Method: commitTransaction + + + Mandatory: + No + + + Parameters: + trxid + + + Reply: + true for success, false for failure + + + Description + +Signals successful transfer and asks to commit data into permanent storage. + + + + Example JSON/RPC: + + + Query: + +{"method":"commitTransaction","parameters":{"trxid":1234}} + + + + Response: + +{"result":true} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/committransaction/1234 +Content-Type: application/x-www-form-urlencoded +Content-Length: 0 + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + + + + + + +Method: abortTransaction + + + Mandatory: + No + + + Parameters: + trxid + + + Reply: + true for success, false for failure + + + Description + +Signals failed transaction, and that you should rollback any changes. + + + + Example JSON/RPC: + + + Query: + +{"method":"abortTransaction","parameters":{"trxid":1234}} + + + + Response: + +{"result":true} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/aborttransaction/1234 +Content-Type: application/x-www-form-urlencoded +Content-Length: 0 + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":true} + + + + + + + +Method: calculateSOASerial + + + Mandatory: + No + + + Parameters: + domain,sd + + + Reply: + true for success, false for failure + + + Description + +Asks you to calculate a new serial based on the given data and update the serial. + + + + Example JSON/RPC: + + + Query: +{"method":"calculateSOASerial","parameters":{"domain":"unit.test","sd":{"qname":"unit.test","nameserver":"ns.unit.test","hostmaster":"hostmaster.unit.test","ttl":300,"serial":1,"refresh":2,"retry":3,"expire":4,"default_ttl":5,"domain_id":-1,"scopeMask":0}}} + + + + Response: + +{"result":2013060501} + + + + + + Example HTTP/RPC: + + + Query: + +POST /dnsapi/calculatesoaserial/unit.test +Content-Type: application/x-www-form-urlencoded +Content-Length: 198 + +sd[qname]=unit.test&sd[nameserver]=ns.unit.test&sd[hostmaster]=hostmaster.unit.test&sd[ttl]=300&sd[serial]=1&sd[refresh]=2&sd[retry]=3&sd[expire]=4&sd[default_ttl]=5&sd[domain_id]=-1&sd[scopemask]=0 + + + + Response: + +HTTP/1.1 200 OK +Content-Type: text/javascript; charset=utf-8 + +{"result":2013060501} + + + + + + + -- 2.47.3