#!/usr/bin/env python2
import sys
import os
import argparse

from twisted.application import service, internet, reactors
from twisted.application.reactors import installReactor
from twisted.internet.task import deferLater
from twisted.names import dns
from twisted.names import server
from twisted.scripts._twistd_unix import UnixApplicationRunner

# fix import path if openvpn2dns is installed via package manager
if os.path.isdir('/usr/share/openvpn2dns'):
    sys.path.insert(0, '/usr/share/openvpn2dns')

from openvpnzone import OpenVpnAuthorityHandler
from config import ConfigParser, ConfigurationError
from version import STRING as VERSIONSTRING


class OpenVpn2DnsApplication(UnixApplicationRunner):
    def __init__(self, config, twisted_config):
        UnixApplicationRunner.__init__(self, twisted_config)
        self.service_config = config

    def createOrGetApplication(self):
        return service.Application('OpenVPN2DNS')

    def createOpenvpn2DnsService(self):
        self.zones = OpenVpnAuthorityHandler(self.service_config)

        m = service.MultiService()
        for listen in self.service_config.listen_addresses:
            f = server.DNSServerFactory(self.zones, None, None, 2)
            p = dns.DNSDatagramProtocol(f)
            f.noisy = 0
            for (klass, arg) in [(internet.TCPServer, f), (internet.UDPServer, p)]:
                s = klass(listen[1], arg, interface=listen[0])
                s.setServiceParent(m)
        m.setServiceParent(self.application)

    def postApplication(self):
        self.createOpenvpn2DnsService()
        from twisted.internet import reactor
        deferLater(reactor, 1, self.zones.start_notify)
        UnixApplicationRunner.postApplication(self)


def try_parse(callback):
    def parse(value):
        try:
            return callback(value)
        except Exception as e:
            raise argparse.ArgumentTypeError(e.message)
    return parse


class BooleanAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, values if values is not None else True)


parser = argparse.ArgumentParser(description='A pure python DNS server serving'
                                 ' the content of OpenVPN status files')
parser.add_argument('configfile', type=try_parse(ConfigParser.parse_filename),
                    help='file path to configuration file')
parser.add_argument('--reactor', choices=sorted(map(lambda r: r.shortName,
                    reactors.getReactorTypes())),
                    help='Select reactor type for twisted')
parser.add_argument('--daemon', type=try_parse(ConfigParser.parse_boolean),
                    action=BooleanAction, nargs='?', metavar='BOOL',
                    help='Whether detach and run as daemon')
parser.add_argument('--drop-privileges', '--drop', dest='drop',
                    type=try_parse(ConfigParser.parse_boolean),
                    action=BooleanAction, nargs='?', metavar='BOOL',
                    help='Whether drop privileges after opened sockets. User'
                    ' and group information needed (via option or config)')
parser.add_argument('--user', type=try_parse(ConfigParser.parse_userid),
                    help='User id or name to use when dropping privileges after'
                    ' opened sockets (see drop-privileges option)')
parser.add_argument('--group', type=try_parse(ConfigParser.parse_groupid),
                    help='Group id or name to use when dropping privileges after'
                    ' opened sockets (see drop-privileges option)')
parser.add_argument('--log', help='Log destination (file name, "-" for stdout or'
                    ' "syslog" for syslog)')
parser.add_argument('--pid-file', '--pidfile', dest='pidfile', metavar='FILE-NAME',
                    help='Name of the pidfile, recommended for daemon mode')
parser.add_argument('--version', action='version', version='%(prog)s ' + VERSIONSTRING)

args = parser.parse_args()


class DefaultDict(dict):
    def __getitem__(self, name):
        return self.get(name, None)

# parse configuration file:
try:
    config = ConfigParser(args.configfile)
except ConfigurationError as e:
    parser.error(e)

# emulate twisted configuration:
twisted_config = DefaultDict()
twisted_config['nodaemon'] = not (args.daemon or config.daemon or False)
twisted_config['no_save'] = True
twisted_config['originalname'] = 'openvpn2dns'
twisted_config['prefix'] = 'openvpn2dns'
twisted_config['rundir'] = '.'
if (args.drop or config.drop) is True:
    twisted_config['uid'] = args.user or config.user
    if not twisted_config['uid']:
        parser.error('Need user (and group) information to drop privileges')
    twisted_config['gid'] = args.group or config.group
    if not twisted_config['gid']:
        parser.error('Need group information to drop privileges')
if (args.log or config.log) == 'syslog':
    twisted_config['syslog'] = True
elif args.log or config.log:
    twisted_config['log'] = args.log or config.log
twisted_config['pidfile'] = args.pidfile or config.pidfile


if args.reactor or config.reactor:
    installReactor(args.reactor or config.reactor)

# run appliation:
OpenVpn2DnsApplication(config, twisted_config).run()
