From 71aa34c212d6868fc7b95eb8284e24ce613afebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Old=C5=99ich=20Jedli=C4=8Dka?= Date: Sat, 2 Jun 2018 11:22:57 +0200 Subject: [PATCH] Added simple DNS server --- README.md | 2 +- requirements.txt | 1 + setup.py | 4 +- subregsim.conf.sample | 16 +++++++- subregsim/__main__.py | 62 +++++++++++++++++++++++++----- subregsim/api.py | 89 +++++++++++++++++++++++++++++++------------ subregsim/dns.py | 23 +++++++++++ 7 files changed, 158 insertions(+), 39 deletions(-) create mode 100644 subregsim/dns.py diff --git a/README.md b/README.md index bcb1c07..2d79c13 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ git clone -q https://github.com/oldium/subregsim.git cd subregsim ``` -Install dependencies: +Install dependencies (currently PySimpleSOAP needs a patch): ``` pip install -r requirements.txt diff --git a/requirements.txt b/requirements.txt index 4e47856..871f4ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ future git+https://github.com/oldium/pysimplesoap.git@fix-typeerror#egg=PySimpleSOAP configargparse +dnslib diff --git a/setup.py b/setup.py index 5c1d3d1..ce5d3b7 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,9 @@ def find_version(*file_paths): install_requires=['future', 'PySimpleSOAP', 'configargparse'], - extras_require={}, + extras_require={ + 'dns': ['dnslib'], + }, entry_points={ 'console_scripts': [ diff --git a/subregsim.conf.sample b/subregsim.conf.sample index 617b365..11685fd 100644 --- a/subregsim.conf.sample +++ b/subregsim.conf.sample @@ -11,7 +11,7 @@ domain = example.com # Host name or IP address to listen on (use 127.0.0.1 for testing on localhost, # or 0.0.0.0 to accept any address for testing with Docker) -host = 0.0.0.0 +#host = 127.0.0.1 # Port to listen on when in HTTP mode #port = 80 @@ -25,5 +25,17 @@ host = 0.0.0.0 # SSL server certificate in PEM format (may contain private key) #ssl-certificate = /config/server-certificate.crt -# SSL server private key in PEM format (not necessary if present in the certificate file) +# SSL server private key in PEM format (not necessary if present in the +# certificate file) #ssl-private-key = /config/server-certificate.key + +# If you want to enable simple DNS server serving added records, uncomment the +# following line +#dns = true + +# DNS server port to listen on +#dns-port = 53 + +# DNS server host name or IP address to listen on (use 127.0.0.1 for testing on +# localhost, or 0.0.0.0 to accept any address for testing with Docker) +#dns-host = 127.0.0.1 diff --git a/subregsim/__main__.py b/subregsim/__main__.py index 1029ccb..abdfcdc 100644 --- a/subregsim/__main__.py +++ b/subregsim/__main__.py @@ -4,12 +4,13 @@ from __future__ import (absolute_import, print_function) -__version__ = "0.1" +__version__ = "0.2" import configargparse import logging import signal import threading +import time from .api import (Api, ApiDispatcher, ApiHttpServer) @@ -22,6 +23,12 @@ except: has_ssl = False +try: + from . import dns + has_dns = True +except: + has_dns = False + def parse_command_line(): parser = configargparse.ArgumentParser(description="Subreg.cz API simulator suitable for Python lexicon module.") required_group = parser.add_argument_group("required arguments") @@ -31,9 +38,11 @@ def parse_command_line(): optional_group = parser.add_argument_group("optional arguments") optional_group.add_argument("-c", "--config", metavar="FILE", is_config_file=True, help="configuration file for all options (can be specified only on command-line)") - optional_group.add_argument("--host", default="localhost", env_var="SUBREGSIM_HOST", help="server listening host name or IP address (defaults to localhost)") - optional_group.add_argument("--port", type=int, default=80, env_var="SUBREGSIM_PORT", help="server listening port (defaults to 80)") - optional_group.add_argument("--url", default="http://localhost:8008/", env_var="SUBREGSIM_URL", help="API root URL for WSDL generation (defaults to http://localhost:8008/)") + + web_group = parser.add_argument_group("optional server arguments") + web_group.add_argument("--host", default="localhost", env_var="SUBREGSIM_HOST", help="server listening host name or IP address (defaults to localhost)") + web_group.add_argument("--port", type=int, default=80, env_var="SUBREGSIM_PORT", help="server listening port (defaults to 80)") + web_group.add_argument("--url", default="http://localhost:8008/", env_var="SUBREGSIM_URL", help="API root URL for WSDL generation (defaults to http://localhost:8008/)") if has_ssl: ssl_group = parser.add_argument_group("optional SSL arguments") @@ -42,6 +51,12 @@ def parse_command_line(): ssl_group.add_argument("--ssl-certificate", dest="ssl_certificate", metavar="PEM-FILE", default=None, env_var="SUBREGSIM_SSL_CERTIFICATE", help="specifies server certificate") ssl_group.add_argument("--ssl-private-key", dest="ssl_private_key", metavar="PEM-FILE", default=None, env_var="SUBREGSIM_SSL_PRIVATE_KEY", help="specifies server privatey key (not necessary if private key is part of certificate file)") + if has_dns: + dns_group = parser.add_argument_group("optional DNS server arguments") + dns_group.add_argument("--dns", dest="dns", action="store_true", default=False, env_var="SUBREGSIM_DNS", help="enables DNS server") + dns_group.add_argument("--dns-host", dest="dns_host", default="localhost", env_var="SUBREGSIM_DNS_HOST", help="DNS server listening host name or IP address (defaults to localhost)") + dns_group.add_argument("--dns-port", dest="dns_port", type=int, default=53, metavar="PORT", env_var="SUBREGSIM_DNS_PORT", help="DNS server listening port (defaults to 53)") + parsed = parser.parse_args() if has_ssl and parsed.ssl and ('ssl_certificate' not in parsed or not parsed.ssl_certificate): @@ -49,9 +64,10 @@ def parse_command_line(): return parsed +terminated = False + class TerminationHandler(object): - def __init__(self, httpd): - self.httpd = httpd + def __init__(self): signal.signal(signal.SIGINT, self.terminate) signal.signal(signal.SIGTERM, self.terminate) @@ -62,7 +78,9 @@ def terminate(self, signum, frame): log.info("Shutting down due to termination request") else: log.info("Shutting down due to interrupt request") - self.httpd.shutdown() + + global terminated + terminated = True def main(): @@ -73,8 +91,10 @@ def main(): dispatcher.register_api(api) use_ssl = has_ssl and arguments.ssl + use_dns = has_dns and arguments.dns httpd = ApiHttpServer((arguments.host, arguments.port), use_ssl, dispatcher) + stop_servers = [ httpd ] if use_ssl: log.info("Starting HTTPS server to listen on {}:{}...".format(arguments.host, arguments.ssl_port)) @@ -85,13 +105,35 @@ def main(): else: log.info("Starting HTTP server to listen on {}:{}...".format(arguments.host, arguments.port)) - TerminationHandler(httpd) + if use_dns: + log.info("Starting DNS server to listen on {}:{}...".format(arguments.dns_host, arguments.dns_port)) + api_resolver = dns.ApiDnsResolver(api) + dns_udp = dns.ApiDns(api_resolver, arguments.dns_host, arguments.dns_port, False) + dns_tcp = dns.ApiDns(api_resolver, arguments.dns_host, arguments.dns_port, True) + + stop_servers.append(dns_udp) + stop_servers.append(dns_tcp) + + dns_udp.start_thread() + dns_tcp.start_thread() + + TerminationHandler() httpd_thread = threading.Thread(target=httpd.run, name="SOAP Server") httpd_thread.start() - while httpd_thread.is_alive(): - httpd_thread.join(timeout=0.5) + while not terminated: + time.sleep(0.5) + + httpd.shutdown() + httpd_thread.join() + + if use_dns: + dns_udp.stop() + dns_tcp.stop() + + dns_udp.thread.join() + dns_tcp.thread.join() if __name__ == '__main__': try: diff --git a/subregsim/api.py b/subregsim/api.py index 45b634c..f3b0e53 100644 --- a/subregsim/api.py +++ b/subregsim/api.py @@ -6,6 +6,7 @@ import logging import random import string +import threading import pysimplesoap.server as soapserver from http.server import HTTPServer @@ -19,6 +20,37 @@ def __init__(self, username, password, domain): self.domain = domain self.db = [] self.ssid = None + self.sn = 1 + self.db_lock = threading.Lock() + + def toZone(self): + zone = [] + zone.append("$ORIGIN .") + zone.append("$TTL 1800") + + with self.db_lock: + zone.append("{} IN SOA ns.example.com admin.example.com ( {} 86400 900 1209600 1800 )".format( + self.domain, + self.sn, + )) + + for rr in self.db: + name = rr["name"] + type = rr["type"] + content = rr["content"] if "content" in rr else "" + prio = rr["prio"] + ttl = rr["ttl"] if rr["ttl"] != 1800 else "" + + if type != "MX": + prio = "" + + if type == "TXT": + content = '"{}"'.format(content.replace('"', r'\"')) + + zone.append("{} {} IN {} {} {}".format( + name, ttl, type, prio, content)) + + return "\n".join(zone) def login(self, login, password): log.info("Login: {}/{}".format(login, password)) @@ -211,7 +243,9 @@ def add_dns_record(self, ssid, domain, record): if 'prio' not in new_record: new_record['prio'] = 0 - self.db.append(new_record) + with self.db_lock: + self.db.append(new_record) + self.sn += 1 return { "response": { @@ -276,22 +310,24 @@ def modify_dns_record(self, ssid, domain, record): } } - found_items = [item for item in self.db if item["id"] == record["id"]] - if len(found_items) == 0: - return { - "response": { - "status": "error", - "error": { - "errormsg": "Record does not exist", - "errorcode": { - "major": 524, - "minor": 1003 + with self.db_lock: + found_items = [item for item in self.db if item["id"] == record["id"]] + if len(found_items) == 0: + return { + "response": { + "status": "error", + "error": { + "errormsg": "Record does not exist", + "errorcode": { + "major": 524, + "minor": 1003 + } } } } - } - found_items[0].update(record) + found_items[0].update(record) + self.sn += 1 return { "response": { @@ -356,21 +392,24 @@ def delete_dns_record(self, ssid, domain, record): } } - before_length = len(self.db) - self.db = [item for item in self.db if item["id"] != record["id"]] - if before_length == len(self.db): - return { - "response": { - "status": "error", - "error": { - "errormsg": "Record does not exist", - "errorcode": { - "major": 524, - "minor": 1003 + with self.db_lock: + before_length = len(self.db) + self.db = [item for item in self.db if item["id"] != record["id"]] + if before_length == len(self.db): + return { + "response": { + "status": "error", + "error": { + "errormsg": "Record does not exist", + "errorcode": { + "major": 524, + "minor": 1003 + } } } } - } + + self.sn += 1 return { "response": { diff --git a/subregsim/dns.py b/subregsim/dns.py new file mode 100644 index 0000000..b5d6307 --- /dev/null +++ b/subregsim/dns.py @@ -0,0 +1,23 @@ +''' +Subreg.cz API simulator suitable for Python lexicon module. +''' + +from __future__ import (absolute_import, print_function) +import logging +import dnslib.server +import dnslib.zoneresolver + +log = logging.getLogger(__name__) + +class ApiDnsResolver(dnslib.server.BaseResolver): + def __init__(self, api): + dnslib.server.BaseResolver.__init__(self) + self.api = api + + def resolve(self, request, handler): + resolver = dnslib.zoneresolver.ZoneResolver(self.api.toZone(), True) + return resolver.resolve(request, handler) + +class ApiDns(dnslib.server.DNSServer): + def __init__(self, resolver, address, port, tcp=False): + dnslib.server.DNSServer.__init__(self, resolver, address, port, tcp)