heinrich23

wsdd

May 16th, 2026
483
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 74.90 KB | Jokes | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. # Implements a target service according to the Web Service Discovery
  4. # specification.
  5. #
  6. # The purpose is to enable non-Windows devices to be found by the 'Network
  7. # (Neighborhood)' from Windows machines.
  8. #
  9. # see http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf and
  10. # related documents for details (look at README for more references)
  11. #
  12. # (c) Steffen Christgau, 2017-2024
  13.  
  14. import sys
  15. import signal
  16. import socket
  17. import asyncio
  18. import struct
  19. import argparse
  20. import uuid
  21. import time
  22. import random
  23. import logging
  24. import platform
  25. import ctypes.util
  26. import collections
  27. import xml.etree.ElementTree as ElementTree
  28. import http
  29. import http.server
  30. import urllib.request
  31. import urllib.parse
  32. import os
  33. import pwd
  34. import grp
  35. import datetime
  36.  
  37. from typing import Any, Callable, ClassVar, Deque, Dict, List, Optional, Set, Union, Tuple, TYPE_CHECKING
  38.  
  39. # try to load more secure XML module first, fallback to default if not present
  40. try:
  41.     if not TYPE_CHECKING:
  42.         from defusedxml.ElementTree import fromstring as ETfromString
  43. except ModuleNotFoundError:
  44.     from xml.etree.ElementTree import fromstring as ETfromString
  45.  
  46.  
  47. WSDD_VERSION: str = '0.8'
  48.  
  49.  
  50. args: argparse.Namespace
  51. logger: logging.Logger
  52.  
  53.  
  54. class NetworkInterface:
  55.  
  56.     _name: str
  57.     _index: int
  58.     _scope: int
  59.  
  60.     def __init__(self, name: str, scope: int, index: int) -> None:
  61.         self._name = name
  62.         self._scope = scope
  63.         if index is not None:
  64.             self._index = index
  65.         else:
  66.             self._index = socket.if_nametoindex(self._name)
  67.  
  68.     @property
  69.     def name(self) -> str:
  70.         return self._name
  71.  
  72.     @property
  73.     def scope(self) -> int:
  74.         return self._scope
  75.  
  76.     @property
  77.     def index(self) -> int:
  78.         return self._index
  79.  
  80.     def __str__(self) -> str:
  81.         return self._name
  82.  
  83.     def __eq__(self, other) -> bool:
  84.         return self._name == other.name
  85.  
  86.  
  87. class NetworkAddress:
  88.  
  89.     _family: int
  90.     _raw_address: bytes
  91.     _address_str: str
  92.     _interface: NetworkInterface
  93.  
  94.     def __init__(self, family: int, raw: Union[bytes, str], interface: NetworkInterface) -> None:
  95.         self._family = family
  96.         self._raw_address = raw if isinstance(raw, bytes) else socket.inet_pton(family, raw.partition('%')[0])
  97.         self._interface = interface
  98.  
  99.         self._address_str = socket.inet_ntop(self._family, self._raw_address)
  100.  
  101.     @property
  102.     def address_str(self):
  103.         return self._address_str
  104.  
  105.     @property
  106.     def family(self):
  107.         return self._family
  108.  
  109.     @property
  110.     def interface(self):
  111.         return self._interface
  112.  
  113.     @property
  114.     def is_multicastable(self):
  115.         # Nah, this check is not optimal but there are no local flags for
  116.         # addresses, but it should be safe for IPv4 anyways
  117.         # (https://tools.ietf.org/html/rfc5735#page-3)
  118.         return ((self._family == socket.AF_INET) and (self._raw_address[0] == 127)
  119.                 or (self._family == socket.AF_INET6) and (self._raw_address[0:2] != b'\xfe\x80'))
  120.  
  121.     @property
  122.     def raw(self):
  123.         return self._raw_address
  124.  
  125.     @property
  126.     def transport_str(self):
  127.         """the string representation of the local address overridden in network setup (for IPv6)"""
  128.         return self._address_str if self._family == socket.AF_INET else '[{}]'.format(self._address_str)
  129.  
  130.     def __str__(self) -> str:
  131.         return '{}%{}'.format(self._address_str, self._interface.name)
  132.  
  133.     def __eq__(self, other) -> bool:
  134.         return (self._family == other.family and self.raw == other.raw and self.interface == other.interface)
  135.  
  136.  
  137. class UdpAddress(NetworkAddress):
  138.  
  139.     _transport_address: Tuple
  140.     _port: int
  141.  
  142.     def __init__(self, family, transport_address: Tuple, interface: NetworkInterface) -> None:
  143.  
  144.         if not (family == socket.AF_INET or family == socket.AF_INET6):
  145.             raise RuntimeError('Unsupport address address family: {}.'.format(family))
  146.  
  147.         self._transport_address = transport_address
  148.         self._port = transport_address[1]
  149.  
  150.         super().__init__(family, transport_address[0], interface)
  151.  
  152.     @property
  153.     def transport_address(self):
  154.         return self._transport_address
  155.  
  156.     @property
  157.     def port(self):
  158.         return self._port
  159.  
  160.     def __eq__(self, other) -> bool:
  161.         return self.transport_address == other.transport_address
  162.  
  163.  
  164. class INetworkPacketHandler:
  165.  
  166.     def handle_packet(self, msg: str, udp_src_address: UdpAddress) -> None:
  167.         pass
  168.  
  169.  
  170. class MulticastHandler:
  171.     """
  172.    A class for handling multicast traffic on a given interface for a
  173.    given address family. It provides multicast sender and receiver sockets
  174.    """
  175.  
  176.     # base interface addressing information
  177.     address: NetworkAddress
  178.  
  179.     # individual interface-bound sockets for:
  180.     #  - receiving multicast traffic
  181.     #  - sending multicast from a socket bound to WSD port
  182.     #  - sending unicast messages from a random port
  183.     recv_socket: socket.socket
  184.     mc_send_socket: socket.socket
  185.     uc_send_socket: socket.socket
  186.  
  187.     # addresses used for communication and data
  188.     multicast_address: UdpAddress
  189.     listen_address: Tuple
  190.     aio_loop: asyncio.AbstractEventLoop
  191.  
  192.     # dictionary that holds INetworkPacketHandlers instances for sockets created above
  193.     message_handlers: Dict[socket.socket, List[INetworkPacketHandler]]
  194.  
  195.     def __init__(self, address: NetworkAddress, aio_loop: asyncio.AbstractEventLoop) -> None:
  196.         self.address = address
  197.  
  198.         self.recv_socket = socket.socket(self.address.family, socket.SOCK_DGRAM)
  199.         self.recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  200.         self.mc_send_socket = socket.socket(self.address.family, socket.SOCK_DGRAM)
  201.         self.uc_send_socket = socket.socket(self.address.family, socket.SOCK_DGRAM)
  202.         self.uc_send_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  203.  
  204.         self.message_handlers = {}
  205.         self.aio_loop = aio_loop
  206.  
  207.         if self.address.family == socket.AF_INET:
  208.             self.init_v4()
  209.         elif self.address.family == socket.AF_INET6:
  210.             self.init_v6()
  211.  
  212.         logger.info('joined multicast group {0} on {1}'.format(self.multicast_address.transport_str, self.address))
  213.         logger.debug('transport address on {0} is {1}'.format(self.address.interface.name, self.address.transport_str))
  214.         logger.debug('will listen for HTTP traffic on address {0}'.format(self.listen_address))
  215.  
  216.         # register calbacks for incoming data (also for mc)
  217.         self.aio_loop.add_reader(self.recv_socket.fileno(), self.read_socket, self.recv_socket)
  218.         self.aio_loop.add_reader(self.mc_send_socket.fileno(), self.read_socket, self.mc_send_socket)
  219.         self.aio_loop.add_reader(self.uc_send_socket.fileno(), self.read_socket, self.uc_send_socket)
  220.  
  221.     def cleanup(self) -> None:
  222.         self.aio_loop.remove_reader(self.recv_socket)
  223.         self.aio_loop.remove_reader(self.mc_send_socket)
  224.         self.aio_loop.remove_reader(self.uc_send_socket)
  225.  
  226.         self.recv_socket.close()
  227.         self.mc_send_socket.close()
  228.         self.uc_send_socket.close()
  229.  
  230.     def handles_address(self, address: NetworkAddress) -> bool:
  231.         return self.address == address
  232.  
  233.     def init_v6(self) -> None:
  234.         idx = self.address.interface.index
  235.         raw_mc_addr = (WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0x575C, idx)
  236.         self.multicast_address = UdpAddress(self.address.family, raw_mc_addr, self.address.interface)
  237.  
  238.         # v6: member_request = { multicast_addr, intf_idx }
  239.         mreq = (socket.inet_pton(self.address.family, WSD_MCAST_GRP_V6) + struct.pack('@I', idx))
  240.         self.recv_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
  241.         self.recv_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  242.  
  243.         # Could anyone ask the Linux folks for the rationale for this!?
  244.         if platform.system() == 'Linux':
  245.             try:
  246.                 # supported starting from Linux 4.20
  247.                 IPV6_MULTICAST_ALL = 29
  248.                 self.recv_socket.setsockopt(socket.IPPROTO_IPV6, IPV6_MULTICAST_ALL, 0)
  249.             except OSError as e:
  250.                 logger.warning('cannot unset all_multicast: {}'.format(e))
  251.  
  252.         # bind to network interface, i.e. scope and handle OS differences,
  253.         # see Stevens: Unix Network Programming, Section 21.6, last paragraph
  254.         try:
  255.             self.recv_socket.bind((WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0, idx))
  256.         except OSError:
  257.             self.recv_socket.bind(('::', 0, 0, idx))
  258.  
  259.         # bind unicast socket to interface address and WSD's udp port
  260.         self.uc_send_socket.bind((str(self.address), WSD_UDP_PORT, 0, idx))
  261.  
  262.         self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0)
  263.         self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, args.hoplimit)
  264.         self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
  265.  
  266.         self.listen_address = (self.address.address_str, WSD_HTTP_PORT, 0, idx)
  267.  
  268.     def init_v4(self) -> None:
  269.         idx = self.address.interface.index
  270.         raw_mc_addr = (WSD_MCAST_GRP_V4, WSD_UDP_PORT)
  271.         self.multicast_address = UdpAddress(self.address.family, raw_mc_addr, self.address.interface)
  272.  
  273.         # v4: member_request (ip_mreqn) = { multicast_addr, intf_addr, idx }
  274.         mreq = (socket.inet_pton(self.address.family, WSD_MCAST_GRP_V4) + self.address.raw + struct.pack('@I', idx))
  275.         self.recv_socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
  276.  
  277.         if platform.system() == 'Linux':
  278.             IP_MULTICAST_ALL = 49
  279.             self.recv_socket.setsockopt(socket.IPPROTO_IP, IP_MULTICAST_ALL, 0)
  280.  
  281.         try:
  282.             self.recv_socket.bind((WSD_MCAST_GRP_V4, WSD_UDP_PORT))
  283.         except OSError:
  284.             self.recv_socket.bind(('', WSD_UDP_PORT))
  285.  
  286.         # bind unicast socket to interface address and WSD's udp port
  287.         self.uc_send_socket.bind((self.address.address_str, WSD_UDP_PORT))
  288.  
  289.         self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, mreq)
  290.         # OpenBSD requires the optlen to be sizeof(char) for LOOP and TTL options
  291.         # (see also https://github.com/python/cpython/issues/67316)
  292.         self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, struct.pack('B', 0))
  293.         self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, struct.pack('B', args.hoplimit))
  294.  
  295.         self.listen_address = (self.address.address_str, WSD_HTTP_PORT)
  296.  
  297.     def add_handler(self, socket: socket.socket, handler: INetworkPacketHandler) -> None:
  298.         # try:
  299.         #    self.selector.register(socket, selectors.EVENT_READ, self)
  300.         # except KeyError:
  301.         #    # accept attempts of multiple registrations
  302.         #    pass
  303.  
  304.         if socket in self.message_handlers:
  305.             self.message_handlers[socket].append(handler)
  306.         else:
  307.             self.message_handlers[socket] = [handler]
  308.  
  309.     def remove_handler(self, socket: socket.socket, handler) -> None:
  310.         if socket in self.message_handlers:
  311.             if handler in self.message_handlers[socket]:
  312.                 self.message_handlers[socket].remove(handler)
  313.  
  314.     def read_socket(self, key: socket.socket) -> None:
  315.         # TODO: refactor this
  316.         s = None
  317.         if key == self.uc_send_socket:
  318.             s = self.uc_send_socket
  319.         elif key == self.mc_send_socket:
  320.             s = self.mc_send_socket
  321.         elif key == self.recv_socket:
  322.             s = self.recv_socket
  323.         else:
  324.             raise ValueError("Unknown socket passed as key.")
  325.  
  326.         msg, raw_address = s.recvfrom(WSD_MAX_LEN)
  327.         address = UdpAddress(self.address.family, raw_address, self.address.interface)
  328.         if s in self.message_handlers:
  329.             for handler in self.message_handlers[s]:
  330.                 handler.handle_packet(msg.decode('utf-8'), address)
  331.  
  332.     def send(self, msg: bytes, addr: UdpAddress):
  333.         # Request from a client must be answered from a socket that is bound
  334.         # to the WSD port, i.e. the recv_socket. Messages to multicast
  335.         # addresses are sent over the dedicated send socket.
  336.         if addr == self.multicast_address:
  337.             self.mc_send_socket.sendto(msg, addr.transport_address)
  338.         else:
  339.             self.uc_send_socket.sendto(msg, addr.transport_address)
  340.  
  341.  
  342. # constants for WSD XML/SOAP parsing
  343. WSA_URI: str = 'http://schemas.xmlsoap.org/ws/2004/08/addressing'
  344. WSD_URI: str = 'http://schemas.xmlsoap.org/ws/2005/04/discovery'
  345. WSDP_URI: str = 'http://schemas.xmlsoap.org/ws/2006/02/devprof'
  346.  
  347. namespaces: Dict[str, str] = {
  348.     'soap': 'http://www.w3.org/2003/05/soap-envelope',
  349.     'wsa': WSA_URI,
  350.     'wsd': WSD_URI,
  351.     'wsx': 'http://schemas.xmlsoap.org/ws/2004/09/mex',
  352.     'wsdp': WSDP_URI,
  353.     'pnpx': 'http://schemas.microsoft.com/windows/pnpx/2005/10',
  354.     'pub': 'http://schemas.microsoft.com/windows/pub/2005/07'
  355. }
  356.  
  357. WSD_MAX_KNOWN_MESSAGES: int = 10
  358.  
  359. WSD_PROBE: str = WSD_URI + '/Probe'
  360. WSD_PROBE_MATCH: str = WSD_URI + '/ProbeMatches'
  361. WSD_RESOLVE: str = WSD_URI + '/Resolve'
  362. WSD_RESOLVE_MATCH: str = WSD_URI + '/ResolveMatches'
  363. WSD_HELLO: str = WSD_URI + '/Hello'
  364. WSD_BYE: str = WSD_URI + '/Bye'
  365. WSD_GET: str = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Get'
  366. WSD_GET_RESPONSE: str = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse'
  367.  
  368. WSD_TYPE_DEVICE: str = 'wsdp:Device'
  369. PUB_COMPUTER: str = 'pub:Computer'
  370. WSD_TYPE_DEVICE_COMPUTER: str = '{0} {1}'.format(WSD_TYPE_DEVICE, PUB_COMPUTER)
  371.  
  372. WSD_MCAST_GRP_V4: str = '239.255.255.250'
  373. WSD_MCAST_GRP_V6: str = 'ff02::c'  # link-local
  374.  
  375. WSA_ANON: str = WSA_URI + '/role/anonymous'
  376. WSA_DISCOVERY: str = 'urn:schemas-xmlsoap-org:ws:2005:04:discovery'
  377.  
  378. MIME_TYPE_SOAP_XML: str = 'application/soap+xml'
  379.  
  380. # protocol assignments (WSD spec/Section 2.4)
  381. WSD_UDP_PORT: int = 3702
  382. WSD_HTTP_PORT: int = 5357
  383. WSD_MAX_LEN: int = 32767
  384.  
  385. WSDD_LISTEN_PORT = 5359
  386.  
  387. # SOAP/UDP transmission constants
  388. MULTICAST_UDP_REPEAT: int = 4
  389. UNICAST_UDP_REPEAT: int = 2
  390. UDP_MIN_DELAY: int = 50
  391. UDP_MAX_DELAY: int = 250
  392. UDP_UPPER_DELAY: int = 500
  393.  
  394. # servers must recond in 4 seconds after probe arrives
  395. PROBE_TIMEOUT: int = 4
  396. MAX_STARTUP_PROBE_DELAY: int = 3
  397.  
  398. # some globals
  399. wsd_instance_id: int = int(time.time())
  400.  
  401. WSDMessage = Tuple[ElementTree.Element, str]
  402. MessageTypeHandler = Callable[[ElementTree.Element, ElementTree.Element], Optional[WSDMessage]]
  403.  
  404.  
  405. class WSDMessageHandler(INetworkPacketHandler):
  406.     known_messages: Deque[str] = collections.deque([], WSD_MAX_KNOWN_MESSAGES)
  407.  
  408.     handlers: Dict[str, MessageTypeHandler]
  409.     pending_tasks: List[asyncio.Task]
  410.  
  411.     def __init__(self) -> None:
  412.         self.handlers = {}
  413.         self.pending_tasks = []
  414.  
  415.     def cleanup(self):
  416.         pass
  417.  
  418.     # shortcuts for building WSD responses
  419.     def add_endpoint_reference(self, parent: ElementTree.Element, endpoint: Optional[str] = None) -> None:
  420.         epr = ElementTree.SubElement(parent, 'wsa:EndpointReference')
  421.         address = ElementTree.SubElement(epr, 'wsa:Address')
  422.         if endpoint is None:
  423.             address.text = args.uuid.urn
  424.         else:
  425.             address.text = endpoint
  426.  
  427.     def add_metadata_version(self, parent: ElementTree.Element) -> None:
  428.         meta_data = ElementTree.SubElement(parent, 'wsd:MetadataVersion')
  429.         meta_data.text = '1'
  430.  
  431.     def add_types(self, parent: ElementTree.Element) -> None:
  432.         dev_type = ElementTree.SubElement(parent, 'wsd:Types')
  433.         dev_type.text = WSD_TYPE_DEVICE_COMPUTER
  434.  
  435.     def add_xaddr(self, parent: ElementTree.Element, transport_addr: str) -> None:
  436.         if transport_addr:
  437.             item = ElementTree.SubElement(parent, 'wsd:XAddrs')
  438.             item.text = 'http://{0}:{1}/{2}'.format(transport_addr, WSD_HTTP_PORT, args.uuid)
  439.  
  440.     def build_message(self, to_addr: str, action_str: str, request_header: Optional[ElementTree.Element],
  441.                       response: ElementTree.Element) -> str:
  442.         retval = self.xml_to_str(self.build_message_tree(to_addr, action_str, request_header, response)[0])
  443.  
  444.         logger.debug('constructed xml for WSD message: {0}'.format(retval))
  445.  
  446.         return retval
  447.  
  448.     def build_message_tree(self, to_addr: str, action_str: str, request_header: Optional[ElementTree.Element],
  449.                            body: Optional[ElementTree.Element]) -> Tuple[ElementTree.Element, str]:
  450.         """
  451.        Build a WSD message with a given action string including SOAP header.
  452.  
  453.        The message can be constructed based on a response to another
  454.        message (given by its header) and with a optional response that
  455.        serves as the message's body
  456.        """
  457.         root = ElementTree.Element('soap:Envelope')
  458.         header = ElementTree.SubElement(root, 'soap:Header')
  459.  
  460.         to = ElementTree.SubElement(header, 'wsa:To')
  461.         to.text = to_addr
  462.  
  463.         action = ElementTree.SubElement(header, 'wsa:Action')
  464.         action.text = action_str
  465.  
  466.         msg_id = ElementTree.SubElement(header, 'wsa:MessageID')
  467.         msg_id.text = uuid.uuid1().urn
  468.  
  469.         if request_header is not None:
  470.             req_msg_id = request_header.find('./wsa:MessageID', namespaces)
  471.             if req_msg_id is not None:
  472.                 relates_to = ElementTree.SubElement(header, 'wsa:RelatesTo')
  473.                 relates_to.text = req_msg_id.text
  474.  
  475.         self.add_header_elements(header, action_str)
  476.  
  477.         body_root = ElementTree.SubElement(root, 'soap:Body')
  478.         if body is not None:
  479.             body_root.append(body)
  480.  
  481.         for prefix, uri in namespaces.items():
  482.             root.attrib['xmlns:' + prefix] = uri
  483.  
  484.         return root, msg_id.text
  485.  
  486.     def add_header_elements(self, header: ElementTree.Element, extra: Any) -> None:
  487.         pass
  488.  
  489.     def handle_message(self, msg: str, src: Optional[UdpAddress] = None) -> Optional[str]:
  490.         """
  491.        handle a WSD message
  492.        """
  493.         try:
  494.             tree = ETfromString(msg)
  495.         except ElementTree.ParseError:
  496.             return None
  497.  
  498.         header = tree.find('./soap:Header', namespaces)
  499.         if header is None:
  500.             return None
  501.  
  502.         msg_id_tag = header.find('./wsa:MessageID', namespaces)
  503.         if msg_id_tag is None:
  504.             return None
  505.  
  506.         msg_id = str(msg_id_tag.text)
  507.  
  508.         # check for duplicates
  509.         if self.is_duplicated_msg(msg_id):
  510.             logger.debug('known message ({0}): dropping it'.format(msg_id))
  511.             return None
  512.  
  513.         action_tag = header.find('./wsa:Action', namespaces)
  514.         if action_tag is None:
  515.             return None
  516.  
  517.         action: str = str(action_tag.text)
  518.         _, _, action_method = action.rpartition('/')
  519.  
  520.         if src:
  521.             logger.info('{}:{}({}) - - "{} {} UDP" - -'.format(
  522.                 src.transport_str, src.port, src.interface, action_method, msg_id))
  523.         else:
  524.             # http logging is already done by according server
  525.             logger.debug('processing WSD {} message ({})'.format(action_method, msg_id))
  526.  
  527.         body = tree.find('./soap:Body', namespaces)
  528.         if body is None:
  529.             return None
  530.  
  531.         logger.debug('incoming message content is {0}'.format(msg))
  532.         if action in self.handlers:
  533.             handler = self.handlers[action]
  534.             retval = handler(header, body)
  535.             if retval is not None:
  536.                 response, response_type = retval
  537.                 return self.build_message(WSA_ANON, response_type, header, response)
  538.         else:
  539.             logger.debug('unhandled action {0}/{1}'.format(action, msg_id))
  540.  
  541.         return None
  542.  
  543.     def is_duplicated_msg(self, msg_id: str) -> bool:
  544.         """
  545.        Check for a duplicated message.
  546.  
  547.        Implements SOAP-over-UDP Appendix II Item 2
  548.        """
  549.         if msg_id in type(self).known_messages:
  550.             return True
  551.  
  552.         type(self).known_messages.append(msg_id)
  553.  
  554.         return False
  555.  
  556.     def xml_to_str(self, xml: ElementTree.Element) -> str:
  557.         retval = '<?xml version="1.0" encoding="utf-8"?>'
  558.         retval = retval + ElementTree.tostring(xml, encoding='utf-8').decode('utf-8')
  559.  
  560.         return retval
  561.  
  562.  
  563. class WSDUDPMessageHandler(WSDMessageHandler):
  564.     """
  565.    A message handler that handles traffic received via MutlicastHandler.
  566.    """
  567.  
  568.     mch: MulticastHandler
  569.     tearing_down: bool
  570.  
  571.     def __init__(self, mch: MulticastHandler) -> None:
  572.         super().__init__()
  573.  
  574.         self.mch = mch
  575.         self.tearing_down = False
  576.  
  577.     def teardown(self):
  578.         self.tearing_down = True
  579.  
  580.     def send_datagram(self, msg: str, dst: UdpAddress) -> None:
  581.         try:
  582.             self.mch.send(msg.encode('utf-8'), dst)
  583.         except Exception as e:
  584.             logger.error('error while sending packet on {}: {}'.format(self.mch.address.interface, e))
  585.  
  586.     def enqueue_datagram(self, msg: str, address: UdpAddress, msg_type: Optional[str] = None) -> None:
  587.         if msg_type:
  588.             logger.info('scheduling {0} message via {1} to {2}'.format(msg_type, address.interface, address))
  589.  
  590.         schedule_task = self.mch.aio_loop.create_task(self.schedule_datagram(msg, address))
  591.         # Add this task to the pending list during teardown to wait during shutdown
  592.         if self.tearing_down:
  593.             self.pending_tasks.append(schedule_task)
  594.  
  595.     async def schedule_datagram(self, msg: str, address: UdpAddress) -> None:
  596.         """
  597.        Schedule to send the given message to the given address.
  598.  
  599.        Implements SOAP over UDP, Appendix I.
  600.        """
  601.  
  602.         self.send_datagram(msg, address)
  603.  
  604.         delta = 0
  605.         msg_count = MULTICAST_UDP_REPEAT if address == self.mch.multicast_address else UNICAST_UDP_REPEAT
  606.         delta = random.randint(UDP_MIN_DELAY, UDP_MAX_DELAY)
  607.         for i in range(msg_count - 1):
  608.             await asyncio.sleep(delta / 1000.0)
  609.             self.send_datagram(msg, address)
  610.             delta = min(delta * 2, UDP_UPPER_DELAY)
  611.  
  612.  
  613. class WSDDiscoveredDevice:
  614.  
  615.     # a dict of discovered devices with their UUID as key
  616.     instances: Dict[str, 'WSDDiscoveredDevice'] = {}
  617.  
  618.     addresses: Dict[str, Set[str]]
  619.     props: Dict[str, str]
  620.     display_name: str
  621.     last_seen: float
  622.     types: Set[str]
  623.  
  624.     def __init__(self, xml_str: str, xaddr: str, interface: NetworkInterface) -> None:
  625.         self.last_seen = 0.0
  626.         self.addresses = {}
  627.         self.props = {}
  628.         self.display_name = ''
  629.         self.types = set()
  630.  
  631.         self.update(xml_str, xaddr, interface)
  632.  
  633.     def update(self, xml_str: str, xaddr: str, interface: NetworkInterface) -> None:
  634.         try:
  635.             tree = ETfromString(xml_str)
  636.         except ElementTree.ParseError:
  637.             return None
  638.         mds_path = 'soap:Body/wsx:Metadata/wsx:MetadataSection'
  639.         sections = tree.findall(mds_path, namespaces)
  640.         for section in sections:
  641.             dialect = section.attrib['Dialect']
  642.             if dialect == WSDP_URI + '/ThisDevice':
  643.                 self.extract_wsdp_props(section, dialect)
  644.             elif dialect == WSDP_URI + '/ThisModel':
  645.                 self.extract_wsdp_props(section, dialect)
  646.             elif dialect == WSDP_URI + '/Relationship':
  647.                 host_xpath = 'wsdp:Relationship[@Type="{}/host"]/wsdp:Host'.format(WSDP_URI)
  648.                 host_sec = section.find(host_xpath, namespaces)
  649.                 if (host_sec):
  650.                     self.extract_host_props(host_sec)
  651.             else:
  652.                 logger.debug('unknown metadata dialect ({})'.format(dialect))
  653.  
  654.         url = urllib.parse.urlparse(xaddr)
  655.         addr, _, _ = url.netloc.rpartition(':')
  656.         report = True
  657.         if interface.name not in self.addresses:
  658.             self.addresses[interface.name] = set([addr])
  659.         else:
  660.             if addr not in self.addresses[interface.name]:
  661.                 self.addresses[interface.name].add(addr)
  662.             else:
  663.                 report = False
  664.  
  665.         self.last_seen = time.time()
  666.         if ('DisplayName' in self.props) and ('BelongsTo' in self.props) and (report):
  667.             self.display_name = self.props['DisplayName']
  668.             logger.info('discovered {} in {} on {}'.format(self.display_name, self.props['BelongsTo'], addr))
  669.         elif ('FriendlyName' in self.props) and (report):
  670.             self.display_name = self.props['FriendlyName']
  671.             logger.info('discovered {} on {}'.format(self.display_name, addr))
  672.  
  673.         logger.debug(str(self.props))
  674.  
  675.     def extract_wsdp_props(self, root: ElementTree.Element, dialect: str) -> None:
  676.         _, _, propsRoot = dialect.rpartition('/')
  677.         # XPath support is limited, so filter by namespace on our own
  678.         nodes = root.findall('./wsdp:{0}/*'.format(propsRoot), namespaces)
  679.         ns_prefix = '{{{}}}'.format(WSDP_URI)
  680.         prop_nodes = [n for n in nodes if n.tag.startswith(ns_prefix)]
  681.         for node in prop_nodes:
  682.             tag_name = node.tag[len(ns_prefix):]
  683.             self.props[tag_name] = str(node.text)
  684.  
  685.     def extract_host_props(self, root: ElementTree.Element) -> None:
  686.         self.types = set(root.findtext('wsdp:Types', '', namespaces).split(' '))
  687.         if PUB_COMPUTER not in self.types:
  688.             return
  689.  
  690.         comp = root.findtext(PUB_COMPUTER, '', namespaces)
  691.         self.props['DisplayName'], _, self.props['BelongsTo'] = (
  692.             comp.partition('/'))
  693.  
  694.  
  695. class WSDClient(WSDUDPMessageHandler):
  696.  
  697.     instances: ClassVar[List['WSDClient']] = []
  698.     probes: Dict[str, float]
  699.  
  700.     def __init__(self, mch: MulticastHandler) -> None:
  701.         super().__init__(mch)
  702.  
  703.         WSDClient.instances.append(self)
  704.  
  705.         self.mch.add_handler(self.mch.mc_send_socket, self)
  706.         self.mch.add_handler(self.mch.recv_socket, self)
  707.  
  708.         self.probes = {}
  709.  
  710.         self.handlers[WSD_HELLO] = self.handle_hello
  711.         self.handlers[WSD_BYE] = self.handle_bye
  712.         self.handlers[WSD_PROBE_MATCH] = self.handle_probe_match
  713.         self.handlers[WSD_RESOLVE_MATCH] = self.handle_resolve_match
  714.  
  715.         # avoid packet storm when hosts come up by delaying initial probe
  716.         time.sleep(random.randint(0, MAX_STARTUP_PROBE_DELAY))
  717.         self.send_probe()
  718.  
  719.     def cleanup(self) -> None:
  720.         super().cleanup()
  721.         WSDClient.instances.remove(self)
  722.  
  723.         self.mch.remove_handler(self.mch.mc_send_socket, self)
  724.         self.mch.remove_handler(self.mch.recv_socket, self)
  725.  
  726.     def send_probe(self) -> None:
  727.         """WS-Discovery, Section 4.3, Probe message"""
  728.         self.remove_outdated_probes()
  729.  
  730.         probe = ElementTree.Element('wsd:Probe')
  731.         ElementTree.SubElement(probe, 'wsd:Types').text = WSD_TYPE_DEVICE
  732.  
  733.         xml, i = self.build_message_tree(WSA_DISCOVERY, WSD_PROBE, None, probe)
  734.         self.enqueue_datagram(self.xml_to_str(xml), self.mch.multicast_address, msg_type='Probe')
  735.         self.probes[i] = time.time()
  736.  
  737.     def teardown(self) -> None:
  738.         super().teardown()
  739.         self.remove_outdated_probes()
  740.  
  741.     def handle_packet(self, msg: str, src: Optional[UdpAddress] = None) -> None:
  742.         self.handle_message(msg, src)
  743.  
  744.     def __extract_xaddr(self, xaddrs: str) -> Optional[str]:
  745.         for addr in xaddrs.strip().split():
  746.             if (self.mch.address.family == socket.AF_INET6) and ('//[fe80::' in addr):
  747.                 # use first link-local address for IPv6
  748.                 return addr
  749.             elif self.mch.address.family == socket.AF_INET:
  750.                 # use first (and very likely the only) IPv4 address
  751.                 return addr
  752.  
  753.         return None
  754.  
  755.     def handle_hello(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]:
  756.         pm_path = 'wsd:Hello'
  757.         endpoint, xaddrs = self.extract_endpoint_metadata(body, pm_path)
  758.         if not xaddrs:
  759.             logger.info('Hello without XAddrs, sending resolve')
  760.             msg = self.build_resolve_message(str(endpoint))
  761.             self.enqueue_datagram(msg, self.mch.multicast_address)
  762.             return None
  763.  
  764.         xaddr = self.__extract_xaddr(xaddrs)
  765.         if xaddr is None:
  766.             return None
  767.  
  768.         logger.info('Hello from {} on {}'.format(endpoint, xaddr))
  769.         self.perform_metadata_exchange(endpoint, xaddr)
  770.         return None
  771.  
  772.     def handle_bye(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]:
  773.         bye_path = 'wsd:Bye'
  774.         endpoint, _ = self.extract_endpoint_metadata(body, bye_path)
  775.         device_uuid = str(uuid.UUID(endpoint))
  776.         if device_uuid in WSDDiscoveredDevice.instances:
  777.             del WSDDiscoveredDevice.instances[device_uuid]
  778.  
  779.         return None
  780.  
  781.     def handle_probe_match(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]:
  782.         # do not handle to probematches issued not sent by ourself
  783.         rel_msg = header.findtext('wsa:RelatesTo', None, namespaces)
  784.         if rel_msg not in self.probes:
  785.             logger.debug("unknown probe {}".format(rel_msg))
  786.             return None
  787.  
  788.         # if XAddrs are missing, issue resolve request
  789.         pm_path = 'wsd:ProbeMatches/wsd:ProbeMatch'
  790.         endpoint, xaddrs = self.extract_endpoint_metadata(body, pm_path)
  791.         if not xaddrs:
  792.             logger.debug('probe match without XAddrs, sending resolve')
  793.             msg = self.build_resolve_message(str(endpoint))
  794.             self.enqueue_datagram(msg, self.mch.multicast_address)
  795.             return None
  796.  
  797.         xaddr = self.__extract_xaddr(xaddrs)
  798.         if xaddr is None:
  799.             return None
  800.  
  801.         logger.debug('probe match for {} on {}'.format(endpoint, xaddr))
  802.         self.perform_metadata_exchange(endpoint, xaddr)
  803.  
  804.         return None
  805.  
  806.     def build_resolve_message(self, endpoint: str) -> str:
  807.         resolve = ElementTree.Element('wsd:Resolve')
  808.         self.add_endpoint_reference(resolve, endpoint)
  809.  
  810.         return self.build_message(WSA_DISCOVERY, WSD_RESOLVE, None, resolve)
  811.  
  812.     def handle_resolve_match(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]:
  813.         rm_path = 'wsd:ResolveMatches/wsd:ResolveMatch'
  814.         endpoint, xaddrs = self.extract_endpoint_metadata(body, rm_path)
  815.         if not endpoint or not xaddrs:
  816.             logger.debug('resolve match without endpoint/xaddr')
  817.             return None
  818.  
  819.         xaddr = self.__extract_xaddr(xaddrs)
  820.         if xaddr is None:
  821.             return None
  822.  
  823.         logger.debug('resolve match for {} on {}'.format(endpoint, xaddr))
  824.         self.perform_metadata_exchange(endpoint, xaddr)
  825.  
  826.         return None
  827.  
  828.     def extract_endpoint_metadata(self, body: ElementTree.Element, prefix: str) -> Tuple[Optional[str], Optional[str]]:
  829.         prefix = prefix + '/'
  830.         addr_path = 'wsa:EndpointReference/wsa:Address'
  831.  
  832.         endpoint = body.findtext(prefix + addr_path, namespaces=namespaces)
  833.         xaddrs = body.findtext(prefix + 'wsd:XAddrs', namespaces=namespaces)
  834.  
  835.         return endpoint, xaddrs
  836.  
  837.     def perform_metadata_exchange(self, endpoint, xaddr: str):
  838.         if not (xaddr.startswith('http://') or xaddr.startswith('https://')):
  839.             logger.debug('invalid XAddr: {}'.format(xaddr))
  840.             return
  841.  
  842.         host = None
  843.         url = xaddr
  844.         if self.mch.address.family == socket.AF_INET6:
  845.             host = '[{}]'.format(url.partition('[')[2].partition(']')[0])
  846.             url = url.replace(']', '%{}]'.format(self.mch.address.interface))
  847.  
  848.         body = self.build_getmetadata_message(endpoint)
  849.         request = urllib.request.Request(url, data=body.encode('utf-8'), method='POST')
  850.         request.add_header('Content-Type', 'application/soap+xml')
  851.         request.add_header('User-Agent', 'wsdd')
  852.         if host is not None:
  853.             request.add_header('Host', host)
  854.  
  855.         try:
  856.             with urllib.request.urlopen(request, None, args.metadata_timeout) as stream:
  857.                 self.handle_metadata(stream.read(), endpoint, xaddr)
  858.         except urllib.error.URLError as e:
  859.             logger.warning('could not fetch metadata from: {} {}'.format(url, e))
  860.  
  861.     def build_getmetadata_message(self, endpoint) -> str:
  862.         tree, _ = self.build_message_tree(endpoint, WSD_GET, None, None)
  863.         return self.xml_to_str(tree)
  864.  
  865.     def handle_metadata(self, meta: str, endpoint: str, xaddr: str) -> None:
  866.         device_uuid = str(uuid.UUID(endpoint))
  867.         if device_uuid in WSDDiscoveredDevice.instances:
  868.             WSDDiscoveredDevice.instances[device_uuid].update(meta, xaddr, self.mch.address.interface)
  869.         else:
  870.             WSDDiscoveredDevice.instances[device_uuid] = WSDDiscoveredDevice(meta, xaddr, self.mch.address.interface)
  871.  
  872.     def remove_outdated_probes(self) -> None:
  873.         cut = time.time() - PROBE_TIMEOUT * 2
  874.         self.probes = dict(filter(lambda x: x[1] > cut, self.probes.items()))
  875.  
  876.     def add_header_elements(self, header: ElementTree.Element, extra: Any) -> None:
  877.         action_str = extra
  878.         if action_str == WSD_GET:
  879.             reply_to = ElementTree.SubElement(header, 'wsa:ReplyTo')
  880.             addr = ElementTree.SubElement(reply_to, 'wsa:Address')
  881.             addr.text = WSA_ANON
  882.  
  883.             wsa_from = ElementTree.SubElement(header, 'wsa:From')
  884.             addr = ElementTree.SubElement(wsa_from, 'wsa:Address')
  885.             addr.text = args.uuid.urn
  886.  
  887.  
  888. class WSDHost(WSDUDPMessageHandler):
  889.     """Class for handling WSD requests coming from UDP datagrams."""
  890.  
  891.     message_number: ClassVar[int] = 0
  892.     instances: ClassVar[List['WSDHost']] = []
  893.  
  894.     def __init__(self, mch: MulticastHandler) -> None:
  895.         super().__init__(mch)
  896.  
  897.         WSDHost.instances.append(self)
  898.  
  899.         self.mch.add_handler(self.mch.recv_socket, self)
  900.  
  901.         self.handlers[WSD_PROBE] = self.handle_probe
  902.         self.handlers[WSD_RESOLVE] = self.handle_resolve
  903.  
  904.         self.send_hello()
  905.  
  906.     def cleanup(self) -> None:
  907.         super().cleanup()
  908.         WSDHost.instances.remove(self)
  909.  
  910.     def teardown(self) -> None:
  911.         super().teardown()
  912.         self.send_bye()
  913.  
  914.     def handle_packet(self, msg: str, src: UdpAddress) -> None:
  915.         reply = self.handle_message(msg, src)
  916.         if reply:
  917.             self.enqueue_datagram(reply, src)
  918.  
  919.     def send_hello(self) -> None:
  920.         """WS-Discovery, Section 4.1, Hello message"""
  921.         hello = ElementTree.Element('wsd:Hello')
  922.         self.add_endpoint_reference(hello)
  923.         # THINK: Microsoft does not send the transport address here due to privacy reasons. Could make this optional.
  924.         self.add_xaddr(hello, self.mch.address.transport_str)
  925.         self.add_metadata_version(hello)
  926.  
  927.         msg = self.build_message(WSA_DISCOVERY, WSD_HELLO, None, hello)
  928.         self.enqueue_datagram(msg, self.mch.multicast_address, msg_type='Hello')
  929.  
  930.     def send_bye(self) -> None:
  931.         """WS-Discovery, Section 4.2, Bye message"""
  932.         bye = ElementTree.Element('wsd:Bye')
  933.         self.add_endpoint_reference(bye)
  934.  
  935.         msg = self.build_message(WSA_DISCOVERY, WSD_BYE, None, bye)
  936.         self.enqueue_datagram(msg, self.mch.multicast_address, msg_type='Bye')
  937.  
  938.     def handle_probe(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]:
  939.         probe = body.find('./wsd:Probe', namespaces)
  940.         if probe is None:
  941.             return None
  942.  
  943.         scopes = probe.find('./wsd:Scopes', namespaces)
  944.  
  945.         if scopes:
  946.             # THINK: send fault message (see p. 21 in WSD)
  947.             logger.debug('scopes ({}) unsupported but probed'.format(scopes))
  948.             return None
  949.  
  950.         types_elem = probe.find('./wsd:Types', namespaces)
  951.         if types_elem is None:
  952.             logger.debug('Probe message lacks wsd:Types element. Ignored.')
  953.             return None
  954.  
  955.         types = types_elem.text
  956.         if not types == WSD_TYPE_DEVICE:
  957.             logger.debug('unknown discovery type ({}) for probe'.format(types))
  958.             return None
  959.  
  960.         matches = ElementTree.Element('wsd:ProbeMatches')
  961.         match = ElementTree.SubElement(matches, 'wsd:ProbeMatch')
  962.         self.add_endpoint_reference(match)
  963.         self.add_types(match)
  964.         self.add_metadata_version(match)
  965.  
  966.         return matches, WSD_PROBE_MATCH
  967.  
  968.     def handle_resolve(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]:
  969.         resolve = body.find('./wsd:Resolve', namespaces)
  970.         if resolve is None:
  971.             return None
  972.  
  973.         addr = resolve.find('./wsa:EndpointReference/wsa:Address', namespaces)
  974.         if addr is None:
  975.             logger.debug('invalid resolve request: missing endpoint address')
  976.             return None
  977.  
  978.         if not addr.text == args.uuid.urn:
  979.             logger.debug('invalid resolve request: address ({}) does not match own one ({})'.format(
  980.                 addr.text, args.uuid.urn))
  981.             return None
  982.  
  983.         matches = ElementTree.Element('wsd:ResolveMatches')
  984.         match = ElementTree.SubElement(matches, 'wsd:ResolveMatch')
  985.         self.add_endpoint_reference(match)
  986.         self.add_types(match)
  987.         self.add_xaddr(match, self.mch.address.transport_str)
  988.         self.add_metadata_version(match)
  989.  
  990.         return matches, WSD_RESOLVE_MATCH
  991.  
  992.     def add_header_elements(self, header: ElementTree.Element, extra: Any):
  993.         ElementTree.SubElement(header, 'wsd:AppSequence', {
  994.             'InstanceId': str(wsd_instance_id),
  995.             'SequenceId': uuid.uuid1().urn,
  996.             'MessageNumber': str(type(self).message_number)})
  997.  
  998.         type(self).message_number += 1
  999.  
  1000.  
  1001. class WSDHttpMessageHandler(WSDMessageHandler):
  1002.  
  1003.     def __init__(self) -> None:
  1004.         super().__init__()
  1005.  
  1006.         self.handlers[WSD_GET] = self.handle_get
  1007.  
  1008.     def handle_get(self, header: ElementTree.Element, body: ElementTree.Element) -> WSDMessage:
  1009.         # see https://msdn.microsoft.com/en-us/library/hh441784.aspx for an
  1010.         # example. Some of the properties below might be made configurable
  1011.         # in future releases.
  1012.         metadata = ElementTree.Element('wsx:Metadata')
  1013.         section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {'Dialect': WSDP_URI + '/ThisDevice'})
  1014.         device = ElementTree.SubElement(section, 'wsdp:ThisDevice')
  1015.         ElementTree.SubElement(device, 'wsdp:FriendlyName').text = ('WSD Device {0}'.format(args.hostname))
  1016.         ElementTree.SubElement(device, 'wsdp:FirmwareVersion').text = '1.0'
  1017.         ElementTree.SubElement(device, 'wsdp:SerialNumber').text = '1'
  1018.  
  1019.         section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {'Dialect': WSDP_URI + '/ThisModel'})
  1020.         model = ElementTree.SubElement(section, 'wsdp:ThisModel')
  1021.         ElementTree.SubElement(model, 'wsdp:Manufacturer').text = 'wsdd'
  1022.         ElementTree.SubElement(model, 'wsdp:ModelName').text = 'wsdd'
  1023.         ElementTree.SubElement(model, 'pnpx:DeviceCategory').text = 'Computers'
  1024.  
  1025.         section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {'Dialect': WSDP_URI + '/Relationship'})
  1026.         rel = ElementTree.SubElement(section, 'wsdp:Relationship', {'Type': WSDP_URI + '/host'})
  1027.         host = ElementTree.SubElement(rel, 'wsdp:Host')
  1028.         self.add_endpoint_reference(host)
  1029.         ElementTree.SubElement(host, 'wsdp:Types').text = PUB_COMPUTER
  1030.         ElementTree.SubElement(host, 'wsdp:ServiceId').text = args.uuid.urn
  1031.  
  1032.         fmt = '{0}/Domain:{1}' if args.domain else '{0}/Workgroup:{1}'
  1033.         value = args.domain if args.domain else args.workgroup.upper()
  1034.         if args.domain:
  1035.             dh = args.hostname if args.preserve_case else args.hostname.lower()
  1036.         else:
  1037.             dh = args.hostname if args.preserve_case else args.hostname.upper()
  1038.  
  1039.         ElementTree.SubElement(host, PUB_COMPUTER).text = fmt.format(dh, value)
  1040.  
  1041.         return metadata, WSD_GET_RESPONSE
  1042.  
  1043.  
  1044. class WSDHttpServer(http.server.HTTPServer):
  1045.     """ HTTP server both with IPv6 support and WSD handling """
  1046.  
  1047.     mch: MulticastHandler
  1048.     aio_loop: asyncio.AbstractEventLoop
  1049.     wsd_handler: WSDHttpMessageHandler
  1050.     registered: bool
  1051.  
  1052.     def __init__(self, mch: MulticastHandler, aio_loop: asyncio.AbstractEventLoop):
  1053.         # hacky way to convince HTTP/SocketServer of the address family
  1054.         type(self).address_family = mch.address.family
  1055.  
  1056.         self.mch = mch
  1057.         self.aio_loop = aio_loop
  1058.         self.wsd_handler = WSDHttpMessageHandler()
  1059.         self.registered = False
  1060.  
  1061.         # WSDHttpRequestHandler is a BaseHTTPRequestHandler. Passing to the parent constructor is therefore safe and
  1062.         # we can ignore the type error reported by mypy
  1063.         super().__init__(mch.listen_address, WSDHttpRequestHandler)  # type: ignore
  1064.  
  1065.     def server_bind(self) -> None:
  1066.         if self.mch.address.family == socket.AF_INET6:
  1067.             self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  1068.  
  1069.         super().server_bind()
  1070.  
  1071.     def server_activate(self) -> None:
  1072.         super().server_activate()
  1073.         self.aio_loop.add_reader(self.fileno(), self.handle_request)
  1074.         self.registered = True
  1075.  
  1076.     def server_close(self) -> None:
  1077.         if self.registered:
  1078.             self.aio_loop.remove_reader(self.fileno())
  1079.         super().server_close()
  1080.  
  1081.  
  1082. class WSDHttpRequestHandler(http.server.BaseHTTPRequestHandler):
  1083.     """Class for handling WSD requests coming over HTTP"""
  1084.  
  1085.     def log_message(self, fmt, *args) -> None:
  1086.         logger.info("{} - - ".format(self.address_string()) + fmt % args)
  1087.  
  1088.     def do_POST(self) -> None:
  1089.         if self.path != '/' + str(args.uuid):
  1090.             self.send_error(http.HTTPStatus.NOT_FOUND)
  1091.  
  1092.         ct = self.headers['Content-Type']
  1093.         if ct is None or not ct.startswith(MIME_TYPE_SOAP_XML):
  1094.             self.send_error(http.HTTPStatus.BAD_REQUEST, 'Invalid Content-Type')
  1095.  
  1096.         content_length = int(self.headers['Content-Length'])
  1097.         body = self.rfile.read(content_length)
  1098.  
  1099.         response = self.server.wsd_handler.handle_message(body)  # type: ignore
  1100.         if response:
  1101.             self.send_response(http.HTTPStatus.OK)
  1102.             self.send_header('Content-Type', MIME_TYPE_SOAP_XML)
  1103.             self.end_headers()
  1104.             self.wfile.write(response.encode('utf-8'))
  1105.         else:
  1106.             self.send_error(http.HTTPStatus.BAD_REQUEST)
  1107.  
  1108.  
  1109. class ApiServer:
  1110.  
  1111.     address_monitor: 'NetworkAddressMonitor'
  1112.  
  1113.     def __init__(self, aio_loop: asyncio.AbstractEventLoop, listen_address: bytes,
  1114.                  address_monitor: 'NetworkAddressMonitor') -> None:
  1115.         self.server = None
  1116.         self.address_monitor = address_monitor
  1117.  
  1118.         # defer server creation
  1119.         self.create_task = aio_loop.create_task(self.create_server(aio_loop, listen_address))
  1120.  
  1121.     async def create_server(self, aio_loop: asyncio.AbstractEventLoop, listen_address: Any) -> None:
  1122.  
  1123.         # It appears mypy is not able to check the argument to create_task and the return value of start_server
  1124.         # correctly. The docs say start_server returns a coroutine and the create_task takes a coro. And: It works.
  1125.         # Thus, we ignore type errors here.
  1126.         if isinstance(listen_address, int) or listen_address.isnumeric():
  1127.             self.server = await aio_loop.create_task(asyncio.start_server(  # type: ignore
  1128.                 self.on_connect, host='localhost', port=int(listen_address), reuse_address=True,
  1129.                 reuse_port=True))
  1130.         else:
  1131.             self.server = await aio_loop.create_task(asyncio.start_unix_server(  # type: ignore
  1132.                 self.on_connect, path=listen_address))
  1133.  
  1134.     async def on_connect(self, read_stream: asyncio.StreamReader, write_stream: asyncio.StreamWriter) -> None:
  1135.         while True:
  1136.             try:
  1137.                 line = await read_stream.readline()
  1138.                 if line:
  1139.                     self.handle_command(str(line.strip(), 'utf-8'), write_stream)
  1140.                     if not write_stream.is_closing():
  1141.                         await write_stream.drain()
  1142.                 else:
  1143.                     write_stream.close()
  1144.                     return
  1145.             except UnicodeDecodeError as e:
  1146.                 logger.debug('invalid input utf8', e)
  1147.             except Exception as e:
  1148.                 logger.warning('exception in API client', e)
  1149.                 write_stream.close()
  1150.                 return
  1151.  
  1152.     def handle_command(self, line: str, write_stream: asyncio.StreamWriter) -> None:
  1153.         words = line.split()
  1154.         if len(words) == 0:
  1155.             return
  1156.  
  1157.         command = words[0]
  1158.         command_args = words[1:]
  1159.         if command == 'probe' and args.discovery:
  1160.             intf = command_args[0] if command_args else None
  1161.             logger.debug('probing devices on {} upon request'.format(intf))
  1162.             for client in self.get_clients_by_interface(intf):
  1163.                 client.send_probe()
  1164.         elif command == 'clear' and args.discovery:
  1165.             logger.debug('clearing list of known devices')
  1166.             WSDDiscoveredDevice.instances.clear()
  1167.         elif command == 'list' and args.discovery:
  1168.             wsd_type = command_args[0] if command_args else None
  1169.             write_stream.write(bytes(self.get_list_reply(wsd_type), 'utf-8'))
  1170.         elif command == 'quit':
  1171.             write_stream.close()
  1172.         elif command == 'start':
  1173.             self.address_monitor.enumerate()
  1174.         elif command == 'stop':
  1175.             self.address_monitor.teardown()
  1176.         else:
  1177.             logger.debug('could not handle API request: {}'.format(line))
  1178.  
  1179.     def get_clients_by_interface(self, interface: Optional[str]) -> List[WSDClient]:
  1180.         return [c for c in WSDClient.instances if c.mch.address.interface.name == interface or not interface]
  1181.  
  1182.     def get_list_reply(self, wsd_type: Optional[str]) -> str:
  1183.         retval = ''
  1184.         for dev_uuid, dev in WSDDiscoveredDevice.instances.items():
  1185.             if wsd_type and (wsd_type not in dev.types):
  1186.                 continue
  1187.  
  1188.             addrs_str = []
  1189.             for addrs in dev.addresses.items():
  1190.                 addrs_str.append(', '.join(['{}'.format(a) for a in addrs]))
  1191.  
  1192.             retval = retval + '{}\t{}\t{}\t{}\t{}\t{}\n'.format(
  1193.                 dev_uuid,
  1194.                 dev.display_name,
  1195.                 dev.props['BelongsTo'] if 'BelongsTo' in dev.props else '',
  1196.                 datetime.datetime.fromtimestamp(dev.last_seen).isoformat('T', 'seconds'),
  1197.                 ','.join(addrs_str),
  1198.                 ','.join(dev.types))
  1199.  
  1200.         retval += '.\n'
  1201.         return retval
  1202.  
  1203.     async def cleanup(self) -> None:
  1204.         # ensure the server is not created after we have teared down
  1205.         await self.create_task
  1206.         if self.server:
  1207.             self.server.close()
  1208.             await self.server.wait_closed()
  1209.  
  1210.  
  1211. class MetaEnumAfterInit(type):
  1212.  
  1213.     def __call__(cls, *cargs, **kwargs):
  1214.         obj = super().__call__(*cargs, **kwargs)
  1215.         if not args.no_autostart:
  1216.             obj.enumerate()
  1217.         return obj
  1218.  
  1219.  
  1220. class NetworkAddressMonitor(metaclass=MetaEnumAfterInit):
  1221.     """
  1222.    Observes changes of network addresses, handles addition and removal of
  1223.    network addresses, and filters for addresses/interfaces that are or are not
  1224.    handled. The actual OS-specific implementation that detects the changes is
  1225.    done in subclasses. This class is used as a singleton
  1226.    """
  1227.  
  1228.     instance: ClassVar[object] = None
  1229.  
  1230.     interfaces: Dict[int, NetworkInterface]
  1231.     aio_loop: asyncio.AbstractEventLoop
  1232.     mchs: List[MulticastHandler]
  1233.     http_servers: List[WSDHttpServer]
  1234.     teardown_tasks: List[asyncio.Task]
  1235.     active: bool
  1236.  
  1237.     def __init__(self, aio_loop: asyncio.AbstractEventLoop) -> None:
  1238.  
  1239.         if NetworkAddressMonitor.instance is not None:
  1240.             raise RuntimeError('Instance of NetworkAddressMonitor already created')
  1241.  
  1242.         NetworkAddressMonitor.instance = self
  1243.  
  1244.         self.interfaces = {}
  1245.         self.aio_loop = aio_loop
  1246.  
  1247.         self.mchs = []
  1248.         self.http_servers = []
  1249.         self.teardown_tasks = []
  1250.  
  1251.         self.active = False
  1252.  
  1253.     def enumerate(self) -> None:
  1254.         """
  1255.        Performs an initial enumeration of addresses and sets up everything
  1256.        for observing future changes.
  1257.        """
  1258.         if self.active:
  1259.             return
  1260.  
  1261.         self.active = True
  1262.         self.do_enumerate()
  1263.  
  1264.     def do_enumerate(self) -> None:
  1265.         pass
  1266.  
  1267.     def handle_change(self) -> None:
  1268.         """ handle network change message """
  1269.         pass
  1270.  
  1271.     def add_interface(self, interface: NetworkInterface) -> NetworkInterface:
  1272.         # TODO: Cleanup
  1273.         if interface.index in self.interfaces:
  1274.             pass
  1275.             # self.interfaces[idx].name = name
  1276.         else:
  1277.             self.interfaces[interface.index] = interface
  1278.  
  1279.         return self.interfaces[interface.index]
  1280.  
  1281.     def is_address_handled(self, address: NetworkAddress) -> bool:
  1282.         # do not handle anything when we are not active
  1283.         if not self.active:
  1284.             return False
  1285.  
  1286.         # filter out address families we are not interested in
  1287.         if args.ipv4only and address.family != socket.AF_INET:
  1288.             return False
  1289.         if args.ipv6only and address.family != socket.AF_INET6:
  1290.             return False
  1291.  
  1292.         if address.is_multicastable:
  1293.             return False
  1294.  
  1295.         # Use interface only if it's in the list of user-provided interface names
  1296.         if ((args.interface) and (address.interface.name not in args.interface)
  1297.                 and (address.address_str not in args.interface)):
  1298.             return False
  1299.  
  1300.         return True
  1301.  
  1302.     def handle_new_address(self, address: NetworkAddress) -> None:
  1303.         logger.debug('new address {}'.format(address))
  1304.  
  1305.         if not self.is_address_handled(address):
  1306.             logger.debug('ignoring that address on {}'.format(address.interface))
  1307.             return
  1308.  
  1309.         # filter out what is not wanted
  1310.         # Ignore addresses or interfaces we already handle. There can only be
  1311.         # one multicast handler per address family and network interface
  1312.         for mch in self.mchs:
  1313.             if mch.handles_address(address):
  1314.                 return
  1315.  
  1316.         logger.debug('handling traffic for {}'.format(address))
  1317.         mch = MulticastHandler(address, self.aio_loop)
  1318.         self.mchs.append(mch)
  1319.  
  1320.         if not args.no_host:
  1321.             WSDHost(mch)
  1322.             if not args.no_http:
  1323.                 self.http_servers.append(WSDHttpServer(mch, self.aio_loop))
  1324.  
  1325.         if args.discovery:
  1326.             WSDClient(mch)
  1327.  
  1328.     def handle_deleted_address(self, address: NetworkAddress) -> None:
  1329.         logger.info('deleted address {}'.format(address))
  1330.  
  1331.         if not self.is_address_handled(address):
  1332.             return
  1333.  
  1334.         mch: Optional[MulticastHandler] = self.get_mch_by_address(address)
  1335.         if mch is None:
  1336.             return
  1337.  
  1338.         # Do not tear the client/hosts down. Saying goodbye does not work
  1339.         # because the address is already gone (at least on Linux).
  1340.         for c in WSDClient.instances:
  1341.             if c.mch == mch:
  1342.                 c.cleanup()
  1343.                 break
  1344.         for h in WSDHost.instances:
  1345.             if h.mch == mch:
  1346.                 h.cleanup()
  1347.                 break
  1348.         for s in self.http_servers:
  1349.             if s.mch == mch:
  1350.                 s.server_close()
  1351.                 self.http_servers.remove(s)
  1352.  
  1353.         mch.cleanup()
  1354.         self.mchs.remove(mch)
  1355.  
  1356.     def teardown(self) -> None:
  1357.         if not self.active:
  1358.             return
  1359.  
  1360.         self.active = False
  1361.  
  1362.         # return if we are still in tear down process
  1363.         if len(self.teardown_tasks) > 0:
  1364.             return
  1365.  
  1366.         for h in WSDHost.instances:
  1367.             h.teardown()
  1368.             h.cleanup()
  1369.             self.teardown_tasks.extend(h.pending_tasks)
  1370.  
  1371.         for c in WSDClient.instances:
  1372.             c.teardown()
  1373.             c.cleanup()
  1374.             self.teardown_tasks.extend(c.pending_tasks)
  1375.  
  1376.         for s in self.http_servers:
  1377.             s.server_close()
  1378.  
  1379.         self.http_servers.clear()
  1380.  
  1381.         if not self.teardown_tasks:
  1382.             return
  1383.  
  1384.         if not self.aio_loop.is_running():
  1385.             # Wait here for all pending tasks so that the main loop can be finished on termination.
  1386.             self.aio_loop.run_until_complete(asyncio.gather(*self.teardown_tasks))
  1387.         else:
  1388.             for t in self.teardown_tasks:
  1389.                 t.add_done_callback(self.mch_teardown)
  1390.  
  1391.     def mch_teardown(self, task) -> None:
  1392.         if any([not t.done() for t in self.teardown_tasks]):
  1393.             return
  1394.  
  1395.         self.teardown_tasks.clear()
  1396.  
  1397.         for mch in self.mchs:
  1398.             mch.cleanup()
  1399.         self.mchs.clear()
  1400.  
  1401.     def cleanup(self) -> None:
  1402.         self.teardown()
  1403.  
  1404.     def get_mch_by_address(self, address: NetworkAddress) -> Optional[MulticastHandler]:
  1405.         """
  1406.        Get the MCI for the address, its family and the interface.
  1407.        adress must be given as a string.
  1408.        """
  1409.         for retval in self.mchs:
  1410.             if retval.handles_address(address):
  1411.                 return retval
  1412.  
  1413.         return None
  1414.  
  1415.  
  1416. # from rtnetlink.h
  1417. RTMGRP_LINK: int = 1
  1418. RTMGRP_IPV4_IFADDR: int = 0x10
  1419. RTMGRP_IPV6_IFADDR: int = 0x100
  1420.  
  1421. # from netlink.h (struct nlmsghdr)
  1422. NLM_HDR_DEF: str = '@IHHII'
  1423.  
  1424. NLM_F_REQUEST: int = 0x01
  1425. NLM_F_ROOT: int = 0x100
  1426. NLM_F_MATCH: int = 0x200
  1427. NLM_F_DUMP: int = NLM_F_ROOT | NLM_F_MATCH
  1428.  
  1429. # self defines
  1430. NLM_HDR_ALIGNTO: int = 4
  1431.  
  1432. # ifa flags
  1433. IFA_F_DADFAILED: int = 0x08
  1434. IFA_F_HOMEADDRESS: int = 0x10
  1435. IFA_F_DEPRECATED: int = 0x20
  1436. IFA_F_TENTATIVE: int = 0x40
  1437.  
  1438. # from if_addr.h (struct ifaddrmsg)
  1439. IFADDR_MSG_DEF: str = '@BBBBI'
  1440. IFA_ADDRESS: int = 1
  1441. IFA_LOCAL: int = 2
  1442. IFA_LABEL: int = 3
  1443. IFA_FLAGS: int = 8
  1444. IFA_MSG_LEN: int = 8
  1445.  
  1446. RTA_ALIGNTO: int = 4
  1447. RTA_LEN: int = 4
  1448.  
  1449.  
  1450. def align_to(x: int, n: int) -> int:
  1451.     return ((x + n - 1) // n) * n
  1452.  
  1453.  
  1454. class NetlinkAddressMonitor(NetworkAddressMonitor):
  1455.     """
  1456.    Implementation of the AddressMonitor for Netlink sockets, i.e. Linux
  1457.    """
  1458.  
  1459.     RTM_NEWADDR: int = 20
  1460.     RTM_DELADDR: int = 21
  1461.     RTM_GETADDR: int = 22
  1462.  
  1463.     socket: socket.socket
  1464.  
  1465.     def __init__(self, aio_loop: asyncio.AbstractEventLoop) -> None:
  1466.         super().__init__(aio_loop)
  1467.  
  1468.         rtm_groups = RTMGRP_LINK
  1469.         if not args.ipv4only:
  1470.             rtm_groups = rtm_groups | RTMGRP_IPV6_IFADDR
  1471.         if not args.ipv6only:
  1472.             rtm_groups = rtm_groups | RTMGRP_IPV4_IFADDR
  1473.  
  1474.         self.socket = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
  1475.         self.socket.bind((0, rtm_groups))
  1476.         self.aio_loop.add_reader(self.socket.fileno(), self.handle_change)
  1477.  
  1478.         self.NLM_HDR_LEN = struct.calcsize(NLM_HDR_DEF)
  1479.  
  1480.     def do_enumerate(self) -> None:
  1481.         super().do_enumerate()
  1482.  
  1483.         kernel = (0, 0)
  1484.         # Append an unsigned byte to the header for the request.
  1485.         req = struct.pack(NLM_HDR_DEF + 'B', self.NLM_HDR_LEN + 1, self.RTM_GETADDR,
  1486.                           NLM_F_REQUEST | NLM_F_DUMP, 1, 0, socket.AF_PACKET)
  1487.         self.socket.sendto(req, kernel)
  1488.  
  1489.     def handle_change(self) -> None:
  1490.         super().handle_change()
  1491.  
  1492.         buf, src = self.socket.recvfrom(4096)
  1493.         logger.debug('netlink message with {} bytes'.format(len(buf)))
  1494.  
  1495.         offset = 0
  1496.         while offset < len(buf):
  1497.             h_len, h_type, _, _, _ = struct.unpack_from(NLM_HDR_DEF, buf, offset)
  1498.             offset += self.NLM_HDR_LEN
  1499.  
  1500.             msg_len = h_len - self.NLM_HDR_LEN
  1501.             if msg_len < 0:
  1502.                 break
  1503.  
  1504.             if h_type != self.RTM_NEWADDR and h_type != self.RTM_DELADDR:
  1505.                 logger.debug('invalid rtm_message type {}'.format(h_type))
  1506.                 offset += align_to(msg_len, NLM_HDR_ALIGNTO)
  1507.                 continue
  1508.  
  1509.             # decode ifaddrmsg as in if_addr.h
  1510.             ifa_family, _, ifa_flags, ifa_scope, ifa_idx = struct.unpack_from(IFADDR_MSG_DEF, buf, offset)
  1511.             if ((ifa_flags & IFA_F_DADFAILED) or (ifa_flags & IFA_F_HOMEADDRESS)
  1512.                     or (ifa_flags & IFA_F_DEPRECATED) or (ifa_flags & IFA_F_TENTATIVE)):
  1513.                 logger.debug('ignore address with invalid state {}'.format(hex(ifa_flags)))
  1514.                 offset += align_to(msg_len, NLM_HDR_ALIGNTO)
  1515.                 continue
  1516.  
  1517.             logger.debug('RTM new/del addr family: {} flags: {} scope: {} idx: {}'.format(
  1518.                          ifa_family, ifa_flags, ifa_scope, ifa_idx))
  1519.             addr = None
  1520.             i = offset + IFA_MSG_LEN
  1521.             while i - offset < msg_len:
  1522.                 attr_len, attr_type = struct.unpack_from('HH', buf, i)
  1523.                 logger.debug('rt_attr {} {}'.format(attr_len, attr_type))
  1524.  
  1525.                 if attr_len < RTA_LEN:
  1526.                     logger.debug('invalid rtm_attr_len. skipping remainder')
  1527.                     break
  1528.  
  1529.                 if attr_type == IFA_LABEL:
  1530.                     name, = struct.unpack_from(str(attr_len - 4 - 1) + 's', buf, i + 4)
  1531.                     self.add_interface(NetworkInterface(name.decode(), ifa_scope, ifa_idx))
  1532.                 elif attr_type == IFA_LOCAL and ifa_family == socket.AF_INET:
  1533.                     addr = buf[i + 4:i + 4 + 4]
  1534.                 elif attr_type == IFA_ADDRESS and ifa_family == socket.AF_INET6:
  1535.                     addr = buf[i + 4:i + 4 + 16]
  1536.                 elif attr_type == IFA_FLAGS:
  1537.                     _, ifa_flags = struct.unpack_from('HI', buf, i)
  1538.                 i += align_to(attr_len, RTA_ALIGNTO)
  1539.  
  1540.             if addr is None:
  1541.                 logger.debug('no address in RTM message')
  1542.                 offset += align_to(msg_len, NLM_HDR_ALIGNTO)
  1543.                 continue
  1544.  
  1545.             # In case of IPv6 only addresses, there appears to be no IFA_LABEL
  1546.             # message. Therefore, the name is requested by other means (#94)
  1547.             if ifa_idx not in self.interfaces:
  1548.                 try:
  1549.                     logger.debug('unknown interface name for idx {}. resolving manually'.format(ifa_idx))
  1550.                     if_name = socket.if_indextoname(ifa_idx)
  1551.                     self.add_interface(NetworkInterface(if_name, ifa_scope, ifa_idx))
  1552.                 except OSError:
  1553.                     logger.exception('interface detection failed')
  1554.                     # accept this exception (which should not occur)
  1555.                     pass
  1556.  
  1557.             # In case really strange things happen and we could not find out the
  1558.             # interface name for the returned ifa_idx, we... log a message.
  1559.             if ifa_idx in self.interfaces:
  1560.                 address = NetworkAddress(ifa_family, addr, self.interfaces[ifa_idx])
  1561.                 if h_type == self.RTM_NEWADDR:
  1562.                     self.handle_new_address(address)
  1563.                 elif h_type == self.RTM_DELADDR:
  1564.                     self.handle_deleted_address(address)
  1565.             else:
  1566.                 logger.debug('unknown interface index: {}'.format(ifa_idx))
  1567.  
  1568.             offset += align_to(msg_len, NLM_HDR_ALIGNTO)
  1569.  
  1570.     def cleanup(self) -> None:
  1571.         self.aio_loop.remove_reader(self.socket.fileno())
  1572.         self.socket.close()
  1573.         super().cleanup()
  1574.  
  1575.  
  1576. # from sys/net/route.h
  1577. RTA_IFA: int = 0x20
  1578.  
  1579. # from sys/socket.h
  1580. CTL_NET: int = 4
  1581. NET_RT_IFLIST: int = 3
  1582.  
  1583. # from sys/net/if.h
  1584. IFF_LOOPBACK: int = 0x8
  1585. IFF_MULTICAST: int = 0x800 if platform.system() != 'OpenBSD' else 0x8000
  1586.  
  1587. # sys/netinet6/in6_var.h
  1588. IN6_IFF_TENTATIVE: int = 0x02
  1589. IN6_IFF_DUPLICATED: int = 0x04
  1590. IN6_IFF_NOTREADY: int = IN6_IFF_TENTATIVE | IN6_IFF_DUPLICATED
  1591.  
  1592. SA_ALIGNTO: int = ctypes.sizeof(ctypes.c_long) if platform.system() != "Darwin" else ctypes.sizeof(ctypes.c_uint32)
  1593.  
  1594.  
  1595. class RouteSocketAddressMonitor(NetworkAddressMonitor):
  1596.     """
  1597.    Implementation of the AddressMonitor for FreeBSD and Darwin using route sockets
  1598.    """
  1599.  
  1600.     # Common definition for beginning part of if(m?a)?_msghdr structs (see net/if.h/man 4 route).
  1601.     IF_COMMON_HDR_DEF = '@HBBii' if platform.system() != 'OpenBSD' else '@HBBHHHBBiii'
  1602.  
  1603.     # from net/if.h
  1604.     RTM_NEWADDR: int = 0xC
  1605.     RTM_DELADDR: int = 0xD
  1606.     # not tested for OpenBSD
  1607.     RTM_IFINFO: int = 0xE
  1608.  
  1609.     # from route.h (value equals for FreeBSD, Darwin and OpenBSD)
  1610.     RTM_VERSION: int = 0x5
  1611.  
  1612.     # from net/if.h (struct ifa_msghdr)
  1613.     IFA_MSGHDR_DEF: str = IF_COMMON_HDR_DEF + ('hi' if platform.system() != 'OpenBSD' else '')
  1614.     IFA_MSGHDR_SIZE: int = struct.calcsize(IFA_MSGHDR_DEF)
  1615.  
  1616.     # The struct package does not allow to specify those, thus we hard code them as chars (x4).
  1617.     IF_MSG_DEFS: Dict[str, str] = {
  1618.         # if_data in if_msghdr is prepended with an u_short _ifm_spare1, thus the 'H' a the beginning)
  1619.         'FreeBSD': 'hH6c2c8c8c104c8c16c',
  1620.         # There are 8 bytes and 22 uint32_t in the if_data struct (22 x 4 Bytes + 8 = 96 Bytes)
  1621.         # It is also aligned on 4-byte boundary necessitating 2 bytes padding inside if_msghdr
  1622.         'Darwin': 'h2c8c22I',
  1623.         # struct if_data from /src/sys/net/if.h for if_msghdr
  1624.         #  (includes struct timeval which is a int64 + long
  1625.         'OpenBSD': '4c3I13Q1Iql'
  1626.     }
  1627.  
  1628.     socket: socket.socket
  1629.     intf_blacklist: List[str]
  1630.  
  1631.     is_openbsd: bool = False
  1632.  
  1633.     def __init__(self, aio_loop: asyncio.AbstractEventLoop) -> None:
  1634.         super().__init__(aio_loop)
  1635.         self.intf_blacklist = []
  1636.  
  1637.         # Create routing socket to get notified about future changes.
  1638.         # Do this before fetching the current routing information to avoid race condition.
  1639.         self.socket = socket.socket(socket.AF_ROUTE, socket.SOCK_RAW, socket.AF_UNSPEC)
  1640.         self.aio_loop.add_reader(self.socket.fileno(), self.handle_change)
  1641.  
  1642.         self.IF_MSGHDR_SIZE = struct.calcsize(self.IF_COMMON_HDR_DEF + self.IF_MSG_DEFS[platform.system()])
  1643.         self.is_openbsd = platform.system() == 'OpenBSD'
  1644.  
  1645.     def do_enumerate(self) -> None:
  1646.         super().do_enumerate()
  1647.         mib = [CTL_NET, socket.AF_ROUTE, 0, 0, NET_RT_IFLIST, 0]
  1648.         rt_mib = (ctypes.c_int * len(mib))()
  1649.         rt_mib[:] = [ctypes.c_int(m) for m in mib]
  1650.  
  1651.         libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
  1652.  
  1653.         # Ask kernel for routing table size first.
  1654.         rt_size = ctypes.c_size_t()
  1655.         if libc.sysctl(ctypes.byref(rt_mib), ctypes.c_size_t(len(rt_mib)), 0, ctypes.byref(rt_size), 0, 0):
  1656.             raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
  1657.  
  1658.         # Get the initial routing (interface list) data.
  1659.         rt_buf = ctypes.create_string_buffer(rt_size.value)
  1660.         if libc.sysctl(ctypes.byref(rt_mib), ctypes.c_size_t(len(rt_mib)), rt_buf, ctypes.byref(rt_size), 0, 0):
  1661.             raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
  1662.  
  1663.         self.parse_route_socket_response(rt_buf.raw, True)
  1664.  
  1665.     def handle_change(self) -> None:
  1666.         super().handle_change()
  1667.  
  1668.         self.parse_route_socket_response(self.socket.recv(4096), False)
  1669.  
  1670.     def parse_route_socket_response(self, buf: bytes, keep_intf: bool) -> None:
  1671.         offset = 0
  1672.  
  1673.         intf = None
  1674.         intf_flags = 0
  1675.         while offset < len(buf):
  1676.             # unpack route message response
  1677.             if not self.is_openbsd:
  1678.                 rtm_len, rtm_version, rtm_type, addr_mask, flags = struct.unpack_from(
  1679.                     self.IF_COMMON_HDR_DEF, buf, offset)
  1680.             else:
  1681.                 rtm_len, rtm_version, rtm_type, ifa_hdr_len, _, _, _, _, addr_mask, flags, _ = struct.unpack_from(
  1682.                     self.IF_COMMON_HDR_DEF, buf, offset)
  1683.  
  1684.             # exit condition for OpenBSD where always the complete buffer (ie 4096 bytes) is returned
  1685.             if rtm_len == 0:
  1686.                 break
  1687.  
  1688.             # skip over non-understood packets and versions
  1689.             if (rtm_type not in [self.RTM_NEWADDR, self.RTM_DELADDR, self.RTM_IFINFO]) or (
  1690.                     rtm_version != self.RTM_VERSION):
  1691.                 offset += rtm_len
  1692.                 continue
  1693.  
  1694.             if rtm_type == self.RTM_IFINFO:
  1695.                 intf_flags = flags
  1696.  
  1697.             sa_offset = offset + (self.IF_MSGHDR_SIZE if rtm_type == self.RTM_IFINFO else self.IFA_MSGHDR_SIZE)
  1698.  
  1699.             # For a route socket message, and different to a sysctl response,
  1700.             # the link info is stored inside the same rtm message, so it has to
  1701.             # survive multiple rtm messages in such cases
  1702.             if not keep_intf:
  1703.                 intf = None
  1704.  
  1705.             new_intf = self.parse_addrs(buf, sa_offset, offset + rtm_len, intf, addr_mask, rtm_type, intf_flags)
  1706.             intf = new_intf if new_intf else intf
  1707.  
  1708.             offset += rtm_len
  1709.  
  1710.     def clear_addr_scope(self, raw_addr: bytes) -> bytes:
  1711.         addr: bytearray = bytearray(raw_addr)
  1712.         # adapted from in6_clearscope BSD/Mac kernel method (see scope6.c)
  1713.         if addr[0] == 0xfe and (addr[1] & 0xc0) == 0x80:
  1714.             addr[2] = 0
  1715.             addr[3] = 0
  1716.  
  1717.         return bytes(addr)
  1718.  
  1719.     def parse_addrs(self, buf: bytes, offset: int, limit: int, intf: Optional[NetworkInterface], addr_mask: int,
  1720.                     rtm_type: int, flags: int) -> Optional[NetworkInterface]:
  1721.         addr_type_idx = 1
  1722.         addr = None
  1723.         addr_family: int = socket.AF_UNSPEC
  1724.         while offset < limit:
  1725.             while not (addr_type_idx & addr_mask) and (addr_type_idx <= addr_mask):
  1726.                 addr_type_idx = addr_type_idx << 1
  1727.  
  1728.             sa_len, sa_fam = struct.unpack_from('@BB', buf, offset)
  1729.             if sa_fam in [socket.AF_INET, socket.AF_INET6] and addr_type_idx == RTA_IFA:
  1730.                 addr_family = sa_fam
  1731.                 addr_offset = 4 if sa_fam == socket.AF_INET else 8
  1732.                 addr_length = 4 if sa_fam == socket.AF_INET else 16
  1733.                 addr_start = offset + addr_offset
  1734.                 addr = buf[addr_start:addr_start + addr_length]
  1735.                 if sa_fam == socket.AF_INET6:
  1736.                     addr = self.clear_addr_scope(addr)
  1737.             elif sa_fam == socket.AF_LINK:
  1738.                 idx, _, name_len = struct.unpack_from('@HBB', buf, offset + 2)
  1739.                 if idx > 0:
  1740.                     off_name = offset + 8
  1741.                     if_name = (buf[off_name:off_name + name_len]).decode()
  1742.                     intf = self.add_interface(NetworkInterface(if_name, idx, idx))
  1743.  
  1744.             offset += align_to(sa_len, SA_ALIGNTO) if sa_len > 0 else SA_ALIGNTO
  1745.             addr_type_idx = addr_type_idx << 1
  1746.  
  1747.         if rtm_type == self.RTM_IFINFO and intf is not None:
  1748.             if flags & IFF_LOOPBACK or not flags & IFF_MULTICAST:
  1749.                 self.intf_blacklist.append(intf.name)
  1750.             elif intf.name in self.intf_blacklist:
  1751.                 self.intf_blacklist.remove(intf.name)
  1752.  
  1753.         if intf is None or intf.name in self.intf_blacklist or addr is None:
  1754.             return intf
  1755.  
  1756.         address = NetworkAddress(addr_family, addr, intf)
  1757.         if rtm_type == self.RTM_DELADDR:
  1758.             self.handle_deleted_address(address)
  1759.         else:
  1760.             # Too bad, the address may be unuseable (tentative, e.g.) here
  1761.             # but we won't get any further notifcation about the address being
  1762.             # available for use. Thus, we try and may fail here
  1763.             self.handle_new_address(address)
  1764.  
  1765.         return intf
  1766.  
  1767.     def cleanup(self) -> None:
  1768.         self.aio_loop.remove_reader(self.socket.fileno())
  1769.         self.socket.close()
  1770.         super().cleanup()
  1771.  
  1772.  
  1773. def sigterm_handler() -> None:
  1774.     logger.info('received termination/interrupt signal, tearing down')
  1775.     # implictely raise SystemExit to cleanup properly
  1776.     sys.exit(0)
  1777.  
  1778.  
  1779. def parse_args() -> None:
  1780.     global args, logger
  1781.  
  1782.     parser = argparse.ArgumentParser()
  1783.  
  1784.     parser.add_argument(
  1785.         '-i', '--interface',
  1786.         help='interface or address to use',
  1787.         action='append', default=[])
  1788.     parser.add_argument(
  1789.         '-H', '--hoplimit',
  1790.         help='hop limit for multicast packets (default = 1)', type=int,
  1791.         default=1)
  1792.     parser.add_argument(
  1793.         '-U', '--uuid',
  1794.         help='UUID for the target device',
  1795.         default=None)
  1796.     parser.add_argument(
  1797.         '-v', '--verbose',
  1798.         help='increase verbosity',
  1799.         action='count', default=0)
  1800.     parser.add_argument(
  1801.         '-d', '--domain',
  1802.         help='set domain name (disables workgroup)',
  1803.         default=None)
  1804.     parser.add_argument(
  1805.         '-n', '--hostname',
  1806.         help='override (NetBIOS) hostname to be used (default hostname)',
  1807.         # use only the local part of a possible FQDN
  1808.         default=socket.gethostname().partition('.')[0])
  1809.     parser.add_argument(
  1810.         '-w', '--workgroup',
  1811.         help='set workgroup name (default WORKGROUP)',
  1812.         default='WORKGROUP')
  1813.     parser.add_argument(
  1814.         '-A', '--no-autostart',
  1815.         help='do not start networking after launch',
  1816.         action='store_true')
  1817.     parser.add_argument(
  1818.         '-t', '--no-http',
  1819.         help='disable http service (for debugging, e.g.)',
  1820.         action='store_true')
  1821.     parser.add_argument(
  1822.         '-4', '--ipv4only',
  1823.         help='use only IPv4 (default = off)',
  1824.         action='store_true')
  1825.     parser.add_argument(
  1826.         '-6', '--ipv6only',
  1827.         help='use IPv6 (default = off)',
  1828.         action='store_true')
  1829.     parser.add_argument(
  1830.         '-s', '--shortlog',
  1831.         help='log only level and message',
  1832.         action='store_true')
  1833.     parser.add_argument(
  1834.         '-p', '--preserve-case',
  1835.         help='preserve case of the provided/detected hostname',
  1836.         action='store_true')
  1837.     parser.add_argument(
  1838.         '-c', '--chroot',
  1839.         help='directory to chroot into',
  1840.         default=None)
  1841.     parser.add_argument(
  1842.         '-u', '--user',
  1843.         help='drop privileges to user:group',
  1844.         default=None)
  1845.     parser.add_argument(
  1846.         '-D', '--discovery',
  1847.         help='enable discovery operation mode',
  1848.         action='store_true')
  1849.     parser.add_argument(
  1850.         '-l', '--listen',
  1851.         help='listen on path or localhost port in discovery mode',
  1852.         default=None)
  1853.     parser.add_argument(
  1854.         '-o', '--no-host',
  1855.         help='disable server mode operation (host will be undiscoverable)',
  1856.         action='store_true')
  1857.     parser.add_argument(
  1858.         '-V', '--version',
  1859.         help='show version number and exit',
  1860.         action='store_true')
  1861.     parser.add_argument(
  1862.         '--metadata-timeout',
  1863.         help='set timeout for HTTP-based metadata exchange',
  1864.         default=2.0)
  1865.  
  1866.     args = parser.parse_args(sys.argv[1:])
  1867.  
  1868.     if args.version:
  1869.         print('wsdd - Web Service Discovery Daemon, v{}'.format(WSDD_VERSION))
  1870.         sys.exit(0)
  1871.  
  1872.     if args.verbose == 1:
  1873.         log_level = logging.INFO
  1874.     elif args.verbose > 1:
  1875.         log_level = logging.DEBUG
  1876.         asyncio.get_event_loop().set_debug(True)
  1877.         logging.getLogger("asyncio").setLevel(logging.DEBUG)
  1878.     else:
  1879.         log_level = logging.WARNING
  1880.  
  1881.     if args.shortlog:
  1882.         fmt = '%(levelname)s: %(message)s'
  1883.     else:
  1884.         fmt = '%(asctime)s:%(name)s %(levelname)s(pid %(process)d): %(message)s'
  1885.  
  1886.     logging.basicConfig(level=log_level, format=fmt)
  1887.     logger = logging.getLogger('wsdd')
  1888.  
  1889.     if not args.interface:
  1890.         logger.warning('no interface given, using all interfaces')
  1891.  
  1892.     if not args.uuid:
  1893.         def read_uuid_from_file(fn: str) -> Union[None, uuid.UUID]:
  1894.             try:
  1895.                 with open(fn) as f:
  1896.                     s: str = f.readline().strip()
  1897.                     return uuid.UUID(s)
  1898.             except Exception:
  1899.                 return None
  1900.  
  1901.         # machine uuid: try machine-id file first but also check for hostid (FreeBSD)
  1902.         args.uuid = read_uuid_from_file('/etc/machine-id') or \
  1903.             read_uuid_from_file('/etc/hostid') or \
  1904.             uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname())
  1905.  
  1906.         logger.info('using pre-defined UUID {0}'.format(str(args.uuid)))
  1907.     else:
  1908.         args.uuid = uuid.UUID(args.uuid)
  1909.         logger.info('user-supplied device UUID is {0}'.format(str(args.uuid)))
  1910.  
  1911.     for prefix, uri in namespaces.items():
  1912.         ElementTree.register_namespace(prefix, uri)
  1913.  
  1914.  
  1915. def chroot(root: str) -> bool:
  1916.     """
  1917.    Chroot into a separate directory to isolate ourself for increased security.
  1918.    """
  1919.     # preload for socket.gethostbyaddr()
  1920.     import encodings.idna
  1921.  
  1922.     try:
  1923.         os.chroot(root)
  1924.         os.chdir('/')
  1925.         logger.info('chrooted successfully to {}'.format(root))
  1926.     except Exception as e:
  1927.         logger.error('could not chroot to {}: {}'.format(root, e))
  1928.         return False
  1929.  
  1930.     return True
  1931.  
  1932.  
  1933. def get_ids_from_userspec(user_spec: str) -> Tuple[int, int]:
  1934.     uid: int
  1935.     gid: int
  1936.     try:
  1937.         user, _, group = user_spec.partition(':')
  1938.  
  1939.         if user:
  1940.             uid = pwd.getpwnam(user).pw_uid
  1941.  
  1942.         if group:
  1943.             gid = grp.getgrnam(group).gr_gid
  1944.     except Exception as e:
  1945.         raise RuntimeError('could not get uid/gid for {}: {}'.format(user_spec, e))
  1946.  
  1947.     return (uid, gid)
  1948.  
  1949.  
  1950. def drop_privileges(uid: int, gid: int) -> bool:
  1951.     try:
  1952.         if gid is not None:
  1953.             os.setgid(gid)
  1954.             os.setegid(gid)
  1955.             logger.debug('switched uid to {}'.format(uid))
  1956.  
  1957.         if uid is not None:
  1958.             os.setuid(uid)
  1959.             os.seteuid(uid)
  1960.             logger.debug('switched gid to {}'.format(gid))
  1961.  
  1962.         logger.info('running as {} ({}:{})'.format(args.user, uid, gid))
  1963.     except Exception as e:
  1964.         logger.error('dropping privileges failed: {}'.format(e))
  1965.         return False
  1966.  
  1967.     return True
  1968.  
  1969.  
  1970. def create_address_monitor(system: str, aio_loop: asyncio.AbstractEventLoop) -> NetworkAddressMonitor:
  1971.     if system == 'Linux':
  1972.         return NetlinkAddressMonitor(aio_loop)
  1973.     elif system in ['FreeBSD', 'Darwin', 'OpenBSD']:
  1974.         return RouteSocketAddressMonitor(aio_loop)
  1975.     else:
  1976.         raise NotImplementedError('unsupported OS: ' + system)
  1977.  
  1978.  
  1979. def main() -> int:
  1980.     global logger, args
  1981.  
  1982.     parse_args()
  1983.  
  1984.     if args.ipv4only and args.ipv6only:
  1985.         logger.error('Listening to no IP address family.')
  1986.         return 4
  1987.  
  1988.     aio_loop = asyncio.new_event_loop()
  1989.     nm = create_address_monitor(platform.system(), aio_loop)
  1990.  
  1991.     api_server = None
  1992.     if args.listen:
  1993.         api_server = ApiServer(aio_loop, args.listen, nm)
  1994.  
  1995.     # get uid:gid before potential chroot'ing
  1996.     if args.user is not None:
  1997.         ids = get_ids_from_userspec(args.user)
  1998.         if not ids:
  1999.             return 3
  2000.  
  2001.     if args.chroot is not None:
  2002.         if not chroot(args.chroot):
  2003.             return 2
  2004.  
  2005.     if args.user is not None:
  2006.         if not drop_privileges(ids[0], ids[1]):
  2007.             return 3
  2008.  
  2009.     if args.chroot and (os.getuid() == 0 or os.getgid() == 0):
  2010.         logger.warning('chrooted but running as root, consider -u option')
  2011.  
  2012.     # main loop, serve requests coming from any outbound socket
  2013.     aio_loop.add_signal_handler(signal.SIGINT, sigterm_handler)
  2014.     aio_loop.add_signal_handler(signal.SIGTERM, sigterm_handler)
  2015.     try:
  2016.         aio_loop.run_forever()
  2017.     except (SystemExit, KeyboardInterrupt):
  2018.         logger.info('shutting down gracefully...')
  2019.         if api_server is not None:
  2020.             aio_loop.run_until_complete(api_server.cleanup())
  2021.  
  2022.         nm.cleanup()
  2023.         aio_loop.stop()
  2024.     except Exception:
  2025.         logger.exception('error in main loop')
  2026.  
  2027.     logger.info('Done.')
  2028.     return 0
  2029.  
  2030.  
  2031. if __name__ == '__main__':
  2032.     sys.exit(main())
  2033.  
Advertisement
Add Comment
Please, Sign In to add comment