#!/usr/bin/env python3.1
# =============================================================
# Python 3.1 / NetBSD 1.6.2 compatibility patch applied.
# WARNING: asyncio (3.4+) and async/await (3.5+) syntax
# cannot run on Python 3.1. This patch removes typing annotations,
# converts non-standard docstrings, and adds socket shims for 3.3+.
# Full async networking requires Python 3.5+.
# =============================================================

# Implements a target service according to the Web Service Discovery
# specification.
#
# The purpose is to enable non-Windows devices to be found by the 'Network
# (Neighborhood)' from Windows machines.
#
# see http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf and
# related documents for details (look at README for more references)
#
# (c) Steffen Christgau, 2017-2025

import sys
import signal
import socket

# -------------------------------------------------------
# Python 3.1 スレッド互換パッチ
# threading.py が内部で _thread を import するが Python 3.1 には存在しない。
# logging → multiprocessing → threading → _thread という連鎖を断ち切るため、
# 偽の _thread / threading モジュールを sys.modules に先行注入する。
# -------------------------------------------------------

# まず低レベルスレッドモジュールを取得 (Python 3.1 では 'thread')
_thread_low = None
try:
    import thread as _thread_low
except ImportError:
    pass
if _thread_low is None:
    try:
        import _thread as _thread_low
    except ImportError:
        pass

# _thread が sys.modules にない場合、偽モジュールを注入
if '_thread' not in sys.modules:
    import types as _types
    _fake_thread = _types.ModuleType('_thread')
    _fake_thread.error = RuntimeError
    _lock_ctr = [0]

    class _FakeLockObj(object):
        def acquire(self, blocking=1): return True
        def release(self): pass
        def locked(self): return False
        def acquire_lock(self, blocking=1): return True
        def release_lock(self): pass
        def locked_lock(self): return False
        def __enter__(self): return self
        def __exit__(self, *a): pass

    def _start_new_thread(fn, args, kwargs={}):
        try:
            if _thread_low is not None:
                _thread_low.start_new_thread(fn, args, kwargs)
            else:
                fn(*args, **kwargs)
        except Exception:
            pass

    _fake_thread.start_new_thread = _start_new_thread
    _fake_thread.allocate_lock = lambda: _FakeLockObj()
    _fake_thread.get_ident = lambda: 0
    _fake_thread.exit = lambda: None
    _fake_thread.interrupt_main = lambda: None
    _fake_thread.stack_size = lambda *a: 0
    _fake_thread.TIMEOUT_MAX = 9999999.0
    sys.modules['_thread'] = _fake_thread

# threading モジュールを import (今度は _thread が注入済みなので成功するはず)
try:
    import threading as _threading
except Exception:
    # それでも失敗する場合は完全な shim を構築
    import types as _types
    _fake_threading = _types.ModuleType('threading')

    class _DummyLock(object):
        def acquire(self, blocking=True): return True
        def release(self): pass
        def __enter__(self): return self
        def __exit__(self, *a): pass

    class _DummyEvent(object):
        def __init__(self): self._flag = False
        def set(self): self._flag = True
        def clear(self): self._flag = False
        def is_set(self): return self._flag
        def wait(self, timeout=None): pass

    class _DummyThread(object):
        def __init__(self, target=None, args=(), kwargs=None, daemon=False):
            self._target = target
            self._args = args
            self._kwargs = kwargs or {}
        def start(self):
            if self._target:
                try:
                    sys.modules['_thread'].start_new_thread(
                        self._target, self._args, self._kwargs)
                except Exception:
                    pass
        def join(self, timeout=None): pass
        def is_alive(self): return False

    _fake_threading.Thread = _DummyThread
    _fake_threading.Lock = _DummyLock
    _fake_threading.RLock = _DummyLock
    _fake_threading.Event = _DummyEvent
    _fake_threading.Condition = _DummyLock
    _fake_threading.Semaphore = _DummyLock
    _fake_threading.BoundedSemaphore = _DummyLock
    _fake_threading.Timer = _DummyThread
    class _DummyThreadObj2(object):
        name = "MainThread"
        ident = 0
        daemon = False
    _dt2 = _DummyThreadObj2()
    _fake_threading.current_thread = lambda: _dt2
    _fake_threading.currentThread = lambda: _dt2
    _fake_threading.main_thread = lambda: _dt2
    _fake_threading.local = object
    _fake_threading.TIMEOUT_MAX = 9999999.0
    sys.modules['threading'] = _fake_threading
    _threading = _fake_threading

# threading が成功した場合でも不足属性を補完する
if not hasattr(_threading, "RLock"):
    _threading.RLock = _threading.Lock
if not hasattr(_threading, "Condition"):
    _threading.Condition = _threading.Lock
if not hasattr(_threading, "Semaphore"):
    _threading.Semaphore = _threading.Lock
class _DummyThreadObj(object):
    name = "MainThread"
    ident = 0
    daemon = False
_dummy_thread_obj = _DummyThreadObj()
if not hasattr(_threading, "main_thread"):
    _threading.main_thread = lambda: _dummy_thread_obj
if not hasattr(_threading, "current_thread"):
    _threading.current_thread = lambda: _dummy_thread_obj
if not hasattr(_threading, "currentThread"):
    _threading.currentThread = lambda: _dummy_thread_obj
# current_thread が返す値も name を持つよう上書き保証
try:
    _ct = _threading.current_thread()
    if _ct is None or not hasattr(_ct, "name"):
        _threading.current_thread = lambda: _dummy_thread_obj
        _threading.currentThread = lambda: _dummy_thread_obj
except Exception:
    _threading.current_thread = lambda: _dummy_thread_obj
    _threading.currentThread = lambda: _dummy_thread_obj

# multiprocessing も threading に依存するので先に潰しておく
if 'multiprocessing' not in sys.modules:
    import types as _types
    _fake_mp = _types.ModuleType('multiprocessing')
    class _DummyProcess(object):
        name = "MainProcess"
        pid = 0
        daemon = False
    _dummy_process = _DummyProcess()
    _fake_mp.current_process = lambda: _dummy_process
    sys.modules['multiprocessing'] = _fake_mp
# -------------------------------------------------------
# End of スレッド互換パッチ
# -------------------------------------------------------

# callable() は Python 3.0/3.1 で削除、3.2 で復活
try:
    callable
except NameError:
    def callable(obj):
        return hasattr(obj, "__call__")
import struct
try:
    import argparse
except ImportError:
    argparse = None
import uuid
import time
import random
import logging
import platform
import ctypes.util
import collections
import xml.etree.ElementTree as ElementTree
import http
import http.server
import urllib.request
import urllib.parse
import os
import pwd
import grp
import datetime

# -------------------------------------------------------
# Minimal event-loop shim replacing asyncio for Python 3.1
# Uses select() for I/O readiness and threading for tasks.
# -------------------------------------------------------
import select as _select
import time as _time_mod

class _FakeTask(object):
    def __init__(self, thread):
        self._thread = thread
    def done(self):
        return not self._thread.is_alive()
    def add_done_callback(self, cb):
        def _w():
            self._thread.join()
            cb(self)
        _threading.Thread(target=_w, daemon=True).start()

class _FakeEventLoop(object):
    def __init__(self):
        self._readers = {}
        self._lock = _threading.Lock()
        self._stop_event = _threading.Event()
        self._running = False
        import os
        self._wakeup_r, self._wakeup_w = os.pipe()

    def add_reader(self, fd, callback, *args):
        with self._lock:
            self._readers[fd] = (callback, args)
        self._wakeup()

    def remove_reader(self, fd):
        with self._lock:
            self._readers.pop(fd, None)
        self._wakeup()

    def _wakeup(self):
        try:
            import os
            os.write(self._wakeup_w, b"\x00")
        except Exception:
            pass

    def create_task(self, fn):
        if callable(fn):
            t = _threading.Thread(target=fn, daemon=True)
            t.start()
            return _FakeTask(t)
        return _FakeTask(_threading.Thread(target=lambda: None, daemon=True))

    def add_signal_handler(self, sig, callback):
        import signal as _sig
        _sig.signal(sig, lambda s, f: callback())

    def is_running(self):
        return self._running

    def run_forever(self):
        import os
        self._running = True
        self._stop_event.clear()
        while not self._stop_event.is_set():
            with self._lock:
                fds = list(self._readers.keys()) + [self._wakeup_r]
            try:
                readable, _, _ = _select.select(fds, [], [], 1.0)
            except (ValueError, OSError):
                _time_mod.sleep(0.1)
                continue
            for fd in readable:
                if fd == self._wakeup_r:
                    try:
                        os.read(self._wakeup_r, 64)
                    except Exception:
                        pass
                    continue
                with self._lock:
                    entry = self._readers.get(fd)
                if entry:
                    cb, cb_args = entry
                    try:
                        cb(*cb_args)
                    except Exception as e:
                        logger.error("reader callback error: {}".format(e))
        self._running = False

    def run_until_complete(self, fn):
        if callable(fn):
            fn()

    def stop(self):
        self._stop_event.set()
        self._wakeup()

    def set_debug(self, val):
        pass

    def close(self):
        self.stop()

# -------------------------------------------------------
# End of event-loop shim
# -------------------------------------------------------

# --- Python 3.1 compatibility shims ---
import ctypes as _ctypes
import ctypes.util as _ctypes_util

if not hasattr(socket, 'if_nametoindex'):
    _libc_c = _ctypes.CDLL(_ctypes_util.find_library('c'), use_errno=True)
    def _if_nametoindex(name):
        res = _libc_c.if_nametoindex(name.encode('ascii'))
        if res == 0:
            raise OSError('Unknown interface: {0}'.format(name))
        return res
    socket.if_nametoindex = _if_nametoindex

if not hasattr(socket, 'if_indextoname'):
    _libc_c2 = _ctypes.CDLL(_ctypes_util.find_library('c'), use_errno=True)
    def _if_indextoname(idx):
        buf = _ctypes.create_string_buffer(16)
        if not _libc_c2.if_indextoname(idx, buf):
            raise OSError('Unknown interface index: {0}'.format(idx))
        return buf.value.decode('ascii')
    socket.if_indextoname = _if_indextoname

if not hasattr(socket, 'AF_NETLINK'):
    socket.AF_NETLINK = 16
if not hasattr(socket, 'NETLINK_ROUTE'):
    socket.NETLINK_ROUTE = 0
if not hasattr(socket, 'AF_LINK'):
    # AF_LINK: BSD datalink (18 on NetBSD/FreeBSD/OpenBSD, 18 on Darwin)
    socket.AF_LINK = 18
if not hasattr(socket, 'AF_ROUTE'):
    # AF_ROUTE: routing socket (17 on NetBSD/FreeBSD, 17 on Darwin)
    socket.AF_ROUTE = 17
if not hasattr(socket, 'AF_UNSPEC'):
    socket.AF_UNSPEC = 0

if not hasattr(socket, 'inet_pton'):
    def _inet_pton(af, addr):
        if af == socket.AF_INET:
            return socket.inet_aton(addr)
        raise OSError('inet_pton AF_INET6 not supported without native inet_pton')
    socket.inet_pton = _inet_pton

if not hasattr(socket, 'inet_ntop'):
    def _inet_ntop(af, packed):
        if af == socket.AF_INET:
            return socket.inet_ntoa(packed)
        raise OSError('inet_ntop AF_INET6 not supported without native inet_ntop')
    socket.inet_ntop = _inet_ntop
# --- end shims ---


# try to load more secure XML module first, fallback to default if not present
try:
    from defusedxml.ElementTree import fromstring as ETfromString
except ImportError:
    from xml.etree.ElementTree import fromstring as ETfromString

try:
    import systemd.daemon  # type: ignore
except ImportError:
    # Non-systemd host
    pass

WSDD_VERSION = '0.9/nb1.6.2+py3.1'


class NetworkInterface:

    def __init__(self, name, scope, index):
        self._name = name
        self._scope = scope
        if index is not None:
            self._index = index
        else:
            self._index = socket.if_nametoindex(self._name)

    @property
    def name(self):
        return self._name

    @property
    def scope(self):
        return self._scope

    @property
    def index(self):
        return self._index

    def __str__(self):
        return self._name

    def __eq__(self, other):
        return self._name == other.name


class NetworkAddress:

    def __init__(self, family, raw, interface):
        self._family = family
        self._raw_address = raw if isinstance(raw, bytes) else socket.inet_pton(family, raw.partition('%')[0])
        self._interface = interface

        self._address_str = socket.inet_ntop(self._family, self._raw_address)

    @property
    def address_str(self):
        return self._address_str

    @property
    def family(self):
        return self._family

    @property
    def interface(self):
        return self._interface

    @property
    def is_multicastable(self):
        # return true if the (interface) address can be used for creating (link-local) multicasting sockets  #
        # Nah, this check is not optimal but there are no local flags for
        # addresses, but it should be safe for IPv4 anyways
        # (https://tools.ietf.org/html/rfc5735#page-3)
        return ((self._family == socket.AF_INET) and (self._raw_address[0] != 127)
                or (self._family == socket.AF_INET6) and (self._raw_address[0:2] == b'\xfe\x80'))

    @property
    def raw(self):
        return self._raw_address

    @property
    def transport_str(self):
        #the string representation of the local address overridden in network setup (for IPv6)#
        return self._address_str if self._family == socket.AF_INET else '[{}]'.format(self._address_str)

    def __str__(self):
        return '{}%{}'.format(self._address_str, self._interface.name)

    def __eq__(self, other):
        return (self._family == other.family and self.raw == other.raw and self.interface == other.interface)


class UdpAddress(NetworkAddress):

    def __init__(self, family, transport_address, interface):

        if not (family == socket.AF_INET or family == socket.AF_INET6):
            raise RuntimeError('Unsupport address address family: {}.'.format(family))

        self._transport_address = transport_address
        self._port = transport_address[1]

        super().__init__(family, transport_address[0], interface)

    @property
    def transport_address(self):
        return self._transport_address

    @property
    def port(self):
        return self._port

    def __eq__(self, other):
        return self.transport_address == other.transport_address


class INetworkPacketHandler:

    def handle_packet(self, msg, udp_src_address=None):
        pass


class MulticastHandler:
    # A class for handling multicast traffic on a given interface for a
    # given address family. It provides multicast sender and receiver sockets

    # base interface addressing information

    # individual interface-bound sockets for:
    #  - receiving multicast traffic
    #  - sending multicast from a socket bound to WSD port
    #  - sending unicast messages from a random port

    # addresses used for communication and data

    # dictionary that holds INetworkPacketHandlers instances for sockets created above

    def __init__(self, address, aio_loop):
        self.address = address

        self.recv_socket = socket.socket(self.address.family, socket.SOCK_DGRAM)
        self.recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.mc_send_socket = socket.socket(self.address.family, socket.SOCK_DGRAM)
        self.uc_send_socket = socket.socket(self.address.family, socket.SOCK_DGRAM)
        self.uc_send_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        self.message_handlers = {}
        self.aio_loop = aio_loop

        if self.address.family == socket.AF_INET:
            self.init_v4()
        elif self.address.family == socket.AF_INET6:
            self.init_v6()

        logger.info('joined multicast group {0} on {1}'.format(self.multicast_address.transport_str, self.address))
        logger.debug('transport address on {0} is {1}'.format(self.address.interface.name, self.address.transport_str))
        logger.debug('will listen for HTTP traffic on address {0}'.format(self.listen_address))

        # register calbacks for incoming data (also for mc)
        self.aio_loop.add_reader(self.recv_socket.fileno(), self.read_socket, self.recv_socket)
        self.aio_loop.add_reader(self.mc_send_socket.fileno(), self.read_socket, self.mc_send_socket)
        self.aio_loop.add_reader(self.uc_send_socket.fileno(), self.read_socket, self.uc_send_socket)

    def cleanup(self):
        self.aio_loop.remove_reader(self.recv_socket)
        self.aio_loop.remove_reader(self.mc_send_socket)
        self.aio_loop.remove_reader(self.uc_send_socket)

        self.recv_socket.close()
        self.mc_send_socket.close()
        self.uc_send_socket.close()

    def handles_address(self, address):
        return self.address == address

    def init_v6(self):
        idx = self.address.interface.index
        raw_mc_addr = (WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0x575C, idx)
        self.multicast_address = UdpAddress(self.address.family, raw_mc_addr, self.address.interface)

        # v6: member_request = { multicast_addr, intf_idx }
        mreq = (socket.inet_pton(self.address.family, WSD_MCAST_GRP_V6) + struct.pack('@I', idx))
        self.recv_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
        self.recv_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)

        # Could anyone ask the Linux folks for the rationale for this!?
        if platform.system() == 'Linux':
            try:
                # supported starting from Linux 4.20
                IPV6_MULTICAST_ALL = 29
                self.recv_socket.setsockopt(socket.IPPROTO_IPV6, IPV6_MULTICAST_ALL, 0)
            except OSError as e:
                logger.warning('cannot unset all_multicast: {}'.format(e))

        # bind to network interface, i.e. scope and handle OS differences,
        # see Stevens: Unix Network Programming, Section 21.6, last paragraph
        try:
            self.recv_socket.bind((WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0, idx))
        except OSError:
            self.recv_socket.bind(('::', 0, 0, idx))

        # bind unicast socket to interface address and WSD's udp port
        self.uc_send_socket.bind((str(self.address), WSD_UDP_PORT, 0, idx))

        self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0)
        self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, args.hoplimit)
        self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)

        # bind multicast socket to interface address and a user-provided port (or random if unspecified)
        # this allows not-so-smart firewalls to whitelist another port to allow incoming replies
        try:
            self.mc_send_socket.bind((str(self.address), args.source_port, 0, idx))
        except OSError:
            logger.error('specified port {} already in use for {}'.format(args.source_port, str(self.address)))

        self.listen_address = (self.address.address_str, WSD_HTTP_PORT, 0, idx)

    def init_v4(self):
        idx = self.address.interface.index
        raw_mc_addr = (WSD_MCAST_GRP_V4, WSD_UDP_PORT)
        self.multicast_address = UdpAddress(self.address.family, raw_mc_addr, self.address.interface)

        # v4: member_request
        # Linux uses ip_mreqn = { mc_addr(4) + intf_addr(4) + idx(4) } = 12B
        # BSD/NetBSD uses ip_mreq = { mc_addr(4) + intf_addr(4) } = 8B
        _sys = platform.system()
        mc_raw = socket.inet_pton(self.address.family, WSD_MCAST_GRP_V4)
        if _sys == 'SunOS':
            mreq = mc_raw + self.address.raw
        elif _sys == 'Linux':
            mreq = mc_raw + self.address.raw + struct.pack('@I', idx)
        else:
            # BSD: ip_mreq (8B) - no interface index
            mreq = mc_raw + self.address.raw
        self.recv_socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

        if platform.system() == 'Linux':
            IP_MULTICAST_ALL = 49
            self.recv_socket.setsockopt(socket.IPPROTO_IP, IP_MULTICAST_ALL, 0)

        try:
            self.recv_socket.bind((WSD_MCAST_GRP_V4, WSD_UDP_PORT))
        except OSError:
            self.recv_socket.bind(('', WSD_UDP_PORT))

        # bind unicast socket to interface address and WSD's udp port
        self.uc_send_socket.bind((self.address.address_str, WSD_UDP_PORT))

        if platform.system() == 'Linux':
            self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, mreq)
        else:
            # BSD/SunOS: IP_MULTICAST_IF takes just in_addr (4B interface address)
            self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, self.address.raw)
        # OpenBSD requires the optlen to be sizeof(char) for LOOP and TTL options
        # (see also https://github.com/python/cpython/issues/67316)
        self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, struct.pack('B', 0))
        self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, struct.pack('B', args.hoplimit))

        # bind multicast socket to interface address and a user-provided port (or random if unspecified)
        # this allows not-so-smart firewalls to whitelist another port to allow incoming replies
        try:
            self.mc_send_socket.bind((self.address.address_str, args.source_port))
        except OSError:
            logger.error('specified port {} already in use for {}'.format(args.source_port, self.address.address_str))

        self.listen_address = (self.address.address_str, WSD_HTTP_PORT)

    def add_handler(self, socket, handler):
        # try:
        #    self.selector.register(socket, selectors.EVENT_READ, self)
        # except KeyError:
        #    # accept attempts of multiple registrations
        #    pass

        if socket in self.message_handlers:
            self.message_handlers[socket].append(handler)
        else:
            self.message_handlers[socket] = [handler]

    def remove_handler(self, socket, handler):
        if socket in self.message_handlers:
            if handler in self.message_handlers[socket]:
                self.message_handlers[socket].remove(handler)

    def read_socket(self, key):
        # TODO: refactor this
        s = None
        if key == self.uc_send_socket:
            s = self.uc_send_socket
        elif key == self.mc_send_socket:
            s = self.mc_send_socket
        elif key == self.recv_socket:
            s = self.recv_socket
        else:
            raise ValueError("Unknown socket passed as key.")

        msg, raw_address = s.recvfrom(WSD_MAX_LEN)
        address = UdpAddress(self.address.family, raw_address, self.address.interface)
        if s in self.message_handlers:
            for handler in self.message_handlers[s]:
                handler.handle_packet(msg.decode('utf-8'), address)

    def send(self, msg, addr):
        # Request from a client must be answered from a socket that is bound
        # to the WSD port, i.e. the recv_socket. Messages to multicast
        # addresses are sent over the dedicated send socket.
        if addr == self.multicast_address:
            self.mc_send_socket.sendto(msg, addr.transport_address)
        else:
            self.uc_send_socket.sendto(msg, addr.transport_address)


# constants for WSD XML/SOAP parsing
WSA_URI = 'http://schemas.xmlsoap.org/ws/2004/08/addressing'
WSD_URI = 'http://schemas.xmlsoap.org/ws/2005/04/discovery'
WSDP_URI = 'http://schemas.xmlsoap.org/ws/2006/02/devprof'

namespaces = {
    'soap': 'http://www.w3.org/2003/05/soap-envelope',
    'wsa': WSA_URI,
    'wsd': WSD_URI,
    'wsx': 'http://schemas.xmlsoap.org/ws/2004/09/mex',
    'wsdp': WSDP_URI,
    'pnpx': 'http://schemas.microsoft.com/windows/pnpx/2005/10',
    'pub': 'http://schemas.microsoft.com/windows/pub/2005/07'
}

def _ns(xpath):
    """XPath の ns:tag を {URI}tag 形式に展開する (Python 3.1 用)"""
    import re as _re
    def _repl(m):
        p, l = m.group(1), m.group(2)
        return ("{" + namespaces[p] + "}" + l) if p in namespaces else m.group(0)
    return _re.sub(r"(\w+):(\w+|\*)", _repl, xpath)


WSD_MAX_KNOWN_MESSAGES = 10

WSD_PROBE = WSD_URI + '/Probe'
WSD_PROBE_MATCH = WSD_URI + '/ProbeMatches'
WSD_RESOLVE = WSD_URI + '/Resolve'
WSD_RESOLVE_MATCH = WSD_URI + '/ResolveMatches'
WSD_HELLO = WSD_URI + '/Hello'
WSD_BYE = WSD_URI + '/Bye'
WSD_GET = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Get'
WSD_GET_RESPONSE = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse'

WSD_TYPE_DEVICE = 'wsdp:Device'
PUB_COMPUTER = 'pub:Computer'
WSD_TYPE_DEVICE_COMPUTER = '{0} {1}'.format(WSD_TYPE_DEVICE, PUB_COMPUTER)

WSD_MCAST_GRP_V4 = '239.255.255.250'
WSD_MCAST_GRP_V6 = 'ff02::c'  # link-local

WSA_ANON = WSA_URI + '/role/anonymous'
WSA_DISCOVERY = 'urn:schemas-xmlsoap-org:ws:2005:04:discovery'

MIME_TYPE_SOAP_XML = 'application/soap+xml'

# protocol assignments (WSD spec/Section 2.4)
WSD_UDP_PORT = 3702
WSD_HTTP_PORT = 5357
WSD_MAX_LEN = 32767

WSDD_LISTEN_PORT = 5359

# SOAP/UDP transmission constants
MULTICAST_UDP_REPEAT = 4
UNICAST_UDP_REPEAT = 2
UDP_MIN_DELAY = 50
UDP_MAX_DELAY = 250
UDP_UPPER_DELAY = 500

# servers must recond in 4 seconds after probe arrives
PROBE_TIMEOUT = 4
MAX_STARTUP_PROBE_DELAY = 3

# some globals
wsd_instance_id = int(time.time())

# WSDMessage type alias removed
# MessageTypeHandler type alias removed


class WSDMessageHandler(INetworkPacketHandler):
    known_messages = collections.deque([], WSD_MAX_KNOWN_MESSAGES)

    def __init__(self):
        self.handlers = {}
        self.pending_tasks = []

    def cleanup(self):
        pass

    # shortcuts for building WSD responses
    def add_endpoint_reference(self, parent, endpoint=None):
        epr = ElementTree.SubElement(parent, 'wsa:EndpointReference')
        address = ElementTree.SubElement(epr, 'wsa:Address')
        if endpoint is None:
            address.text = args.uuid.urn
        else:
            address.text = endpoint

    def add_metadata_version(self, parent):
        meta_data = ElementTree.SubElement(parent, 'wsd:MetadataVersion')
        meta_data.text = '1'

    def add_types(self, parent):
        dev_type = ElementTree.SubElement(parent, 'wsd:Types')
        dev_type.text = WSD_TYPE_DEVICE_COMPUTER

    def add_xaddr(self, parent, transport_addr):
        if transport_addr:
            item = ElementTree.SubElement(parent, 'wsd:XAddrs')
            item.text = 'http://{0}:{1}/{2}'.format(transport_addr, WSD_HTTP_PORT, args.uuid)

    def build_message(self, to_addr, action_str, request_header,
                      response):
        retval = self.xml_to_str(self.build_message_tree(to_addr, action_str, request_header, response)[0])

        logger.debug('constructed xml for WSD message: {0}'.format(retval))

        return retval

    def build_message_tree(self, to_addr, action_str, request_header,
                           body):
        # Build a WSD message with a given action string including SOAP header.

        # The message can be constructed based on a response to another
        # message (given by its header) and with a optional response that
        # serves as the message's body
        root = ElementTree.Element('soap:Envelope')
        header = ElementTree.SubElement(root, 'soap:Header')

        to = ElementTree.SubElement(header, 'wsa:To')
        to.text = to_addr

        action = ElementTree.SubElement(header, 'wsa:Action')
        action.text = action_str

        msg_id = ElementTree.SubElement(header, 'wsa:MessageID')
        msg_id.text = uuid.uuid1().urn

        if request_header is not None:
            req_msg_id = request_header.find(_ns('./wsa:MessageID'))
            if req_msg_id is not None:
                relates_to = ElementTree.SubElement(header, 'wsa:RelatesTo')
                relates_to.text = req_msg_id.text

        self.add_header_elements(header, action_str)

        body_root = ElementTree.SubElement(root, 'soap:Body')
        if body is not None:
            body_root.append(body)

        for prefix, uri in namespaces.items():
            root.attrib['xmlns:' + prefix] = uri

        return root, msg_id.text

    def add_header_elements(self, header, extra):
        pass

    def handle_message(self, msg, src=None):
        # handle a WSD message
        try:
            tree = ETfromString(msg)
        except ElementTree.ParseError:
            return None

        header = tree.find(_ns('./soap:Header'))
        if header is None:
            return None

        msg_id_tag = header.find(_ns('./wsa:MessageID'))
        if msg_id_tag is None:
            return None

        msg_id = str(msg_id_tag.text)

        # check for duplicates
        if self.is_duplicated_msg(msg_id):
            logger.debug('known message ({0}): dropping it'.format(msg_id))
            return None

        action_tag = header.find(_ns('./wsa:Action'))
        if action_tag is None:
            return None

        action = str(action_tag.text)
        _, _, action_method = action.rpartition('/')

        if src:
            logger.info('{}:{}({}) - - "{} {} UDP" - -'.format(
                src.transport_str, src.port, src.interface, action_method, msg_id))
        else:
            # http logging is already done by according server
            logger.debug('processing WSD {} message ({})'.format(action_method, msg_id))

        body = tree.find(_ns('./soap:Body'))
        if body is None:
            return None

        logger.debug('incoming message content is {0}'.format(msg))
        if action in self.handlers:
            handler = self.handlers[action]
            retval = handler(header, body)
            if retval is not None:
                response, response_type = retval
                return self.build_message(WSA_ANON, response_type, header, response)
        else:
            logger.debug('unhandled action {0}/{1}'.format(action, msg_id))

        return None

    def is_duplicated_msg(self, msg_id):
        # Check for a duplicated message.

        # Implements SOAP-over-UDP Appendix II Item 2
        if msg_id in type(self).known_messages:
            return True

        type(self).known_messages.append(msg_id)

        return False

    def xml_to_str(self, xml):
        retval = '<?xml version="1.0" encoding="utf-8"?>'
        retval = retval + ElementTree.tostring(xml, encoding='utf-8').decode('utf-8')

        return retval


class WSDUDPMessageHandler(WSDMessageHandler):
    # A message handler that handles traffic received via MutlicastHandler.

    def __init__(self, mch):
        super().__init__()

        self.mch = mch
        self.tearing_down = False

    def teardown(self):
        self.tearing_down = True

    def send_datagram(self, msg, dst):
        try:
            self.mch.send(msg.encode('utf-8'), dst)
        except Exception as e:
            logger.error('error while sending packet on {}: {}'.format(self.mch.address.interface, e))

    def enqueue_datagram(self, msg, address, msg_type=None):
        if msg_type:
            logger.info('scheduling {0} message via {1} to {2}'.format(msg_type, address.interface, address))

        _msg, _addr = msg, address
        def _run_schedule(_m=_msg, _a=_addr):
            self.schedule_datagram(_m, _a)
        schedule_task = self.mch.aio_loop.create_task(_run_schedule)
        # Add this task to the pending list during teardown to wait during shutdown
        if self.tearing_down:
            self.pending_tasks.append(schedule_task)

    def schedule_datagram(self, msg, address):
        # Schedule to send the given message to the given address.

        # Implements SOAP over UDP, Appendix I.

        self.send_datagram(msg, address)

        delta = 0
        msg_count = MULTICAST_UDP_REPEAT if address == self.mch.multicast_address else UNICAST_UDP_REPEAT
        delta = random.randint(UDP_MIN_DELAY, UDP_MAX_DELAY)
        for i in range(msg_count - 1):
            time.sleep(delta / 1000.0)
            self.send_datagram(msg, address)
            delta = min(delta * 2, UDP_UPPER_DELAY)


class WSDDiscoveredDevice:

    # a dict of discovered devices with their UUID as key
    instances = {}

    def __init__(self, xml_str, xaddr, interface):
        self.last_seen = 0.0
        self.addresses = {}
        self.props = {}
        self.display_name = ''
        self.types = set()

        self.update(xml_str, xaddr, interface)

    def update(self, xml_str, xaddr, interface):
        try:
            tree = ETfromString(xml_str)
        except ElementTree.ParseError:
            return None
        mds_path = 'soap:Body/wsx:Metadata/wsx:MetadataSection'
        sections = tree.findall(_ns(mds_path))
        for section in sections:
            dialect = section.attrib['Dialect']
            if dialect == WSDP_URI + '/ThisDevice':
                self.extract_wsdp_props(section, dialect)
            elif dialect == WSDP_URI + '/ThisModel':
                self.extract_wsdp_props(section, dialect)
            elif dialect == WSDP_URI + '/Relationship':
                host_xpath = 'wsdp:Relationship[@Type="{}/host"]/wsdp:Host'.format(WSDP_URI)
                host_sec = section.find(_ns(host_xpath))
                if (host_sec is not None):
                    self.extract_host_props(host_sec)
            else:
                logger.debug('unknown metadata dialect ({})'.format(dialect))

        url = urllib.parse.urlparse(xaddr)
        addr, _, _ = url.netloc.rpartition(':')
        report = True
        if interface.name not in self.addresses:
            self.addresses[interface.name] = set([addr])
        else:
            if addr not in self.addresses[interface.name]:
                self.addresses[interface.name].add(addr)
            else:
                report = False

        self.last_seen = time.time()
        if ('DisplayName' in self.props) and ('BelongsTo' in self.props) and (report):
            self.display_name = self.props['DisplayName']
            logger.info('discovered {} in {} on {}'.format(self.display_name, self.props['BelongsTo'], addr))
        elif ('FriendlyName' in self.props) and (report):
            self.display_name = self.props['FriendlyName']
            logger.info('discovered {} on {}'.format(self.display_name, addr))

        logger.debug(str(self.props))

    def extract_wsdp_props(self, root, dialect):
        _, _, propsRoot = dialect.rpartition('/')
        # XPath support is limited, so filter by namespace on our own
        nodes = root.findall(_ns('./wsdp:{0}/*'.format(propsRoot)))
        ns_prefix = '{{{}}}'.format(WSDP_URI)
        prop_nodes = [n for n in nodes if n.tag.startswith(ns_prefix)]
        for node in prop_nodes:
            tag_name = node.tag[len(ns_prefix):]
            self.props[tag_name] = str(node.text)

    def extract_host_props(self, root):
        self.types = set(root.findtext(_ns('wsdp:Types'), '').split(' '))
        if PUB_COMPUTER not in self.types:
            return

        comp = root.findtext(_ns(PUB_COMPUTER), '')
        self.props['DisplayName'], _, self.props['BelongsTo'] = (
            comp.partition('/'))


class WSDClient(WSDUDPMessageHandler):

    instances = []

    def __init__(self, mch):
        super().__init__(mch)

        WSDClient.instances.append(self)

        self.mch.add_handler(self.mch.mc_send_socket, self)
        self.mch.add_handler(self.mch.recv_socket, self)

        self.probes = {}

        self.handlers[WSD_HELLO] = self.handle_hello
        self.handlers[WSD_BYE] = self.handle_bye
        self.handlers[WSD_PROBE_MATCH] = self.handle_probe_match
        self.handlers[WSD_RESOLVE_MATCH] = self.handle_resolve_match

        # avoid packet storm when hosts come up by delaying initial probe
        time.sleep(random.randint(0, MAX_STARTUP_PROBE_DELAY))
        self.send_probe()

    def cleanup(self):
        super().cleanup()
        WSDClient.instances.remove(self)

        self.mch.remove_handler(self.mch.mc_send_socket, self)
        self.mch.remove_handler(self.mch.recv_socket, self)

    def send_probe(self):
        #WS-Discovery, Section 4.3, Probe message#
        self.remove_outdated_probes()

        probe = ElementTree.Element('wsd:Probe')
        ElementTree.SubElement(probe, 'wsd:Types').text = WSD_TYPE_DEVICE

        xml, i = self.build_message_tree(WSA_DISCOVERY, WSD_PROBE, None, probe)
        self.enqueue_datagram(self.xml_to_str(xml), self.mch.multicast_address, msg_type='Probe')
        self.probes[i] = time.time()

    def teardown(self):
        super().teardown()
        self.remove_outdated_probes()

    def handle_packet(self, msg, src):
        self.handle_message(msg, src)

    def __extract_xaddr(self, xaddrs):
        for addr in xaddrs.strip().split():
            if (self.mch.address.family == socket.AF_INET6) and ('//[fe80::' in addr):
                # use first link-local address for IPv6
                return addr
            elif self.mch.address.family == socket.AF_INET:
                # use first (and very likely the only) IPv4 address
                return addr

        return None

    def handle_hello(self, header, body):
        pm_path = 'wsd:Hello'
        endpoint, xaddrs = self.extract_endpoint_metadata(body, pm_path)
        if not xaddrs:
            logger.info('Hello without XAddrs, sending resolve')
            msg = self.build_resolve_message(str(endpoint))
            self.enqueue_datagram(msg, self.mch.multicast_address)
            return None

        xaddr = self.__extract_xaddr(xaddrs)
        if xaddr is None:
            return None

        logger.info('Hello from {} on {}'.format(endpoint, xaddr))
        self.perform_metadata_exchange(endpoint, xaddr)
        return None

    def handle_bye(self, header, body):
        bye_path = 'wsd:Bye'
        endpoint, _ = self.extract_endpoint_metadata(body, bye_path)
        device_uri = str(urllib.parse.urlparse(endpoint).geturl())
        if device_uri in WSDDiscoveredDevice.instances:
            del WSDDiscoveredDevice.instances[device_uri]

        return None

    def handle_probe_match(self, header, body):
        # do not handle to probematches issued not sent by ourself
        rel_msg = header.findtext(_ns('wsa:RelatesTo'), None)
        if rel_msg not in self.probes:
            logger.debug("unknown probe {}".format(rel_msg))
            return None

        # if XAddrs are missing, issue resolve request
        pm_path = 'wsd:ProbeMatches/wsd:ProbeMatch'
        endpoint, xaddrs = self.extract_endpoint_metadata(body, pm_path)
        if not xaddrs:
            logger.debug('probe match without XAddrs, sending resolve')
            msg = self.build_resolve_message(str(endpoint))
            self.enqueue_datagram(msg, self.mch.multicast_address)
            return None

        xaddr = self.__extract_xaddr(xaddrs)
        if xaddr is None:
            return None

        logger.debug('probe match for {} on {}'.format(endpoint, xaddr))
        self.perform_metadata_exchange(endpoint, xaddr)

        return None

    def build_resolve_message(self, endpoint):
        resolve = ElementTree.Element('wsd:Resolve')
        self.add_endpoint_reference(resolve, endpoint)

        return self.build_message(WSA_DISCOVERY, WSD_RESOLVE, None, resolve)

    def handle_resolve_match(self, header, body):
        rm_path = 'wsd:ResolveMatches/wsd:ResolveMatch'
        endpoint, xaddrs = self.extract_endpoint_metadata(body, rm_path)
        if not endpoint or not xaddrs:
            logger.debug('resolve match without endpoint/xaddr')
            return None

        xaddr = self.__extract_xaddr(xaddrs)
        if xaddr is None:
            return None

        logger.debug('resolve match for {} on {}'.format(endpoint, xaddr))
        self.perform_metadata_exchange(endpoint, xaddr)

        return None

    def extract_endpoint_metadata(self, body, prefix):
        prefix = prefix + '/'
        addr_path = 'wsa:EndpointReference/wsa:Address'

        endpoint = body.findtext(_ns(prefix + addr_path))
        xaddrs = body.findtext(_ns(prefix + 'wsd:XAddrs'))

        return endpoint, xaddrs

    def perform_metadata_exchange(self, endpoint, xaddr):
        if not (xaddr.startswith('http://') or xaddr.startswith('https://')):
            logger.debug('invalid XAddr: {}'.format(xaddr))
            return

        host = None
        url = xaddr
        if self.mch.address.family == socket.AF_INET6:
            host = '[{}]'.format(url.partition('[')[2].partition(']')[0])
            url = url.replace(']', '%{}]'.format(self.mch.address.interface))

        body = self.build_getmetadata_message(endpoint)
        request = urllib.request.Request(url, data=body.encode('utf-8'), method='POST')
        request.add_header('Content-Type', 'application/soap+xml')
        request.add_header('User-Agent', 'wsdd')
        if host is not None:
            request.add_header('Host', host)

        try:
            with urllib.request.urlopen(request, None, args.metadata_timeout) as stream:
                self.handle_metadata(stream.read(), endpoint, xaddr)
        except urllib.error.URLError as e:
            logger.warning('could not fetch metadata from: {} {}'.format(url, e))
        except (socket.timeout, IOError):
            logger.warning('metadata exchange with {} timed out'.format(url))

    def build_getmetadata_message(self, endpoint):
        tree, _ = self.build_message_tree(endpoint, WSD_GET, None, None)
        return self.xml_to_str(tree)

    def handle_metadata(self, meta, endpoint, xaddr):
        device_uri = urllib.parse.urlparse(endpoint).geturl()
        if device_uri in WSDDiscoveredDevice.instances:
            WSDDiscoveredDevice.instances[device_uri].update(meta, xaddr, self.mch.address.interface)
        else:
            WSDDiscoveredDevice.instances[device_uri] = WSDDiscoveredDevice(meta, xaddr, self.mch.address.interface)

    def remove_outdated_probes(self):
        cut = time.time() - PROBE_TIMEOUT * 2
        self.probes = dict(filter(lambda x: x[1] > cut, self.probes.items()))

    def add_header_elements(self, header, extra):
        action_str = extra
        if action_str == WSD_GET:
            reply_to = ElementTree.SubElement(header, 'wsa:ReplyTo')
            addr = ElementTree.SubElement(reply_to, 'wsa:Address')
            addr.text = WSA_ANON

            wsa_from = ElementTree.SubElement(header, 'wsa:From')
            addr = ElementTree.SubElement(wsa_from, 'wsa:Address')
            addr.text = args.uuid.urn


class WSDHost(WSDUDPMessageHandler):
    #Class for handling WSD requests coming from UDP datagrams.#

    message_number = 0
    instances = []

    def __init__(self, mch):
        super().__init__(mch)

        WSDHost.instances.append(self)

        self.mch.add_handler(self.mch.recv_socket, self)

        self.handlers[WSD_PROBE] = self.handle_probe
        self.handlers[WSD_RESOLVE] = self.handle_resolve

        self.send_hello()

    def cleanup(self):
        super().cleanup()
        WSDHost.instances.remove(self)

    def teardown(self):
        super().teardown()
        self.send_bye()

    def handle_packet(self, msg, src):
        reply = self.handle_message(msg, src)
        if reply:
            self.enqueue_datagram(reply, src)

    def send_hello(self):
        #WS-Discovery, Section 4.1, Hello message#
        hello = ElementTree.Element('wsd:Hello')
        self.add_endpoint_reference(hello)
        # THINK: Microsoft does not send the transport address here due to privacy reasons. Could make this optional.
        self.add_xaddr(hello, self.mch.address.transport_str)
        self.add_metadata_version(hello)

        msg = self.build_message(WSA_DISCOVERY, WSD_HELLO, None, hello)
        self.enqueue_datagram(msg, self.mch.multicast_address, msg_type='Hello')

    def send_bye(self):
        #WS-Discovery, Section 4.2, Bye message#
        bye = ElementTree.Element('wsd:Bye')
        self.add_endpoint_reference(bye)

        msg = self.build_message(WSA_DISCOVERY, WSD_BYE, None, bye)
        self.enqueue_datagram(msg, self.mch.multicast_address, msg_type='Bye')

    def handle_probe(self, header, body):
        probe = body.find(_ns('./wsd:Probe'))
        if probe is None:
            return None

        scopes = probe.find(_ns('./wsd:Scopes'))

        if scopes:
            # THINK: send fault message (see p. 21 in WSD)
            logger.debug('scopes ({}) unsupported but probed'.format(scopes))
            return None

        types_elem = probe.find(_ns('./wsd:Types'))
        if types_elem is None:
            logger.debug('Probe message lacks wsd:Types element. Ignored.')
            return None

        types = types_elem.text
        if not types == WSD_TYPE_DEVICE:
            logger.debug('unknown discovery type ({}) for probe'.format(types))
            return None

        matches = ElementTree.Element('wsd:ProbeMatches')
        match = ElementTree.SubElement(matches, 'wsd:ProbeMatch')
        self.add_endpoint_reference(match)
        self.add_types(match)
        self.add_metadata_version(match)

        return matches, WSD_PROBE_MATCH

    def handle_resolve(self, header, body):
        resolve = body.find(_ns('./wsd:Resolve'))
        if resolve is None:
            return None

        addr = resolve.find(_ns('./wsa:EndpointReference/wsa:Address'))
        if addr is None:
            logger.debug('invalid resolve request: missing endpoint address')
            return None

        if not addr.text == args.uuid.urn:
            logger.debug('invalid resolve request: address ({}) does not match own one ({})'.format(
                addr.text, args.uuid.urn))
            return None

        matches = ElementTree.Element('wsd:ResolveMatches')
        match = ElementTree.SubElement(matches, 'wsd:ResolveMatch')
        self.add_endpoint_reference(match)
        self.add_types(match)
        self.add_xaddr(match, self.mch.address.transport_str)
        self.add_metadata_version(match)

        return matches, WSD_RESOLVE_MATCH

    def add_header_elements(self, header, extra):
        ElementTree.SubElement(header, 'wsd:AppSequence', {
            'InstanceId': str(wsd_instance_id),
            'SequenceId': uuid.uuid1().urn,
            'MessageNumber': str(type(self).message_number)})

        type(self).message_number += 1


class WSDHttpMessageHandler(WSDMessageHandler):

    def __init__(self):
        super().__init__()

        self.handlers[WSD_GET] = self.handle_get

    def handle_get(self, header, body):
        # see https://msdn.microsoft.com/en-us/library/hh441784.aspx for an
        # example. Some of the properties below might be made configurable
        # in future releases.
        metadata = ElementTree.Element('wsx:Metadata')
        section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {'Dialect': WSDP_URI + '/ThisDevice'})
        device = ElementTree.SubElement(section, 'wsdp:ThisDevice')
        ElementTree.SubElement(device, 'wsdp:FriendlyName').text = ('WSD Device {0}'.format(args.hostname))
        ElementTree.SubElement(device, 'wsdp:FirmwareVersion').text = '1.0'
        ElementTree.SubElement(device, 'wsdp:SerialNumber').text = '1'

        section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {'Dialect': WSDP_URI + '/ThisModel'})
        model = ElementTree.SubElement(section, 'wsdp:ThisModel')
        ElementTree.SubElement(model, 'wsdp:Manufacturer').text = 'wsdd'
        ElementTree.SubElement(model, 'wsdp:ModelName').text = 'wsdd'
        ElementTree.SubElement(model, 'pnpx:DeviceCategory').text = 'Computers'

        section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {'Dialect': WSDP_URI + '/Relationship'})
        rel = ElementTree.SubElement(section, 'wsdp:Relationship', {'Type': WSDP_URI + '/host'})
        host = ElementTree.SubElement(rel, 'wsdp:Host')
        self.add_endpoint_reference(host)
        ElementTree.SubElement(host, 'wsdp:Types').text = PUB_COMPUTER
        ElementTree.SubElement(host, 'wsdp:ServiceId').text = args.uuid.urn

        fmt = '{0}/Domain:{1}' if args.domain else '{0}/Workgroup:{1}'
        value = args.domain if args.domain else args.workgroup.upper()
        if args.domain:
            dh = args.hostname if args.preserve_case else args.hostname.lower()
        else:
            dh = args.hostname if args.preserve_case else args.hostname.upper()

        ElementTree.SubElement(host, PUB_COMPUTER).text = fmt.format(dh, value)

        return metadata, WSD_GET_RESPONSE


class WSDHttpServer(http.server.HTTPServer):
    # HTTP server both with IPv6 support and WSD handling #

    def __init__(self, mch, aio_loop):
        # hacky way to convince HTTP/SocketServer of the address family
        type(self).address_family = mch.address.family

        self.mch = mch
        self.aio_loop = aio_loop
        self.wsd_handler = WSDHttpMessageHandler()
        self.registered = False

        # WSDHttpRequestHandler is a BaseHTTPRequestHandler. Passing to the parent constructor is therefore safe and
        # we can ignore the type error reported by mypy
        super().__init__(mch.listen_address, WSDHttpRequestHandler)  # type: ignore

    def server_bind(self):
        if self.mch.address.family == socket.AF_INET6:
            self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)

        super().server_bind()

    def server_activate(self):
        super().server_activate()
        self.aio_loop.add_reader(self.fileno(), self.handle_request)
        self.registered = True

    def server_close(self):
        if self.registered:
            self.aio_loop.remove_reader(self.fileno())
        super().server_close()


class WSDHttpRequestHandler(http.server.BaseHTTPRequestHandler):
    #Class for handling WSD requests coming over HTTP#

    def log_message(self, fmt, *args):
        logger.info("{} - - ".format(self.address_string()) + fmt % args)

    def do_POST(self):
        if self.path != '/' + str(args.uuid):
            self.send_error(404)

        ct = self.headers['Content-Type']
        if ct is None or not ct.startswith(MIME_TYPE_SOAP_XML):
            self.send_error(400, 'Invalid Content-Type')

        content_length = int(self.headers['Content-Length'])
        body = self.rfile.read(content_length)

        response = self.server.wsd_handler.handle_message(body)  # type: ignore
        if response:
            self.send_response(200)
            self.send_header('Content-Type', MIME_TYPE_SOAP_XML)
            self.end_headers()
            self.wfile.write(response.encode('utf-8'))
        else:
            self.send_error(400)


class ApiServer:

    def __init__(self, aio_loop, listen_address, address_monitor):
        self.clients = []
        self.address_monitor = address_monitor
        self._server_socket = None
        self._stop_event = _threading.Event()
        self._thread = _threading.Thread(
            target=self._serve, args=(listen_address,), daemon=True)
        self._thread.start()

    def _make_server_socket(self, listen_address):
        if isinstance(listen_address, socket.socket):
            return listen_address
        elif isinstance(listen_address, int) or (
                isinstance(listen_address, str) and listen_address.isnumeric()):
            srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            srv.bind(('localhost', int(listen_address)))
            srv.listen(5)
            return srv
        else:
            import os
            if os.path.exists(listen_address):
                os.unlink(listen_address)
            srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            srv.bind(listen_address)
            srv.listen(5)
            return srv

    def _serve(self, listen_address):
        try:
            self._server_socket = self._make_server_socket(listen_address)
            self._server_socket.settimeout(1.0)
        except Exception as e:
            logger.error('ApiServer: could not create socket: {}'.format(e))
            return
        while not self._stop_event.is_set():
            try:
                conn, addr = self._server_socket.accept()
                t = _threading.Thread(
                    target=self.on_connect, args=(conn,), daemon=True)
                t.start()
            except socket.timeout:
                continue
            except Exception as e:
                if not self._stop_event.is_set():
                    logger.warning('ApiServer accept error: {}'.format(e))
                break

    def on_connect(self, conn):
        self.clients.append(conn)
        f = conn.makefile("rb")
        try:
            while True:
                line = f.readline()
                if not line:
                    break
                try:
                    self.handle_command(line.strip().decode('utf-8'), conn)
                except UnicodeDecodeError as e:
                    logger.debug('invalid input utf8: {}'.format(e))
                except Exception as e:
                    logger.warning('exception in API client: {}'.format(e))
                    break
        finally:
            if conn in self.clients:
                self.clients.remove(conn)
            try:
                conn.close()
            except Exception:
                pass

    def cleanup(self):
        self._stop_event.set()
        for client in self.clients:
            try:
                client.close()
            except Exception:
                pass
        if self._server_socket:
            try:
                self._server_socket.close()
            except Exception:
                pass

class MetaEnumAfterInit(type):

    def __call__(cls, *cargs, **kwargs):
        obj = super().__call__(*cargs, **kwargs)
        if not args.no_autostart:
            obj.enumerate()
        return obj


class NetworkAddressMonitor(metaclass=MetaEnumAfterInit):
    """
    Observes changes of network addresses, handles addition and removal of
    network addresses, and filters for addresses/interfaces that are or are not
    handled. The actual OS-specific implementation that detects the changes is
    done in subclasses. This class is used as a singleton
    """

    instance = None

    def __init__(self, aio_loop):

        if NetworkAddressMonitor.instance is not None:
            raise RuntimeError('Instance of NetworkAddressMonitor already created')

        NetworkAddressMonitor.instance = self

        self.interfaces = {}
        self.aio_loop = aio_loop

        self.mchs = []
        self.http_servers = []
        self.teardown_tasks = []

        self.active = False

    def enumerate(self):
        """
        Performs an initial enumeration of addresses and sets up everything
        for observing future changes.
        """
        if self.active:
            return

        self.active = True
        self.do_enumerate()

    def do_enumerate(self):
        pass

    def handle_change(self):
        # handle network change message #
        pass

    def add_interface(self, interface):
        # TODO: Cleanup
        if interface.index in self.interfaces:
            pass
            # self.interfaces[idx].name = name
        else:
            self.interfaces[interface.index] = interface

        return self.interfaces[interface.index]

    def get_handled_address_families(self):
        # get a set of handles address families for filtering during enumeration  #
        if not self.active:
            return set()

        if args.ipv4only:
            return {socket.AF_INET}
        if args.ipv6only:
            return {socket.AF_INET6}

        return {socket.AF_INET, socket.AF_INET6}

    def is_address_handled(self, address):
        # do not handle anything when we are not active
        if not self.active:
            return False

        if not address.is_multicastable:
            return False

        # Use interface only if it's in the list of user-provided interface names
        if ((args.interface) and (address.interface.name not in args.interface)
                and (address.address_str not in args.interface)):
            return False

        return True

    def handle_new_address(self, address):
        logger.debug('new address {}'.format(address))

        if not self.is_address_handled(address):
            logger.debug('ignoring that address on {}'.format(address.interface))
            return

        # filter out what is not wanted
        # Ignore addresses or interfaces we already handle. There can only be
        # one multicast handler per address family and network interface
        for mch in self.mchs:
            if mch.handles_address(address):
                return

        logger.debug('handling traffic for {}'.format(address))
        mch = MulticastHandler(address, self.aio_loop)
        self.mchs.append(mch)

        if not args.no_host:
            WSDHost(mch)
            if not args.no_http:
                self.http_servers.append(WSDHttpServer(mch, self.aio_loop))

        if args.discovery:
            WSDClient(mch)

    def handle_deleted_address(self, address):
        logger.info('deleted address {}'.format(address))

        if not self.is_address_handled(address):
            return

        mch = self.get_mch_by_address(address)
        if mch is None:
            return

        # Do not tear the client/hosts down. Saying goodbye does not work
        # because the address is already gone (at least on Linux).
        for c in WSDClient.instances:
            if c.mch == mch:
                c.cleanup()
                break
        for h in WSDHost.instances:
            if h.mch == mch:
                h.cleanup()
                break
        for s in self.http_servers:
            if s.mch == mch:
                s.server_close()
                self.http_servers.remove(s)

        mch.cleanup()
        self.mchs.remove(mch)

    def teardown(self):
        if not self.active:
            return

        self.active = False

        # return if we are still in tear down process
        if len(self.teardown_tasks) > 0:
            return

        for h in WSDHost.instances:
            h.teardown()
            h.cleanup()
            self.teardown_tasks.extend(h.pending_tasks)

        for c in WSDClient.instances:
            c.teardown()
            c.cleanup()
            self.teardown_tasks.extend(c.pending_tasks)

        for s in self.http_servers:
            s.server_close()

        self.http_servers.clear()

        if not self.teardown_tasks:
            return

        if not self.aio_loop.is_running():
            # Wait here for all pending tasks so that the main loop can be finished on termination.
            for _t in self.teardown_tasks:
                if hasattr(_t, "_thread"):
                    _t._thread.join(timeout=5)
        else:
            for t in self.teardown_tasks:
                t.add_done_callback(self.mch_teardown)

    def mch_teardown(self, task):
        if any([not t.done() for t in self.teardown_tasks]):
            return

        self.teardown_tasks.clear()

        for mch in self.mchs:
            mch.cleanup()
        self.mchs.clear()

    def cleanup(self):
        self.teardown()

    def get_mch_by_address(self, address):
        # Get the MCI for the address, its family and the interface.
        # adress must be given as a string.
        for retval in self.mchs:
            if retval.handles_address(address):
                return retval

        return None


# from rtnetlink.h
RTMGRP_LINK = 1
RTMGRP_IPV4_IFADDR = 0x10
RTMGRP_IPV6_IFADDR = 0x100

# from netlink.h (struct nlmsghdr)
NLM_HDR_DEF = '@IHHII'

NLM_F_REQUEST = 0x01
NLM_F_ROOT = 0x100
NLM_F_MATCH = 0x200
NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH

# self defines
NLM_HDR_ALIGNTO = 4

# ifa flags
IFA_F_DADFAILED = 0x08
IFA_F_HOMEADDRESS = 0x10
IFA_F_DEPRECATED = 0x20
IFA_F_TENTATIVE = 0x40

# from if_addr.h (struct ifaddrmsg)
IFADDR_MSG_DEF = '@BBBBI'
IFA_ADDRESS = 1
IFA_LOCAL = 2
IFA_LABEL = 3
IFA_FLAGS = 8
IFA_MSG_LEN = 8

RTA_ALIGNTO = 4
RTA_LEN = 4


def align_to(x, n):
    return ((x + n - 1) // n) * n


class NetlinkAddressMonitor(NetworkAddressMonitor):
    # Implementation of the AddressMonitor for Netlink sockets, i.e. Linux

    RTM_NEWADDR = 20
    RTM_DELADDR = 21
    RTM_GETADDR = 22

    def __init__(self, aio_loop):
        super().__init__(aio_loop)

        rtm_groups = RTMGRP_LINK
        if not args.ipv4only:
            rtm_groups = rtm_groups | RTMGRP_IPV6_IFADDR
        if not args.ipv6only:
            rtm_groups = rtm_groups | RTMGRP_IPV4_IFADDR

        self.socket = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
        self.socket.bind((0, rtm_groups))
        self.aio_loop.add_reader(self.socket.fileno(), self.handle_change)

        self.NLM_HDR_LEN = struct.calcsize(NLM_HDR_DEF)

    def do_enumerate(self):
        super().do_enumerate()

        kernel = (0, 0)
        # Append an unsigned byte to the header for the request.
        req = struct.pack(NLM_HDR_DEF + 'B', self.NLM_HDR_LEN + 1, self.RTM_GETADDR,
                          NLM_F_REQUEST | NLM_F_DUMP, 1, 0, socket.AF_PACKET)
        self.socket.sendto(req, kernel)

    def handle_change(self):
        super().handle_change()

        buf, src = self.socket.recvfrom(4096)
        logger.debug('netlink message with {} bytes'.format(len(buf)))
        handled_families = self.get_handled_address_families()

        offset = 0
        while offset < len(buf):
            h_len, h_type, _, _, _ = struct.unpack_from(NLM_HDR_DEF, buf, offset)
            offset += self.NLM_HDR_LEN

            msg_len = h_len - self.NLM_HDR_LEN
            if msg_len < 0:
                break

            if h_type != self.RTM_NEWADDR and h_type != self.RTM_DELADDR:
                logger.debug('invalid rtm_message type {}'.format(h_type))
                offset += align_to(msg_len, NLM_HDR_ALIGNTO)
                continue

            # decode ifaddrmsg as in if_addr.h
            ifa_family, _, ifa_flags, ifa_scope, ifa_idx = struct.unpack_from(IFADDR_MSG_DEF, buf, offset)
            if ifa_family not in handled_families:
                offset += align_to(msg_len, NLM_HDR_ALIGNTO)
                logger.debug('ignore address with wrong address family {}'.format(ifa_family))
                continue

            if ((ifa_flags & IFA_F_DADFAILED) or (ifa_flags & IFA_F_HOMEADDRESS)
                    or (ifa_flags & IFA_F_DEPRECATED) or (ifa_flags & IFA_F_TENTATIVE)):
                logger.debug('ignore address with invalid state {}'.format(hex(ifa_flags)))
                offset += align_to(msg_len, NLM_HDR_ALIGNTO)
                continue

            logger.debug('RTM new/del addr family: {} flags: {} scope: {} idx: {}'.format(
                         ifa_family, ifa_flags, ifa_scope, ifa_idx))
            addr = None
            i = offset + IFA_MSG_LEN
            while i - offset < msg_len:
                attr_len, attr_type = struct.unpack_from('HH', buf, i)
                logger.debug('rt_attr {} {}'.format(attr_len, attr_type))

                if attr_len < RTA_LEN:
                    logger.debug('invalid rtm_attr_len. skipping remainder')
                    break

                if attr_type == IFA_LABEL:
                    name, = struct.unpack_from(str(attr_len - 4 - 1) + 's', buf, i + 4)
                    self.add_interface(NetworkInterface(name.decode(), ifa_scope, ifa_idx))
                elif attr_type == IFA_LOCAL and ifa_family == socket.AF_INET:
                    addr = buf[i + 4:i + 4 + 4]
                elif attr_type == IFA_ADDRESS and ifa_family == socket.AF_INET6:
                    addr = buf[i + 4:i + 4 + 16]
                elif attr_type == IFA_FLAGS:
                    _, ifa_flags = struct.unpack_from('HI', buf, i)
                i += align_to(attr_len, RTA_ALIGNTO)

            if addr is None:
                logger.debug('no address in RTM message')
                offset += align_to(msg_len, NLM_HDR_ALIGNTO)
                continue

            # In case of IPv6 only addresses, there appears to be no IFA_LABEL
            # message. Therefore, the name is requested by other means (#94)
            if ifa_idx not in self.interfaces:
                try:
                    logger.debug('unknown interface name for idx {}. resolving manually'.format(ifa_idx))
                    if_name = socket.if_indextoname(ifa_idx)
                    self.add_interface(NetworkInterface(if_name, ifa_scope, ifa_idx))
                except OSError:
                    logger.exception('interface detection failed')
                    # accept this exception (which should not occur)
                    pass

            # In case really strange things happen and we could not find out the
            # interface name for the returned ifa_idx, we... log a message.
            if ifa_idx in self.interfaces:
                address = NetworkAddress(ifa_family, addr, self.interfaces[ifa_idx])
                if h_type == self.RTM_NEWADDR:
                    self.handle_new_address(address)
                elif h_type == self.RTM_DELADDR:
                    self.handle_deleted_address(address)
            else:
                logger.debug('unknown interface index: {}'.format(ifa_idx))

            offset += align_to(msg_len, NLM_HDR_ALIGNTO)

    def cleanup(self):
        self.aio_loop.remove_reader(self.socket.fileno())
        self.socket.close()
        super().cleanup()


# from sys/net/route.h
RTA_IFA = 0x20

# from sys/socket.h
CTL_NET = 4
NET_RT_IFLIST = 3

# from sys/net/if.h
IFF_LOOPBACK = 0x8
IFF_MULTICAST = 0x800 if platform.system() != 'OpenBSD' else 0x8000

# sys/netinet6/in6_var.h
IN6_IFF_TENTATIVE = 0x02
IN6_IFF_DUPLICATED = 0x04
IN6_IFF_NOTREADY = IN6_IFF_TENTATIVE | IN6_IFF_DUPLICATED

SA_ALIGNTO = ctypes.sizeof(ctypes.c_long) if platform.system() != "Darwin" else ctypes.sizeof(ctypes.c_uint32)


class RouteSocketAddressMonitor(NetworkAddressMonitor):
    # Implementation of the AddressMonitor for FreeBSD and Darwin using route sockets

    # Common definition for beginning part of if(m?a)?_msghdr structs (see net/if.h/man 4 route).
    IF_COMMON_HDR_DEF = '@HBBii' if platform.system() != 'OpenBSD' else '@HBBHHHBBiii'

    # from net/if.h
    RTM_NEWADDR = 0xC
    RTM_DELADDR = 0xD
    # not tested for OpenBSD
    RTM_IFINFO = 0xE

    # from route.h
    # FreeBSD/Darwin/OpenBSD/DragonFly=5, NetBSD=3
    _RTM_VERSION_MAP = {'NetBSD': 3}
    RTM_VERSION = 0x5

    # from net/if.h (struct ifa_msghdr)
    if platform.system() == 'OpenBSD':
        IFA_MSGHDR_DEF = IF_COMMON_HDR_DEF
    elif platform.system() == 'NetBSD':
        # NetBSD ifa_msghdr: common(12B) + ifam_index(H=2B) + ifam_metric(i=4B) + 2B pad = 20B
        IFA_MSGHDR_DEF = IF_COMMON_HDR_DEF + 'Hi'
    else:
        IFA_MSGHDR_DEF = IF_COMMON_HDR_DEF + 'hi'
    IFA_MSGHDR_SIZE = struct.calcsize(IFA_MSGHDR_DEF)

    # The struct package does not allow to specify those, thus we hard code them as chars (x4).
    IF_MSG_DEFS = {
        # if_data in if_msghdr is prepended with an u_short _ifm_spare1, thus the 'H' a the beginning)
        'FreeBSD': 'hH6c2c8c8c104c8c16c',
        # There are 8 bytes and 22 uint32_t in the if_data struct (22 x 4 Bytes + 8 = 96 Bytes)
        # It is also aligned on 4-byte boundary necessitating 2 bytes padding inside if_msghdr
        'Darwin': 'h2c8c22I',
        # struct if_data from /src/sys/net/if.h for if_msghdr
        #  (includes struct timeval which is a int64 + long
        'OpenBSD': '4c3I13Q1Iql',
        # NetBSD if_msghdr: ifm_index(H)+2Bpad + if_data(22x4B=88B) = 92B extra
        # (rtm_len=104 = IF_COMMON_HDR(12) + 92)
        'NetBSD': 'H2x22I',
        # DragonFlyBSD is structurally similar to FreeBSD
        'DragonFly': 'hH6c2c8c8c104c8c16c',
    }

    def __init__(self, aio_loop):
        super().__init__(aio_loop)
        self.intf_blacklist = []

        # Create routing socket to get notified about future changes.
        # Do this before fetching the current routing information to avoid race condition.
        self.socket = socket.socket(socket.AF_ROUTE, socket.SOCK_RAW, socket.AF_UNSPEC)
        self.aio_loop.add_reader(self.socket.fileno(), self.handle_change)

        _sys = platform.system()
        if _sys not in self.IF_MSG_DEFS:
            # 未知のBSD系はFreeBSDと同じ構造体レイアウトにフォールバック
            _sys = 'FreeBSD'
        self.IF_MSGHDR_SIZE = struct.calcsize(self.IF_COMMON_HDR_DEF + self.IF_MSG_DEFS[_sys])
        self.is_openbsd = platform.system() == 'OpenBSD'
        self.RTM_VERSION = self._RTM_VERSION_MAP.get(platform.system(), 0x5)

    def do_enumerate(self):
        super().do_enumerate()
        mib = [CTL_NET, socket.AF_ROUTE, 0, 0, NET_RT_IFLIST, 0]
        rt_mib = (ctypes.c_int * len(mib))()
        rt_mib[:] = [ctypes.c_int(m) for m in mib]

        libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)

        # Ask kernel for routing table size first.
        rt_size = ctypes.c_size_t()
        if libc.sysctl(ctypes.byref(rt_mib), ctypes.c_size_t(len(rt_mib)), 0, ctypes.byref(rt_size), 0, 0):
            raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))

        # Get the initial routing (interface list) data.
        rt_buf = ctypes.create_string_buffer(rt_size.value)
        if libc.sysctl(ctypes.byref(rt_mib), ctypes.c_size_t(len(rt_mib)), rt_buf, ctypes.byref(rt_size), 0, 0):
            raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))

        self.parse_route_socket_response(rt_buf.raw, True)

    def handle_change(self):
        super().handle_change()

        self.parse_route_socket_response(self.socket.recv(4096), False)

    def parse_route_socket_response(self, buf, keep_intf):
        offset = 0

        intf = None
        intf_flags = 0
        IFM_INDEX_OFFSET = struct.calcsize(self.IF_COMMON_HDR_DEF)
        while offset < len(buf):
            # unpack route message response
            if not self.is_openbsd:
                rtm_len, rtm_version, rtm_type, addr_mask, flags = struct.unpack_from(
                    self.IF_COMMON_HDR_DEF, buf, offset)
            else:
                rtm_len, rtm_version, rtm_type, ifa_hdr_len, _, _, _, _, addr_mask, flags, _ = struct.unpack_from(
                    self.IF_COMMON_HDR_DEF, buf, offset)

            # exit condition for OpenBSD where always the complete buffer (ie 4096 bytes) is returned
            if rtm_len == 0:
                break

            # skip over non-understood packets and versions
            if (rtm_type not in [self.RTM_NEWADDR, self.RTM_DELADDR, self.RTM_IFINFO]) or (
                    rtm_version != self.RTM_VERSION):
                offset += rtm_len
                continue

            if rtm_type == self.RTM_IFINFO:
                intf_flags = flags
                # NetBSD RTM_IFINFO may not carry AF_LINK addr; get ifm_index directly
                try:
                    ifm_index, = struct.unpack_from('@H', buf, offset + IFM_INDEX_OFFSET)
                    if ifm_index > 0:
                        try:
                            if_name = socket.if_indextoname(ifm_index)
                        except Exception:
                            if_name = None
                        if if_name:
                            intf = self.add_interface(NetworkInterface(if_name, ifm_index, ifm_index))
                except Exception as _e:
                    pass

            sa_offset = offset + (self.IF_MSGHDR_SIZE if rtm_type == self.RTM_IFINFO else self.IFA_MSGHDR_SIZE)

            # For a route socket message, and different to a sysctl response,
            # the link info is stored inside the same rtm message, so it has to
            # survive multiple rtm messages in such cases
            if not keep_intf:
                intf = None

            new_intf = self.parse_addrs(buf, sa_offset, offset + rtm_len, intf, addr_mask, rtm_type, intf_flags)
            intf = new_intf if new_intf else intf

            offset += rtm_len

    def clear_addr_scope(self, raw_addr):
        addr = bytearray(raw_addr)
        # adapted from in6_clearscope BSD/Mac kernel method (see scope6.c)
        if addr[0] == 0xfe and (addr[1] & 0xc0) == 0x80:
            addr[2] = 0
            addr[3] = 0

        return bytes(addr)

    def parse_addrs(self, buf, offset, limit, intf, addr_mask,
                    rtm_type, flags):
        addr_type_idx = 1
        addr = None
        addr_family = socket.AF_UNSPEC

        handled_families = self.get_handled_address_families()

        while offset < limit:
            while not (addr_type_idx & addr_mask) and (addr_type_idx <= addr_mask):
                addr_type_idx = addr_type_idx << 1

            sa_len, sa_fam = struct.unpack_from('@BB', buf, offset)
            if sa_fam in handled_families and addr_type_idx == RTA_IFA:
                addr_family = sa_fam
                addr_offset = 4 if sa_fam == socket.AF_INET else 8
                addr_length = 4 if sa_fam == socket.AF_INET else 16
                addr_start = offset + addr_offset
                addr = buf[addr_start:addr_start + addr_length]
                if sa_fam == socket.AF_INET6:
                    addr = self.clear_addr_scope(addr)
            elif sa_fam == socket.AF_LINK:  # type: ignore[attr-defined]
                idx, _, name_len = struct.unpack_from('@HBB', buf, offset + 2)
                if idx > 0:
                    off_name = offset + 8
                    if_name = (buf[off_name:off_name + name_len]).decode()
                    intf = self.add_interface(NetworkInterface(if_name, idx, idx))

            offset += align_to(sa_len, SA_ALIGNTO) if sa_len > 0 else SA_ALIGNTO
            addr_type_idx = addr_type_idx << 1

        if rtm_type == self.RTM_IFINFO and intf is not None:
            if flags & IFF_LOOPBACK or not flags & IFF_MULTICAST:
                self.intf_blacklist.append(intf.name)
            elif intf.name in self.intf_blacklist:
                self.intf_blacklist.remove(intf.name)

        if intf is None or intf.name in self.intf_blacklist or addr is None:
            return intf

        address = NetworkAddress(addr_family, addr, intf)
        if rtm_type == self.RTM_DELADDR:
            self.handle_deleted_address(address)
        else:
            # Too bad, the address may be unuseable (tentative, e.g.) here
            # but we won't get any further notifcation about the address being
            # available for use. Thus, we try and may fail here
            self.handle_new_address(address)

        return intf

    def cleanup(self):
        self.aio_loop.remove_reader(self.socket.fileno())
        self.socket.close()
        super().cleanup()


class DladmAddressMonitor(NetworkAddressMonitor):

    class sockaddr(ctypes.Structure):
        _fields_ = [("family", ctypes.c_ushort),
                    ("dummy", ctypes.c_ushort),
                    ("data", ctypes.c_ubyte * 14)]

    class ifaddrs(ctypes.Structure):
        pass

    ifaddrs._fields_ = [("next", ctypes.POINTER(ifaddrs)),
                        ("name", ctypes.c_char_p),
                        ("flags", ctypes.c_ulonglong),
                        ("addr", ctypes.POINTER(sockaddr)),
                        ("netmask", ctypes.POINTER(sockaddr)),
                        ("dstaddr", ctypes.POINTER(sockaddr)),
                        ("data", ctypes.c_void_p)]

    def freeifaddrs(self, ifa):
        libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
        while ifa.next:
            curr = ifa
            ifa = ifa.next[0]
            libc.free(curr.name)
            libc.free(curr.addr)
            libc.free(curr.netmask)
            libc.free(curr.dstaddr)
            libc.free(curr.data)
            del curr

    def do_enumerate(self):
        super().do_enumerate()
        libsocket = ctypes.CDLL(ctypes.util.find_library('socket'), use_errno=True)
        ifas = self.ifaddrs()
        handled_families = self.get_handled_address_families()
        if libsocket.getifaddrs(ctypes.byref(ifas)) == 0:
            ifa = ifas
            ifa_idx = 0
            while ifa.next:
                if ifa.name:
                    if ifa.addr[0].family not in handled_families:
                        ifa = ifa.next[0]
                        ifa_idx += 1
                        continue

                    logger.debug("{}%{}".format(
                        socket.inet_ntop(ifa.addr[0].family, bytes(ifa.addr[0].data[:4])),
                        ifa.name.decode()))
                    addr = socket.inet_ntop(ifa.addr[0].family, bytes(ifa.addr[0].data[:4]))
                    intf = NetworkInterface(ifa.name.decode(), 0, ifa_idx)
                    self.add_interface(intf)
                    self.handle_new_address(NetworkAddress(ifa.addr[0].family, addr, intf))
                    ifa_idx += 1

                ifa = ifa.next[0]
            self.freeifaddrs(ifas)


def sigterm_handler():
    logger.info('received termination/interrupt signal, tearing down')
    # implictely raise SystemExit to cleanup properly
    sys.exit(0)


def parse_args():
    global args, logger

    # argparse は Python 3.2+ のみ。Python 3.1 では optparse で代替する。
    if argparse is not None:
        parser = argparse.ArgumentParser()
        parser.add_argument('-i', '--interface', help='interface or address to use',
            action='append', default=[])
        parser.add_argument('-H', '--hoplimit', help='hop limit for multicast packets (default = 1)',
            type=int, default=1)
        parser.add_argument('-U', '--uuid', help='UUID for the target device', default=None)
        parser.add_argument('-v', '--verbose', help='increase verbosity', action='count', default=0)
        parser.add_argument('-d', '--domain', help='set domain name (disables workgroup)', default=None)
        parser.add_argument('-n', '--hostname', help='override (NetBIOS) hostname to be used (default hostname)',
            default=socket.gethostname().partition('.')[0])
        parser.add_argument('-w', '--workgroup', help='set workgroup name (default WORKGROUP)', default='WORKGROUP')
        parser.add_argument('-A', '--no-autostart', help='do not start networking after launch', action='store_true')
        parser.add_argument('-t', '--no-http', help='disable http service (for debugging, e.g.)', action='store_true')
        parser.add_argument('-4', '--ipv4only', help='use only IPv4 (default = off)', action='store_true')
        parser.add_argument('-6', '--ipv6only', help='use IPv6 (default = off)', action='store_true')
        parser.add_argument('-s', '--shortlog', help='log only level and message', action='store_true')
        parser.add_argument('-p', '--preserve-case', help='preserve case of the provided/detected hostname', action='store_true')
        parser.add_argument('-c', '--chroot', help='directory to chroot into', default=None)
        parser.add_argument('-u', '--user', help='drop privileges to user:group', default=None)
        parser.add_argument('-D', '--discovery', help='enable discovery operation mode', action='store_true')
        parser.add_argument('-l', '--listen', help='listen on path or localhost port in discovery mode', default=None)
        parser.add_argument('-o', '--no-host', help='disable server mode operation (host will be undiscoverable)', action='store_true')
        parser.add_argument('-V', '--version', help='show version number and exit', action='store_true')
        parser.add_argument('--metadata-timeout', help='set timeout for HTTP-based metadata exchange', default=2.0)
        parser.add_argument('--source-port', help='send multicast traffic/receive replies on this port', type=int, default=0)
        args = parser.parse_args(sys.argv[1:])
    else:
        # Python 3.1 用: optparse による代替実装
        import optparse

        class _Args(object):
            pass

        # optparse は action='append' と action='count' を持つが
        # --no-autostart のようなハイフン入りオプションは dest に変換される
        parser = optparse.OptionParser()
        parser.add_option('-i', '--interface', action='append', dest='interface', default=[])
        parser.add_option('-H', '--hoplimit', type='int', dest='hoplimit', default=1)
        parser.add_option('-U', '--uuid', dest='uuid', default=None)
        parser.add_option('-v', '--verbose', action='count', dest='verbose', default=0)
        parser.add_option('-d', '--domain', dest='domain', default=None)
        parser.add_option('-n', '--hostname', dest='hostname',
            default=socket.gethostname().partition('.')[0])
        parser.add_option('-w', '--workgroup', dest='workgroup', default='WORKGROUP')
        parser.add_option('-A', '--no-autostart', action='store_true', dest='no_autostart', default=False)
        parser.add_option('-t', '--no-http', action='store_true', dest='no_http', default=False)
        parser.add_option('-4', '--ipv4only', action='store_true', dest='ipv4only', default=False)
        parser.add_option('-6', '--ipv6only', action='store_true', dest='ipv6only', default=False)
        parser.add_option('-s', '--shortlog', action='store_true', dest='shortlog', default=False)
        parser.add_option('-p', '--preserve-case', action='store_true', dest='preserve_case', default=False)
        parser.add_option('-c', '--chroot', dest='chroot', default=None)
        parser.add_option('-u', '--user', dest='user', default=None)
        parser.add_option('-D', '--discovery', action='store_true', dest='discovery', default=False)
        parser.add_option('-l', '--listen', dest='listen', default=None)
        parser.add_option('-o', '--no-host', action='store_true', dest='no_host', default=False)
        parser.add_option('-V', '--version', action='store_true', dest='version', default=False)
        parser.add_option('--metadata-timeout', type='float', dest='metadata_timeout', default=2.0)
        parser.add_option('--source-port', type='int', dest='source_port', default=0)

        opts, _ = parser.parse_args(sys.argv[1:])
        args = opts

    if args.version:
        print('wsdd - Web Service Discovery Daemon, v{}'.format(WSDD_VERSION))
        sys.exit(0)

    if args.verbose == 1:
        log_level = logging.INFO
    elif args.verbose > 1:
        log_level = logging.DEBUG
        pass  # set_debug not supported
        pass  # asyncio logger not available
    else:
        log_level = logging.WARNING

    if args.shortlog:
        fmt = '%(levelname)s: %(message)s'
    else:
        fmt = '%(asctime)s:%(name)s %(levelname)s(pid %(process)d): %(message)s'

    logging.basicConfig(level=log_level, format=fmt)
    logger = logging.getLogger('wsdd')

    if not args.interface:
        logger.warning('no interface given, using all interfaces')

    if not args.uuid:
        def read_uuid_from_file(fn):
            try:
                with open(fn) as f:
                    s = f.readline().strip()
                    return uuid.UUID(s)
            except Exception:
                return None

        # machine uuid: try machine-id file first but also check for hostid (FreeBSD)
        args.uuid = read_uuid_from_file('/etc/machine-id') or \
            read_uuid_from_file('/etc/hostid') or \
            uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname())

        logger.info('using pre-defined UUID {0}'.format(str(args.uuid)))
    else:
        args.uuid = uuid.UUID(args.uuid)
        logger.info('user-supplied device UUID is {0}'.format(str(args.uuid)))

    for prefix, uri in namespaces.items():
        if hasattr(ElementTree, "register_namespace"):
            ElementTree.register_namespace(prefix, uri)
        else:
            # Python 3.1 用: 内部辞書を直接操作して名前空間プレフィックスを登録
            try:
                import xml.etree.ElementTree as _ET_mod
                if hasattr(_ET_mod, "_namespace_map"):
                    _ET_mod._namespace_map[uri] = prefix
                elif hasattr(_ET_mod, "_namespaces"):
                    _ET_mod._namespaces[uri] = prefix
            except Exception:
                pass


def chroot(root):
    # Chroot into a separate directory to isolate ourself for increased security.
    # preload for socket.gethostbyaddr()
    import encodings.idna

    try:
        os.chroot(root)
        os.chdir('/')
        logger.info('chrooted successfully to {}'.format(root))
    except Exception as e:
        logger.error('could not chroot to {}: {}'.format(root, e))
        return False

    return True


def get_ids_from_userspec(user_spec):
    uid = None
    gid = None
    try:
        user, _, group = user_spec.partition(':')

        if user:
            pw = pwd.getpwnam(user)
            uid = pw.pw_uid
            gid = pw.pw_gid  # グループ未指定時はユーザーのプライマリグループを使用

        if group:
            gid = grp.getgrnam(group).gr_gid
    except Exception as e:
        raise RuntimeError('could not get uid/gid for {}: {}'.format(user_spec, e))

    return (uid, gid)


def drop_privileges(uid, gid):
    try:
        if gid is not None:
            os.setgid(gid)
            os.setegid(gid)
            logger.debug('switched uid to {}'.format(uid))

        if uid is not None:
            os.setuid(uid)
            os.seteuid(uid)
            logger.debug('switched gid to {}'.format(gid))

        logger.info('running as {} ({}:{})'.format(args.user, uid, gid))
    except Exception as e:
        logger.error('dropping privileges failed: {}'.format(e))
        return False

    return True


def create_address_monitor(system, aio_loop):
    if system == 'Linux':
        return NetlinkAddressMonitor(aio_loop)
    elif system in ['FreeBSD', 'Darwin', 'OpenBSD', 'NetBSD', 'DragonFly']:
        return RouteSocketAddressMonitor(aio_loop)
    elif system == 'SunOS':
        return DladmAddressMonitor(aio_loop)
    else:
        raise NotImplementedError('unsupported OS: ' + system)


def main():
    global logger, args  # noqa: F824

    parse_args()

    if args.ipv4only and args.ipv6only:
        logger.error('Listening to no IP address family.')
        return 4

    aio_loop = _FakeEventLoop()
    nm = create_address_monitor(platform.system(), aio_loop)

    api_server = None
    if args.listen:
        api_server = ApiServer(aio_loop, args.listen, nm)
    elif 'systemd' in sys.modules:
        fds = systemd.daemon.listen_fds()
        if fds:
            api_server = ApiServer(aio_loop, socket.socket(fileno=fds[0]), nm)

    # get uid:gid before potential chroot'ing
    if args.user is not None:
        ids = get_ids_from_userspec(args.user)
        if not ids:
            return 3

    if args.chroot is not None:
        if not chroot(args.chroot):
            return 2

    if args.user is not None:
        if not drop_privileges(ids[0], ids[1]):
            return 3

    if args.chroot and (os.getuid() == 0 or os.getgid() == 0):
        logger.warning('chrooted but running as root, consider -u option')

    # main loop, serve requests coming from any outbound socket
    aio_loop.add_signal_handler(signal.SIGINT, sigterm_handler)
    aio_loop.add_signal_handler(signal.SIGTERM, sigterm_handler)
    try:
        aio_loop.run_forever()
    except (SystemExit, KeyboardInterrupt):
        logger.info('shutting down gracefully...')
        if api_server is not None:
            api_server.cleanup()

        nm.cleanup()
        aio_loop.stop()
    except Exception:
        logger.exception('error in main loop')

    logger.info('Done.')
    return 0


if __name__ == '__main__':
    sys.exit(main())
