]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
remotebackend: Convert unit tests to python
authorAki Tuomi <cmouse@cmouse.fi>
Wed, 20 Mar 2024 13:49:27 +0000 (15:49 +0200)
committerAki Tuomi <cmouse@cmouse.fi>
Thu, 4 Apr 2024 07:56:53 +0000 (10:56 +0300)
22 files changed:
modules/remotebackend/.gitignore
modules/remotebackend/Gemfile [deleted file]
modules/remotebackend/Gemfile.lock [deleted file]
modules/remotebackend/Makefile.am
modules/remotebackend/__init__.py [new file with mode: 0644]
modules/remotebackend/example.rb [deleted file]
modules/remotebackend/meson.build
modules/remotebackend/pdns_unittest.py [new file with mode: 0644]
modules/remotebackend/requirements.txt [new file with mode: 0644]
modules/remotebackend/test-remotebackend-pipe.cc
modules/remotebackend/testrunner.sh
modules/remotebackend/unittest.rb [deleted file]
modules/remotebackend/unittest_http.py [new file with mode: 0755]
modules/remotebackend/unittest_http.rb [deleted file]
modules/remotebackend/unittest_json.py [new file with mode: 0755]
modules/remotebackend/unittest_json.rb [deleted file]
modules/remotebackend/unittest_pipe.py [new file with mode: 0755]
modules/remotebackend/unittest_pipe.rb [deleted file]
modules/remotebackend/unittest_post.py [new file with mode: 0755]
modules/remotebackend/unittest_post.rb [deleted file]
modules/remotebackend/unittest_zeromq.py [new file with mode: 0755]
modules/remotebackend/unittest_zeromq.rb [deleted file]

index a97aa916d2a1b276d9a50bba94092e645b32024e..ad3a79377ffd1f146ef7f4b111cdc32f001cf420 100644 (file)
@@ -1,3 +1,4 @@
 *.test
 *.trs
 *.log
+venv
diff --git a/modules/remotebackend/Gemfile b/modules/remotebackend/Gemfile
deleted file mode 100644 (file)
index 2762b54..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-source "https://rubygems.org"
-
-gem "json"
-gem "webrick"
-gem "zeromqrb"
-gem "sqlite3"
diff --git a/modules/remotebackend/Gemfile.lock b/modules/remotebackend/Gemfile.lock
deleted file mode 100644 (file)
index dbf6065..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-GEM
-  remote: https://rubygems.org/
-  specs:
-    ffi (1.15.3)
-    ffi-rzmq (2.0.7)
-      ffi-rzmq-core (>= 1.0.7)
-    ffi-rzmq-core (1.0.7)
-      ffi
-    json (2.5.1)
-    sqlite3 (1.4.2)
-    webrick (1.7.0)
-    zeromqrb (0.1.3)
-      ffi-rzmq
-
-PLATFORMS
-  ruby
-
-DEPENDENCIES
-  json
-  sqlite3
-  webrick
-  zeromqrb
-
-BUNDLED WITH
-   2.1.4
index 1e2032a59d36fd46fa44f4a286474234cb420b63..db6a4a4ac46fbea28db1e2a3c0b91b3b1bd6a122 100644 (file)
@@ -22,14 +22,13 @@ EXTRA_DIST = \
        OBJECTFILES \
        OBJECTLIBS \
        testrunner.sh \
-       unittest_http.rb \
-       unittest_json.rb \
-       unittest_pipe.rb \
-       unittest_zeromq.rb \
-       unittest_post.rb \
-       unittest.rb \
-       Gemfile \
-       Gemfile.lock
+       unittest_http.py \
+       unittest_json.py \
+       unittest_pipe.py \
+       unittest_zeromq.py \
+       unittest_post.py \
+       pdns_unittest.py \
+       requirements.txt
 
 EXTRA_PROGRAMS = \
        remotebackend_pipe.test \
@@ -66,7 +65,9 @@ TESTS_ENVIRONMENT = \
        BOOST_TEST_LOG_LEVEL=message; \
        export BOOST_TEST_LOG_LEVEL; \
        REMOTEBACKEND_ZEROMQ=$(REMOTEBACKEND_ZEROMQ); \
-       export REMOTEBACKEND_ZEROMQ;
+       export REMOTEBACKEND_ZEROMQ; \
+       abs_srcdir=$(abs_srcdir)
+       export abs_srcdir;
 
 TEST_EXTENSIONS = .test
 
diff --git a/modules/remotebackend/__init__.py b/modules/remotebackend/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/modules/remotebackend/example.rb b/modules/remotebackend/example.rb
deleted file mode 100644 (file)
index 8d8b2af..0000000
+++ /dev/null
@@ -1,202 +0,0 @@
-#!/usr/bin/ruby
-
-require 'rubygems'
-require 'bundler/setup'
-require 'json'
-
-## this is an example stub for remote backend
-## to add more methods, just write
-## def do_<methodname>(args)
-## end
-## look at the existing methods to find out 
-## how to customize this. 
-
-## WARNING: this contains some code that 
-## should never be used in production, but 
-## is provided to give a more comprehensive
-## example code. 
-
-## Code provided only as example, not suitable
-## for production. 
-
-## Usage: 
-##  launch=remote
-##  remote-dnssec=yes
-##  remote-connection-string=pipe:command=/path/to/example.rb,timeout=2000
-
-class RequestHandler
-
-public
-  def initialize
-    @_log = []
-    @initialized = false
-    @default_ttl = 3600
-  end
-
-protected
-
-  ## YOUR METHODS GO AFTER THIS LINE
-
-  def do_initialize(args)
-     if @initialized
-       raise "Cannot reinitialize"
-     end 
-     log "Example backend v1.0 starting"
-     @initialized = true
-     true
-  end
-
-  ## used to tell that we do NSEC3 NARROW
-  def do_getdomainmetadata(args)
-     if args["name"] == "example.com"
-       if args["kind"] == "NSEC3NARROW"
-           return "1"
-       elsif args["kind"] == "NSEC3PARAM"
-           return "1 1 1 fe"
-       end
-     end
-     false
-  end
-
-  ## returns keys, do not use in production
-  def do_getdomainkeys(args)
-     if args["name"] == "example.com"
-        return [ 
-          {
-             "id" => 1,
-             "flags" => 257,
-             "active" => true,
-             "published" => true,
-             "content" => "Private-key-format: v1.2
-Algorithm: 8 (RSASHA256)
-Modulus: ovvzf1fHdptdXsBrBLSqmGqdEKwR2B9st/KBgh8xQKoQzTGUG00CsPjF/J59IBU+EU/IIInMn0MxLLTyUKa2DJUkR6i7UKif5jKX1c7yvWzrFKLGOHjugUX2++r+o789biUte1qpWp3Kc2RYL18oPco4zpo6JcsPmhOK3aUCDJXmuWgHl1KudCQIiPkISArXVn4oOp+skQq+mUBl1Pysc4D+6sl77ERR2fW6xJ4ZRPOIKr445RJJmKgoMG8yRrR3it1RmV49hZlvMosQjBUoNcqhqOI0n4l8HOLyna7KIzoNKG62GtUCZh8uy8IjdUiWPYGEtkZ9zE0bnnF+R7HGvQ==
-PublicExponent: AQAB
-PrivateExponent: Lp/c3IUD7o4re7uX4dS9KLT3EZnn0OfMdiLNoafCszjzbX/NWrIBHxdLrCS6rr7k7pbgLU6+VqEmJB/vYdsPITJZGpbOXxieBYBbpzJ4hm/uIA0gn28Y66pUKWTkS3ud2zCPfkZFREL3c2M1Rvf1zxdWgOPl1oHsiKsmgpl9qJOSKHMWFC+m/pUMJ7iOMgyDRV+PNeb/8P1jVOAYyQMEnu+enw2ro2NiWXNikbnaWrIv3IxVZAyZG4/H8+1vfQFPDWztosOy7OhV3WyMJkfwcXrlGoyLlxyAgkh/jeCnmPllxlJZGTgCtoVYd/n8osMXCDKxpAhsfdfCPeNOcjocgQ==
-Prime1: +T+s7wv+zVqONJqkAKw4OCVzxBc5FWrmDPcjPCUeKIK/K/3+XjmIqTlbvBKf+7rm+AGVnXAbqk90+jzE3mKI8HMG/rM2cx01986xNQsIqwi2VAt25huPhEyrtNzos6lmrCYaioaQnNpMvMLun3DvcaygkDUXxH7Dg+6BTHeUfnk=
-Prime2: p2YbBveBK3XyGMuVrDH9CvvpgKEoko+mPwLoKNpBoHrGxeOdCQmlPbnr0GrtZpy4sBNc5+shz2c6c1J3GlgPndT7zi2+MFGfWIGV48SAknVLfOU4iUpaGllnxcbjZeytG6WHdy2RaR3ReeGvdWxmxeuv084c2zC/7/vkcmgOqWU=
-Exponent1: EdVFeUEBdQ3imM7rpwSrbRD47HHA6tBgL1NLWRVKyBk6tloQ5gr1xS3Oa3FlsuwXdG0gmEgaIqBWvUS1zTd9lr6UJIsL/UZ8wwMt2J62ew4/hVngouwb45pcuq8HkzsulmiPg5PHKwHPdb34tr2s1BRG1KqHzc5IDNt2stLnc/k=
-Exponent2: oT+Iv1BAu7WUa/AHj+RjJGZ+iaozo+H9uOq66Uc8OjKqMErNpLwG0Qu7rHqjjdlfSjSMpNXpLpj4Q8fm9JhpCpbzq6qCbpbhUGcbFFjfpLSZ74f5yr21R3ZhsLChsTenlF8Bu3pIfKH9e1M7KXgvE22xY+xB/Z3a9XeFmfLEVMU=
-Coefficient: vG8tLZBE4s3bftN5INv2/o3knEcaoUAPfakSsjM2uLwQCGiUbBOOlp3QSdTU4MiLjDsza3fKIptdwYP9PvSkhGhtLPjBpKjRk1J1+sct3dfT66JPClJc1A8bLQPj4ZpO/BkJe6ji4HYfOp7Rjn9z8rTqwEfbP64CZV3/frUzIkQ="
-          },
-          {
-             "id" => 2,
-             "flags" => 256,
-             "active" => true,
-             "published" => true,
-             "content" => "Private-key-format: v1.2
-Algorithm: 8 (RSASHA256)
-Modulus: wKPNcDwkCd2DKxfdkMqTFOV2ITdgxIDaOd4vQ2QtphMBY9yYwmEkNsVdVFz7VVuQHdls20JUe+brFUhs1zEMMbokulFP/qVAItAeEWcqtkPULT+mmX5HsexpFVAZ5+UXuerObk/HMiIMt1CvkIWhmjSIkAI6dFRlf/93zTjy0+vwrNWZPXSzLccK5TfJmxdYdGPcsHkg6UmqEFPQuyZpmlmpg3IwjL5YddTDobAoABz/BrH7WsW0q/PyVubITo8JuFiBI5Fmw+3ef3PVUt1jtUCGASvtqNXW4wtWrgqvQKg/odthpceQ4QagV9XSlOdml527thnf9cMpm0Gh4Ox5HQ==
-PublicExponent: AQAB
-PrivateExponent: f+M+26fRdQstrUomuZ0Cj/jVt69/+nRga9JpJiA3fe1YGue0MjczR3k3QG6KHFyxDF/vuJAMbkUbBAIU37ecFNcy0s5wgOlL7tCjZYJMBLx6+58qBvSivCfqi0+mIyEf4zlS2kD0SP/52SkjpJpScoE1uAUCsX/l8lezPPb1nmH3RDwJwX1NVhsErHCAmxGDoj4nPCEhKgHkdbR0i8geXGdWR4slyq1EhuGJal4p5sNvzDQTYRy6r49rpbNHw9F7ojomIhTUCUjOXAX0X1HB5UTXRMpgpCNEjRG1a+aqxp/ZSMHSEGCv67fua5Qrd/qX1Ppns/oqZfCfTpTD3v/sMQ==
-Prime1: +0zQuFi7rZDTMGMIKiF6UOG5+pKwGxHmgKPOGF6fk3tIuSomgiVD3DLz5Y6kYk0kKls6IiA6X2esYwNXAaLe0dyMzpAnU4URXhFW7fUnHP0zA7NmaFRYPHstPeU59/JS+zmVlj4Ok1oeGocSGAFYGxXa+Sot0fyCXpAjZboDWg8=
-Prime2: xD4hprQmcn5gmLqYO9+nEEJTNyNccbAciiKjRJxIE7w6muuKESx0uUn5XdnzSxhbVkK16kkEqW3s+Y+VoLxwRj2fuvoPfx8nTQXY1esgcIZCG8ubvHW5T0bzee5gyX3cMvaxkoeM7euYgvh0UwR/FG910SwAlmMZjSwXay2YlhM=
-Exponent1: 6vcWzNcCnDWmkT53WtU0hb2Y4+YVzSm+iRcf039d20rRY3g6y0NGoPPvQftOTi9smkH0KAZULfJEp8tupbQAfN6ntVfpvVjVNUwnKJUo/hzsfxBVt0Ttv5c4ZQAYZHHqDsX3zKO3gyUmso0KaPGQzLpxpLlAYG+mAf7paeszyRc=
-Exponent2: ouvWMjk0Bi/ncETRqDuYzkXSIl+oGvaT6xawp4B70m6d1QohWPqoeT/x2Dne44R4J9hAgR5X0XXinJnZJlXrfFUi7C84eFhb33UwPQD0sJa2Aa97Pu4Zh7im4J7IGd/01Ra7+6Ovm8LRnkI5CMcd3dBfZuX6IuBpUSu+0YtMN6M=
-Coefficient: 5lP9IFknvFgaXKCs8MproehHSFhFTWac4557HIn03KrnlGOKDcY6DC/vgu1e42bEZ4J0RU0EELp5u4tAEYcumIaIVhfzRsajYRGln2mHe6o6nTO+FbANKuhyVmBEvTVczPOcYLrFXKVTglKAs+8W96dYIMDhiAwxi9zijLKKQ1k="
-          }
-        ]
-     end
-     false
-  end
-
-  ## Example lookup
-  ## Returns SOA, MX, NS and A records for example.com
-  ## also static A record for test.example.com
-  ## and dynamic A record for anything else in example.com domain 
-  def do_lookup(args)
-     if args["qname"] == "example.com" and args["qtype"].downcase == "soa"
-       return [
-          record("SOA","example.com", "sns.dns.icann.org noc.dns.icann.org 2013012485 7200 3600 1209600 3600"),
-              ]
-     elsif args["qname"] == "example.com" and args["qtype"].downcase == "any"
-       return [ 
-          record("SOA","example.com", "sns.dns.icann.org noc.dns.icann.org 2013012485 7200 3600 1209600 3600"),
-          record("NS","example.com","sns.dns.icann.org"),
-          record("MX","example.com","10 test.example.com")
-              ]
-     elsif args["qname"] == "test.example.com" and args["qtype"].downcase == "any"
-       return [
-          record("A","test.example.com","127.0.0.1")
-       ]
-     elsif args["qname"] =~ /(.*)\.example\.com$/ and args["qtype"].downcase == "any"
-       ip = 0
-       $1.downcase.each_byte do |b| ip = ip + b end
-       ip_2 = ip/256
-       ip = ip%256
-       return [
-          record("A",args["qname"], "127.0.#{ip_2}.#{ip}")
-       ]
-     end
-     false
-  end
-
-  ## AXFR support
-  ## Do note that having AXFR here is somewhat stupid since
-  ## we generate records above. But it is still included
-  ## for sake of having an example. Do not do this in production.
-  def do_list(args)
-     if args["zonename"] == "example.com"
-       return [
-          record("SOA","example.com", "sns.dns.icann.org noc.dns.icann.org 2013012485 7200 3600 1209600 3600"),
-          record("NS","example.com","sns.dns.icann.org"),
-          record("MX","example.com","10 test.example.com"),
-          record("A","test.example.com","127.0.0.1")
-       ]
-     end
-     false          
-  end
-
-  ## Please see https://doc.powerdns.com/authoritative/backends/remote.html for methods to add here
-  ## Just remember to prefix them with do_
-
-  ## Some helpers after this 
-
-  def record_ttl(qtype,qname,content,ttl)
-    {:qtype => qtype, :qname => qname, :content => content, :ttl => ttl, :auth => 1}
-  end
-
-  def record(qtype,qname,content)
-    record_ttl(qtype,qname,content,@default_ttl,)
-  end
-
-  def log(message)
-    @_log << message
-  end
-
-  ## Flushes log array and returns it.
-  def consume_log
-    ret = @_log
-    @_log = []
-    ret
-  end
-
-public
-  def run
-     STDOUT.sync=true
-     STDIN.each_line do |line|
-        # So far rapidjson has managed to follow RFC4627
-        # and hasn't done formatted json so there should be
-        # no newlines. 
-        msg = JSON.parse(line)
-       
-        # it's a good idea to prefix methods with do_ to prevent
-        # clashes with initialize et al, and downcase it for simplicity
-        method = "do_#{msg["method"].downcase}".to_sym
-        if self.respond_to? method
-          result = self.send method, msg["parameters"]
-        else
-          log "Method #{msg["method"]} not implemented"
-          result = false
-        end
-        # build and emit result
-        reply = { :result => result, :log => consume_log }
-        puts reply.to_json
-     end
-  end
-end
-
-begin 
-  RequestHandler.new.run
-rescue Interrupt
-  # ignore this exception, caused by ctrl+c in foreground mode
-end
index 2b124f81cb395a56dd54a6de11932d6110b2d3ec..0577d9be8039361c74a7319ada0a8fcae5598012 100644 (file)
@@ -47,14 +47,12 @@ if get_option('unit-tests-backends')
   }
 
   module_remotebackend_test_sources_extra = files(
-    'example.rb',
-    'Gemfile',
-    'Gemfile.lock',
-    'unittest_http.rb',
-    'unittest_json.rb',
-    'unittest_pipe.rb',
-    'unittest_post.rb',
-    'unittest.rb',
-    'unittest_zeromq.rb',
+    'requirements.txt',
+    'pdns_unittest.py',
+    'unittest_http.py',
+    'unittest_json.py',
+    'unittest_pipe.py',
+    'unittest_post.py',
+    'unittest_zeromq.py',
   )
 endif
diff --git a/modules/remotebackend/pdns_unittest.py b/modules/remotebackend/pdns_unittest.py
new file mode 100644 (file)
index 0000000..06cbd00
--- /dev/null
@@ -0,0 +1,332 @@
+import pdns.remotebackend
+import sys
+import io
+import time
+
+
+# define a simple $domain
+
+ID_DOMAIN = {
+    1: 'unit.test.',
+}
+
+DOMAINS = {
+    'unit.test.': {
+        'id': 1,
+        'ttl': 300,
+        'name': 'unit.test.',
+        'notified_serial': 0,
+        'meta': {},
+        'keys': {},
+        'rr': {
+            'unit.test.' : {
+                'SOA': ["ns.unit.test. hostmaster.unit.test. 1 2 3 4 5"],
+                'NS':  ["ns1.unit.test.", "ns2.unit.test."],
+            },
+            'ns1.unit.test.': {
+               'A': ["10.0.0.1"]
+            },
+            'ns2.unit.test.': {
+               'A': ["10.0.0.2"]
+            },
+            'empty.unit.test.': {}
+        },
+        'kind': 'native',
+    },
+    'master.test.': {
+        'id': 2,
+        'ttl': 300,
+        'name': 'master.test.',
+        'notified_serial': 2,
+        'meta': {},
+        'keys': {},
+        'rr': {
+            'master.test.': {
+                'SOA': ["ns.master.test. hostmaster.master.test. 1 2 3 4 5"],
+            }
+        },
+        'kind': 'master',
+    },
+}
+
+TSIG_KEYS = {
+    'test.': {
+        'name': 'test.',
+        'algorithm': 'NULL.',
+        'content': 'NULL',
+    }
+}
+
+MASTERS = {
+    'ns1.unit.test.': {
+        'ip': '10.0.0.1'
+    }
+}
+
+class Handler(pdns.remotebackend.Handler):
+    def get_domain(self, domain):
+        if not domain.endswith("."):
+            domain = domain + "."
+        while len(domain) > 0:
+            if domain in DOMAINS:
+                return DOMAINS[domain]
+            p = domain.find(".")
+            if p == -1:
+                break
+            domain = domain[p+1:]
+        return None
+
+    def do_lookup(self, qname='', qtype='', **kwargs):
+        domain = self.get_domain(qname)
+        if domain:
+            self.result = []
+            rrset = domain['rr'].get(qname, {'qtype': []})
+            rr = rrset.get(qtype, [])
+            for r in rr:
+                self.result.append(self.record(qname=qname, qtype=qtype, content=r, ttl=domain['ttl']))
+
+    def do_list(self, zonename="", **kwargs):
+        domain = self.get_domain(zonename)
+        if domain:
+            self.result = []
+            for qname, rrset in domain['rr'].items():
+                for qtype, rr in rrset.items():
+                    for r in rr:
+                        self.result.append(self.record(qname=qname, qtype=qtype, content=r, ttl=domain['ttl']))
+
+    def do_getalldomainmetadata(self, name='', **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            self.result = domain['meta']
+
+    def do_getdomainmetadata(self, name='', kind='', **kwargs):
+        self.do_getalldomainmetadata(name=name)
+        if self.result:
+            self.result = self.result[kind]
+
+    def do_setdomainmetadata(self, name='', kind='', value=None, **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            if value is None:
+                del domain['meta'][kind]
+            else:
+                domain['meta'][kind] = value
+            self.result = True
+
+    def do_adddomainkey(self, name='', key={}, **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            k_id = len(domain['keys']) + 1
+            key['id'] = k_id
+            domain['keys'][k_id] = key
+            self.result = k_id
+
+    def do_getdomainkeys(self, name='', **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            self.result = []
+            for k_id, k in domain['keys'].items():
+                self.result.append(k)
+
+    def do_activatedomainkey(self, name='', **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            key = domain['keys'].get(int(kwargs['id']))
+            if key:
+                key['active'] = True
+                self.result = True
+
+    def do_deactivatedomainkey(self, name='', **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            key = domain['keys'].get(int(kwargs['id']))
+            if key:
+                key['active'] = False
+                self.result = True
+
+    def do_publishdomainkey(self, name='', **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            key = domain['keys'].get(int(kwargs['id']))
+            if key:
+                key['published'] = True
+                self.result = True
+
+    def do_unpublishdomainkey(self, name='', **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            key = domain['keys'].get(int(kwargs['id']))
+            if key:
+                key['published'] = False
+                self.result = True
+
+    def do_removedomainkey(self, name='', **kwargs):
+        domain = self.get_domain(name)
+        if domain:
+            k_id = int(kwargs['id'])
+            if k_id in domain['keys']:
+                del domain['keys'][k_id]
+                self.result = True
+
+    def do_getbeforeandafternamesabsolute(self, qname='', **kwargs):
+        if qname == 'middle.unit.test.':
+            self.result = {
+                'unhashed': 'middle.',
+                'before': 'begin.',
+                'after': 'stop.',
+            }
+
+    def do_setnotified(self, **kwargs):
+        d_id = int(kwargs['id'])
+        domain_name = ID_DOMAIN.get(d_id)
+        domain = DOMAINS.get(domain_name)
+        if domain:
+            domain['notified_serial'] = kwargs['serial']
+            self.result = True
+
+    def fill_domaininfo(self, name=""):
+        domain = self.get_domain(name)
+        if domain:
+            self.result.append({
+                'id': domain['id'],
+                'zone': domain['name'],
+                'masters': MASTERS,
+                'notified_serial': domain['notified_serial'],
+                'serial': domain['notified_serial'],
+                'last_check': int(time.time()),
+                'kind': domain['kind']
+            })
+
+    def do_getdomaininfo(self, name="", **kwargs):
+        self.result = []
+        self.fill_domaininfo(name)
+        if self.result:
+            self.result = self.result[0]
+
+    def do_ismaster(self, name='', ip='', **kwargs):
+        ips = MASTERS.get(name, [])
+        if ip in ips:
+            self.result = True
+
+    def do_supermasterbackend(self, domain='', nsset=[], **kwargs):
+        d_id = len(DOMAINS) + 1
+        dom = domain.lower()
+        domain = {
+            'id': d_id,
+            'name': dom,
+            'kind': 'slave',
+            'notified_serial': 0,
+            'meta': {},
+            'keys': {},
+            'rr': {
+                dom: {
+                    'SOA': ["ns.%s hostmaster.%s 1 2 3 4 5" % (dom, dom)],
+                }
+            },
+            'ttl': 300,
+        }
+
+        nsset = []
+        for rr in nsset:
+            nsset.append(self.record(qname=rr['qname'], qtype=rr['qtype'], content=rr['content'], ttl=rr['ttl']))
+
+        domain['rr'][dom]['NS'] = nsset
+        DOMAINS[dom] = domain
+
+        self.result = [{
+            'nameserver': 'ns.%s' % dom,
+            'account': ''
+        }]
+
+
+    def do_createslavedomain(self, domain='', **kwargs):
+        d_id = len(DOMAINS) + 1
+        dom = domain.lower()
+        domain = {
+            'id': d_id,
+            'name': dom,
+            'kind': 'slave',
+            'notified_serial': 0,
+            'ttl': 300,
+            'meta': {},
+            'keys': {},
+            'rr': {
+            }
+        }
+        self.result = True
+
+    def do_feedrecord(self, rr={}, **kwargs):
+        qname = rr['qname']
+        qtype = rr['qtype']
+        domain = self.get_domain(qname)
+        if domain:
+            if not qname in domain['rr']:
+                domain['rr'][qname] = {qtype: []}
+            elif not qtype in domain['rr'][qname]:
+                 domain['rr'][qname][qtype] = []
+            domain['rr'][qname][qtype].append(self.record(
+                qname=qname,
+                qtype=qtype,
+                content=rr['content'],
+                ttl=rr.get('ttl', domain['ttl']))
+            )
+            self.result = True
+
+    def do_replacerrset(self, qname='', qtype='', rrset=[], **kwargs):
+        domain = self.get_domain(qname)
+        if domain and qname in domain['rr']:
+            if qtype in domain['rr'][qname]:
+                del domain['rr'][qname][qtype]
+        for row in rrset:
+            self.do_feedrecord(rr=row)
+
+    def do_feedents(self, **kwargs):
+        self.result = True
+
+    def do_feedents3(self, **kwargs):
+        self.result = True
+
+    def do_gettsigkey(self, name='', **kwargs):
+        if name in TSIG_KEYS:
+            self.result = TSIG_KEYS[name]
+
+    def do_settsigkey(self, name='', algorithm='', content='', **kwargs):
+        TSIG_KEYS[name] = {
+            'name': name,
+            'algorithm': algorithm,
+            'content': content,
+        }
+        self.result = True
+
+    def do_gettsigkeys(self, **kwargs):
+        self.result = []
+        for name, key in TSIG_KEYS.items():
+            self.result.append(key)
+
+    def do_deletetsigkey(self, name='', **kwargs):
+        if name in TSIG_KEYS:
+            del TSIG_KEYS[name]
+            self.result = True
+
+    def do_starttransaction(self, **kwargs):
+        self.result = True
+
+    def do_committransaction(self, **kwargs):
+        self.result = True
+
+    def do_aborttransaction(self, **kwargs):
+        self.result = True
+
+    def do_directbackendcmd(self, query='', **kwargs):
+        self.result = query
+
+    def do_getalldomains(self, **kwargs):
+        self.result = []
+        for name in DOMAINS.keys():
+            self.fill_domaininfo(name=name)
+
+    def do_getupdatedmasters(self, **kwargs):
+        self.result = []
+        for name in DOMAINS.keys():
+            if DOMAINS[name]['kind'] == 'master':
+                self.fill_domaininfo(name=name)
diff --git a/modules/remotebackend/requirements.txt b/modules/remotebackend/requirements.txt
new file mode 100644 (file)
index 0000000..b596e0f
--- /dev/null
@@ -0,0 +1,2 @@
+Pdns-Remotebackend==0.8.0
+pyzmq==25.1.2
index 6165adb2c022c4a99b93db397c7d2aa175298d7d..8061adaf636dbb3f68321d13dcffc538246946e6 100644 (file)
@@ -76,7 +76,7 @@ struct RemotebackendSetup
       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-connection-string") = "pipe:command=unittest_pipe.py";
       ::arg().set("remote-dnssec") = "yes";
       backendUnderTest = std::move(BackendMakers().all()[0]);
       // load few record types to help out
index 754f8e105f547e802d32d9e1cb9545b72ae832fa..40ba41947c71743fdb9fbdc6dc6a26b4694c62e1 100755 (executable)
@@ -1,9 +1,20 @@
 #!/usr/bin/env bash
+set -eu
+
 new_api=0
-mode=$1
+mode=${1-}
+
+progdir=${abs_srcdir-$PWD}
+
+if [ ! -d venv ]; then
+  flock .create_testenv bash -c "python3 -m venv venv && source venv/bin/activate && pip install wheel && pip install -r ${progdir}/requirements.txt"
+  rm .create_testenv
+fi
+
+source venv/bin/activate
 
 # we could be ran with new API
-while [ "$1" != "" ]
+while [ "${1-}" != "" ]
 do
  if [ "$1" == "--" ]; then
    new_api=1
@@ -13,7 +24,7 @@ do
  shift
 done
 
-webrick_pid=""
+httpd_pid=""
 socat_pid=""
 zeromq_pid=""
 socat=$(which socat)
@@ -21,13 +32,13 @@ socat=$(which socat)
 function start_web() {
   local service_logfile="${mode_name%\.test}_server.log"
 
-  ./unittest_"${1}".rb >> "${service_logfile}" 2>&1 &
-  webrick_pid=$!
+  ${progdir}/unittest_"${1}".py >> "${service_logfile}" 2>&1 &
+  httpd_pid=$!
 
   local timeout=0
   while [ ${timeout} -lt 20 ]; do
     local res
-    res=$(curl http://localhost:62434/ping 2>/dev/null)
+    res=$(curl http://localhost:62434/ping 2>/dev/null || true)
     if [ "$res" == "pong" ]; then
       # server is up and running
       return 0
@@ -37,11 +48,11 @@ function start_web() {
     (( timeout=timeout+1 ))
   done
 
-  if kill -0 ${webrick_pid} 2>/dev/null; then
+  if kill -0 ${httpd_pid} 2>/dev/null; then
     # if something is wrong with curl (i.e. curl isn't installed, localhost is firewalled ...)
     # the status check will fail -- cleanup required!
     echo >&2 "WARNING: Timeout (${timeout}s) reached: \"${1}\" test service process is running but status check failed"
-    kill -KILL ${webrick_pid} 2>/dev/null
+    kill -KILL ${httpd_pid} 2>/dev/null
   fi
 
   echo >&2 "ERROR: A timeout (${timeout}s) was reached while waiting for \"${1}\" test service to start!"
@@ -50,22 +61,22 @@ function start_web() {
 }
 
 function stop_web() {
-  if [ -z "${webrick_pid}" ]; then
+  if [ -z "${httpd_pid}" ]; then
     # should never happen - why was stop_web() called?
     echo >&2 "ERROR: Unable to stop \"${1}\" test service: Did we ever start the service?"
     exit 99
   fi
 
-  if ! kill -0 ${webrick_pid} 2>/dev/null; then
+  if ! kill -0 ${httpd_pid} 2>/dev/null; then
     # should never happen - did the test crashed the service?
-    echo >&2 "ERROR: Unable to stop \"${1}\" test service: service (${webrick_pid}) not running"
+    echo >&2 "ERROR: Unable to stop \"${1}\" test service: service (${httpd_pid}) not running"
     exit 69
   fi
 
-  kill -TERM ${webrick_pid}
+  kill -TERM ${httpd_pid}
   local timeout=0
   while [ ${timeout} -lt 5 ]; do
-    if ! kill -0 ${webrick_pid} 2>/dev/null; then
+    if ! kill -0 ${httpd_pid} 2>/dev/null; then
       # service was stopped
       return 0
     fi
@@ -74,9 +85,9 @@ function stop_web() {
     (( timeout=timeout+1 ))
   done
 
-  if kill -0 ${webrick_pid} 2>/dev/null; then
+  if kill -0 ${httpd_pid} 2>/dev/null; then
     echo >&2 "WARNING: Timeout (${timeout}s) reached - killing \"${1}\" test service ..."
-    kill -KILL ${webrick_pid} 2>/dev/null
+    kill -KILL ${httpd_pid} 2>/dev/null
     return $?
   fi
 }
@@ -89,8 +100,9 @@ function start_zeromq() {
 
   local service_logfile="${mode_name%\.test}_server.log"
 
-  ./unittest_zeromq.rb >> "${service_logfile}" 2>&1 &
+  ${progdir}/unittest_zeromq.py >> "${service_logfile}" 2>&1 &
   zeromq_pid=$!
+  echo "ZeroMQ running as $zeromq_pid"
 
   local timeout=0
   while [ ${timeout} -lt 5 ]; do
@@ -152,7 +164,7 @@ function start_unix() {
     exit 77
   fi
 
-  $socat unix-listen:/tmp/remotebackend.sock exec:./unittest_pipe.rb &
+  $socat unix-listen:/tmp/remotebackend.sock exec:${progdir}/unittest_pipe.py &
   socat_pid=$!
 
   local timeout=0
@@ -208,10 +220,11 @@ function stop_unix() {
 }
 
 function run_test() {
+ rv=0
  if [ $new_api -eq 0 ]; then
-   ./"$mode_name"
+   ./"$mode_name" || rv=$?
  else
-    $mode
+   $mode || rv=$?
  fi
 }
 
@@ -223,27 +236,27 @@ case "$mode_name" in
   ;;
   remotebackend_unix.test)
     start_unix
-    run_test ; rv=$?
+    run_test
     stop_unix
   ;;
   remotebackend_http.test)
     start_web "http"
-    run_test ; rv=$?
+    run_test
     stop_web "http"
   ;;
   remotebackend_post.test)
     start_web "post"
-    run_test ; rv=$?
+    run_test
     stop_web "post"
   ;;
   remotebackend_json.test)
     start_web "json"
-    run_test ; rv=$?
+    run_test
     stop_web "json"
   ;;
   remotebackend_zeromq.test)
     start_zeromq
-    run_test ; rv=$?
+    run_test
     stop_zeromq
   ;;
   *)
diff --git a/modules/remotebackend/unittest.rb b/modules/remotebackend/unittest.rb
deleted file mode 100644 (file)
index b38e72e..0000000
+++ /dev/null
@@ -1,293 +0,0 @@
-require 'rubygems'
-require 'bundler/setup'
-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"],
-      "NS" => ["ns1.unit.test.", "ns2.unit.test."],
-  },
-  "ns1.unit.test." => {
-       "A" => ["10.0.0.1"]
-  },
-  "ns2.unit.test." => {
-       "A" => ["10.0.0.2"]
-  }, 
-  "empty.unit.test." => {}
-}
-
-$meta = {}
-
-$keys = {}
-
-$tsigkeys = { "test." => {:name => "test.", :algorithm => "NULL.", :content => "NULL"} }
-
-$masters = { :name => "ns1.unit.test.", :ip => "10.0.0.1" }
-
-class Handler
-   def initialize
-   end
-
-   def rr(qname, qtype, content, ttl, auth = 1, domain_id = -1)
-      {:qname => qname, :qtype => qtype, :content => content, :ttl => ttl.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 and args["qname"] != "empty.unit.test"
-     [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_getalldomainmetadata(args)
-     return [ $meta[args["name"]] ] if $meta.has_key?(args["name"])
-     return [false]
-   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_publishdomainkey(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]["published"] = true
-         return [true]
-      end
-     end
-     [false]
-   end
-
-   def do_unpublishdomainkey(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]["published"] = 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 $tsigkeys.has_key? args["name"]
-       return [{:algorithm => $tsigkeys[args["name"]][:algorithm], :content => $tsigkeys[args["name"]][:content] }]
-     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
-     if args["name"] == "master.test."
-       return [{
-               :id => 2,
-               :zone => "master.test.",
-               :masters => ["10.0.0.1"],
-               :notified_serial => $notified_serial,
-               :serial => $notified_serial,
-               :last_check => Time.now.to_i,
-               :kind => 'master'
-       }]
-     end
-     [false]
-   end
-
-   def do_ismaster(args)
-     $masters[:name] == args["name"] && $masters[:ip] == args["ip"]
-   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_settsigkey(args) 
-      $tsigkeys[args["name"]] = { :name => args["name"], :algorithm => args["algorithm"], :content => args["content"] }
-      [true]
-   end
-
-   def do_deletetsigkey(args)
-      $tsigkeys.delete args["name"] if $tsigkeys.has_key? args["name"]
-      [true]
-   end
-
-   def do_gettsigkeys(*args)
-      return [$tsigkeys.values]
-   end
-
-   def do_starttransaction(args) 
-     [true]
-   end
-
-   def do_committransaction(args)
-     [true]
-   end
-
-   def do_aborttransaction(args)
-     [true]
-   end
-
-   def do_directbackendcmd(args)
-     [args["query"]]
-   end
-
-   def do_getalldomains(args)
-     [do_getdomaininfo({'name'=>'unit.test.'})]
-   end
-
-   def do_getupdatedmasters(args)
-     [do_getdomaininfo({'name'=>'master.test.'})]
-   end
-end
-
diff --git a/modules/remotebackend/unittest_http.py b/modules/remotebackend/unittest_http.py
new file mode 100755 (executable)
index 0000000..561340f
--- /dev/null
@@ -0,0 +1,191 @@
+#!/usr/bin/env python
+
+import http.server
+import json
+import re
+
+from pdns_unittest import Handler
+from urllib.parse import parse_qsl, urlparse, unquote
+
+class DNSBackendServer(http.server.HTTPServer):
+    def __init__(self, *args, **kwargs):
+        self.handler = Handler()
+        super().__init__(*args, **kwargs)
+
+    def finish_request(self, request, client_address):
+        """Finish one request b instantiating RequestHandlerClass."""
+        h = self.RequestHandlerClass(request, client_address, self, handler=self.handler)
+
+
+class DNSBackendHandler(http.server.BaseHTTPRequestHandler):
+    def __init__(self, *args, **kwargs):
+        self.handler = kwargs['handler']
+        super().__init__(*args)
+
+    def url_to_args(self):
+        url = urlparse(self.path)
+        parts = list(map(lambda part: unquote(part), url.path.split("/")))
+        parts.pop(0)
+        self.method = None
+
+        if parts.pop(0) != 'dns':
+            return
+
+        self.method = parts.pop(0).lower()
+        self.args = {}
+
+        if self.method == 'lookup':
+            self.args['qname'] = parts.pop(0)
+            self.args['qtype'] = parts.pop(0)
+        elif self.method == 'list':
+            self.args['id'] = int(parts.pop(0))
+            self.args['zonename'] = parts.pop(0)
+        elif self.method in ('getbeforeandafternamesabsolute', 'getbeforeandafternames'):
+            self.args['id'] = int(parts.pop(0))
+            self.args['qname'] = parts.pop(0)
+        elif self.method in ('getdomainmetadata', 'setdomainmetadata'):
+            self.args['name'] = parts.pop(0)
+            self.args['kind'] = parts.pop(0)
+        elif self.method == 'getdomainkeys':
+            self.args['name'] = parts.pop(0)
+        elif self.method in ('removedomainkey', 'activatedomainkey', 'deactivatedomainkey'):
+            self.args['id'] = int(parts.pop(0))
+            self.args['name'] = parts.pop(0)
+        elif self.method in ('adddomainkey', 'gettsigkey', 'getdomaininfo', 'settsigkey', 'deletetsigkey', 'getalldomainmetadata'):
+            self.args['name'] = parts.pop(0)
+        elif self.method == 'setnotified':
+            self.args['id'] = int(parts.pop(0))
+        elif self.method == 'feedents':
+            self.args['id'] = int(parts.pop(0))
+            self.args['trxid'] = int(parts.pop(0))
+        elif self.method == 'ismaster':
+            self.args['name'] = parts.pop(0)
+            self.args['ip'] = parts.pop(0)
+        elif self.method in ('supermasterbackend', 'createslavedomain'):
+            self.args['ip'] = parts.pop(0)
+            self.args['domain'] = parts.pop(0)
+        elif self.method in ('feedents3', 'starttransaction'):
+            self.args['id'] = int(parts.pop(0))
+            self.args['domain'] = parts.pop(0)
+            self.args['trxid'] = int(parts.pop(0))
+        elif self.method in ('feedrecord', 'committransaction', 'aborttransaction'):
+            self.args['trxid'] = int(parts.pop(0))
+        elif self.method == 'replacerrset':
+            self.args['id'] = int(parts.pop(0))
+            self.args['qname'] = parts.pop(0)
+            self.args['qtype'] = parts.pop(0)
+        assert len(parts) == 0, parts
+
+        self.parse_qsl(url.query)
+
+    def parse_qsl(self, qs):
+        res = {}
+        for key, value in parse_qsl(qs):
+            m = re.match(r"^(.*)\[(.*)\]\[(.*)\]", key)
+            if m:
+                k1 = m.group(1)
+                k2 = int(m.group(2))
+                k3 = m.group(3)
+                if k1 not in res:
+                    res[k1] = list({})
+                while len(res[k1]) <= k2:
+                    res[k1].append({})
+                res[k1][k2][k3] = value
+            else:
+                m = re.match(r"^(.*)\[(.*)\]", key)
+                if m:
+                    k1 = m.group(1)
+                    k2 = m.group(2)
+                    if k1 not in res:
+                        if k2 == '':
+                            res[k1] = list()
+                        else:
+                            res[k1] = {}
+                    if k2 == '':
+                        res[k1].append(value)
+                    else:                        
+                        res[k1][k2] = value
+                else:
+                    res[key] = value
+        self.args = self.args | res
+
+    def do_GET(self):
+        if self.path == '/ping':
+            self.send_response(200)
+            self.end_headers()
+            self.wfile.write("pong".encode())
+            return
+        self.do_POST()
+
+    def do_DELETE(self):
+        self.do_POST()
+
+    def do_PATCH(self):
+        self.do_POST()
+
+    def do_PUT(self):
+        self.do_POST()
+
+    def do_POST(self):
+        self.url_to_args()        
+
+        if not self.method:
+            self.send_error(404)
+            return
+
+        try:
+            length = 0
+            if 'content-length' in self.headers:
+                length = int(self.headers.get('content-length'))
+            if length > 0:
+                qs = self.rfile.read(length).decode()
+                self.parse_qsl(qs)
+
+            if self.method == "adddomainkey":
+                self.args['key'] = {
+                    'flags': self.args['flags'],
+                    'active': self.args['active'],
+                    'published': self.args['published'],
+                    'content': self.args['content']
+                }
+                del self.args['flags']
+                del self.args['active']
+                del self.args['published']
+                del self.args['content']
+
+            if 'serial' in self.args:
+                self.args['serial'] = int(self.args['serial'])
+
+            self.log_error("%r", self.args)
+            method = "do_%s" % self.method
+
+            self.handler.result = False
+            self.handler.log = []
+
+            if callable(getattr(self.handler, method, None)):
+                getattr(self.handler, method)(**self.args)
+                result = json.dumps({'result':self.handler.result,'log':self.handler.log}).encode()
+                self.log_error("%r", self.handler.result)
+                self.send_response(200)
+                self.send_header("content-type", "text/javascript");
+                self.send_header("content-length", len(result))
+                self.end_headers()
+                self.wfile.write(result)
+            else:
+                self.send_error(404, message=json.dumps({'error': 'No such method'}))
+        except BrokenPipeError as e2:
+            raise e2
+        except Exception as e:
+            raise e
+            self.log_error("Exception handling request: %r", e)
+            self.send_error(400, message=str(e))
+
+
+def main():
+    server = DNSBackendServer(('', 62434), DNSBackendHandler)
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        pass
+
+main()
diff --git a/modules/remotebackend/unittest_http.rb b/modules/remotebackend/unittest_http.rb
deleted file mode 100755 (executable)
index dc94a10..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rubygems'
-require 'bundler/setup'
-require 'json'
-require 'thread'
-require 'webrick'
-require './unittest'
-
-class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet
-   def initialize(server, dnsbackend)
-     @dnsbackend = dnsbackend
-     @semaphore = Mutex.new
-     @f = File.open("/tmp/remotebackend.txt.#{$$}","ab")
-     @f.set_encoding 'UTF-8'
-   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"
-        {
-         "id" => url.shift,
-          "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", "settsigkey", "deletetsigkey", "getalldomainmetadata"
-        {
-             "name" => url.shift
-        }
-     when "setnotified", "feedents"
-        {
-             "id" => url.shift.to_i
-        }
-     when "ismaster"
-        {
-             "name" => url.shift,
-             "ip" => url.shift
-        }
-     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,
-           "published" => args.delete("published").to_i,
-           "content" => args.delete("content")
-        }
-     end
-
-     args = parse_arrays args
-     begin
-        @f.puts "#{Time.now.to_f} [http]: #{({:method=>method,:parameters=>args}).to_json}"
-     rescue Encoding::UndefinedConversionError
-        # this fails with encoding error for feedEnts3
-     end
-
-     @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
-
-        @f.puts "#{Time.now.to_f} [http]: #{res.body}"
-     end
-   end
-
-   def do_DELETE(req,res)
-     do_GET(req,res)
-   end
-   
-   def do_POST(req,res)
-     do_GET(req,res)
-   end 
-
-   def do_PATCH(req,res)
-     do_GET(req,res)
-   end
-  
-   def do_PUT(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
-server.mount_proc("/ping"){ |req,resp| resp.body = "pong" }
-
-trap('INT') { server.stop }
-trap('TERM') { server.stop }
-
-server.start
diff --git a/modules/remotebackend/unittest_json.py b/modules/remotebackend/unittest_json.py
new file mode 100755 (executable)
index 0000000..7c08dbd
--- /dev/null
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+
+import http.server
+import json
+from pdns_unittest import Handler
+
+
+class DNSBackendServer(http.server.HTTPServer):
+    def __init__(self, *args, **kwargs):
+        self.handler = Handler()
+        super().__init__(*args, **kwargs)
+
+    def finish_request(self, request, client_address):
+        """Finish one request b instantiating RequestHandlerClass."""
+        h = self.RequestHandlerClass(request, client_address, self, handler=self.handler)
+
+
+class DNSBackendHandler(http.server.BaseHTTPRequestHandler):
+    def __init__(self, *args, **kwargs):
+        self.handler = kwargs['handler']
+        super().__init__(*args)
+
+    def do_GET(self):
+        if self.path == '/ping':
+            self.send_response(200)
+            self.end_headers()
+            self.wfile.write("pong".encode())
+            return
+        self.send_error(404)
+
+    def do_POST(self):
+        if self.path != "/dns/endpoint.json":
+            self.send_error(404)
+            return
+
+        try:
+            length = int(self.headers.get('content-length'))
+            message = json.loads(self.rfile.read(length).decode())
+
+            method = "do_" + message['method'].lower()
+            args = message['parameters']
+            self.handler.result = False
+            self.handler.log = []
+
+            if callable(getattr(self.handler, method, None)):
+                getattr(self.handler, method)(**args)
+                result = json.dumps({'result':self.handler.result,'log':self.handler.log}).encode()
+
+                self.send_response(200)
+                self.send_header("content-type", "text/javascript");
+                self.send_header("content-length", len(result))
+                self.end_headers()
+                self.wfile.write(result)
+            else:
+                self.send_error(404, message=json.dumps({'error': 'No such method'}))
+        except BrokenPipeError as e2:
+            raise e2
+        except Exception as e:
+            self.send_error(400, message=str(e))
+
+
+def main():
+    server = DNSBackendServer(('', 62434), DNSBackendHandler)
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        pass
+
+main()
diff --git a/modules/remotebackend/unittest_json.rb b/modules/remotebackend/unittest_json.rb
deleted file mode 100755 (executable)
index 2c713b7..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rubygems'
-require 'bundler/setup'
-require 'json'
-require 'thread'
-require 'webrick'
-require './unittest'
-
-class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet
-   def initialize(server, dnsbackend)
-     @dnsbackend = dnsbackend
-     @semaphore = Mutex.new
-     @f = File.open("/tmp/remotebackend.txt.#{$$}","a")
-     @f.sync
-   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 "#{Time.now.to_f} [http/json]: #{({:method=>method,:parameters=>args}).to_json}"    
-
-     @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
-        @f.puts "#{Time.now.to_f} [http/json]: #{res.body}"
-     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
-server.mount_proc("/ping"){ |req,resp| resp.body = "pong" }
-
-trap('INT') { server.stop }
-trap('TERM') { server.stop }
-
-server.start
diff --git a/modules/remotebackend/unittest_pipe.py b/modules/remotebackend/unittest_pipe.py
new file mode 100755 (executable)
index 0000000..52c1c6c
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+
+from pdns_unittest import Handler
+from pdns.remotebackend import PipeConnector
+import os
+
+connector = PipeConnector(Handler)
+connector.run()
diff --git a/modules/remotebackend/unittest_pipe.rb b/modules/remotebackend/unittest_pipe.rb
deleted file mode 100755 (executable)
index 70ab586..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rubygems'
-require 'bundler/setup'
-require 'json'
-require './unittest'
-
-h = Handler.new()
-f = File.open "/tmp/remotebackend.txt.#{$$}","a"
-f.sync
-
-STDOUT.sync = true
-begin
-  STDIN.each_line do |line|
-    f.puts "#{Time.now.to_f}: [pipe] #{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
-      else
-         res, log = h.send(method,args)
-      end
-      puts ({:result => res, :log => log}).to_json
-      f.puts "#{Time.now.to_f} [pipe]: #{({:result => res, :log => log}).to_json}"
-    rescue JSON::ParserError
-      puts ({:result => false, :log => "Cannot parse input #{line}"}).to_json
-      f.puts "#{Time.now.to_f} [pipe]: #{({:result => false, :log => "Cannot parse input #{line}"}).to_json}"
-      next
-    end
-  end
-rescue SystemExit, Interrupt
-end
diff --git a/modules/remotebackend/unittest_post.py b/modules/remotebackend/unittest_post.py
new file mode 100755 (executable)
index 0000000..3109d25
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+
+import http.server
+import json
+from urllib.parse import parse_qs, urlparse
+from pdns_unittest import Handler
+
+
+class DNSBackendServer(http.server.HTTPServer):
+    def __init__(self, *args, **kwargs):
+        self.handler = Handler()
+        super().__init__(*args, **kwargs)
+
+    def finish_request(self, request, client_address):
+        """Finish one request b instantiating RequestHandlerClass."""
+        h = self.RequestHandlerClass(request, client_address, self, handler=self.handler)
+
+
+class DNSBackendHandler(http.server.BaseHTTPRequestHandler):
+    def __init__(self, *args, **kwargs):
+        self.handler = kwargs['handler']
+        super().__init__(*args)
+
+    def do_GET(self):
+        if self.path == '/ping':
+            self.send_response(200)
+            self.end_headers()
+            self.wfile.write("pong".encode())
+            return
+        self.send_error(404)
+
+    def do_POST(self):
+        path = urlparse(self.path).path
+        if not path.startswith('/dns/'):
+            self.send_error(404)
+            return
+
+        try:
+            length = int(self.headers.get('content-length'))
+            args = json.loads(parse_qs(self.rfile.read(length).decode())['parameters'][0])
+            method = "do_%s" % path[5:].lower()
+            self.log_error("%r", args)
+
+            self.handler.result = False
+            self.handler.log = []
+
+            if callable(getattr(self.handler, method, None)):
+                getattr(self.handler, method)(**args)
+                result = json.dumps({'result':self.handler.result,'log':self.handler.log}).encode()
+
+                self.send_response(200)
+                self.send_header("content-type", "text/javascript");
+                self.send_header("content-length", len(result))
+                self.end_headers()
+                self.wfile.write(result)
+            else:
+                self.send_error(404, message=json.dumps({'error': 'No such method'}))
+        except BrokenPipeError as e2:
+            raise e2
+        except Exception as e:
+            self.send_error(400, message=str(e))
+
+
+def main():
+    server = DNSBackendServer(('', 62434), DNSBackendHandler)
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        pass
+
+main()
diff --git a/modules/remotebackend/unittest_post.rb b/modules/remotebackend/unittest_post.rb
deleted file mode 100755 (executable)
index db82974..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rubygems'
-require 'bundler/setup'
-require 'json'
-require 'thread'
-require 'webrick'
-require './unittest'
-
-class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet
-   def initialize(server, dnsbackend)
-     @dnsbackend = dnsbackend
-     @semaphore = Mutex.new
-     @f = File.open("/tmp/remotebackend.txt.#{$$}","a")
-     @f.sync
-   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 "#{Time.now.to_f} [http/post]: #{({:method=>method,:parameters=>args}).to_json}"    
-
-     @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
-        @f.puts "#{Time.now.to_f} [http/post]: #{res.body}"
-     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
-server.mount_proc("/ping"){ |req,resp| resp.body = "pong" }
-  
-trap('INT') { server.stop }
-trap('TERM') { server.stop }
-
-server.start
diff --git a/modules/remotebackend/unittest_zeromq.py b/modules/remotebackend/unittest_zeromq.py
new file mode 100755 (executable)
index 0000000..62b5f96
--- /dev/null
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+import zmq
+import json
+import os
+from urllib.parse import parse_qs, urlparse
+from pdns_unittest import Handler
+
+def run(socket, handler):
+    while True:
+        message = socket.recv()
+        try:
+            message = json.loads(message.decode().strip())
+            method = "do_%s" % message['method'].lower()
+            args = message['parameters']
+            handler.result = False
+            handler.log = []
+            if callable(getattr(handler, method, None)):
+                getattr(handler, method)(**args)
+                result = json.dumps({'result': handler.result,'log': handler.log})
+                socket.send(result.encode())
+        except KeyboardInterrupt as e3:
+            return
+        except BrokenPipeError as e2:
+            raise e2
+        except Exception as e:
+            print(e)
+            socket.send(json.dumps({'result':False}).encode())
+
+
+def main():
+    context = zmq.Context()
+    socket = context.socket(zmq.REP)
+    socket.bind("ipc:///tmp/remotebackend.0")
+    handler = Handler()
+    print("Listening on ipc:///tmp/remotebackend.0")
+
+    try:
+        run(socket, handler)
+    except KeyboardInterrupt as e:
+        pass
+
+    os.unlink("/tmp/remotebackend.0")
+main()
diff --git a/modules/remotebackend/unittest_zeromq.rb b/modules/remotebackend/unittest_zeromq.rb
deleted file mode 100755 (executable)
index bf5b0f4..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rubygems'
-require 'bundler/setup'
-require 'json'
-require 'zero_mq'
-require './unittest'
-
-h = Handler.new()
-f = File.open "/tmp/remotebackend.txt.#{$$}","a"
-f.sync = true
-
-runcond=true
-
-trap('INT') { runcond = false }
-trap('TERM') { runcond = false }
-
-begin
-  context = ZeroMQ::Context.new
-  socket = context.socket ZMQ::REP
-  socket.bind("ipc:///tmp/remotebackend.0")
-  print "[#{Time.now.to_s}] ZeroMQ unit test responder running\n"
-
-  while(runcond) do
-    line = ""
-    rc = socket.recv_string line
-    # expect json
-    input = {}
-    line = line.strip
-
-    f.puts "#{Time.now.to_f}: [zmq] #{line}"
-    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
-      else
-         res, log = h.send(method,args)
-      end
-      socket.send_string ({:result => res, :log => log}).to_json + "\n" , 0
-      f.puts "#{Time.now.to_f} [zmq]: #{({:result => res, :log => log}).to_json}"
-    rescue JSON::ParserError
-      socket.send_string ({:result => false, :log => "Cannot parse input #{line}"}).to_json + "\n";
-      f.puts "#{Time.now.to_f} [zmq]: #{({:result => false, :log => "Cannot parse input #{line}"}).to_json}"
-      next
-    end
-  end
-rescue SystemExit, Interrupt, Errno::EINTR
-end
-
-print "[#{Time.now.to_s}] ZeroMQ unit test responder ended\n"