]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
remotebackend: Convert regression tests to python
authorAki Tuomi <cmouse@cmouse.fi>
Thu, 21 Mar 2024 14:11:18 +0000 (16:11 +0200)
committerAki Tuomi <cmouse@cmouse.fi>
Thu, 4 Apr 2024 07:56:53 +0000 (10:56 +0300)
16 files changed:
modules/remotebackend/regression-tests/Gemfile [deleted file]
modules/remotebackend/regression-tests/Gemfile.lock [deleted symlink]
modules/remotebackend/regression-tests/__init__.py [new file with mode: 0644]
modules/remotebackend/regression-tests/backend.py [new file with mode: 0755]
modules/remotebackend/regression-tests/backend.rb [deleted file]
modules/remotebackend/regression-tests/dnsbackend.py [new file with mode: 0755]
modules/remotebackend/regression-tests/dnsbackend.rb [deleted file]
modules/remotebackend/regression-tests/http-backend.py [new file with mode: 0755]
modules/remotebackend/regression-tests/http-backend.rb [deleted file]
modules/remotebackend/regression-tests/pipe-backend.py [new file with mode: 0755]
modules/remotebackend/regression-tests/pipe-backend.rb [deleted file]
modules/remotebackend/regression-tests/unix-backend.py [new file with mode: 0755]
modules/remotebackend/regression-tests/unix-backend.rb [deleted file]
modules/remotebackend/regression-tests/zeromq-backend.py [new file with mode: 0755]
modules/remotebackend/regression-tests/zeromq-backend.rb [deleted file]
regression-tests/backends/remote-master

diff --git a/modules/remotebackend/regression-tests/Gemfile b/modules/remotebackend/regression-tests/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/regression-tests/Gemfile.lock b/modules/remotebackend/regression-tests/Gemfile.lock
deleted file mode 120000 (symlink)
index 412e45f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-../Gemfile.lock
\ No newline at end of file
diff --git a/modules/remotebackend/regression-tests/__init__.py b/modules/remotebackend/regression-tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/modules/remotebackend/regression-tests/backend.py b/modules/remotebackend/regression-tests/backend.py
new file mode 100755 (executable)
index 0000000..59a8f0b
--- /dev/null
@@ -0,0 +1,228 @@
+#!/usr/bin/env python
+
+import sqlite3
+from pdns.remotebackend import Handler
+
+
+class BackendHandler(Handler):
+    def __init__(self, options={}):
+        super().__init__(options=options)
+        self.dbpath = options['dbpath']
+        self.db = sqlite3.connect(self.dbpath)
+
+    def get_domain_id(self, name):
+        cur = self.db.execute("SELECT id FROM domains WHERE name = ?", (name,))
+        row = cur.fetchone()
+        if not row:
+            self.result = False
+            raise KeyError
+        return int(row[0])
+
+    def record(self, qname='', qtype='', content='', ttl=1, prio=0, auth=1, domain_id=-1):
+        """Generate one record"""
+        if ttl == -1:
+            ttl = self.ttl
+        if qtype in ('MX', 'SRV'):
+            content = "%d %s" % (prio, content)
+        return {'qtype': qtype, 'qname': qname, 'content': content,
+                'ttl': ttl, 'auth': auth, 'domain_id': domain_id}
+
+    # ends up here as qname=qname, id=id
+    def getbeforename(self, **kwargs):
+        cur = self.db.execute("SELECT ordername FROM records WHERE ordername < :qname AND domain_id = :id ORDER BY ordername DESC LIMIT 1", kwargs)
+        row = cur.fetchone()
+        if not row:
+            cur = self.db.execute("SELECT ordername FROM records WHERE domain_id = :id ORDER by ordername DESC LIMIT 1", kwargs)
+            row = cur.fetchone()
+        result = row[0]
+        if row[0] is None:
+            result = ''
+        return result
+
+    def getaftername(self, **kwargs):
+        cur = self.db.execute("SELECT ordername FROM records WHERE ordername > :qname AND domain_id = :id ORDER BY ordername LIMIT 1", kwargs)
+        row = cur.fetchone()
+        if row is None:
+            cur = self.db.execute("SELECT ordername FROM records WHERE domain_id = :id ORDER by ordername LIMIT 1", kwargs)
+            row = cur.fetchone()
+        result = row[0]
+        if row[0] is None:
+            result = ''
+        return result
+
+    def do_getbeforeandafternamesabsolute(self, **kwargs):
+        self.result = {
+            'before':   self.getbeforename(**kwargs),
+            'after':    self.getaftername(**kwargs),
+            'unhashed': kwargs['qname']
+        }
+
+    def do_getbeforeandafternames(self, **kwargs):
+        self.do_getbeforeandafternamesabsolute(**kwargs)
+
+    def do_getdomainkeys(self, name, **kwargs):
+        self.result = []
+        cur = self.db.execute("SELECT cryptokeys.id, flags, active, published, content FROM domains JOIN cryptokeys ON domains.id = cryptokeys.domain_id WHERE domains.name = :name", {'name':name})
+        for row in cur.fetchall():
+            self.result.append({
+                'id': row[0],
+                'flags': row[1],
+                'active': row[2] != 0,
+                'published': row[3],
+                'content': row[4]
+            })
+        if len(self.result) == 0:
+            self.result = False
+        self.log.append(self.dbpath)
+
+    def do_lookup(self, qname='', qtype='', domain_id=-1, **kwargs):
+        self.result = []
+        if kwargs.get('zone-id', -1) > 0:
+            domain_id = kwargs['zone-id']
+        if domain_id > -1:
+            if qtype == "ANY":
+                sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname AND domain_id = :domain_id"
+            else:
+                sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname AND type = :qtype AND domain_id = :domain_id"
+        else:
+            if qtype == "ANY":
+                sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname"
+            else:
+                sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname AND type = :qtype"
+        cur = self.db.execute(sql, {'qname': qname, 'qtype': qtype, 'domain_id': domain_id})
+        for row in cur.fetchall():            
+            self.result.append(self.record(qname=row[1],qtype=row[2],content=row[3],ttl=row[4],prio=row[5],auth=row[6],domain_id=row[0]))
+
+    def do_getdomaininfo(self, name='', **kwargs):
+        self.result = False
+        cur = self.db.execute("SELECT domain_id,name,content FROM records WHERE name = :name AND type = 'SOA'", {'name': name})
+        for row in cur.fetchall():
+            self.result = {
+                'zone': row[1],
+                'serial': int(row[2].split(' ')[2]),
+                'kind': 'native',
+                'id': row[0],
+            }
+
+    def do_getalldomains(self):
+        self.result = []
+        cur = self.db.execute("SELECT domain_id,name,content FROM records WHERE name = :name AND type = 'SOA'", {'name': name})
+        for row in cur.fetchall():
+            self.result.append({
+                'zone': row[1],
+                'serial': int(row[2].split(' ')[2]),
+                'kind': 'native',
+                'id': row[0],
+            })
+
+    def do_list(self, zonename='', domain_id=-1, **kwargs):
+        if domain_id == -1:
+            try:
+                domain_id = self.get_domain_id(zonename)
+            except KeyError:
+                return
+        if domain_id > -1:
+            self.result = []
+            cur = self.db.execute("SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE domain_id = ?", (domain_id,))
+            for row in cur.fetchall():
+                self.result.append(self.record(qname=row[1],qtype=row[2],content=row[3],ttl=row[4],prio=row[5],auth=row[6],domain_id=row[0]))
+
+    def do_adddomainkey(self, name, key, **kwargs):
+        try:
+            domain_id = self.get_domain_id(name)
+        except KeyError:
+            return
+        key['domain_id'] = domain_id
+
+        cur = self.db.execute("INSERT INTO cryptokeys (domain_id, flags, active, published, content) VALUES(:domain_id, :flags, :active, :published, :content)", key)
+        self.db.commit()
+
+        self.result = cur.lastrowid
+        self.log.append(self.dbpath)
+
+    def do_deactivatedomainkey(self, **kwargs):
+        try:
+            domain_id = self.get_domain_id(kwargs['name'])
+        except KeyError:
+            return
+        kwargs['domain_id'] = domain_id
+
+        self.db.execute("UPDATE cryptokeys SET active = 0 WHERE domain_id = :domain_id AND id = :id", kwargs)
+        self.db.commit()
+
+        self.result = True
+
+    def do_activatedomainkey(self, **kwargs):
+        try:
+            domain_id = self.get_domain_id(kwargs['name'])
+        except KeyError:
+            return
+        kwargs['domain_id'] = domain_id
+
+        self.db.execute("UPDATE cryptokeys SET active = 1 WHERE domain_id = :domain_id AND id = :id", kwargs)
+        self.db.commit()
+
+        self.result = True
+
+    def do_unpublishdomainkey(self, **kwargs):
+        try:
+            domain_id = self.get_domain_id(kwargs['name'])
+        except KeyError:
+            return
+        kwargs['domain_id'] = domain_id
+
+        self.db.execute("UPDATE cryptokeys SET published = 0 WHERE domain_id = :domain_id AND id = :id", kwargs)
+        self.db.commit()
+
+        self.result = True
+
+    def do_publishdomainkey(self, **kwargs):
+        try:
+            domain_id = self.get_domain_id(kwargs['name'])
+        except KeyError:
+            return
+        kwargs['domain_id'] = domain_id
+
+        self.db.execute("UPDATE cryptokeys SET published = 1 WHERE domain_id = :domain_id AND id = :id", kwargs)
+        self.db.commit()
+
+        self.result = True
+
+    def do_getalldomainmetadata(self, name, **kwargs):
+        cur = self.db.execute("SELECT kind, content FROM domainmetadata JOIN domains WHERE name = :name", {'name': name})
+        self.result = {}
+        for row in cur.fetchall():
+            if not row[0] in self.result:
+                self.result[row[0]] = list()
+            self.result[row[0]].append(row[1])
+
+    def do_getdomainmetadata(self, name, kind, **kwargs):
+        cur = self.db.execute("SELECT content FROM domainmetadata JOIN domains WHERE name = :name AND kind = :kind", {'name': name, 'kind': kind})
+        self.result = cur.fetchall()
+
+    def do_setdomainmetadata(self, name, kind, value, **kwargs):
+        try:
+            domain_id = self.get_domain_id(name)
+        except KeyError:
+            return
+
+        self.db.execute("DELETE FROM domainmetadata WHERE domain_id = :domain_id AND kind = :kind", {
+                'domain_id': domain_id,
+                'kind': kind
+        })
+        if value:
+            self.db.execute("INSERT INTO domainmetadata (domain_id,kind,content) VALUES(:domain_id, :kind, :content)", {
+                'domain_id': domain_id,
+                'kind': kind,
+                'content': content
+            })
+        self.db.commit()
+
+    def do_starttransaction(self, trxid, **kwargs):
+        pass
+
+    def do_committransaction(self, trxid, **kwargs):
+        pass
+
+    def do_directbackendcmd(self, query, **kwargs):
+        self.result = query
diff --git a/modules/remotebackend/regression-tests/backend.rb b/modules/remotebackend/regression-tests/backend.rb
deleted file mode 100755 (executable)
index 0c4bf47..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rubygems'
-require 'json'
-require 'sqlite3'
-
-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
-
-class Handler
-   def initialize(dbpath)
-     @dbpath = dbpath
-     @db = SQLite3::Database.new @dbpath
-   end
-
-   def db
-      if block_given?
-        @db.transaction
-        begin
-           yield @db
-        rescue
-           @db.rollback
-           return
-         end
-         @db.commit
-      else
-         @db
-      end
-   end
-
-   def do_initialize(*args)
-     return true, "Test bench initialized"
-   end
-
-   def getbeforename(qname, id)
-        before = db.get_first_value("SELECT ordername FROM records WHERE ordername < ? AND domain_id = ? ORDER BY ordername DESC", qname, id)
-        if (before.nil?) 
-           before = db.get_first_value("SELECT ordername FROM records WHERE domain_id = ? ORDER by ordername DESC LIMIT 1", id)
-        end
-        before
-   end
-
-  def getaftername(qname, id)
-        after = db.get_first_value("SELECT ordername FROM records WHERE ordername > ? AND domain_id = ? ORDER BY ordername", qname, id)
-        if (after.nil?)
-           after = db.get_first_value("SELECT ordername FROM records WHERE domain_id = ? ORDER by ordername LIMIT 1", id)
-        end
-        after
-   end
-
-
-   def do_getbeforeandafternamesabsolute(args)
-        args["qname"] = "" if args["qname"].nil?
-        return [{:before => getbeforename(args["qname"],args["id"]), :after => getaftername(args["qname"],args["id"]), :unhashed => args["qname"]}, nil]
-   end
-
-   def do_getbeforeandafternames(args)
-        args["qname"] = "" if args["qname"].nil?
-        return [{:before => getbeforename(args["qname"],args["id"]), :after => getaftername(args["qname"],args["id"]), :unhashed => args["qname"]}, nil]
-   end
-
-   def do_getdomainkeys(args)
-       ret = []
-       db.execute("SELECT cryptokeys.id,flags,active, published, content FROM domains JOIN cryptokeys ON domains.id = cryptokeys.domain_id WHERE domains.name = ?", [args["name"]]) do |row|
-          ret << {:id => row[0].to_i, :flags => row[1].to_i, :active => !(row[2].to_i.zero?), :published => row[3], :content => row[4]}
-       end 
-       return false if ret.empty?
-       return [ret,nil]
-   end
-
-   def do_lookup(args)
-     ret = []
-     loop do
-        begin
-          sargs = {}
-          if (args["zone-id"].to_i > 0)
-             sargs["domain_id"] = args["zone-id"].to_i
-             if (args["qtype"] == "ANY")
-                sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname AND domain_id = :domain_id"
-                sargs["qname"] = args["qname"]
-             else
-                sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname AND type = :qtype AND domain_id = :domain_id"
-                sargs["qname"] = args["qname"]
-                sargs["qtype"] = args["qtype"]
-             end
-          else
-             if (args["qtype"] == "ANY")
-                sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname"
-                sargs["qname"] = args["qname"]
-             else
-                sql = "SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE name = :qname AND type = :qtype"
-                sargs["qname"] = args["qname"]
-                sargs["qtype"] = args["qtype"]
-             end  
-          end
-          db.execute(sql, sargs) do |row|
-            if (row[2] == "MX" || row[2] == "SRV")
-              ret << rr(row[1], row[2], row[5]+" "+row[3], row[4], row[6], row[0])
-            else
-              ret << rr(row[1], row[2], row[3], row[4], row[6], row[0])
-            end
-          end
-        rescue Exception => e
-          e.backtrace
-          return false, [e.message]
-        end
-        break
-     end
-     return false unless ret.size > 0
-     return [ret,nil]
-   end
-  
-   def do_getdomaininfo(args) 
-     ret = {}
-     sql = "SELECT name,content FROM records WHERE name = :name AND type = 'SOA'"
-     db.execute(sql, args) do |row|
-       ret[:zone] = row[0]
-       ret[:serial] = row[1].split(' ')[2].to_i
-       ret[:kind] = "native"
-     end
-     return [ret,nil] if ret.has_key?(:zone)
-     return false
-   end
-
-   def do_list(args)
-     target = args["zonename"]
-     ret = []
-     loop do
-        begin
-          d_id = db.get_first_value("SELECT id FROM domains WHERE name = ?", target)
-          return false if d_id.nil?
-          db.execute("SELECT domain_id,name,type,content,ttl,prio,auth FROM records WHERE domain_id = ?", d_id) do |row|
-            if (row[2] == "MX" || row[2] == "SRV")
-              ret << rr(row[1], row[2], row[5]+" "+row[3], row[4], row[6], row[0])
-            else
-              ret << rr(row[1], row[2], row[3], row[4], row[6], row[0])
-            end
-          end
-        rescue Exception => e
-          e.backtrace
-          return false, [e.message]
-        end
-        break
-     end
-     return false unless ret.size > 0
-     return [ret,nil]
-   end
-
-   def do_adddomainkey(args)
-     d_id = db.get_first_value("SELECT id FROM domains WHERE name = ?", args["name"])
-     return false if d_id.nil?
-     sql = "INSERT INTO cryptokeys (domain_id, flags, active, published, content) VALUES(?,?,?,?,?)"
-     active = args["key"]["active"]
-     if (active) 
-        active = 1
-     else
-        active = 0
-     end
-     published = args["key"]["published"]
-     if (published)
-         published = 1
-     else
-         published = 0
-     end
-     db do |tx|
-        tx.execute(sql, [d_id, args["key"]["flags"].to_i, active, published, args["key"]["content"]])
-     end
-     return db.get_first_value("SELECT last_insert_rowid()").to_i
-   end
-
-   def do_deactivatedomainkey(args)
-     d_id = db.get_first_value("SELECT id FROM domains WHERE name = ?", args["name"])
-     return false if d_id.nil?
-     db do |tx|
-       tx.execute("UPDATE cryptokeys SET active = 0 WHERE domain_id = ? AND id = ?", [d_id, args["id"]])  
-     end
-     return true
-   end
-
-   def do_activatedomainkey(args)
-     d_id = db.get_first_value("SELECT id FROM domains WHERE name = ?", args["name"])
-     return false if d_id.nil?
-     db do |tx|
-       db.execute("UPDATE cryptokeys SET active = 1 WHERE domain_id = ? AND id = ?", [d_id, args["id"]])
-     end
-     return true
-   end
-
-   def do_unpublishdomainkey(args)
-     d_id = db.get_first_value("SELECT id FROM domains WHERE name = ?", args["name"])
-     return false if d_id.nil?
-     db do |tx|
-       tx.execute("UPDATE cryptokeys SET published = 0 WHERE domain_id = ? AND id = ?", [d_id, args["id"]])
-     end
-     return true
-   end
-
-   def do_publishdomainkey(args)
-     d_id = db.get_first_value("SELECT id FROM domains WHERE name = ?", args["name"])
-     return false if d_id.nil?
-     db do |tx|
-       db.execute("UPDATE cryptokeys SET published = 1 WHERE domain_id = ? AND id = ?", [d_id, args["id"]])
-     end
-     return true
-   end
-
-   def do_getdomainmetadata(args) 
-       ret = []
-        sql = "SELECT content FROM domainmetadata JOIN domains WHERE name = :name AND kind = :kind"
-        sargs = {:name => args["name"], :kind => args["kind"]}
-        db.execute(sql,sargs) do |row|
-          ret << row[0]
-        end
-        return false unless ret.size > 0
-        return [ret,nil]
-   end
-
-   def do_setdomainmetadata(args)
-        d_id = db.get_first_value("SELECT id FROM domains WHERE name = ?", args["name"])
-        return false if d_id.nil?
-        db do |tx|
-           sql = "DELETE FROM domainmetadata WHERE domain_id = ? AND kind = ?"
-           tx.execute(sql, [d_id, args["kind"]])
-           unless args["value"].nil?
-             sql = "INSERT INTO domainmetadata (domain_id,kind,content) VALUES(?,?,?)"
-             args["value"].each do |value|
-               STDERR.puts"Executing INSERT INTO domainmetadata (domain_id,kind,content) VALUES(#{d_id}, #{args["kind"]}, #{value})"
-               tx.execute(sql,[d_id, args["kind"], value])
-             end
-           end
-        end
-       return true
-   end
-
-   def do_directbackendcmd(args)
-     return [args["query"]]
-   end
-end
diff --git a/modules/remotebackend/regression-tests/dnsbackend.py b/modules/remotebackend/regression-tests/dnsbackend.py
new file mode 100755 (executable)
index 0000000..ba35286
--- /dev/null
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+
+import http.server
+import json
+import re
+
+from urllib.parse import parse_qsl, urlparse, unquote
+
+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'])
+
+            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.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))
diff --git a/modules/remotebackend/regression-tests/dnsbackend.rb b/modules/remotebackend/regression-tests/dnsbackend.rb
deleted file mode 100644 (file)
index 2e36fa5..0000000
+++ /dev/null
@@ -1,144 +0,0 @@
-require 'json'
-require 'thread'
-
-class DNSBackendHandler < WEBrick::HTTPServlet::AbstractServlet
-   def initialize(server, dnsbackend)
-     @dnsbackend = dnsbackend
-     @semaphore = Mutex.new
-     unless defined? @@f
-       @@f = File.open("/tmp/remotebackend.txt.#{$$}","a")
-       @@f.sync
-     end
-     @dnsbackend.do_initialize({})
-   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,
-             "name" => url.shift
-        } 
-     when "adddomainkey", "gettsigkey", "getdomaininfo"
-        {
-             "name" => 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.force_encoding("UTF-8"))
-
-     method = "do_#{method}"
-    
-     # get more arguments
-     req.each do |k,v|
-        attr = k[/x-remotebackend-(.*)/i,1]
-        if attr 
-          args[attr.downcase] = v.force_encoding("UTF-8")
-        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
-     @@f.puts "#{Time.now.to_f} [http]: #{({: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]: #{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
diff --git a/modules/remotebackend/regression-tests/http-backend.py b/modules/remotebackend/regression-tests/http-backend.py
new file mode 100755 (executable)
index 0000000..1fc8e5f
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+import http.server
+from backend import BackendHandler
+from dnsbackend import DNSBackendHandler
+import os
+
+class DNSBackendServer(http.server.HTTPServer):
+    def __init__(self, *args, **kwargs):
+        path = os.path.dirname(os.path.realpath(__file__))
+        self.handler = BackendHandler(options={'dbpath': os.path.join(path, 'remote.sqlite3')})
+        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)
+
+def main():
+    server = DNSBackendServer(('', 62434), DNSBackendHandler)
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        pass
+
+main()
diff --git a/modules/remotebackend/regression-tests/http-backend.rb b/modules/remotebackend/regression-tests/http-backend.rb
deleted file mode 100755 (executable)
index 6b8f7ff..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env ruby
-require "rubygems"
-require 'bundler/setup'
-require "webrick"
-$:.unshift File.dirname(__FILE__)
-require "dnsbackend"
-require "backend"
-require "pathname"
-
-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(Pathname.new(File.join(File.dirname(__FILE__),"remote.sqlite3")).realpath.to_s)
-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/regression-tests/pipe-backend.py b/modules/remotebackend/regression-tests/pipe-backend.py
new file mode 100755 (executable)
index 0000000..997ad4b
--- /dev/null
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+
+from pdns.remotebackend import PipeConnector
+from backend import BackendHandler
+import os
+
+def main():
+    path = os.path.dirname(os.path.realpath(__file__))
+    connector = PipeConnector(BackendHandler, options={'dbpath': os.path.join(path, 'remote.sqlite3'), 'rawlog':'/tmp/raw.json'})
+    connector.run()
+
+main()
diff --git a/modules/remotebackend/regression-tests/pipe-backend.rb b/modules/remotebackend/regression-tests/pipe-backend.rb
deleted file mode 100755 (executable)
index 993000e..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env ruby
-require "rubygems"
-require 'bundler/setup'
-require 'json'
-$:.unshift File.dirname(__FILE__)
-require "backend"
-
-h = Handler.new(Pathname.new(File.join(File.dirname(__FILE__),"remote.sqlite3")).realpath.to_s)
-
-f = File.open "/tmp/remotebackend.txt.#{$$}","a"
-f.sync = true
-
-STDOUT.sync = true
-begin 
-  STDIN.each_line do |line|
-    # expect json
-    input = {}
-    line = line.strip
-    f.puts "#{Time.now.to_f}: [pipe] #{line}"
-    next if line.empty?
-    begin
-      input = JSON.parse(line)
-      next unless input and input["method"]
-      method = "do_#{input["method"].downcase}"
-      args = input["parameters"]
-
-      if h.respond_to?(method.to_sym) == false
-         res = false
-      elsif args.size > 0
-         res, log = h.send(method,args)
-      else
-         res, log = h.send(method)
-      end
-      puts ({:result => res, :log => log}).to_json
-      f.puts "#{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/regression-tests/unix-backend.py b/modules/remotebackend/regression-tests/unix-backend.py
new file mode 100755 (executable)
index 0000000..997ad4b
--- /dev/null
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+
+from pdns.remotebackend import PipeConnector
+from backend import BackendHandler
+import os
+
+def main():
+    path = os.path.dirname(os.path.realpath(__file__))
+    connector = PipeConnector(BackendHandler, options={'dbpath': os.path.join(path, 'remote.sqlite3'), 'rawlog':'/tmp/raw.json'})
+    connector.run()
+
+main()
diff --git a/modules/remotebackend/regression-tests/unix-backend.rb b/modules/remotebackend/regression-tests/unix-backend.rb
deleted file mode 100755 (executable)
index c4adbb9..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env ruby
-require "rubygems"
-require 'bundler/setup'
-require 'json'
-$:.unshift File.dirname(__FILE__)
-require "backend"
-
-h = Handler.new(Pathname.new(File.join(File.dirname(__FILE__),"remote.sqlite3")).realpath.to_s)
-
-f = File.open "/tmp/remotebackend.txt.#{$$}","a"
-f.sync = true
-
-STDOUT.sync = true
-begin 
-  STDIN.each_line do |line|
-    # expect json
-    input = {}
-    line = line.strip
-    f.puts "#{Time.now.to_f}: [unix] #{line}"
-    next if line.empty?
-    begin
-      input = JSON.parse(line)
-      next unless input and input["method"]
-      method = "do_#{input["method"].downcase}"
-      args = input["parameters"]
-
-      if h.respond_to?(method.to_sym) == false
-         res = false
-      elsif args.size > 0
-         res, log = h.send(method,args)
-      else
-         res, log = h.send(method)
-      end
-      puts ({:result => res, :log => log}).to_json
-      f.puts "#{Time.now.to_f} [unix]: #{({:result => res, :log => log}).to_json}"
-    rescue JSON::ParserError
-      f.puts "#{Time.now.to_f} [unix]: #{({:result => false, :log => "Cannot parse input #{line}"}).to_json}"
-      next
-    end
-  end
-rescue SystemExit, Interrupt
-end
diff --git a/modules/remotebackend/regression-tests/zeromq-backend.py b/modules/remotebackend/regression-tests/zeromq-backend.py
new file mode 100755 (executable)
index 0000000..ff12670
--- /dev/null
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+import zmq
+import json
+import os
+from urllib.parse import parse_qs, urlparse
+from backend import BackendHandler
+
+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():
+    path = os.path.dirname(os.path.realpath(__file__))
+    context = zmq.Context()
+    socket = context.socket(zmq.REP)
+    socket.bind("ipc:///tmp/pdns.0")
+    handler = BackendHandler(options={'dbpath': os.path.join(path, 'remote.sqlite3')})
+
+    try:
+        run(socket, handler)
+    except KeyboardInterrupt as e:
+        pass
+
+    os.unlink("/tmp/remotebackend.0")
+main()
diff --git a/modules/remotebackend/regression-tests/zeromq-backend.rb b/modules/remotebackend/regression-tests/zeromq-backend.rb
deleted file mode 100755 (executable)
index 2af3bd9..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env ruby
-require "rubygems"
-require 'bundler/setup'
-require 'json'
-require 'zero_mq'
-$:.unshift File.dirname(__FILE__)
-require "backend"
-
-h = Handler.new(Pathname.new(File.join(File.dirname(__FILE__),"remote.sqlite3")).realpath.to_s)
-
-f = File.open "/tmp/remotebackend.txt.#{$$}","a"
-f.sync = true
-
-begin
-  context = ZeroMQ::Context.new
-  socket = context.socket ZMQ::REP
-  socket.bind("ipc:///tmp/pdns.0") or raise "Cannot bind to IPC socket"
-
-  while(true) 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)
-      next unless input and input["method"]
-      method = "do_#{input["method"].downcase}"
-      args = input["parameters"] || []
-
-      if h.respond_to?(method.to_sym) == false
-         res = false
-      elsif args.size > 0
-         res, log = h.send(method,args)
-      else
-         res, log = h.send(method)
-      end
-      socket.send_string ({:result => res, :log => log}).to_json, 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
-      f.puts "#{Time.now.to_f} [zmq]: #{({:result => false, :log => "Cannot parse input #{line}"}).to_json}"
-      next
-    end
-  end
-rescue SystemExit, Interrupt
-end
index 774568b3b139d4619f1c2ea53020760ef377738a..8aca1fafb026e7d93aee2d2aab2096920ce55e32 100644 (file)
@@ -5,6 +5,15 @@ case $context in
                narrow=$(echo $context | cut -d- -f 4)
                testsdir=../modules/remotebackend/regression-tests/
 
+               if [ ! -d $testsdir/../venv ]; then
+                       python3 -m venv $testsdir/../venv
+                       source $testsdir/../venv/bin/activate
+                       pip install -U wheel
+                       pip install -r $testsdir/../requirements.txt
+               else
+                       source $testsdir/../venv/bin/activate
+               fi
+
                # cleanup unbound-host.conf to avoid failures
                rm -f unbound-host.conf
 
@@ -19,7 +28,7 @@ case $context in
                        connstr="http:url=http://localhost:62434/dns"
                        rm -f remotebackend-server.log
                        rm -f remotebackend-access.log
-                       $testsdir/http-backend.rb &
+                       $testsdir/http-backend.py &
                        echo $! > pdns-remotebackend.pid
                        set +e
                        # make sure it runs before continuing
@@ -37,17 +46,17 @@ case $context in
                        ;;
                zeromq)
                        connstr="zeromq:endpoint=ipc:///tmp/pdns.0"
-                       $testsdir/zeromq-backend.rb &
+                       $testsdir/zeromq-backend.py &
                        echo $! > pdns-remotebackend.pid
                        ;;
                unix)
                        connstr="unix:path=$testsdir/remote.socket"
                         rm -f $testsdir/remote.socket
-                       socat unix-listen:$testsdir/remote.socket,fork exec:$testsdir/unix-backend.rb &
+                       socat unix-listen:$testsdir/remote.socket,fork exec:$testsdir/unix-backend.py &
                        echo $! > pdns-remotebackend.pid
                        ;;
                pipe)
-                       connstr="pipe:command=$testsdir/pipe-backend.rb"
+                       connstr="pipe:command=$testsdir/pipe-backend.py"
                        ;;
                *)
                        echo "Invalid usage"