#!python

import os
import sys
import socket
import winreg

import win32serviceutil
import win32service

port = socket.getservbyname('domain')


class Forwarder:
    """
    Windows Server 2008 and Windows Server 2008 R2 DNS Servers do not
    support listening on tunneled IPv6 addresses.  See
    http://social.technet.microsoft.com/Forums/en-US/winserverPN/thread/fe12c783-f6b8-4560-9a9a-8ab7c46b80cb
    for details.

    This forwarder can be installed as a service and run on any such server,
    and it will forward DNS requests to the IPv6 localhost address.
    """

    dest_addr = ('::1', port)

    def __init__(self, listen_address):
        self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
        address = (listen_address, port)
        self.socket.bind(address)
        # set a timeout so the service can terminate gracefully
        self.socket.settimeout(2)
        self.dest = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
        self.dest.settimeout(2)

    def stop(self):
        self.run = False

    def serve_forever(self):
        self.run = True
        while self.run:
            self.relay_message()

    def relay_message(self):
        try:
            mesg, requester = self.socket.recvfrom(2**16)
            print('received %(mesg)r from %(requester)s' % vars())
            self.dest.sendto(mesg, self.dest_addr)
            resp, src = self.dest.recvfrom(2**16)
            print('response %(resp)r' % vars())
            self.socket.sendto(resp, requester)
        except socket.timeout:
            pass


class RegConfig:
    def __init__(self, root_path, tree=winreg.HKEY_CURRENT_USER):
        self.root_path = root_path
        self.tree = tree

    def _get_key(self):
        return winreg.CreateKey(self.tree, self.root_path)

    @staticmethod
    def infer_key_type(value):
        if isinstance(value, int):
            return winreg.REG_DWORD
        if isinstance(value, str):
            if '%' in value:
                return winreg.REG_EXPAND_SZ
            return winreg.REG_SZ
        raise ValueError('Unable to infer type for {value}'.format(**vars()))

    def __setitem__(self, key, value):
        keytype = self.infer_key_type(value)
        self.set(key, value, keytype)

    def set(self, key, value, keytype):
        winreg.SetValueEx(self._get_key(), key, None, keytype, value)

    def __getitem__(self, key):
        try:
            value, type = winreg.QueryValueEx(self._get_key(), key)
        except WindowsError:
            raise KeyError(key)
        return value

    def get(self, key, default=None):
        try:
            value = self[key]
        except KeyError:
            value = default
        return value


class ForwardingService(win32serviceutil.ServiceFramework):
    r"""
    _svc_name_: The name of the service (used in the Windows registry).
    DEFAULT: The capitalized name of the current directory.

    _svc_display_name_: The name that will appear in the Windows Service Manager.
    DEFAULT: The capitalized name of the current directory.

    log_dir: The desired location of the stdout and stderr log files.
    DEFAULT: %system%\LogFiles\%(_svc_display_name_)s
    """
    _svc_name_ = 'dns_forward'
    "The name of the service"
    _svc_display_name_ = 'DNS Forwarding Service'
    "The Service Manager display name."
    log_dir = os.path.join(
        os.environ['SYSTEMROOT'], 'System32', 'LogFiles', _svc_display_name_
    )
    "The log directory for the stderr and stdout logs."

    config = RegConfig(
        r'Software\jaraco.net\DNS Forwarding Service', winreg.HKEY_LOCAL_MACHINE
    )

    def SvcDoRun(self):
        """Called when the Windows Service runs."""
        self.init_logging()
        self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
        self.forwarder = Forwarder(self.config.get('Listen Address', '::0'))
        self.ReportServiceStatus(win32service.SERVICE_RUNNING)
        self.forwarder.serve_forever()

    def SvcStop(self):
        """Called when Windows receives a service stop request."""

        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        self.forwarder.stop()
        self.ReportServiceStatus(win32service.SERVICE_STOPPED)

    def init_logging(self):
        "redirect output to avoid crashing the service"
        if not os.path.exists(self.log_dir):
            os.makedirs(self.log_dir)
        sys.stdout = open(os.path.join(ForwardingService.log_dir, 'stdout.log'), 'a')
        sys.stderr = open(os.path.join(ForwardingService.log_dir, 'stderr.log'), 'a')

    @classmethod
    def handle_command_line(cls):
        def listen_setter(opts):
            opts = dict(opts)
            if '-b' in opts:
                ForwardingService.config['Listen Address'] = opts['-b']

        params = dict(
            customInstallOptions='b:',  # use -b to specify bind address
            customOptionHandler=listen_setter,
        )
        win32serviceutil.HandleCommandLine(cls, **params)


def main():
    addr = ForwardingService.config['Listen Address']
    Forwarder(addr).serve_forever()


__name__ == '__main__' and main()
