diff --git a/Dockerfile b/Dockerfile index 94c65de..67c1cf4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,7 @@ FROM python:3-alpine WORKDIR /usr/src/app -RUN apk add --no-cache curl bash && curl https://raw.githubusercontent.com/christgau/wsdd/v0.7.0/src/wsdd.py -o wsdd.py && apk del curl - -copy docker-cmd.sh . +COPY wsdd.py . +COPY docker-cmd.sh . CMD [ "./docker-cmd.sh"] diff --git a/wsdd.py b/wsdd.py new file mode 100644 index 0000000..f73e56e --- /dev/null +++ b/wsdd.py @@ -0,0 +1,787 @@ +#!/usr/bin/env python3 + +# 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-2019 + +import sys +import signal +import socket +import selectors +import struct +import argparse +import uuid +import time +import random +import logging +import platform +import ctypes.util +import collections +import xml.etree.ElementTree as ElementTree +import http.server + + +# sockaddr C type, with a larger data field to capture IPv6 addresses +# unfortunately, the structures differ on Linux and FreeBSD +if platform.system() == 'Linux': + class sockaddr(ctypes.Structure): + _fields_ = [('family', ctypes.c_uint16), + ('data', ctypes.c_uint8 * 24)] +else: + class sockaddr(ctypes.Structure): + _fields_ = [('length', ctypes.c_uint8), + ('family', ctypes.c_uint8), + ('data', ctypes.c_uint8 * 24)] + + +class if_addrs(ctypes.Structure): + pass + + +if_addrs._fields_ = [('next', ctypes.POINTER(if_addrs)), + ('name', ctypes.c_char_p), + ('flags', ctypes.c_uint), + ('addr', ctypes.POINTER(sockaddr)), + ('netmask', ctypes.POINTER(sockaddr))] + + +class HTTPv6Server(http.server.HTTPServer): + """Simple HTTP server with IPv6 support""" + address_family = socket.AF_INET6 + + def server_bind(self): + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + super().server_bind() + + +class MulticastInterface: + """ + A class for handling multicast traffic on a given interface for a + given address family. It provides multicast sender and receiver sockets + """ + def __init__(self, family, address, intf_name): + self.address = address + self.family = family + self.interface = intf_name + self.recv_socket = socket.socket(self.family, socket.SOCK_DGRAM) + self.recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.send_socket = socket.socket(self.family, socket.SOCK_DGRAM) + self.transport_address = address + self.multicast_address = None + self.listen_address = None + + if family == socket.AF_INET: + self.init_v4() + elif family == socket.AF_INET6: + self.init_v6() + + logger.info('joined multicast group {0} on {2}%{1}'.format( + self.multicast_address, self.interface, self.address)) + logger.debug('transport address on {0} is {1}'.format( + self.interface, self.transport_address)) + logger.debug('will listen for HTTP traffic on address {0}'.format( + self.listen_address)) + + def init_v6(self): + idx = socket.if_nametoindex(self.interface) + self.multicast_address = (WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0x575C, idx) + + # v6: member_request = { multicast_addr, intf_idx } + mreq = ( + socket.inet_pton(self.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) + + # 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)) + + self.send_socket.setsockopt( + socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0) + self.send_socket.setsockopt( + socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, args.hoplimit) + self.send_socket.setsockopt( + socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx) + + self.transport_address = '[{0}]'.format(self.address) + self.listen_address = (self.address, WSD_HTTP_PORT, 0, idx) + + def init_v4(self): + idx = socket.if_nametoindex(self.interface) + self.multicast_address = (WSD_MCAST_GRP_V4, WSD_UDP_PORT) + + # v4: member_request (ip_mreqn) = { multicast_addr, intf_addr, idx } + mreq = ( + socket.inet_pton(self.family, WSD_MCAST_GRP_V4) + + socket.inet_pton(self.family, self.address) + + struct.pack('@I', idx)) + self.recv_socket.setsockopt( + socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + + try: + self.recv_socket.bind((WSD_MCAST_GRP_V4, WSD_UDP_PORT)) + except OSError: + self.recv_socket.bind(('', WSD_UDP_PORT)) + + self.send_socket.setsockopt( + socket.IPPROTO_IP, socket.IP_MULTICAST_IF, mreq) + self.send_socket.setsockopt( + socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0) + self.send_socket.setsockopt( + socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, args.hoplimit) + + self.listen_address = (self.address, WSD_HTTP_PORT) + + +# 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' +} + +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' + +# protocol assignments (WSD spec/Section 2.4) +WSD_UDP_PORT = 3702 +WSD_HTTP_PORT = 5357 +WSD_MAX_LEN = 32767 + +# SOAP/UDP transmission constants +MULTICAST_UDP_REPEAT = 4 +UNICAST_UDP_REPEAT = 2 +UDP_MIN_DELAY = 50 +UDP_MAX_DELAY = 250 +UDP_UPPER_DELAY = 500 + +# some globals +wsd_known_messages = collections.deque([]) +wsd_message_number = 1 +wsd_instance_id = int(time.time()) +send_queue = [] + +args = None +logger = None + + +# shortcuts for building WSD responses +def wsd_add_metadata_version(parent): + meta_data = ElementTree.SubElement(parent, 'wsd:MetadataVersion') + meta_data.text = '1' + + +def wsd_add_types(parent): + dev_type = ElementTree.SubElement(parent, 'wsd:Types') + dev_type.text = WSD_TYPE_DEVICE_COMPUTER + + +def wsd_add_endpoint_reference(uuid_, parent): + endpoint = ElementTree.SubElement(parent, 'wsa:EndpointReference') + address = ElementTree.SubElement(endpoint, 'wsa:Address') + address.text = uuid_.urn + + +def wsd_add_xaddr(uuid_, parent, transport_addr): + if transport_addr: + item = ElementTree.SubElement(parent, 'wsd:XAddrs') + item.text = 'http://{0}:{1}/{2}'.format( + transport_addr, WSD_HTTP_PORT, uuid_) + + +def wsd_build_message(to_addr, action_str, request_header, response): + """ + 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 + """ + global wsd_message_number + + env = ElementTree.Element('soap:Envelope') + header = ElementTree.SubElement(env, '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: + req_msg_id = request_header.find('./wsa:MessageID', namespaces) + if req_msg_id is not None: + relates_to = ElementTree.SubElement(header, 'wsa:RelatesTo') + relates_to.text = req_msg_id.text + + seq = ElementTree.SubElement(header, 'wsd:AppSequence', { + 'InstanceId': str(wsd_instance_id), + 'SequenceId': uuid.uuid1().urn, + 'MessageNumber': str(wsd_message_number)}) + wsd_message_number = wsd_message_number + 1 + + body = ElementTree.SubElement(env, 'soap:Body') + body.append(response) + + for prefix, uri in namespaces.items(): + env.attrib['xmlns:' + prefix] = uri + + xml = b'' + xml = xml + ElementTree.tostring(env, encoding='utf-8') + + logger.debug('constructed xml for WSD message: {0}'.format(xml)) + + return xml + + +# WSD message type handling +def wsd_handle_probe(uuid_, probe): + scopes = probe.find('./wsd:Scopes', namespaces) + + if scopes: + # THINK: send fault message (see p. 21 in WSD) + logger.warn('Scopes are not supported but were probed ({}).'.format( + scopes)) + return None + + types_elem = probe.find('./wsd:Types', namespaces) + 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 ({0}) during probe'.format(types)) + return None + + matches = ElementTree.Element('wsd:ProbeMatches') + match = ElementTree.SubElement(matches, 'wsd:ProbeMatch') + wsd_add_endpoint_reference(uuid_, match) + wsd_add_types(match) + wsd_add_metadata_version(match) + + return matches + + +def wsd_handle_resolve(resolve, xaddr): + addr = resolve.find('./wsa:EndpointReference/wsa:Address', namespaces) + if addr is None: + logger.debug('invalid resolve request: missing endpoint address') + return None + + if addr.text not in args.urn2uuid: + logger.debug(('invalid resolve request: address ({}) does not match ' + 'own one ({})').format(addr.text, args.uuid)) + return None + uuid_ = args.urn2uuid[addr.text] + matches = ElementTree.Element('wsd:ResolveMatches') + match = ElementTree.SubElement(matches, 'wsd:ResolveMatch') + wsd_add_endpoint_reference(uuid_, match) + wsd_add_types(match) + wsd_add_xaddr(uuid_, match, xaddr) + wsd_add_metadata_version(match) + + return matches + + +def wsd_handle_get(uuid_, hostname): + # see https://msdn.microsoft.com/en-us/library/hh441784.aspx for an example + 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(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') + wsd_add_endpoint_reference(uuid_, host) + ElementTree.SubElement(host, 'wsdp:Types').text = PUB_COMPUTER + ElementTree.SubElement(host, 'wsdp:ServiceId').text = uuid_.urn + if args.domain: + ElementTree.SubElement(host, PUB_COMPUTER).text = ( + '{0}/Domain:{1}'.format( + hostname if args.preserve_case else hostname.lower(), + args.domain)) + else: + ElementTree.SubElement(host, PUB_COMPUTER).text = ( + '{0}/Workgroup:{1}'.format( + hostname if args.preserve_case else hostname.upper(), + args.workgroup.upper())) + + return metadata + + +def wsd_is_duplicated_msg(msg_id): + """ + Check for a duplicated message. + + Implements SOAP-over-UDP Appendix II Item 2 + """ + if msg_id in wsd_known_messages: + return True + + wsd_known_messages.append(msg_id) + if len(wsd_known_messages) > WSD_MAX_KNOWN_MESSAGES: + wsd_known_messages.popleft() + + return False + + +def wsd_handle_message(data, interface, src_address): + """ + handle a WSD message that might be received by a MulticastInterface class + """ + tree = ElementTree.fromstring(data) + header = tree.find('./soap:Header', namespaces) + msg_id = header.find('./wsa:MessageID', namespaces).text + urn = header.find('./wsa:To', namespaces).text + try: + uuid_ = args.urn2uuid[urn] + except KeyError: + if urn in ['urn:schemas-xmlsoap-org:ws:2005:04:discovery']: + urn = None + else: + logger.debug('Go unknown wsa:To: {}'.format(data)) + + # if message came over multicast interface, check for duplicates + if interface and wsd_is_duplicated_msg(msg_id): + logger.debug('known message ({0}): dropping it'.format(msg_id)) + return [] + + response = None + action = header.find('./wsa:Action', namespaces).text + body = tree.find('./soap:Body', namespaces) + _, _, action_method = action.rpartition('/') + + if interface: + logger.info('{}:{}({}) - - "{} {} UDP" - -'.format( + src_address[0], src_address[1], interface.interface, + action_method, msg_id + )) + else: + # http logging is already done by according server + logger.debug('processing WSD {} message ({})'.format( + action_method, msg_id)) + + logger.debug('incoming message content is {0}'.format(data)) + if action == WSD_PROBE: + probe = body.find('./wsd:Probe', namespaces) + responses = [] + for uuid_ in args.uuid: + response = wsd_handle_probe(uuid_, probe) + if response: + responses.append( + wsd_build_message(WSA_ANON, WSD_PROBE_MATCH, header, response) + ) + return responses + elif action == WSD_RESOLVE: + resolve = body.find('./wsd:Resolve', namespaces) + response = wsd_handle_resolve(resolve, interface.transport_address) + if response: + return [wsd_build_message(WSA_ANON, WSD_RESOLVE_MATCH, header, response)] + else: + return [] + elif action == WSD_GET: + uuid_ = args.urn2uuid[urn] + hostname = args.uuid2hostname[str(uuid_)] + return [wsd_build_message( + WSA_ANON, + WSD_GET_RESPONSE, + header, + wsd_handle_get(uuid_, hostname))] + else: + logger.debug('unhandled action {0}/{1}'.format(action, msg_id)) + return [] + + +class WSDUdpRequestHandler(): + """Class for handling WSD requests coming from UDP datagrams.""" + def __init__(self, interface): + self.interface = interface + + def handle_request(self): + msg, address = self.interface.recv_socket.recvfrom(WSD_MAX_LEN) + msgs = wsd_handle_message(msg, self.interface, address) + for msg in msgs: + self.enqueue_datagram(msg, address=address) + + def send_hello(self, uuid_): + """WS-Discovery, Section 4.1, Hello message""" + hello = ElementTree.Element('wsd:Hello') + wsd_add_endpoint_reference(uuid_, hello) + # THINK: Microsoft does not send the transport address here due + # to privacy reasons. Could make this optional. + wsd_add_xaddr(uuid_, hello, self.interface.transport_address) + wsd_add_metadata_version(hello) + + msg = wsd_build_message(WSA_DISCOVERY, WSD_HELLO, None, hello) + self.enqueue_datagram(msg, msg_type='Hello') + + def send_bye(self, uuid_): + """WS-Discovery, Section 4.2, Bye message""" + bye = ElementTree.Element('wsd:Bye') + wsd_add_endpoint_reference(uuid_, bye) + + msg = wsd_build_message(WSA_DISCOVERY, WSD_BYE, None, bye) + self.enqueue_datagram(msg, msg_type='Bye') + + def enqueue_datagram(self, msg, address=None, msg_type=None): + """ + Add an outgoing WSD (SOAP) message to the queue of outstanding messages + + Implements SOAP over UDP, Appendix I. + """ + if not address: + address = self.interface.multicast_address + + if msg_type: + logger.debug('scheduling {0} message via {1} to {2}'.format( + msg_type, self.interface.interface, address)) + + msg_count = ( + MULTICAST_UDP_REPEAT + if address == self.interface.multicast_address + else UNICAST_UDP_REPEAT) + + due_time = time.time() + t = random.randint(UDP_MIN_DELAY, UDP_MAX_DELAY) + for i in range(msg_count): + send_queue.append([due_time, self.interface, address, msg]) + due_time += t / 1000 + t = min(t * 2, UDP_UPPER_DELAY) + + +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(s): + uuid_ = s.path.strip('/') + if not uuid_ in args.uuid2hostname: + s.send_error(404) + + if s.headers['Content-Type'] != 'application/soap+xml': + s.send_error(400, 'Invalid Content-Type') + + content_length = int(s.headers['Content-Length']) + body = s.rfile.read(content_length) + + responses = wsd_handle_message(body, None, None) + if len(responses) == 1: + s.send_response(200) + s.send_header('Content-Type', 'application/soap+xml') + s.end_headers() + s.wfile.write(responses[0]) + else: + s.send_error(500) + + +def enumerate_host_interfaces(): + """Get all addresses of all installed interfaces, except loopback""" + libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True) + addr = ctypes.POINTER(if_addrs)() + retval = libc.getifaddrs(ctypes.byref(addr)) + if retval: + raise OSError(ctypes.get_errno()) + + addrs = [] + ptr = addr + while ptr: + deref = ptr[0] + family = deref.addr[0].family if deref.addr else None + if family == socket.AF_INET: + addrs.append(( + deref.name.decode(), family, + socket.inet_ntop(family, bytes(deref.addr[0].data[2:6])))) + elif family == socket.AF_INET6: + if bytes(deref.addr[0].data[6:8]) == b'\xfe\x80': + addrs.append(( + deref.name.decode(), family, + socket.inet_ntop(family, bytes(deref.addr[0].data[6:22])))) + + ptr = deref.next + + libc.freeifaddrs(addr) + + # filter detected addresses by command line arguments, + # always exclude 'lo' interface + if args.ipv4only: + addrs = [x for x in addrs if x[1] == socket.AF_INET] + + if args.ipv6only: + addrs = [x for x in addrs if x[1] == socket.AF_INET6] + + addrs = [x for x in addrs if not x[0].startswith('lo')] + if args.interface: + addrs = [x for x in addrs if x[0] in args.interface] + + return addrs + + +def sigterm_handler(signum, frame): + if signum == signal.SIGTERM: + logger.info('received SIGTERM, tearing down') + # implictely raise SystemExit to cleanup properly + sys.exit(0) + + +def parse_args(): + global args, logger + + parser = argparse.ArgumentParser() + + parser.add_argument( + '-i', '--interface', + help='interface 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, + nargs='*' + ) + 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)', + nargs='*') + parser.add_argument( + '-w', '--workgroup', + help='set workgroup name (default WORKGROUP)', + default='WORKGROUP') + parser.add_argument( + '-t', '--nohttp', + 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') + + args = parser.parse_args(sys.argv[1:]) + + if args.verbose == 1: + log_level = logging.INFO + elif args.verbose > 1: + log_level = logging.DEBUG + 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.hostname: + args.hostname.append(socket.gethostname().partition('.')[0]) + + if not args.uuid: + args.uuid = [uuid.uuid5(uuid.NAMESPACE_DNS, hostname) for hostname in args.hostname] + logger.info('using pre-defined UUID {0}'.format(str(args.uuid))) + else: + if len(args.uuid) != len(args.hostname): + raise ValueError("please give the same number of uuid and hostname") + args.uuid = [uuid.UUID(uuid_) for uuid_ in args.uuid] + logger.info('user-supplied device UUID is {0}'.format(str(args.uuid))) + args.uuid2hostname = {str(uuid_): hostname for uuid_, hostname in zip(args.uuid, args.hostname)} + args.urn2uuid = {uuid_.urn: uuid_ for uuid_ in args.uuid} + args.hostname2uuid = {hostname: uuid_ for uuid_, hostname in zip(args.uuid, args.hostname)} + + for prefix, uri in namespaces.items(): + ElementTree.register_namespace(prefix, uri) + + +def send_outstanding_messages(block=False): + """ + Send all queued datagrams for which the timeout has been reached. If block + is true then all queued messages will be sent but with their according + delay. + """ + if len(send_queue) == 0: + return None + + # reverse ordering for faster removal of the last element + send_queue.sort(key=lambda x: x[0], reverse=True) + + # Emit every message that is "too late". Note that in case the system + # time jumps forward, multiple outstanding message which have a + # delay between them are sent out without that delay. + now = time.time() + while len(send_queue) > 0 and (send_queue[-1][0] <= now or block): + interface = send_queue[-1][1] + addr = send_queue[-1][2] + msg = send_queue[-1][3] + try: + interface.send_socket.sendto(msg, addr) + except Exception as e: + logger.error('error while sending packet on {}: {}'.format( + interface.interface, e)) + + del send_queue[-1] + if block and len(send_queue) > 0: + delay = send_queue[-1][0] - now + if delay > 0: + time.sleep(delay) + now = time.time() + + if len(send_queue) > 0 and not block: + return send_queue[-1][0] - now + + return None + + +def serve_wsd_requests(addresses): + """ + Multicast handling: send Hello message on startup, receive from multicast + sockets and handle the messages, and emit Bye message when process gets + terminated by signal + """ + s = selectors.DefaultSelector() + udp_srvs = [] + + for address in addresses: + interface = MulticastInterface(address[1], address[2], address[0]) + udp_srv = WSDUdpRequestHandler(interface) + udp_srvs.append(udp_srv) + s.register(interface.recv_socket, selectors.EVENT_READ, udp_srv) + + if not args.nohttp: + klass = ( + http.server.HTTPServer + if interface.family == socket.AF_INET + else HTTPv6Server) + http_srv = klass(interface.listen_address, WSDHttpRequestHandler) + s.register(http_srv.fileno(), selectors.EVENT_READ, http_srv) + + # everything is set up, announce ourself and serve requests + try: + for srv in udp_srvs: + for uuid_ in args.uuid: + srv.send_hello(uuid_) + + while True: + try: + timeout = send_outstanding_messages() + events = s.select(timeout) + for key, mask in events: + key.data.handle_request() + except (SystemExit, KeyboardInterrupt): + # silently exit the loop + logger.debug('got termination signal') + break + except Exception: + logger.exception('error in main loop') + finally: + logger.info('shutting down gracefully...') + + # say goodbye + for srv in udp_srvs: + for uuid_ in args.uuid: + srv.send_bye(uuid_) + + send_outstanding_messages(True) + + +def main(): + parse_args() + + addresses = enumerate_host_interfaces() + if not addresses: + logger.error("No multicast addresses available. Exiting.") + return 1 + + signal.signal(signal.SIGTERM, sigterm_handler) + serve_wsd_requests(addresses) + logger.info('Done.') + return 0 + + +if __name__ == '__main__': + sys.exit(main())