From: Aki Tuomi Date: Wed, 20 Mar 2024 13:49:27 +0000 (+0200) Subject: remotebackend: Convert unit tests to python X-Git-Tag: rec-5.1.0-alpha1~60^2~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b31c8b31b24e3aca8bbb58fe49bd6f39af320c21;p=thirdparty%2Fpdns.git remotebackend: Convert unit tests to python --- diff --git a/modules/remotebackend/.gitignore b/modules/remotebackend/.gitignore index a97aa916d2..ad3a79377f 100644 --- a/modules/remotebackend/.gitignore +++ b/modules/remotebackend/.gitignore @@ -1,3 +1,4 @@ *.test *.trs *.log +venv diff --git a/modules/remotebackend/Gemfile b/modules/remotebackend/Gemfile deleted file mode 100644 index 2762b54308..0000000000 --- a/modules/remotebackend/Gemfile +++ /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 index dbf6065483..0000000000 --- a/modules/remotebackend/Gemfile.lock +++ /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 diff --git a/modules/remotebackend/Makefile.am b/modules/remotebackend/Makefile.am index 1e2032a59d..db6a4a4ac4 100644 --- a/modules/remotebackend/Makefile.am +++ b/modules/remotebackend/Makefile.am @@ -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 index 0000000000..e69de29bb2 diff --git a/modules/remotebackend/example.rb b/modules/remotebackend/example.rb deleted file mode 100644 index 8d8b2af060..0000000000 --- a/modules/remotebackend/example.rb +++ /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_(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 diff --git a/modules/remotebackend/meson.build b/modules/remotebackend/meson.build index 2b124f81cb..0577d9be80 100644 --- a/modules/remotebackend/meson.build +++ b/modules/remotebackend/meson.build @@ -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 index 0000000000..06cbd0011d --- /dev/null +++ b/modules/remotebackend/pdns_unittest.py @@ -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 index 0000000000..b596e0f4b1 --- /dev/null +++ b/modules/remotebackend/requirements.txt @@ -0,0 +1,2 @@ +Pdns-Remotebackend==0.8.0 +pyzmq==25.1.2 diff --git a/modules/remotebackend/test-remotebackend-pipe.cc b/modules/remotebackend/test-remotebackend-pipe.cc index 6165adb2c0..8061adaf63 100644 --- a/modules/remotebackend/test-remotebackend-pipe.cc +++ b/modules/remotebackend/test-remotebackend-pipe.cc @@ -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 diff --git a/modules/remotebackend/testrunner.sh b/modules/remotebackend/testrunner.sh index 754f8e105f..40ba41947c 100755 --- a/modules/remotebackend/testrunner.sh +++ b/modules/remotebackend/testrunner.sh @@ -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 index b38e72e1d4..0000000000 --- a/modules/remotebackend/unittest.rb +++ /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 index 0000000000..561340fc2d --- /dev/null +++ b/modules/remotebackend/unittest_http.py @@ -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 index dc94a1071d..0000000000 --- a/modules/remotebackend/unittest_http.rb +++ /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 index 0000000000..7c08dbd094 --- /dev/null +++ b/modules/remotebackend/unittest_json.py @@ -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 index 2c713b75a2..0000000000 --- a/modules/remotebackend/unittest_json.rb +++ /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 index 0000000000..52c1c6c4fc --- /dev/null +++ b/modules/remotebackend/unittest_pipe.py @@ -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 index 70ab58695f..0000000000 --- a/modules/remotebackend/unittest_pipe.rb +++ /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 index 0000000000..3109d25ff6 --- /dev/null +++ b/modules/remotebackend/unittest_post.py @@ -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 index db82974a77..0000000000 --- a/modules/remotebackend/unittest_post.rb +++ /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 index 0000000000..62b5f968ce --- /dev/null +++ b/modules/remotebackend/unittest_zeromq.py @@ -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 index bf5b0f4470..0000000000 --- a/modules/remotebackend/unittest_zeromq.rb +++ /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"