From: Aki Tuomi Date: Thu, 21 Mar 2024 14:11:18 +0000 (+0200) Subject: remotebackend: Convert regression tests to python X-Git-Tag: rec-5.1.0-alpha1~60^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5d370adee20e0689a59cf9da90f548d846c323d5;p=thirdparty%2Fpdns.git remotebackend: Convert regression tests to python --- diff --git a/modules/remotebackend/regression-tests/Gemfile b/modules/remotebackend/regression-tests/Gemfile deleted file mode 100644 index 2762b54308..0000000000 --- a/modules/remotebackend/regression-tests/Gemfile +++ /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 index 412e45f205..0000000000 --- a/modules/remotebackend/regression-tests/Gemfile.lock +++ /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 index 0000000000..e69de29bb2 diff --git a/modules/remotebackend/regression-tests/backend.py b/modules/remotebackend/regression-tests/backend.py new file mode 100755 index 0000000000..59a8f0bec4 --- /dev/null +++ b/modules/remotebackend/regression-tests/backend.py @@ -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 index 0c4bf47958..0000000000 --- a/modules/remotebackend/regression-tests/backend.rb +++ /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 index 0000000000..ba35286a52 --- /dev/null +++ b/modules/remotebackend/regression-tests/dnsbackend.py @@ -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 index 2e36fa5d86..0000000000 --- a/modules/remotebackend/regression-tests/dnsbackend.rb +++ /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 index 0000000000..1fc8e5faf1 --- /dev/null +++ b/modules/remotebackend/regression-tests/http-backend.py @@ -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 index 6b8f7fff6f..0000000000 --- a/modules/remotebackend/regression-tests/http-backend.rb +++ /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 index 0000000000..997ad4b05a --- /dev/null +++ b/modules/remotebackend/regression-tests/pipe-backend.py @@ -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 index 993000e9f0..0000000000 --- a/modules/remotebackend/regression-tests/pipe-backend.rb +++ /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 index 0000000000..997ad4b05a --- /dev/null +++ b/modules/remotebackend/regression-tests/unix-backend.py @@ -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 index c4adbb90c8..0000000000 --- a/modules/remotebackend/regression-tests/unix-backend.rb +++ /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 index 0000000000..ff12670f1f --- /dev/null +++ b/modules/remotebackend/regression-tests/zeromq-backend.py @@ -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 index 2af3bd9f9d..0000000000 --- a/modules/remotebackend/regression-tests/zeromq-backend.rb +++ /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 diff --git a/regression-tests/backends/remote-master b/regression-tests/backends/remote-master index 774568b3b1..8aca1fafb0 100644 --- a/regression-tests/backends/remote-master +++ b/regression-tests/backends/remote-master @@ -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"