diff -Nru wsdd-0.7.0/CHANGELOG.md wsdd-0.7.1/CHANGELOG.md --- wsdd-0.7.0/CHANGELOG.md 2021-11-20 20:11:29.000000000 +0000 +++ wsdd-0.7.1/CHANGELOG.md 2023-03-04 11:21:05.000000000 +0000 @@ -1,5 +1,25 @@ # Changelog +## Unreleased + +## [0.7.1] - 2023-03-04 + +### Added + +- GitHub workflow for static analyses added (syntax, format, and type checks are performed). +- Added EnvironmentFile and according example for systemd-based distros. +- Make wsdd work (again) on MacOS (#139). Thanks to Eugene Gershnik. +- Application profile for UFW has been added (#169) + +### Fixed + +- Use of implicitly present async I/O loop instead created one for API servers. Fixes regression due to changed API in Python 3.10 (see #162) + +### Changed + +- Source code is spiced with type hints now. +- man page moved to section 8. + ## [0.7.0] - 2021-11-20 ### Added diff -Nru wsdd-0.7.0/CONTRIBUTING.md wsdd-0.7.1/CONTRIBUTING.md --- wsdd-0.7.0/CONTRIBUTING.md 2021-11-20 20:11:29.000000000 +0000 +++ wsdd-0.7.1/CONTRIBUTING.md 2023-03-04 11:21:05.000000000 +0000 @@ -4,6 +4,7 @@ * Code conforms to PEP8. Use pep8 or pycodestyle with their default settings to check for compliance. Consider using a pre-commit hook to prevent non-conforming code from entering the repo. * Avoid inline comments, prefer (short) block comments. * Add documentation strings to function if required. +* Use type hints. * If you want, add yourself to the AUTHORS file. # Commit Conventions diff -Nru wsdd-0.7.0/debian/changelog wsdd-0.7.1/debian/changelog --- wsdd-0.7.0/debian/changelog 2022-02-10 23:34:36.000000000 +0000 +++ wsdd-0.7.1/debian/changelog 2023-07-10 17:39:33.000000000 +0000 @@ -1,3 +1,9 @@ +wsdd (2:0.7.1-1~simonarons) jammy; urgency=medium + + * New upstream release. + + -- Simon Arons Mon, 10 Jul 2023 17:39:33 +0000 + wsdd (2:0.7.0-1) unstable; urgency=medium * Initial release (Closes: #1005270) diff -Nru wsdd-0.7.0/debian/patches/series wsdd-0.7.1/debian/patches/series --- wsdd-0.7.0/debian/patches/series 2022-02-10 23:34:36.000000000 +0000 +++ wsdd-0.7.1/debian/patches/series 2023-07-10 17:39:33.000000000 +0000 @@ -1,2 +1 @@ -wsdd-0.7.0-setup-py.patch -wsdd-fix-manpages.patch +wsdd-0.7.1-setup-py.patch diff -Nru wsdd-0.7.0/debian/patches/wsdd-0.7.0-setup-py.patch wsdd-0.7.1/debian/patches/wsdd-0.7.0-setup-py.patch --- wsdd-0.7.0/debian/patches/wsdd-0.7.0-setup-py.patch 2022-02-10 23:34:36.000000000 +0000 +++ wsdd-0.7.1/debian/patches/wsdd-0.7.0-setup-py.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,37 +0,0 @@ -Index: wsdd/setup.py -=================================================================== ---- /dev/null -+++ wsdd/setup.py -@@ -0,0 +1,32 @@ -+#!/usr/bin/env python3 -+ -+# Permission is hereby granted, free of charge, to any person obtaining a copy -+# of this software and associated documentation files (the "Software"), to deal -+# in the Software without restriction, including without limitation the rights -+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -+# copies of the Software, and to permit persons to whom the Software is -+# furnished to do so, subject to the following conditions: -+# -+# The above copyright notice and this permission notice shall be included in -+# all copies or substantial portions of the Software. -+# -+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -+# THE SOFTWARE. -+ -+from distutils.core import setup -+ -+setup(name='wsdd', -+ version='0.7.0', -+ description='Web Serice Discovery Daemon', -+ author='Steffen Christgau', -+ author_email='mail@s14u.de', -+ url='https://github.com/christgau/wsdd', -+ packages=['wsd',], -+ scripts=['wsdd',], -+ ) -+ diff -Nru wsdd-0.7.0/debian/patches/wsdd-0.7.1-setup-py.patch wsdd-0.7.1/debian/patches/wsdd-0.7.1-setup-py.patch --- wsdd-0.7.0/debian/patches/wsdd-0.7.1-setup-py.patch 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/debian/patches/wsdd-0.7.1-setup-py.patch 2023-07-10 17:39:33.000000000 +0000 @@ -0,0 +1,37 @@ +Index: wsdd/setup.py +=================================================================== +--- /dev/null ++++ wsdd/setup.py +@@ -0,0 +1,32 @@ ++#!/usr/bin/env python3 ++ ++# Permission is hereby granted, free of charge, to any person obtaining a copy ++# of this software and associated documentation files (the "Software"), to deal ++# in the Software without restriction, including without limitation the rights ++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ++# copies of the Software, and to permit persons to whom the Software is ++# furnished to do so, subject to the following conditions: ++# ++# The above copyright notice and this permission notice shall be included in ++# all copies or substantial portions of the Software. ++# ++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ++# THE SOFTWARE. ++ ++from distutils.core import setup ++ ++setup(name='wsdd', ++ version='0.7.1', ++ description='Web Serice Discovery Daemon', ++ author='Steffen Christgau', ++ author_email='mail@s14u.de', ++ url='https://github.com/christgau/wsdd', ++ packages=['wsd',], ++ scripts=['wsdd',], ++ ) ++ diff -Nru wsdd-0.7.0/debian/patches/wsdd-fix-manpages.patch wsdd-0.7.1/debian/patches/wsdd-fix-manpages.patch --- wsdd-0.7.0/debian/patches/wsdd-fix-manpages.patch 2022-02-10 23:34:36.000000000 +0000 +++ wsdd-0.7.1/debian/patches/wsdd-fix-manpages.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,498 +0,0 @@ -Index: wsdd/man/wsdd.1 -=================================================================== ---- wsdd.orig/man/wsdd.1 -+++ /dev/null -@@ -1,244 +0,0 @@ --.TH wsdd 1 --.SH NAME --wsdd \- A Web Service Discovery host and client daemon. --.SH SYNOPSIS --.B wsdd [\fBoptions\fR] --.SH DESCRIPTION --.PP --.B wsdd --implements both a Web Service Discovery (WSD) host and a WSD client daemon. The --host implementation enables (Samba) hosts, like your local NAS device, to be --found by Web Service Discovery clients like Windows. The client mode allows --searching for other WSD hosts on the local network. --.PP --The default mode of operation is the host mode. The client or discovery mode --must be enabled explictely. Unless configured otherwise, the host mode is always --active. Both modes can be used at the same time. --.SH OPTIONS --.SS General options --.TP --\fB\-4\fR, \fB\-\-ipv4only\fR --See below. --.TP --\fB\-6\fR, \fB\-\-ipv6only\fR --Restrict to the given address family. If both options are specified no --addreses will be available and wsdd will exit. --.TP --\fB\-A\fR, \fB\-\-no-autostart\fR --Do not start networking activities automatically when the program is started. --The API interface (see below) can be used to start and stop the networking --activities while the application is running. --.TP --\fB\-c \fIDIRECTORY\fR, \fB\-\-chroot \fIDIRECTORY\fR --chroot into the given \fIDIRECTORY\fR after initialization has been performed --and right before the handling of incoming messages starts. The new root directory --can be empty. Consider using the \fB-u\fR option as well. --.TP --\fB\-h\fR, \fB\-\-help\fR --Show help and exit. --.TP --\fB\-H \fIHOPLIMIT\fR, \fB\-\-hoplimit \fIHOPLIMIT\fR --Set the hop limit for multicast packets. The default is 1 which should --prevent packets from leaving the local network segment. --.TP --\fB\-i \fIINTERFACE/ADDRESS\fR, \fB\-\-interface \fIINTERFACE/ADDRESS\fR --Specify on which interfaces wsdd will be listening on. If no interfaces are --specified, all interfaces are used. Loop-back interfaces are never used, --even when explicitly specified. For interfaces with IPv6 addresses, --only link-local addresses will be used for announcing the host on the --network. This option can be provided multiple times in order to restrict --traffic to more than one interface. --This option also accepts IP addresses that the service should bind to. --For IPv6, only link local addresses are actually considered as noted above. --.TP --\fB\-s\fR, \fB\-\-shortlog\fR --Use a shorter logging format that only includes the level and message. --This is useful in cases where the logging mechanism, like systemd on Linux, --automatically prepends a date and process name plus ID to the log message. --.TP --\fB\-u \fIUSER[:GROUP]\fR, \fB\-\-user \fIUSER[:GROUP]\fR --Change user (and group) when running before handling network packets. --Together with \fB\-c\fR this option can be used to increase security --if the execution environment, like the init system, cannot ensure this in --another way. --.TP --\fB\-U \fIUUID\fR, \fB\-\-uuid \fIUUID\fR --The WSD specification requires a device to have a unique address that is --stable across reboots or changes in networks. In the context of the --standard, it is assumed that this is something like a serial number. wsdd --uses the UUID version 5 with the DNS namespace and the host name of the --local machine as inputs. Thus, the host name should be stable and not be --modified, e.g. by DHCP. However, if you want wsdd to use a specific UUID --you can use this option. --.TP --\fB\-v\fR, \fB\-\-verbose\fR --Additively increase verbosity of the log output. A single occurrence of ---v/--verbose sets the log level to INFO. More -v options set the log level --to DEBUG. --.TP --\fB\-V\fR, \fB\-\-version\fR --Show the version number and exit. --.SS Host Mode Options --.TP --\fB\-d \fIDOMAIN\fR, \fB\-\-domain \fIDOMAIN\fR --Assume that the host running wsdd joined an ADS domain. This will make --wsdd report the host being a domain member. It disables workgroup --membership reporting. The (provided) hostname is automatically converted --to lower case. Use the `-p` option to change this behavior. --.TP --\fB\-n \fIHOSTNAME\fR, \fB\-\-hostname \fIHOSTNAME\fR --Override the host name wsdd uses during discovery. By default the machine's --host name is used (look at hostname(1)). Only the host name part of a --possible FQDN will be used in the default case. --.TP --\fB\-o\fR, \fB\-\-no-host\fR --Disable host operation mode. If this option is provided, the host cannot be --discovered by other (Windows) hosts. It can be useful when client/discovery --mode is used and no announcement of the host that runs wsdd should be made. --.TP --\fB\-p\fR, \fB\-\-preserve-case\fR --Preserve the hostname as it is. Without this option, the hostname is --converted as follows. For workgroup environments (see -w) the hostname --is made upper case by default. Vice versa it is made lower case for usage --in domains (see -d). --.TP --\fB\-t\fR, \fB\-\-no-http\fR --Do not service HTTP requests of the WSD protocol. This option is intended --for debugging purposes where another process may handle the Get messages. --.TP --\fB\-w \fIWORKGROUP\fR, \fB\-\-workgroup \fIWORKGROUP\fR --By default wsdd reports the host is a member of a workgroup rather than a --domain (use the -d/--domain option to override this). With -w/--workgroup --the default workgroup name can be changed. The default work group name is --WORKGROUP. The (provided) hostname is automatically converted to upper --case. Use the `-p` option to change this behavior. --.SS Client/Discovery Mode Options --.TP --\fB\-D\fR, \fB\-\-discovery\fR --Enable discovery mode to search for other WSD hosts/servers. Found hosts --are logged with INFO priority. The server interface (see below) --can be used for a programatic interface. Refer to the man page for --details of the server interface. --.TP --\fB\-l \fIPATH/PORT\fR, \fB\-\-listen \fIPATH/PORT\fR --Specify the location of the socket for the server programming interface. --If the option value is numeric, it is interpreted as numeric port for a --TCP server port. Then, the server socket is bound to the localhost only. --If the option value is non-numeric, it is assumed to be a path to UNIX --domain socket to which a client can connect to. -- --.SH EXAMPLE USAGE --.SS Handle traffic on eth0 and eth2 only, but only with IPv6 addresses -- -- wsdd \-i eth0 \-i eth2 \-6 -- -- or -- -- wsdd \-\-interface eth0 \-\-interface eth2 \-\-ipv6only --.SS Set the Workgroup according to smb.conf, be verbose, run with dropped privileges, and change the root directory to an (empty) directory -- -- SMB_GROUP=$(grep \-i '^\s*workgroup\s*=' smb.conf | cut \-f2 \-d= | tr \-d '[:blank:]') -- -- wsdd \-v \-w $SMB_GROUP -u daemon:daemon -c /var/run/wsdd/chroot --.SH FIREWALL SETUP --.PP --Traffic for the following ports, directions and addresses must be allowed: --.TP --- Incoming and outgoing traffic to udp/3702 with multicast destination: 239.255.255.250 for IPv4 and ff02::c for IPv6 --.TP --- Outgoing unicast traffic from udp/3702 --.TP --- Incoming traffic to tcp/5357 --.PP --You should further restrict the traffic to the (link-)local subnet, e.g. by --using the `fe80::/10` address space for IPv6. Please note that IGMP traffic --must be enabled in order to get IPv4 multicast traffic working. --.SH API INTERFACE --Wsdd provides a text-based, line-oriented API interface to query information --and trigger actions. The interface can be used with TCP and UNIX domain sockets --(see \fB\-l/\-\-listen\fR option). The TCP socket is bound to the local host --only. The following commands can be issued: --.SS \fBclear\fR - Clear list of discovered devices --Clears the list of all discovered devices. Use the \fBprobe\fR command to --search for devices again. This command does not return any data and is only --available in discover mode. --.SS \fBlist\fR - List discovered devices --Returns a tab-separated list of discovered devices with the following information. --The possibly empty list of detected hosts is always terminated with a single --dot ('.') in an otherwise empty line. This command is only available in discover mode. --.TP --UUID --UUID of the discovered device. Note that a multi-homed device should appear --only once but with multiple addresses (see below) --.TP --name --The name reported by the device. For discovered Windows hosts, it is the --configured computer name that is reported here. --.TP --association --Specifies the domain or workgroup to which the discovered host belongs to. The --type of the association (workgroup or domain) is separated from its value by a --colon. --.TP --last_seen --The date and time the device was last seen as a consequence of Probe/Hello --messages provided in ISO8601 with seconds. --.TP --addresses --List of all transport addresses that were collected during the discovery --process delimited by commas. Addresses are provided along with the interface --(separated by '%') on which they were discovered. IPv6 addresses are reported --on square brackets. Note that the reported addresses may not match the actual --device on which the device may be reached. --.SS \fBprobe \fI[INTERFACE]\fR - Search for devices --Triggers a Probe message on the provided INTERFACE (eth0, e.g.) to search for --WSD hosts. If no interface is provided, all interfaces wsdd listens on are probed. --A Probe messages initiates the discovery message flow. It may take some time for --hosts to be actually discovered. This command does not return any data and is --only available in discovery mode. --.SS \fBstart\fR - Start networking activities --This command starts the networking acitivies of wsdd if they haven't been --started yet. "Hello" messages are emitted and the host is announced on the --network(s) when the host mode is active. If the discovery mode is enabled a --probe process is also started. -- --.SS \fBstop\fR - Stop networking activities --This is the reverse operation to start. When this command is received, "Bye" --messages are sent in order to notify hosts in the network about the host's --disappearance. All networking sockets used for the WSD protocol are closed as --well. Activities can be restarted with the start operation. -- --.SH SECURITY --.PP --wsdd does not implement any security feature, e.g. by using TLS for the http --service. This is because wsdd's intended usage is within private, i.e. home, --LANs. The \fIHello\fR message contains the hosts transport address, i.e. the IP --address which speeds up discovery (avoids \fIResolve\fR message). --.SH KNOWN ISSUES --.SS Using only IPv6 on FreeBSD --If wsdd is running on FreeBSD using IPv6 only, the host running wsdd may not be --reliably discovered. The reason appears to be that Windows is not always able --to connect to the HTTP service for unknown reasons. As a workaround, run wsdd --with IPv4 only. --.SS Tunnel/Bridge Interface --.PP --If tunnel/bridge interfaces like those created by OpenVPN or Docker exist, they --may interfere with wsdd if executed without providing an interface that it --should bind to (so it binds to all). In such cases, the wsdd hosts appears after --wsdd has been started but it disappears when an update of the Network view in --Windows Explorer is forced, either by refreshing the view or by a reboot of the --Windows machine. To solve this issue, the interface that is connected to the --network on which the host should be announced needs to be specified with the ---i/--interface option. This prevents the usage of the tunnel/bridge --interfaces. --.PP --Background: Tunnel/bridge interfaces may cause \fIResolve\fR requests from Windows --hosts to be delivered to wsdd multiple times, i.e. duplicates of such request --are created. If wsdd receives such a request first from a tunnel/bridge it uses --the transport address (IP address) of that interface and sends the response via --unicast. Further duplicates are not processed due to the duplicate message --detection which is based on message UUIDs. The Windows host which receives the --response appears to detect a mismatch between the transport address in the --\fIResolveMatch\fR message (which is the tunnel/bridge address) and the IP of the --sending host/interface (LAN IP, e.g.). Subsequently, the wsdd host is ignored by --Windows. -Index: wsdd/man/wsdd.8 -=================================================================== ---- /dev/null -+++ wsdd/man/wsdd.8 -@@ -0,0 +1,244 @@ -+.TH wsdd 8 -+.SH NAME -+wsdd \- A Web Service Discovery host and client daemon. -+.SH SYNOPSIS -+.B wsdd [\fBoptions\fR] -+.SH DESCRIPTION -+.PP -+.B wsdd -+implements both a Web Service Discovery (WSD) host and a WSD client daemon. The -+host implementation enables (Samba) hosts, like your local NAS device, to be -+found by Web Service Discovery clients like Windows. The client mode allows -+searching for other WSD hosts on the local network. -+.PP -+The default mode of operation is the host mode. The client or discovery mode -+must be enabled explictely. Unless configured otherwise, the host mode is always -+active. Both modes can be used at the same time. -+.SH OPTIONS -+.SS General options -+.TP -+\fB\-4\fR, \fB\-\-ipv4only\fR -+See below. -+.TP -+\fB\-6\fR, \fB\-\-ipv6only\fR -+Restrict to the given address family. If both options are specified no -+addreses will be available and wsdd will exit. -+.TP -+\fB\-A\fR, \fB\-\-no-autostart\fR -+Do not start networking activities automatically when the program is started. -+The API interface (see below) can be used to start and stop the networking -+activities while the application is running. -+.TP -+\fB\-c \fIDIRECTORY\fR, \fB\-\-chroot \fIDIRECTORY\fR -+chroot into the given \fIDIRECTORY\fR after initialization has been performed -+and right before the handling of incoming messages starts. The new root directory -+can be empty. Consider using the \fB-u\fR option as well. -+.TP -+\fB\-h\fR, \fB\-\-help\fR -+Show help and exit. -+.TP -+\fB\-H \fIHOPLIMIT\fR, \fB\-\-hoplimit \fIHOPLIMIT\fR -+Set the hop limit for multicast packets. The default is 1 which should -+prevent packets from leaving the local network segment. -+.TP -+\fB\-i \fIINTERFACE/ADDRESS\fR, \fB\-\-interface \fIINTERFACE/ADDRESS\fR -+Specify on which interfaces wsdd will be listening on. If no interfaces are -+specified, all interfaces are used. Loop-back interfaces are never used, -+even when explicitly specified. For interfaces with IPv6 addresses, -+only link-local addresses will be used for announcing the host on the -+network. This option can be provided multiple times in order to restrict -+traffic to more than one interface. -+This option also accepts IP addresses that the service should bind to. -+For IPv6, only link local addresses are actually considered as noted above. -+.TP -+\fB\-s\fR, \fB\-\-shortlog\fR -+Use a shorter logging format that only includes the level and message. -+This is useful in cases where the logging mechanism, like systemd on Linux, -+automatically prepends a date and process name plus ID to the log message. -+.TP -+\fB\-u \fIUSER[:GROUP]\fR, \fB\-\-user \fIUSER[:GROUP]\fR -+Change user (and group) when running before handling network packets. -+Together with \fB\-c\fR this option can be used to increase security -+if the execution environment, like the init system, cannot ensure this in -+another way. -+.TP -+\fB\-U \fIUUID\fR, \fB\-\-uuid \fIUUID\fR -+The WSD specification requires a device to have a unique address that is -+stable across reboots or changes in networks. In the context of the -+standard, it is assumed that this is something like a serial number. wsdd -+uses the UUID version 5 with the DNS namespace and the host name of the -+local machine as inputs. Thus, the host name should be stable and not be -+modified, e.g. by DHCP. However, if you want wsdd to use a specific UUID -+you can use this option. -+.TP -+\fB\-v\fR, \fB\-\-verbose\fR -+Additively increase verbosity of the log output. A single occurrence of -+-v/--verbose sets the log level to INFO. More -v options set the log level -+to DEBUG. -+.TP -+\fB\-V\fR, \fB\-\-version\fR -+Show the version number and exit. -+.SS Host Mode Options -+.TP -+\fB\-d \fIDOMAIN\fR, \fB\-\-domain \fIDOMAIN\fR -+Assume that the host running wsdd joined an ADS domain. This will make -+wsdd report the host being a domain member. It disables workgroup -+membership reporting. The (provided) hostname is automatically converted -+to lower case. Use the `-p` option to change this behavior. -+.TP -+\fB\-n \fIHOSTNAME\fR, \fB\-\-hostname \fIHOSTNAME\fR -+Override the host name wsdd uses during discovery. By default the machine's -+host name is used (look at hostname(1)). Only the host name part of a -+possible FQDN will be used in the default case. -+.TP -+\fB\-o\fR, \fB\-\-no-host\fR -+Disable host operation mode. If this option is provided, the host cannot be -+discovered by other (Windows) hosts. It can be useful when client/discovery -+mode is used and no announcement of the host that runs wsdd should be made. -+.TP -+\fB\-p\fR, \fB\-\-preserve-case\fR -+Preserve the hostname as it is. Without this option, the hostname is -+converted as follows. For workgroup environments (see -w) the hostname -+is made upper case by default. Vice versa it is made lower case for usage -+in domains (see -d). -+.TP -+\fB\-t\fR, \fB\-\-no-http\fR -+Do not service HTTP requests of the WSD protocol. This option is intended -+for debugging purposes where another process may handle the Get messages. -+.TP -+\fB\-w \fIWORKGROUP\fR, \fB\-\-workgroup \fIWORKGROUP\fR -+By default wsdd reports the host is a member of a workgroup rather than a -+domain (use the -d/--domain option to override this). With -w/--workgroup -+the default workgroup name can be changed. The default work group name is -+WORKGROUP. The (provided) hostname is automatically converted to upper -+case. Use the `-p` option to change this behavior. -+.SS Client/Discovery Mode Options -+.TP -+\fB\-D\fR, \fB\-\-discovery\fR -+Enable discovery mode to search for other WSD hosts/servers. Found hosts -+are logged with INFO priority. The server interface (see below) -+can be used for a programatic interface. Refer to the man page for -+details of the server interface. -+.TP -+\fB\-l \fIPATH/PORT\fR, \fB\-\-listen \fIPATH/PORT\fR -+Specify the location of the socket for the server programming interface. -+If the option value is numeric, it is interpreted as numeric port for a -+TCP server port. Then, the server socket is bound to the localhost only. -+If the option value is non-numeric, it is assumed to be a path to UNIX -+domain socket to which a client can connect to. -+ -+.SH EXAMPLE USAGE -+.SS Handle traffic on eth0 and eth2 only, but only with IPv6 addresses -+ -+ wsdd \-i eth0 \-i eth2 \-6 -+ -+ or -+ -+ wsdd \-\-interface eth0 \-\-interface eth2 \-\-ipv6only -+.SS Set the Workgroup according to smb.conf, be verbose, run with dropped privileges, and change the root directory to an (empty) directory -+ -+ SMB_GROUP=$(grep \-i '^\s*workgroup\s*=' smb.conf | cut \-f2 \-d= | tr \-d '[:blank:]') -+ -+ wsdd \-v \-w $SMB_GROUP -u daemon:daemon -c /var/run/wsdd/chroot -+.SH FIREWALL SETUP -+.PP -+Traffic for the following ports, directions and addresses must be allowed: -+.TP -+- Incoming and outgoing traffic to udp/3702 with multicast destination: 239.255.255.250 for IPv4 and ff02::c for IPv6 -+.TP -+- Outgoing unicast traffic from udp/3702 -+.TP -+- Incoming traffic to tcp/5357 -+.PP -+You should further restrict the traffic to the (link-)local subnet, e.g. by -+using the `fe80::/10` address space for IPv6. Please note that IGMP traffic -+must be enabled in order to get IPv4 multicast traffic working. -+.SH API INTERFACE -+Wsdd provides a text-based, line-oriented API interface to query information -+and trigger actions. The interface can be used with TCP and UNIX domain sockets -+(see \fB\-l/\-\-listen\fR option). The TCP socket is bound to the local host -+only. The following commands can be issued: -+.SS \fBclear\fR - Clear list of discovered devices -+Clears the list of all discovered devices. Use the \fBprobe\fR command to -+search for devices again. This command does not return any data and is only -+available in discover mode. -+.SS \fBlist\fR - List discovered devices -+Returns a tab-separated list of discovered devices with the following information. -+The possibly empty list of detected hosts is always terminated with a single -+dot ('.') in an otherwise empty line. This command is only available in discover mode. -+.TP -+UUID -+UUID of the discovered device. Note that a multi-homed device should appear -+only once but with multiple addresses (see below) -+.TP -+name -+The name reported by the device. For discovered Windows hosts, it is the -+configured computer name that is reported here. -+.TP -+association -+Specifies the domain or workgroup to which the discovered host belongs to. The -+type of the association (workgroup or domain) is separated from its value by a -+colon. -+.TP -+last_seen -+The date and time the device was last seen as a consequence of Probe/Hello -+messages provided in ISO8601 with seconds. -+.TP -+addresses -+List of all transport addresses that were collected during the discovery -+process delimited by commas. Addresses are provided along with the interface -+(separated by '%') on which they were discovered. IPv6 addresses are reported -+on square brackets. Note that the reported addresses may not match the actual -+device on which the device may be reached. -+.SS \fBprobe \fI[INTERFACE]\fR - Search for devices -+Triggers a Probe message on the provided INTERFACE (eth0, e.g.) to search for -+WSD hosts. If no interface is provided, all interfaces wsdd listens on are probed. -+A Probe messages initiates the discovery message flow. It may take some time for -+hosts to be actually discovered. This command does not return any data and is -+only available in discovery mode. -+.SS \fBstart\fR - Start networking activities -+This command starts the networking acitivies of wsdd if they haven't been -+started yet. "Hello" messages are emitted and the host is announced on the -+network(s) when the host mode is active. If the discovery mode is enabled a -+probe process is also started. -+ -+.SS \fBstop\fR - Stop networking activities -+This is the reverse operation to start. When this command is received, "Bye" -+messages are sent in order to notify hosts in the network about the host's -+disappearance. All networking sockets used for the WSD protocol are closed as -+well. Activities can be restarted with the start operation. -+ -+.SH SECURITY -+.PP -+wsdd does not implement any security feature, e.g. by using TLS for the http -+service. This is because wsdd's intended usage is within private, i.e. home, -+LANs. The \fIHello\fR message contains the hosts transport address, i.e. the IP -+address which speeds up discovery (avoids \fIResolve\fR message). -+.SH KNOWN ISSUES -+.SS Using only IPv6 on FreeBSD -+If wsdd is running on FreeBSD using IPv6 only, the host running wsdd may not be -+reliably discovered. The reason appears to be that Windows is not always able -+to connect to the HTTP service for unknown reasons. As a workaround, run wsdd -+with IPv4 only. -+.SS Tunnel/Bridge Interface -+.PP -+If tunnel/bridge interfaces like those created by OpenVPN or Docker exist, they -+may interfere with wsdd if executed without providing an interface that it -+should bind to (so it binds to all). In such cases, the wsdd hosts appears after -+wsdd has been started but it disappears when an update of the Network view in -+Windows Explorer is forced, either by refreshing the view or by a reboot of the -+Windows machine. To solve this issue, the interface that is connected to the -+network on which the host should be announced needs to be specified with the -+-i/--interface option. This prevents the usage of the tunnel/bridge -+interfaces. -+.PP -+Background: Tunnel/bridge interfaces may cause \fIResolve\fR requests from Windows -+hosts to be delivered to wsdd multiple times, i.e. duplicates of such request -+are created. If wsdd receives such a request first from a tunnel/bridge it uses -+the transport address (IP address) of that interface and sends the response via -+unicast. Further duplicates are not processed due to the duplicate message -+detection which is based on message UUIDs. The Windows host which receives the -+response appears to detect a mismatch between the transport address in the -+\fIResolveMatch\fR message (which is the tunnel/bridge address) and the IP of the -+sending host/interface (LAN IP, e.g.). Subsequently, the wsdd host is ignored by -+Windows. diff -Nru wsdd-0.7.0/etc/systemd/wsdd.defaults wsdd-0.7.1/etc/systemd/wsdd.defaults --- wsdd-0.7.0/etc/systemd/wsdd.defaults 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/etc/systemd/wsdd.defaults 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,5 @@ +# Additional arguments for wsdd can be provided here. +# Use, e.g., "-i eth0" to restrict operations to a specific interface +# Refer to the wsdd(8) man page for details + +WSDD_PARAMS="" diff -Nru wsdd-0.7.0/etc/systemd/wsdd.service wsdd-0.7.1/etc/systemd/wsdd.service --- wsdd-0.7.0/etc/systemd/wsdd.service 2021-11-20 20:11:29.000000000 +0000 +++ wsdd-0.7.1/etc/systemd/wsdd.service 2023-03-04 11:21:05.000000000 +0000 @@ -1,16 +1,21 @@ [Unit] Description=Web Services Dynamic Discovery host daemon +Documentation=man:wsdd(8) ; Start after the network has been configured After=network-online.target Wants=network-online.target -; It makes sense to have Samba running when wsdd starts, but is not required -;Wants=smb.service +; It makes sense to have Samba running when wsdd starts, but is not required. +; Thus, the next to lines are disabled and use BindsTo only. +; One may also add any of these services to After for stronger binding. +;BindsTo=smb.service +;BindsTo=samba.service [Service] Type=simple +EnvironmentFile=/etc/default/wsdd ; The service is put into an empty runtime directory chroot, ; i.e. the runtime directory which usually resides under /run -ExecStart=/usr/bin/wsdd --shortlog --chroot=/run/wsdd +ExecStart=/usr/bin/wsdd --shortlog --chroot=/run/wsdd $WSDD_PARAMS DynamicUser=yes User=wsdd Group=wsdd diff -Nru wsdd-0.7.0/etc/ufw/applications.d/wsdd wsdd-0.7.1/etc/ufw/applications.d/wsdd --- wsdd-0.7.0/etc/ufw/applications.d/wsdd 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/etc/ufw/applications.d/wsdd 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,4 @@ +[Wsdd] +title=Wsdd +description=Web Service Discovery host daemon implementation +ports=3702/udp|5357/tcp diff -Nru wsdd-0.7.0/.github/workflows/codeql-analysis.yml wsdd-0.7.1/.github/workflows/codeql-analysis.yml --- wsdd-0.7.0/.github/workflows/codeql-analysis.yml 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/.github/workflows/codeql-analysis.yml 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,39 @@ +name: "CodeQL" + +on: + push: + branches: ["master", "feat/workflows"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master", "feat/workflows"] + schedule: + - cron: '0 0 * * */10' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: ['python'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff -Nru wsdd-0.7.0/.github/workflows/static-analyses.yml wsdd-0.7.1/.github/workflows/static-analyses.yml --- wsdd-0.7.0/.github/workflows/static-analyses.yml 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/.github/workflows/static-analyses.yml 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,38 @@ +name: "PEP8 and mypy" + +on: + push: + branches: ["master", "feat/workflows"] + pull_request: + branches: ["master", "feat/workflows"] + schedule: + - cron: '0 0 * * */10' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + + - name: Python 3.7 syntax check + run: python -m py_compile src/wsdd.py + + - name: Lint with flake8 + run: | + pip install flake8 + flake8 --count --show-source --statistics src + + - name: mypy type check + run: | + pip install mypy==0.910 + mypy --python-version=3.7 src/wsdd.py + mypy --python-version=3.8 src/wsdd.py + mypy --python-version=3.9 src/wsdd.py + mypy --python-version=3.10 src/wsdd.py + mypy --python-version=3.11 src/wsdd.py diff -Nru wsdd-0.7.0/man/wsdd.1 wsdd-0.7.1/man/wsdd.1 --- wsdd-0.7.0/man/wsdd.1 2021-11-20 20:11:29.000000000 +0000 +++ wsdd-0.7.1/man/wsdd.1 1970-01-01 00:00:00.000000000 +0000 @@ -1,244 +0,0 @@ -.TH wsdd 1 -.SH NAME -wsdd \- A Web Service Discovery host and client daemon. -.SH SYNOPSIS -.B wsdd [\fBoptions\fR] -.SH DESCRIPTION -.PP -.B wsdd -implements both a Web Service Discovery (WSD) host and a WSD client daemon. The -host implementation enables (Samba) hosts, like your local NAS device, to be -found by Web Service Discovery clients like Windows. The client mode allows -searching for other WSD hosts on the local network. -.PP -The default mode of operation is the host mode. The client or discovery mode -must be enabled explictely. Unless configured otherwise, the host mode is always -active. Both modes can be used at the same time. -.SH OPTIONS -.SS General options -.TP -\fB\-4\fR, \fB\-\-ipv4only\fR -See below. -.TP -\fB\-6\fR, \fB\-\-ipv6only\fR -Restrict to the given address family. If both options are specified no -addreses will be available and wsdd will exit. -.TP -\fB\-A\fR, \fB\-\-no-autostart\fR -Do not start networking activities automatically when the program is started. -The API interface (see below) can be used to start and stop the networking -activities while the application is running. -.TP -\fB\-c \fIDIRECTORY\fR, \fB\-\-chroot \fIDIRECTORY\fR -chroot into the given \fIDIRECTORY\fR after initialization has been performed -and right before the handling of incoming messages starts. The new root directory -can be empty. Consider using the \fB-u\fR option as well. -.TP -\fB\-h\fR, \fB\-\-help\fR -Show help and exit. -.TP -\fB\-H \fIHOPLIMIT\fR, \fB\-\-hoplimit \fIHOPLIMIT\fR -Set the hop limit for multicast packets. The default is 1 which should -prevent packets from leaving the local network segment. -.TP -\fB\-i \fIINTERFACE/ADDRESS\fR, \fB\-\-interface \fIINTERFACE/ADDRESS\fR -Specify on which interfaces wsdd will be listening on. If no interfaces are -specified, all interfaces are used. Loop-back interfaces are never used, -even when explicitly specified. For interfaces with IPv6 addresses, -only link-local addresses will be used for announcing the host on the -network. This option can be provided multiple times in order to restrict -traffic to more than one interface. -This option also accepts IP addresses that the service should bind to. -For IPv6, only link local addresses are actually considered as noted above. -.TP -\fB\-s\fR, \fB\-\-shortlog\fR -Use a shorter logging format that only includes the level and message. -This is useful in cases where the logging mechanism, like systemd on Linux, -automatically prepends a date and process name plus ID to the log message. -.TP -\fB\-u \fIUSER[:GROUP]\fR, \fB\-\-user \fIUSER[:GROUP]\fR -Change user (and group) when running before handling network packets. -Together with \fB\-c\fR this option can be used to increase security -if the execution environment, like the init system, cannot ensure this in -another way. -.TP -\fB\-U \fIUUID\fR, \fB\-\-uuid \fIUUID\fR -The WSD specification requires a device to have a unique address that is -stable across reboots or changes in networks. In the context of the -standard, it is assumed that this is something like a serial number. wsdd -uses the UUID version 5 with the DNS namespace and the host name of the -local machine as inputs. Thus, the host name should be stable and not be -modified, e.g. by DHCP. However, if you want wsdd to use a specific UUID -you can use this option. -.TP -\fB\-v\fR, \fB\-\-verbose\fR -Additively increase verbosity of the log output. A single occurrence of --v/--verbose sets the log level to INFO. More -v options set the log level -to DEBUG. -.TP -\fB\-V\fR, \fB\-\-version\fR -Show the version number and exit. -.SS Host Mode Options -.TP -\fB\-d \fIDOMAIN\fR, \fB\-\-domain \fIDOMAIN\fR -Assume that the host running wsdd joined an ADS domain. This will make -wsdd report the host being a domain member. It disables workgroup -membership reporting. The (provided) hostname is automatically converted -to lower case. Use the `-p` option to change this behavior. -.TP -\fB\-n \fIHOSTNAME\fR, \fB\-\-hostname \fIHOSTNAME\fR -Override the host name wsdd uses during discovery. By default the machine's -host name is used (look at hostname(1)). Only the host name part of a -possible FQDN will be used in the default case. -.TP -\fB\-o\fR, \fB\-\-no-host\fR -Disable host operation mode. If this option is provided, the host cannot be -discovered by other (Windows) hosts. It can be useful when client/discovery -mode is used and no announcement of the host that runs wsdd should be made. -.TP -\fB\-p\fR, \fB\-\-preserve-case\fR -Preserve the hostname as it is. Without this option, the hostname is -converted as follows. For workgroup environments (see -w) the hostname -is made upper case by default. Vice versa it is made lower case for usage -in domains (see -d). -.TP -\fB\-t\fR, \fB\-\-no-http\fR -Do not service HTTP requests of the WSD protocol. This option is intended -for debugging purposes where another process may handle the Get messages. -.TP -\fB\-w \fIWORKGROUP\fR, \fB\-\-workgroup \fIWORKGROUP\fR -By default wsdd reports the host is a member of a workgroup rather than a -domain (use the -d/--domain option to override this). With -w/--workgroup -the default workgroup name can be changed. The default work group name is -WORKGROUP. The (provided) hostname is automatically converted to upper -case. Use the `-p` option to change this behavior. -.SS Client/Discovery Mode Options -.TP -\fB\-D\fR, \fB\-\-discovery\fR -Enable discovery mode to search for other WSD hosts/servers. Found hosts -are logged with INFO priority. The server interface (see below) -can be used for a programatic interface. Refer to the man page for -details of the server interface. -.TP -\fB\-l \fIPATH/PORT\fR, \fB\-\-listen \fIPATH/PORT\fR -Specify the location of the socket for the server programming interface. -If the option value is numeric, it is interpreted as numeric port for a -TCP server port. Then, the server socket is bound to the localhost only. -If the option value is non-numeric, it is assumed to be a path to UNIX -domain socket to which a client can connect to. - -.SH EXAMPLE USAGE -.SS Handle traffic on eth0 and eth2 only, but only with IPv6 addresses - - wsdd \-i eth0 \-i eth2 \-6 - - or - - wsdd \-\-interface eth0 \-\-interface eth2 \-\-ipv6only -.SS Set the Workgroup according to smb.conf, be verbose, run with dropped privileges, and change the root directory to an (empty) directory - - SMB_GROUP=$(grep \-i '^\s*workgroup\s*=' smb.conf | cut \-f2 \-d= | tr \-d '[:blank:]') - - wsdd \-v \-w $SMB_GROUP -u daemon:daemon -c /var/run/wsdd/chroot -.SH FIREWALL SETUP -.PP -Traffic for the following ports, directions and addresses must be allowed: -.TP -- Incoming and outgoing traffic to udp/3702 with multicast destination: 239.255.255.250 for IPv4 and ff02::c for IPv6 -.TP -- Outgoing unicast traffic from udp/3702 -.TP -- Incoming traffic to tcp/5357 -.PP -You should further restrict the traffic to the (link-)local subnet, e.g. by -using the `fe80::/10` address space for IPv6. Please note that IGMP traffic -must be enabled in order to get IPv4 multicast traffic working. -.SH API INTERFACE -Wsdd provides a text-based, line-oriented API interface to query information -and trigger actions. The interface can be used with TCP and UNIX domain sockets -(see \fB\-l/\-\-listen\fR option). The TCP socket is bound to the local host -only. The following commands can be issued: -.SS \fBclear\fR - Clear list of discovered devices -Clears the list of all discovered devices. Use the \fBprobe\fR command to -search for devices again. This command does not return any data and is only -available in discover mode. -.SS \fBlist\fR - List discovered devices -Returns a tab-separated list of discovered devices with the following information. -The possibly empty list of detected hosts is always terminated with a single -dot ('.') in an otherwise empty line. This command is only available in discover mode. -.TP -UUID -UUID of the discovered device. Note that a multi-homed device should appear -only once but with multiple addresses (see below) -.TP -name -The name reported by the device. For discovered Windows hosts, it is the -configured computer name that is reported here. -.TP -association -Specifies the domain or workgroup to which the discovered host belongs to. The -type of the association (workgroup or domain) is separated from its value by a -colon. -.TP -last_seen -The date and time the device was last seen as a consequence of Probe/Hello -messages provided in ISO8601 with seconds. -.TP -addresses -List of all transport addresses that were collected during the discovery -process delimited by commas. Addresses are provided along with the interface -(separated by '%') on which they were discovered. IPv6 addresses are reported -on square brackets. Note that the reported addresses may not match the actual -device on which the device may be reached. -.SS \fBprobe \fI[INTERFACE]\fR - Search for devices -Triggers a Probe message on the provided INTERFACE (eth0, e.g.) to search for -WSD hosts. If no interface is provided, all interfaces wsdd listens on are probed. -A Probe messages initiates the discovery message flow. It may take some time for -hosts to be actually discovered. This command does not return any data and is -only available in discovery mode. -.SS \fBstart\fR - Start networking activities -This command starts the networking acitivies of wsdd if they haven't been -started yet. "Hello" messages are emitted and the host is announced on the -network(s) when the host mode is active. If the discovery mode is enabled a -probe process is also started. - -.SS \fBstop\fR - Stop networking activities -This is the reverse operation to start. When this command is received, "Bye" -messages are sent in order to notify hosts in the network about the host's -disappearance. All networking sockets used for the WSD protocol are closed as -well. Activities can be restarted with the start operation. - -.SH SECURITY -.PP -wsdd does not implement any security feature, e.g. by using TLS for the http -service. This is because wsdd's intended usage is within private, i.e. home, -LANs. The \fIHello\fR message contains the hosts transport address, i.e. the IP -address which speeds up discovery (avoids \fIResolve\fR message). -.SH KNOWN ISSUES -.SS Using only IPv6 on FreeBSD -If wsdd is running on FreeBSD using IPv6 only, the host running wsdd may not be -reliably discovered. The reason appears to be that Windows is not always able -to connect to the HTTP service for unknown reasons. As a workaround, run wsdd -with IPv4 only. -.SS Tunnel/Bridge Interface -.PP -If tunnel/bridge interfaces like those created by OpenVPN or Docker exist, they -may interfere with wsdd if executed without providing an interface that it -should bind to (so it binds to all). In such cases, the wsdd hosts appears after -wsdd has been started but it disappears when an update of the Network view in -Windows Explorer is forced, either by refreshing the view or by a reboot of the -Windows machine. To solve this issue, the interface that is connected to the -network on which the host should be announced needs to be specified with the --i/--interface option. This prevents the usage of the tunnel/bridge -interfaces. -.PP -Background: Tunnel/bridge interfaces may cause \fIResolve\fR requests from Windows -hosts to be delivered to wsdd multiple times, i.e. duplicates of such request -are created. If wsdd receives such a request first from a tunnel/bridge it uses -the transport address (IP address) of that interface and sends the response via -unicast. Further duplicates are not processed due to the duplicate message -detection which is based on message UUIDs. The Windows host which receives the -response appears to detect a mismatch between the transport address in the -\fIResolveMatch\fR message (which is the tunnel/bridge address) and the IP of the -sending host/interface (LAN IP, e.g.). Subsequently, the wsdd host is ignored by -Windows. diff -Nru wsdd-0.7.0/man/wsdd.8 wsdd-0.7.1/man/wsdd.8 --- wsdd-0.7.0/man/wsdd.8 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/man/wsdd.8 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,244 @@ +.TH wsdd 8 +.SH NAME +wsdd \- A Web Service Discovery host and client daemon. +.SH SYNOPSIS +.B wsdd [\fBoptions\fR] +.SH DESCRIPTION +.PP +.B wsdd +implements both a Web Service Discovery (WSD) host and a WSD client daemon. The +host implementation enables (Samba) hosts, like your local NAS device, to be +found by Web Service Discovery clients like Windows. The client mode allows +searching for other WSD hosts on the local network. +.PP +The default mode of operation is the host mode. The client or discovery mode +must be enabled explictely. Unless configured otherwise, the host mode is always +active. Both modes can be used at the same time. +.SH OPTIONS +.SS General options +.TP +\fB\-4\fR, \fB\-\-ipv4only\fR +See below. +.TP +\fB\-6\fR, \fB\-\-ipv6only\fR +Restrict to the given address family. If both options are specified no +addreses will be available and wsdd will exit. +.TP +\fB\-A\fR, \fB\-\-no-autostart\fR +Do not start networking activities automatically when the program is started. +The API interface (see below) can be used to start and stop the networking +activities while the application is running. +.TP +\fB\-c \fIDIRECTORY\fR, \fB\-\-chroot \fIDIRECTORY\fR +chroot into the given \fIDIRECTORY\fR after initialization has been performed +and right before the handling of incoming messages starts. The new root directory +can be empty. Consider using the \fB-u\fR option as well. +.TP +\fB\-h\fR, \fB\-\-help\fR +Show help and exit. +.TP +\fB\-H \fIHOPLIMIT\fR, \fB\-\-hoplimit \fIHOPLIMIT\fR +Set the hop limit for multicast packets. The default is 1 which should +prevent packets from leaving the local network segment. +.TP +\fB\-i \fIINTERFACE/ADDRESS\fR, \fB\-\-interface \fIINTERFACE/ADDRESS\fR +Specify on which interfaces wsdd will be listening on. If no interfaces are +specified, all interfaces are used. Loop-back interfaces are never used, +even when explicitly specified. For interfaces with IPv6 addresses, +only link-local addresses will be used for announcing the host on the +network. This option can be provided multiple times in order to restrict +traffic to more than one interface. +This option also accepts IP addresses that the service should bind to. +For IPv6, only link local addresses are actually considered as noted above. +.TP +\fB\-s\fR, \fB\-\-shortlog\fR +Use a shorter logging format that only includes the level and message. +This is useful in cases where the logging mechanism, like systemd on Linux, +automatically prepends a date and process name plus ID to the log message. +.TP +\fB\-u \fIUSER[:GROUP]\fR, \fB\-\-user \fIUSER[:GROUP]\fR +Change user (and group) when running before handling network packets. +Together with \fB\-c\fR this option can be used to increase security +if the execution environment, like the init system, cannot ensure this in +another way. +.TP +\fB\-U \fIUUID\fR, \fB\-\-uuid \fIUUID\fR +The WSD specification requires a device to have a unique address that is +stable across reboots or changes in networks. In the context of the +standard, it is assumed that this is something like a serial number. wsdd +uses the UUID version 5 with the DNS namespace and the host name of the +local machine as inputs. Thus, the host name should be stable and not be +modified, e.g. by DHCP. However, if you want wsdd to use a specific UUID +you can use this option. +.TP +\fB\-v\fR, \fB\-\-verbose\fR +Additively increase verbosity of the log output. A single occurrence of +-v/--verbose sets the log level to INFO. More -v options set the log level +to DEBUG. +.TP +\fB\-V\fR, \fB\-\-version\fR +Show the version number and exit. +.SS Host Mode Options +.TP +\fB\-d \fIDOMAIN\fR, \fB\-\-domain \fIDOMAIN\fR +Assume that the host running wsdd joined an ADS domain. This will make +wsdd report the host being a domain member. It disables workgroup +membership reporting. The (provided) hostname is automatically converted +to lower case. Use the `-p` option to change this behavior. +.TP +\fB\-n \fIHOSTNAME\fR, \fB\-\-hostname \fIHOSTNAME\fR +Override the host name wsdd uses during discovery. By default the machine's +host name is used (look at hostname(1)). Only the host name part of a +possible FQDN will be used in the default case. +.TP +\fB\-o\fR, \fB\-\-no-host\fR +Disable host operation mode. If this option is provided, the host cannot be +discovered by other (Windows) hosts. It can be useful when client/discovery +mode is used and no announcement of the host that runs wsdd should be made. +.TP +\fB\-p\fR, \fB\-\-preserve-case\fR +Preserve the hostname as it is. Without this option, the hostname is +converted as follows. For workgroup environments (see -w) the hostname +is made upper case by default. Vice versa it is made lower case for usage +in domains (see -d). +.TP +\fB\-t\fR, \fB\-\-no-http\fR +Do not service HTTP requests of the WSD protocol. This option is intended +for debugging purposes where another process may handle the Get messages. +.TP +\fB\-w \fIWORKGROUP\fR, \fB\-\-workgroup \fIWORKGROUP\fR +By default wsdd reports the host is a member of a workgroup rather than a +domain (use the -d/--domain option to override this). With -w/--workgroup +the default workgroup name can be changed. The default work group name is +WORKGROUP. The (provided) hostname is automatically converted to upper +case. Use the `-p` option to change this behavior. +.SS Client/Discovery Mode Options +.TP +\fB\-D\fR, \fB\-\-discovery\fR +Enable discovery mode to search for other WSD hosts/servers. Found hosts +are logged with INFO priority. The server interface (see below) +can be used for a programatic interface. Refer to the man page for +details of the server interface. +.TP +\fB\-l \fIPATH/PORT\fR, \fB\-\-listen \fIPATH/PORT\fR +Specify the location of the socket for the server programming interface. +If the option value is numeric, it is interpreted as numeric port for a +TCP server port. Then, the server socket is bound to the localhost only. +If the option value is non-numeric, it is assumed to be a path to UNIX +domain socket to which a client can connect to. + +.SH EXAMPLE USAGE +.SS Handle traffic on eth0 and eth2 only, but only with IPv6 addresses + + wsdd \-i eth0 \-i eth2 \-6 + + or + + wsdd \-\-interface eth0 \-\-interface eth2 \-\-ipv6only +.SS Set the Workgroup according to smb.conf, be verbose, run with dropped privileges, and change the root directory to an (empty) directory + + SMB_GROUP=$(grep \-i '^\s*workgroup\s*=' smb.conf | cut \-f2 \-d= | tr \-d '[:blank:]') + + wsdd \-v \-w $SMB_GROUP -u daemon:daemon -c /var/run/wsdd/chroot +.SH FIREWALL SETUP +.PP +Traffic for the following ports, directions and addresses must be allowed: +.TP +- Incoming and outgoing traffic to udp/3702 with multicast destination: 239.255.255.250 for IPv4 and ff02::c for IPv6 +.TP +- Outgoing unicast traffic from udp/3702 +.TP +- Incoming traffic to tcp/5357 +.PP +You should further restrict the traffic to the (link-)local subnet, e.g. by +using the `fe80::/10` address space for IPv6. Please note that IGMP traffic +must be enabled in order to get IPv4 multicast traffic working. +.SH API INTERFACE +Wsdd provides a text-based, line-oriented API interface to query information +and trigger actions. The interface can be used with TCP and UNIX domain sockets +(see \fB\-l/\-\-listen\fR option). The TCP socket is bound to the local host +only. The following commands can be issued: +.SS \fBclear\fR - Clear list of discovered devices +Clears the list of all discovered devices. Use the \fBprobe\fR command to +search for devices again. This command does not return any data and is only +available in discover mode. +.SS \fBlist\fR - List discovered devices +Returns a tab-separated list of discovered devices with the following information. +The possibly empty list of detected hosts is always terminated with a single +dot ('.') in an otherwise empty line. This command is only available in discover mode. +.TP +UUID +UUID of the discovered device. Note that a multi-homed device should appear +only once but with multiple addresses (see below) +.TP +name +The name reported by the device. For discovered Windows hosts, it is the +configured computer name that is reported here. +.TP +association +Specifies the domain or workgroup to which the discovered host belongs to. The +type of the association (workgroup or domain) is separated from its value by a +colon. +.TP +last_seen +The date and time the device was last seen as a consequence of Probe/Hello +messages provided in ISO8601 with seconds. +.TP +addresses +List of all transport addresses that were collected during the discovery +process delimited by commas. Addresses are provided along with the interface +(separated by '%') on which they were discovered. IPv6 addresses are reported +on square brackets. Note that the reported addresses may not match the actual +device on which the device may be reached. +.SS \fBprobe \fI[INTERFACE]\fR - Search for devices +Triggers a Probe message on the provided INTERFACE (eth0, e.g.) to search for +WSD hosts. If no interface is provided, all interfaces wsdd listens on are probed. +A Probe messages initiates the discovery message flow. It may take some time for +hosts to be actually discovered. This command does not return any data and is +only available in discovery mode. +.SS \fBstart\fR - Start networking activities +This command starts the networking acitivies of wsdd if they haven't been +started yet. "Hello" messages are emitted and the host is announced on the +network(s) when the host mode is active. If the discovery mode is enabled a +probe process is also started. + +.SS \fBstop\fR - Stop networking activities +This is the reverse operation to start. When this command is received, "Bye" +messages are sent in order to notify hosts in the network about the host's +disappearance. All networking sockets used for the WSD protocol are closed as +well. Activities can be restarted with the start operation. + +.SH SECURITY +.PP +wsdd does not implement any security feature, e.g. by using TLS for the http +service. This is because wsdd's intended usage is within private, i.e. home, +LANs. The \fIHello\fR message contains the hosts transport address, i.e. the IP +address which speeds up discovery (avoids \fIResolve\fR message). +.SH KNOWN ISSUES +.SS Using only IPv6 on FreeBSD +If wsdd is running on FreeBSD using IPv6 only, the host running wsdd may not be +reliably discovered. The reason appears to be that Windows is not always able +to connect to the HTTP service for unknown reasons. As a workaround, run wsdd +with IPv4 only. +.SS Tunnel/Bridge Interface +.PP +If tunnel/bridge interfaces like those created by OpenVPN or Docker exist, they +may interfere with wsdd if executed without providing an interface that it +should bind to (so it binds to all). In such cases, the wsdd hosts appears after +wsdd has been started but it disappears when an update of the Network view in +Windows Explorer is forced, either by refreshing the view or by a reboot of the +Windows machine. To solve this issue, the interface that is connected to the +network on which the host should be announced needs to be specified with the +-i/--interface option. This prevents the usage of the tunnel/bridge +interfaces. +.PP +Background: Tunnel/bridge interfaces may cause \fIResolve\fR requests from Windows +hosts to be delivered to wsdd multiple times, i.e. duplicates of such request +are created. If wsdd receives such a request first from a tunnel/bridge it uses +the transport address (IP address) of that interface and sends the response via +unicast. Further duplicates are not processed due to the duplicate message +detection which is based on message UUIDs. The Windows host which receives the +response appears to detect a mismatch between the transport address in the +\fIResolveMatch\fR message (which is the tunnel/bridge address) and the IP of the +sending host/interface (LAN IP, e.g.). Subsequently, the wsdd host is ignored by +Windows. diff -Nru wsdd-0.7.0/README.md wsdd-0.7.1/README.md --- wsdd-0.7.0/README.md 2021-11-20 20:11:29.000000000 +0000 +++ wsdd-0.7.1/README.md 2023-03-04 11:21:05.000000000 +0000 @@ -37,7 +37,7 @@ # Requirements -wsdd requires Python 3.7 and later only. It runs on Linux and FreeBSD. Other Unixes, such +wsdd requires Python 3.7 and later only. It runs on Linux, FreeBSD and MacOS. Other Unixes, such as OpenBSD or NetBSD, might work as well but were not tested. Although Samba is not strictly required by wsdd itself, it makes sense to run @@ -46,7 +46,10 @@ # Installation -## Operating System and Distribution-Dependant Instructions +## Operating System and Distribution-Depending Instructions + +This section provides instructions how to install wsdd on different OS distributions. +Sufficient privileges are assumed to be in effect, e.g. by being root or using sudo. ### Arch Linux @@ -54,34 +57,80 @@ ### CentOS, Fedora, RHEL -wsdd is included in RedHat/CentOS' EPEL repository. After setting that up, you +wsdd is included in RedHat/CentOS' EPEL repository. After [setting that up](https://docs.fedoraproject.org/en-US/epel/), you can install wsdd like on Fedora where it is sufficient to issue -`dnf install wsdd` +``` +dnf install wsdd +``` + +### Debian-based Distributions + +#### Debian + +There is currently no package for wsdd in the official Debian repositories. +However, wsdd is considered to be part of the next Debian release, *Bookworm*, which in the testing phase. +A wsdd package is also available in *unstable*. + +To install wsdd under Bullseye and earlier see the "Others" section below. + +#### Ubuntu/Mint -### Debian/Ubuntu +Starting from Ubuntu 22.04 LTS (*Jammy Jellyfish*), wsdd has landed in the official *universe* package repository. +Thus, it is sufficient to install it via -There are user-maintained packages for which you need to add the repository to -`/etc/apt/sources.list.d` with a file containing the following line +``` +apt install wsdd +``` + +This also applies to Linux Mint, starting from version 21 (Vanessa). +For older Ubuntu LTS or Linux Mint releases, see the "Others" section below. + +#### Others -`deb https://pkg.ltec.ch/public/ distro main` +There are user-maintained packages for which you need to add the repository to the apt repo list and download the according GPG public key: + +``` +wget -O- https://pkg.ltec.ch/public/conf/ltec-ag.gpg.key | gpg --dearmour > /usr/share/keyrings/wsdd.gpg +source /etc/os-release +echo "deb [signed-by=/usr/share/keyrings/wsdd.gpg] https://pkg.ltec.ch/public/ ${UBUNTU_CODENAME:-${VERSION_CODENAME:-UNKNOWN}} main" > /etc/apt/sources.list.d/wsdd.list + +``` -Replace `distro` with the name of your distro, e.g. `buster` or `xenial` (issue -`lsb_release -cs` if unsure). After an `apt update` you can install wsdd with -`apt install wsdd`. +Note that the repository only provides packages for Debian and Ubuntu LTS releases up to *Buster* and *Focal Fossa* (20.04), respectively. +The `wsdd.list` file created by the command above should be checked to refer to an appropriate distro code name. -You also need to import the public key of the repository like this `apt-key adv --fetch-keys https://pkg.ltec.ch/public/conf/ltec-ag.gpg.key`. +After the GPG public key file and repository have been created, install wsdd via: + +``` +apt update +apt install wsdd +``` + +### FreeBSD + +The wsdd port can be installed via + +``` +pkg install py39-wsdd +``` ### Gentoo You can choose between two overlays: the GURU project and an [author-maintained -dedicated overlay](https://github.com/christgau/wsdd-gentoo). After setting up -one of them you can install wsdd with +dedicated overlay](https://github.com/christgau/wsdd-gentoo) which can be selected as follows + ``` emerge eselect-repository eselect repository enable guru emerge --sync +``` + +After setting up one of them you can install wsdd with + + +``` emerge wsdd ``` @@ -91,8 +140,8 @@ want to, rename it to wsdd, and run it from there. The init scripts/unit files assume that wsdd is installed under `/usr/bin/wsdd` or `/usr/local/bin/wsdd` in case of FreeBSD. There are no configuration files. No special privileges are -required to run wsdd, so it is advisable to run the service as an unprivileged -user such as _nobody_. +required to run wsdd, so it is advisable to run the service as an unprivileged, +possibly dedicated, user for the service. The `etc` directory of the repo contains sample configuration files for different init(1) systems, namely FreeBSD's rc.d, Gentoo's openrc, and systemd @@ -116,13 +165,18 @@ using the `fe80::/10` address space for IPv6. Please note that IGMP traffic must be enabled in order to get IPv4 multicast traffic working. +For UFW, an application profile can be found under `etc/ufw/applications.d`. +Note that UFW profile only allow to grant the traffic on specific UDP and TCP +ports, but a restriction on the IP range (like link local for IPv6) or the +multicast traffic is not possible. + ## Options By default wsdd runs in host mode and binds to all interfaces with only warnings and error messages enabled. In this configuration the host running wsdd is discovered with its configured hostname and belong to a default workgroup. The discovery mode, which allows to search for other WSD-compatible -devices must be enabled explicitely. Both modes can be used simultanously. See +devices must be enabled explicitly. Both modes can be used simultaneously. See below for details. ### General options diff -Nru wsdd-0.7.0/setup.cfg wsdd-0.7.1/setup.cfg --- wsdd-0.7.0/setup.cfg 2021-11-20 20:11:29.000000000 +0000 +++ wsdd-0.7.1/setup.cfg 2023-03-04 11:21:05.000000000 +0000 @@ -1,3 +1,5 @@ [flake8] max-line-length=120 +# F401 - unused import (due to optional import of encoding package) +# W503 - binary operators on newline (not end of line) ignore=F401,W503 diff -Nru wsdd-0.7.0/src/wsdd.py wsdd-0.7.1/src/wsdd.py --- wsdd-0.7.0/src/wsdd.py 2021-11-20 20:11:29.000000000 +0000 +++ wsdd-0.7.1/src/wsdd.py 2023-03-04 11:21:05.000000000 +0000 @@ -34,14 +34,137 @@ import grp import datetime +from typing import Any, Callable, ClassVar, Deque, Dict, List, Optional, Set, Union, Tuple, TYPE_CHECKING + # try to load more secure XML module first, fallback to default if not present try: - from defusedxml.ElementTree import fromstring as ETfromString + if not TYPE_CHECKING: + from defusedxml.ElementTree import fromstring as ETfromString except ModuleNotFoundError: from xml.etree.ElementTree import fromstring as ETfromString -WSDD_VERSION = '0.7.0' +WSDD_VERSION: str = '0.7.1' + + +args: argparse.Namespace +logger: logging.Logger + + +class NetworkInterface: + + _name: str + _index: int + _scope: int + + def __init__(self, name: str, scope: int, index: int) -> None: + self._name = name + self._scope = scope + if index is not None: + self._index = index + else: + self._index = socket.if_nametoindex(self._name) + + @property + def name(self) -> str: + return self._name + + @property + def scope(self) -> int: + return self._scope + + @property + def index(self) -> int: + return self._index + + def __str__(self) -> str: + return self._name + + def __eq__(self, other) -> bool: + return self._name == other.name + + +class NetworkAddress: + + _family: int + _raw_address: bytes + _address_str: str + _interface: NetworkInterface + + def __init__(self, family: int, raw: Union[bytes, str], interface: NetworkInterface) -> None: + self._family = family + self._raw_address = raw if isinstance(raw, bytes) else socket.inet_pton(family, raw) + self._interface = interface + + self._address_str = socket.inet_ntop(self._family, self._raw_address) + + @property + def address_str(self): + return self._address_str + + @property + def family(self): + return self._family + + @property + def interface(self): + return self._interface + + @property + def is_multicastable(self): + # Nah, this check is not optimal but there are no local flags for + # addresses, but it should be safe for IPv4 anyways + # (https://tools.ietf.org/html/rfc5735#page-3) + return ((self._family == socket.AF_INET) and (self._raw_address[0] == 127) + or (self._family == socket.AF_INET6) and (self._raw_address[0:2] != b'\xfe\x80')) + + @property + def raw(self): + return self._raw_address + + @property + def transport_str(self): + """the string representation of the local address overridden in network setup (for IPv6)""" + return self._address_str if self._family == socket.AF_INET else '[{}]'.format(self._address_str) + + def __str__(self) -> str: + return '{}%{}'.format(self._address_str, self._interface.name) + + def __eq__(self, other) -> bool: + return (self._family == other.family and self.raw == other.raw and self.interface == other.interface) + + +class UdpAddress(NetworkAddress): + + _transport_address: Tuple + _port: int + + def __init__(self, family, transport_address: Tuple, interface: NetworkInterface) -> None: + + if not (family == socket.AF_INET or family == socket.AF_INET6): + raise RuntimeError('Unsupport address address family: {}.'.format(family)) + + self._transport_address = transport_address + self._port = transport_address[1] + + super().__init__(family, transport_address[0], interface) + + @property + def transport_address(self): + return self._transport_address + + @property + def port(self): + return self._port + + def __eq__(self, other) -> bool: + return self.transport_address == other.transport_address + + +class INetworkPacketHandler: + + def handle_packet(self, msg: str, udp_src_address: UdpAddress) -> None: + pass class MulticastHandler: @@ -49,52 +172,53 @@ A class for handling multicast traffic on a given interface for a given address family. It provides multicast sender and receiver sockets """ - # TODO: this one needs some cleanup - def __init__(self, family, address, interface, aio_loop): - # network address, family, and interface name + + # base interface addressing information + address: NetworkAddress + + # individual interface-bound sockets for: + # - receiving multicast traffic + # - sending multicast from a socket bound to WSD port + # - sending unicast messages from a random port + recv_socket: socket.socket + mc_send_socket: socket.socket + uc_send_socket: socket.socket + + # addresses used for communication and data + multicast_address: UdpAddress + listen_address: Tuple + aio_loop: asyncio.AbstractEventLoop + + # dictionary that holds INetworkPacketHandlers instances for sockets created above + message_handlers: Dict[socket.socket, List[INetworkPacketHandler]] + + def __init__(self, address: NetworkAddress, aio_loop: asyncio.AbstractEventLoop) -> None: self.address = address - self.family = family - self.interface = interface - # create individual interface-bound sockets for: - # - receiving multicast traffic - # - sending multicast from a socket bound to WSD port - # - sending unicast messages from a random port - self.recv_socket = socket.socket(self.family, socket.SOCK_DGRAM) + self.recv_socket = socket.socket(self.address.family, socket.SOCK_DGRAM) self.recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.mc_send_socket = socket.socket(self.family, socket.SOCK_DGRAM) - self.uc_send_socket = socket.socket(self.family, socket.SOCK_DGRAM) + self.mc_send_socket = socket.socket(self.address.family, socket.SOCK_DGRAM) + self.uc_send_socket = socket.socket(self.address.family, socket.SOCK_DGRAM) self.uc_send_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # the string representation of the local address - # overridden in network setup (for IPv6) - self.transport_address = address - - # family-specific multicast address and local HTTP server (tuples) - self.multicast_address = None - self.listen_address = None - - # dictionary that holds objects with a handle_request method for the - # sockets created above self.message_handlers = {} self.aio_loop = aio_loop - if family == socket.AF_INET: + if self.address.family == socket.AF_INET: self.init_v4() - elif family == socket.AF_INET6: + elif self.address.family == socket.AF_INET6: self.init_v6() - logger.info('joined multicast group {0} on {2}%{1}'.format(self.multicast_address, self.interface.name, - self.address)) - logger.debug('transport address on {0} is {1}'.format(self.interface.name, self.transport_address)) + logger.info('joined multicast group {0} on {1}'.format(self.multicast_address.transport_str, self.address)) + logger.debug('transport address on {0} is {1}'.format(self.address.interface.name, self.address.transport_str)) logger.debug('will listen for HTTP traffic on address {0}'.format(self.listen_address)) # register calbacks for incoming data (also for mc) - self.aio_loop.add_reader(self.recv_socket.fileno(), self.handle_request, self.recv_socket) - self.aio_loop.add_reader(self.mc_send_socket.fileno(), self.handle_request, self.mc_send_socket) - self.aio_loop.add_reader(self.uc_send_socket.fileno(), self.handle_request, self.uc_send_socket) + self.aio_loop.add_reader(self.recv_socket.fileno(), self.read_socket, self.recv_socket) + self.aio_loop.add_reader(self.mc_send_socket.fileno(), self.read_socket, self.mc_send_socket) + self.aio_loop.add_reader(self.uc_send_socket.fileno(), self.read_socket, self.uc_send_socket) - def cleanup(self): + def cleanup(self) -> None: self.aio_loop.remove_reader(self.recv_socket) self.aio_loop.remove_reader(self.mc_send_socket) self.aio_loop.remove_reader(self.uc_send_socket) @@ -103,15 +227,16 @@ self.mc_send_socket.close() self.uc_send_socket.close() - def handles(self, family, addr, interface): - return self.family == family and self.address == addr and self.interface.name == interface.name + def handles_address(self, address: NetworkAddress) -> bool: + return self.address == address - def init_v6(self): - idx = socket.if_nametoindex(self.interface.name) - self.multicast_address = (WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0x575C, idx) + def init_v6(self) -> None: + idx = self.address.interface.index + raw_mc_addr = (WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0x575C, idx) + self.multicast_address = UdpAddress(self.address.family, raw_mc_addr, self.address.interface) # v6: member_request = { multicast_addr, intf_idx } - mreq = (socket.inet_pton(self.family, WSD_MCAST_GRP_V6) + struct.pack('@I', idx)) + mreq = (socket.inet_pton(self.address.family, WSD_MCAST_GRP_V6) + struct.pack('@I', idx)) self.recv_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) self.recv_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) @@ -132,22 +257,21 @@ self.recv_socket.bind(('::', 0, 0, idx)) # bind unicast socket to interface address and WSD's udp port - self.uc_send_socket.bind((self.address, WSD_UDP_PORT, 0, idx)) + self.uc_send_socket.bind((str(self.address), WSD_UDP_PORT, 0, idx)) self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0) self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, args.hoplimit) self.mc_send_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx) - self.transport_address = '[{0}]'.format(self.address) - self.listen_address = (self.address, WSD_HTTP_PORT, 0, idx) + self.listen_address = (self.address.address_str, WSD_HTTP_PORT, 0, idx) - def init_v4(self): - idx = socket.if_nametoindex(self.interface.name) - self.multicast_address = (WSD_MCAST_GRP_V4, WSD_UDP_PORT) + def init_v4(self) -> None: + idx = self.address.interface.index + raw_mc_addr = (WSD_MCAST_GRP_V4, WSD_UDP_PORT) + self.multicast_address = UdpAddress(self.address.family, raw_mc_addr, self.address.interface) # v4: member_request (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)) + mreq = (socket.inet_pton(self.address.family, WSD_MCAST_GRP_V4) + self.address.raw + struct.pack('@I', idx)) self.recv_socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) if platform.system() == 'Linux': @@ -160,15 +284,15 @@ self.recv_socket.bind(('', WSD_UDP_PORT)) # bind unicast socket to interface address and WSD's udp port - self.uc_send_socket.bind((self.address, WSD_UDP_PORT)) + self.uc_send_socket.bind((self.address.address_str, WSD_UDP_PORT)) self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, mreq) self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0) self.mc_send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, args.hoplimit) - self.listen_address = (self.address, WSD_HTTP_PORT) + self.listen_address = (self.address.address_str, WSD_HTTP_PORT) - def add_handler(self, socket, handler): + def add_handler(self, socket: socket.socket, handler: INetworkPacketHandler) -> None: # try: # self.selector.register(socket, selectors.EVENT_READ, self) # except KeyError: @@ -180,12 +304,13 @@ else: self.message_handlers[socket] = [handler] - def remove_handler(self, socket, handler): + def remove_handler(self, socket: socket.socket, handler) -> None: if socket in self.message_handlers: if handler in self.message_handlers[socket]: self.message_handlers[socket].remove(handler) - def handle_request(self, key): + def read_socket(self, key: socket.socket) -> None: + # TODO: refactor this s = None if key == self.uc_send_socket: s = self.uc_send_socket @@ -196,27 +321,28 @@ else: raise ValueError("Unknown socket passed as key.") - msg, address = s.recvfrom(WSD_MAX_LEN) + msg, raw_address = s.recvfrom(WSD_MAX_LEN) + address = UdpAddress(self.address.family, raw_address, self.address.interface) if s in self.message_handlers: for handler in self.message_handlers[s]: - handler.handle_request(msg, address) + handler.handle_packet(msg.decode('utf-8'), address) - def send(self, msg, addr): + def send(self, msg: bytes, addr: UdpAddress): # Request from a client must be answered from a socket that is bound # to the WSD port, i.e. the recv_socket. Messages to multicast # addresses are sent over the dedicated send socket. if addr == self.multicast_address: - self.mc_send_socket.sendto(msg, addr) + self.mc_send_socket.sendto(msg, addr.transport_address) else: - self.uc_send_socket.sendto(msg, addr) + self.uc_send_socket.sendto(msg, addr.transport_address) # constants for WSD XML/SOAP parsing -WSA_URI = 'http://schemas.xmlsoap.org/ws/2004/08/addressing' -WSD_URI = 'http://schemas.xmlsoap.org/ws/2005/04/discovery' -WSDP_URI = 'http://schemas.xmlsoap.org/ws/2006/02/devprof' +WSA_URI: str = 'http://schemas.xmlsoap.org/ws/2004/08/addressing' +WSD_URI: str = 'http://schemas.xmlsoap.org/ws/2005/04/discovery' +WSDP_URI: str = 'http://schemas.xmlsoap.org/ws/2006/02/devprof' -namespaces = { +namespaces: Dict[str, str] = { 'soap': 'http://www.w3.org/2003/05/soap-envelope', 'wsa': WSA_URI, 'wsd': WSD_URI, @@ -226,59 +352,61 @@ 'pub': 'http://schemas.microsoft.com/windows/pub/2005/07' } -WSD_MAX_KNOWN_MESSAGES = 10 +WSD_MAX_KNOWN_MESSAGES: int = 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_PROBE: str = WSD_URI + '/Probe' +WSD_PROBE_MATCH: str = WSD_URI + '/ProbeMatches' +WSD_RESOLVE: str = WSD_URI + '/Resolve' +WSD_RESOLVE_MATCH: str = WSD_URI + '/ResolveMatches' +WSD_HELLO: str = WSD_URI + '/Hello' +WSD_BYE: str = WSD_URI + '/Bye' +WSD_GET: str = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Get' +WSD_GET_RESPONSE: str = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse' + +WSD_TYPE_DEVICE: str = 'wsdp:Device' +PUB_COMPUTER: str = 'pub:Computer' +WSD_TYPE_DEVICE_COMPUTER: str = '{0} {1}'.format(WSD_TYPE_DEVICE, PUB_COMPUTER) -WSD_MCAST_GRP_V4 = '239.255.255.250' -WSD_MCAST_GRP_V6 = 'ff02::c' # link-local +WSD_MCAST_GRP_V4: str = '239.255.255.250' +WSD_MCAST_GRP_V6: str = 'ff02::c' # link-local -WSA_ANON = WSA_URI + '/role/anonymous' -WSA_DISCOVERY = 'urn:schemas-xmlsoap-org:ws:2005:04:discovery' +WSA_ANON: str = WSA_URI + '/role/anonymous' +WSA_DISCOVERY: str = 'urn:schemas-xmlsoap-org:ws:2005:04:discovery' -MIME_TYPE_SOAP_XML = 'application/soap+xml' +MIME_TYPE_SOAP_XML: str = 'application/soap+xml' # protocol assignments (WSD spec/Section 2.4) -WSD_UDP_PORT = 3702 -WSD_HTTP_PORT = 5357 -WSD_MAX_LEN = 32767 +WSD_UDP_PORT: int = 3702 +WSD_HTTP_PORT: int = 5357 +WSD_MAX_LEN: int = 32767 WSDD_LISTEN_PORT = 5359 # SOAP/UDP transmission constants -MULTICAST_UDP_REPEAT = 4 -UNICAST_UDP_REPEAT = 2 -UDP_MIN_DELAY = 50 -UDP_MAX_DELAY = 250 -UDP_UPPER_DELAY = 500 +MULTICAST_UDP_REPEAT: int = 4 +UNICAST_UDP_REPEAT: int = 2 +UDP_MIN_DELAY: int = 50 +UDP_MAX_DELAY: int = 250 +UDP_UPPER_DELAY: int = 500 # servers must recond in 4 seconds after probe arrives -PROBE_TIMEOUT = 4 -MAX_STARTUP_PROBE_DELAY = 3 +PROBE_TIMEOUT: int = 4 +MAX_STARTUP_PROBE_DELAY: int = 3 # some globals -wsd_instance_id = int(time.time()) -send_queue = [] +wsd_instance_id: int = int(time.time()) -args = None -logger = None +WSDMessage = Tuple[ElementTree.Element, str] +MessageTypeHandler = Callable[[ElementTree.Element, ElementTree.Element], Optional[WSDMessage]] -class WSDMessageHandler: - known_messages = collections.deque([], WSD_MAX_KNOWN_MESSAGES) +class WSDMessageHandler(INetworkPacketHandler): + known_messages: Deque[str] = collections.deque([], WSD_MAX_KNOWN_MESSAGES) - def __init__(self): + handlers: Dict[str, MessageTypeHandler] + pending_tasks: List[asyncio.Task] + + def __init__(self) -> None: self.handlers = {} self.pending_tasks = [] @@ -286,7 +414,7 @@ pass # shortcuts for building WSD responses - def add_endpoint_reference(self, parent, endpoint=None): + def add_endpoint_reference(self, parent: ElementTree.Element, endpoint: Optional[str] = None) -> None: epr = ElementTree.SubElement(parent, 'wsa:EndpointReference') address = ElementTree.SubElement(epr, 'wsa:Address') if endpoint is None: @@ -294,27 +422,29 @@ else: address.text = endpoint - def add_metadata_version(self, parent): + def add_metadata_version(self, parent: ElementTree.Element) -> None: meta_data = ElementTree.SubElement(parent, 'wsd:MetadataVersion') meta_data.text = '1' - def add_types(self, parent): + def add_types(self, parent: ElementTree.Element) -> None: dev_type = ElementTree.SubElement(parent, 'wsd:Types') dev_type.text = WSD_TYPE_DEVICE_COMPUTER - def add_xaddr(self, parent, transport_addr): + def add_xaddr(self, parent: ElementTree.Element, transport_addr: str) -> None: if transport_addr: item = ElementTree.SubElement(parent, 'wsd:XAddrs') item.text = 'http://{0}:{1}/{2}'.format(transport_addr, WSD_HTTP_PORT, args.uuid) - def build_message(self, to_addr, action_str, request_header, response): - retval = self.xml_to_buffer(self.build_message_tree(to_addr, action_str, request_header, response)[0]) + def build_message(self, to_addr: str, action_str: str, request_header: Optional[ElementTree.Element], + response: ElementTree.Element) -> str: + retval = self.xml_to_str(self.build_message_tree(to_addr, action_str, request_header, response)[0]) logger.debug('constructed xml for WSD message: {0}'.format(retval)) return retval - def build_message_tree(self, to_addr, action_str, request_header, body): + def build_message_tree(self, to_addr: str, action_str: str, request_header: Optional[ElementTree.Element], + body: Optional[ElementTree.Element]) -> Tuple[ElementTree.Element, str]: """ Build a WSD message with a given action string including SOAP header. @@ -351,12 +481,12 @@ return root, msg_id.text - def add_header_elements(self, header, extra): + def add_header_elements(self, header: ElementTree.Element, extra: Any) -> None: pass - def handle_message(self, msg, mch, src_address): + def handle_message(self, msg: str, src: Optional[UdpAddress] = None) -> Optional[str]: """ - handle a WSD message that might be received by a MulticastHandler + handle a WSD message """ try: tree = ETfromString(msg) @@ -371,10 +501,10 @@ if msg_id_tag is None: return None - msg_id = msg_id_tag.text + msg_id = str(msg_id_tag.text) - # if message came over a MulticastHandler, check for duplicates - if mch and self.is_duplicated_msg(msg_id): + # check for duplicates + if self.is_duplicated_msg(msg_id): logger.debug('known message ({0}): dropping it'.format(msg_id)) return None @@ -382,12 +512,12 @@ if action_tag is None: return None - action = action_tag.text + action: str = str(action_tag.text) _, _, action_method = action.rpartition('/') - if mch: + if src: logger.info('{}:{}({}) - - "{} {} UDP" - -'.format( - src_address[0], src_address[1], mch.interface.name, action_method, msg_id)) + src.transport_str, src.port, src.interface, action_method, msg_id)) else: # http logging is already done by according server logger.debug('processing WSD {} message ({})'.format(action_method, msg_id)) @@ -402,14 +532,13 @@ retval = handler(header, body) if retval is not None: response, response_type = retval - return self.build_message( - WSA_ANON, response_type, header, response) + return self.build_message(WSA_ANON, response_type, header, response) else: logger.debug('unhandled action {0}/{1}'.format(action, msg_id)) return None - def is_duplicated_msg(self, msg_id): + def is_duplicated_msg(self, msg_id: str) -> bool: """ Check for a duplicated message. @@ -422,9 +551,9 @@ return False - def xml_to_buffer(self, xml): - retval = b'' - retval = retval + ElementTree.tostring(xml, encoding='utf-8') + def xml_to_str(self, xml: ElementTree.Element) -> str: + retval = '' + retval = retval + ElementTree.tostring(xml, encoding='utf-8').decode('utf-8') return retval @@ -434,7 +563,10 @@ A message handler that handles traffic received via MutlicastHandler. """ - def __init__(self, mch): + mch: MulticastHandler + tearing_down: bool + + def __init__(self, mch: MulticastHandler) -> None: super().__init__() self.mch = mch @@ -443,25 +575,22 @@ def teardown(self): self.tearing_down = True - def send_datagram(self, msg, dst): + def send_datagram(self, msg: str, dst: UdpAddress) -> None: try: - self.mch.send(msg, dst) + self.mch.send(msg.encode('utf-8'), dst) except Exception as e: - logger.error('error while sending packet on {}: {}'.format(self.mch.interface.name, e)) - - def enqueue_datagram(self, msg, address=None, msg_type=None): - if not address: - address = self.mch.multicast_address + logger.error('error while sending packet on {}: {}'.format(self.mch.address.interface, e)) + def enqueue_datagram(self, msg: str, address: UdpAddress, msg_type: Optional[str] = None) -> None: if msg_type: - logger.info('scheduling {0} message via {1} to {2}'.format(msg_type, self.mch.interface.name, address)) + logger.info('scheduling {0} message via {1} to {2}'.format(msg_type, address.interface, address)) schedule_task = self.mch.aio_loop.create_task(self.schedule_datagram(msg, address)) # Add this task to the pending list during teardown to wait during shutdown if self.tearing_down: self.pending_tasks.append(schedule_task) - async def schedule_datagram(self, msg, address): + async def schedule_datagram(self, msg: str, address: UdpAddress) -> None: """ Schedule to send the given message to the given address. @@ -482,17 +611,22 @@ class WSDDiscoveredDevice: # a dict of discovered devices with their UUID as key - instances = {} + instances: Dict[str, 'WSDDiscoveredDevice'] = {} - def __init__(self, xml_str, xaddr, interface): - self.last_seen = None + addresses: Dict[str, Set[str]] + props: Dict[str, str] + display_name: str + last_seen: float + + def __init__(self, xml_str: str, xaddr: str, interface: NetworkInterface) -> None: + self.last_seen = 0.0 self.addresses = {} self.props = {} self.display_name = '' self.update(xml_str, xaddr, interface) - def update(self, xml_str, xaddr, interface): + def update(self, xml_str: str, xaddr: str, interface: NetworkInterface) -> None: try: tree = ETfromString(xml_str) except ElementTree.ParseError: @@ -516,26 +650,25 @@ url = urllib.parse.urlparse(xaddr) addr, _, _ = url.netloc.rpartition(':') report = True - if interface not in self.addresses: - self.addresses[interface] = set([addr]) + if interface.name not in self.addresses: + self.addresses[interface.name] = set([addr]) else: - if addr not in self.addresses[interface]: - self.addresses[interface].add(addr) + if addr not in self.addresses[interface.name]: + self.addresses[interface.name].add(addr) else: report = False self.last_seen = time.time() if ('DisplayName' in self.props) and ('BelongsTo' in self.props) and (report): self.display_name = self.props['DisplayName'] - logger.info('discovered {} in {} on {}%{}'.format(self.display_name, self.props['BelongsTo'], addr, - interface.interface.name)) + logger.info('discovered {} in {} on {}'.format(self.display_name, self.props['BelongsTo'], addr)) elif ('FriendlyName' in self.props) and (report): self.display_name = self.props['FriendlyName'] - logger.info('discovered {} on {}%{}'.format(self.display_name, addr, interface.interface.name)) + logger.info('discovered {} on {}'.format(self.display_name, addr)) logger.debug(str(self.props)) - def extract_wsdp_props(self, root, dialect): + def extract_wsdp_props(self, root: ElementTree.Element, dialect: str) -> None: _, _, propsRoot = dialect.rpartition('/') # XPath support is limited, so filter by namespace on our own nodes = root.findall('./wsdp:{0}/*'.format(propsRoot), namespaces) @@ -543,11 +676,11 @@ prop_nodes = [n for n in nodes if n.tag.startswith(ns_prefix)] for node in prop_nodes: tag_name = node.tag[len(ns_prefix):] - self.props[tag_name] = node.text + self.props[tag_name] = str(node.text) - def extract_host_props(self, root): + def extract_host_props(self, root: ElementTree.Element) -> None: types = root.findtext('wsdp:Types', '', namespaces) - self.props['types'] = types.split(' ') + self.props['types'] = types.split(' ')[0] if types != PUB_COMPUTER: return @@ -558,9 +691,10 @@ class WSDClient(WSDUDPMessageHandler): - instances = [] + instances: ClassVar[List['WSDClient']] = [] + probes: Dict[str, float] - def __init__(self, mch): + def __init__(self, mch: MulticastHandler) -> None: super().__init__(mch) WSDClient.instances.append(self) @@ -579,14 +713,14 @@ time.sleep(random.randint(0, MAX_STARTUP_PROBE_DELAY)) self.send_probe() - def cleanup(self): + def cleanup(self) -> None: super().cleanup() WSDClient.instances.remove(self) self.mch.remove_handler(self.mch.mc_send_socket, self) self.mch.remove_handler(self.mch.recv_socket, self) - def send_probe(self): + def send_probe(self) -> None: """WS-Discovery, Section 4.3, Probe message""" self.remove_outdated_probes() @@ -594,50 +728,53 @@ ElementTree.SubElement(probe, 'wsd:Types').text = WSD_TYPE_DEVICE xml, i = self.build_message_tree(WSA_DISCOVERY, WSD_PROBE, None, probe) - self.enqueue_datagram(self.xml_to_buffer(xml), msg_type='Probe') + self.enqueue_datagram(self.xml_to_str(xml), self.mch.multicast_address, msg_type='Probe') self.probes[i] = time.time() - def teardown(self): + def teardown(self) -> None: super().teardown() self.remove_outdated_probes() - def handle_request(self, msg, address): - self.handle_message(msg, self.mch, address) + def handle_packet(self, msg: str, src: Optional[UdpAddress] = None) -> None: + self.handle_message(msg, src) - def handle_hello(self, header, body): + def handle_hello(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]: pm_path = 'wsd:Hello' endpoint, xaddrs = self.extract_endpoint_metadata(body, pm_path) if not xaddrs: logger.info('Hello without XAddrs, sending resolve') - msg = self.build_resolve_message(endpoint) - self.enqueue_datagram(msg) - return + msg = self.build_resolve_message(str(endpoint)) + self.enqueue_datagram(msg, self.mch.multicast_address) + return None xaddr = None for addr in xaddrs.strip().split(): - if (self.mch.family == socket.AF_INET6) and ('//[fe80::' in addr): + if (self.mch.address.family == socket.AF_INET6) and ('//[fe80::' in addr): # use first link-local address for IPv6 xaddr = addr break - elif self.mch.family == socket.AF_INET: + elif self.mch.address.family == socket.AF_INET: # use first (and very likely the only) IPv4 address xaddr = addr break if xaddr is None: - return + return None logger.info('Hello from {} on {}'.format(endpoint, xaddr)) self.perform_metadata_exchange(endpoint, xaddr) + return None - def handle_bye(self, header, body): + def handle_bye(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]: bye_path = 'wsd:Bye' endpoint, _ = self.extract_endpoint_metadata(body, bye_path) device_uuid = str(uuid.UUID(endpoint)) if device_uuid in WSDDiscoveredDevice.instances: - del(WSDDiscoveredDevice.instances[device_uuid]) + del WSDDiscoveredDevice.instances[device_uuid] + + return None - def handle_probe_match(self, header, body): + def handle_probe_match(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]: # do not handle to probematches issued not sent by ourself rel_msg = header.findtext('wsa:RelatesTo', None, namespaces) if rel_msg not in self.probes: @@ -649,32 +786,36 @@ endpoint, xaddrs = self.extract_endpoint_metadata(body, pm_path) if not xaddrs: logger.debug('probe match without XAddrs, sending resolve') - msg = self.build_resolve_message(endpoint) - self.enqueue_datagram(msg) - return + msg = self.build_resolve_message(str(endpoint)) + self.enqueue_datagram(msg, self.mch.multicast_address) + return None xaddr = xaddrs.strip() logger.debug('probe match for {} on {}'.format(endpoint, xaddr)) self.perform_metadata_exchange(endpoint, xaddr) - def build_resolve_message(self, endpoint): + return None + + def build_resolve_message(self, endpoint: str) -> str: resolve = ElementTree.Element('wsd:Resolve') self.add_endpoint_reference(resolve, endpoint) return self.build_message(WSA_DISCOVERY, WSD_RESOLVE, None, resolve) - def handle_resolve_match(self, header, body): + def handle_resolve_match(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]: rm_path = 'wsd:ResolveMatches/wsd:ResolveMatch' endpoint, xaddrs = self.extract_endpoint_metadata(body, rm_path) if not endpoint or not xaddrs: logger.debug('resolve match without endpoint/xaddr') - return + return None xaddr = xaddrs.strip() logger.debug('resolve match for {} on {}'.format(endpoint, xaddr)) self.perform_metadata_exchange(endpoint, xaddr) - def extract_endpoint_metadata(self, body, prefix): + return None + + def extract_endpoint_metadata(self, body: ElementTree.Element, prefix: str) -> Tuple[Optional[str], Optional[str]]: prefix = prefix + '/' addr_path = 'wsa:EndpointReference/wsa:Address' @@ -683,19 +824,19 @@ return endpoint, xaddrs - def perform_metadata_exchange(self, endpoint, xaddr): + def perform_metadata_exchange(self, endpoint, xaddr: str): if not (xaddr.startswith('http://') or xaddr.startswith('https://')): logger.debug('invalid XAddr: {}'.format(xaddr)) return host = None url = xaddr - if self.mch.family == socket.AF_INET6: + if self.mch.address.family == socket.AF_INET6: host = '[{}]'.format(url.partition('[')[2].partition(']')[0]) - url = url.replace(']', '%{}]'.format(self.mch.interface.name)) + url = url.replace(']', '%{}]'.format(self.mch.address.interface)) body = self.build_getmetadata_message(endpoint) - request = urllib.request.Request(url, data=body, method='POST') + request = urllib.request.Request(url, data=body.encode('utf-8'), method='POST') request.add_header('Content-Type', 'application/soap+xml') request.add_header('User-Agent', 'wsdd') if host is not None: @@ -707,22 +848,22 @@ except urllib.error.URLError as e: logger.warning('could not fetch metadata from: {} {}'.format(url, e)) - def build_getmetadata_message(self, endpoint): + def build_getmetadata_message(self, endpoint) -> str: tree, _ = self.build_message_tree(endpoint, WSD_GET, None, None) - return self.xml_to_buffer(tree) + return self.xml_to_str(tree) - def handle_metadata(self, meta, endpoint, xaddr): + def handle_metadata(self, meta: str, endpoint: str, xaddr: str) -> None: device_uuid = str(uuid.UUID(endpoint)) if device_uuid in WSDDiscoveredDevice.instances: - WSDDiscoveredDevice.instances[device_uuid].update(meta, xaddr, self.mch) + WSDDiscoveredDevice.instances[device_uuid].update(meta, xaddr, self.mch.address.interface) else: - WSDDiscoveredDevice.instances[device_uuid] = WSDDiscoveredDevice(meta, xaddr, self.mch) + WSDDiscoveredDevice.instances[device_uuid] = WSDDiscoveredDevice(meta, xaddr, self.mch.address.interface) - def remove_outdated_probes(self): + def remove_outdated_probes(self) -> None: cut = time.time() - PROBE_TIMEOUT * 2 self.probes = dict(filter(lambda x: x[1] > cut, self.probes.items())) - def add_header_elements(self, header, extra): + def add_header_elements(self, header: ElementTree.Element, extra: Any) -> None: action_str = extra if action_str == WSD_GET: reply_to = ElementTree.SubElement(header, 'wsa:ReplyTo') @@ -737,10 +878,10 @@ class WSDHost(WSDUDPMessageHandler): """Class for handling WSD requests coming from UDP datagrams.""" - message_number = 0 - instances = [] + message_number: ClassVar[int] = 0 + instances: ClassVar[List['WSDHost']] = [] - def __init__(self, mch): + def __init__(self, mch: MulticastHandler) -> None: super().__init__(mch) WSDHost.instances.append(self) @@ -752,40 +893,39 @@ self.send_hello() - def cleanup(self): + def cleanup(self) -> None: super().cleanup() WSDHost.instances.remove(self) - def teardown(self): + def teardown(self) -> None: super().teardown() self.send_bye() - def handle_request(self, msg, address): - reply = self.handle_message(msg, self.mch, address) + def handle_packet(self, msg: str, src: UdpAddress) -> None: + reply = self.handle_message(msg, src) if reply: - self.enqueue_datagram(reply, address=address) + self.enqueue_datagram(reply, src) - def send_hello(self): + def send_hello(self) -> None: """WS-Discovery, Section 4.1, Hello message""" hello = ElementTree.Element('wsd:Hello') self.add_endpoint_reference(hello) - # THINK: Microsoft does not send the transport address here due - # to privacy reasons. Could make this optional. - self.add_xaddr(hello, self.mch.transport_address) + # THINK: Microsoft does not send the transport address here due to privacy reasons. Could make this optional. + self.add_xaddr(hello, self.mch.address.transport_str) self.add_metadata_version(hello) msg = self.build_message(WSA_DISCOVERY, WSD_HELLO, None, hello) - self.enqueue_datagram(msg, msg_type='Hello') + self.enqueue_datagram(msg, self.mch.multicast_address, msg_type='Hello') - def send_bye(self): + def send_bye(self) -> None: """WS-Discovery, Section 4.2, Bye message""" bye = ElementTree.Element('wsd:Bye') self.add_endpoint_reference(bye) msg = self.build_message(WSA_DISCOVERY, WSD_BYE, None, bye) - self.enqueue_datagram(msg, msg_type='Bye') + self.enqueue_datagram(msg, self.mch.multicast_address, msg_type='Bye') - def handle_probe(self, header, body): + def handle_probe(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]: probe = body.find('./wsd:Probe', namespaces) if probe is None: return None @@ -815,7 +955,7 @@ return matches, WSD_PROBE_MATCH - def handle_resolve(self, header, body): + def handle_resolve(self, header: ElementTree.Element, body: ElementTree.Element) -> Optional[WSDMessage]: resolve = body.find('./wsd:Resolve', namespaces) if resolve is None: return None @@ -834,12 +974,12 @@ match = ElementTree.SubElement(matches, 'wsd:ResolveMatch') self.add_endpoint_reference(match) self.add_types(match) - self.add_xaddr(match, self.mch.transport_address) + self.add_xaddr(match, self.mch.address.transport_str) self.add_metadata_version(match) return matches, WSD_RESOLVE_MATCH - def add_header_elements(self, header, extra): + def add_header_elements(self, header: ElementTree.Element, extra: Any): ElementTree.SubElement(header, 'wsd:AppSequence', { 'InstanceId': str(wsd_instance_id), 'SequenceId': uuid.uuid1().urn, @@ -850,12 +990,12 @@ class WSDHttpMessageHandler(WSDMessageHandler): - def __init__(self): + def __init__(self) -> None: super().__init__() self.handlers[WSD_GET] = self.handle_get - def handle_get(self, header, body): + def handle_get(self, header: ElementTree.Element, body: ElementTree.Element) -> WSDMessage: # see https://msdn.microsoft.com/en-us/library/hh441784.aspx for an # example. Some of the properties below might be made configurable # in future releases. @@ -894,31 +1034,36 @@ class WSDHttpServer(http.server.HTTPServer): """ HTTP server both with IPv6 support and WSD handling """ - def __init__(self, mch, RequestHandlerClass, addr_family, aio_loop): + mch: MulticastHandler + aio_loop: asyncio.AbstractEventLoop + wsd_handler: WSDHttpMessageHandler + registered: bool + + def __init__(self, mch: MulticastHandler, aio_loop: asyncio.AbstractEventLoop): # hacky way to convince HTTP/SocketServer of the address family - type(self).address_family = addr_family + type(self).address_family = mch.address.family - # remember actual address family used by the server instance - self.addr_family = addr_family self.mch = mch self.aio_loop = aio_loop self.wsd_handler = WSDHttpMessageHandler() self.registered = False - super().__init__(mch.listen_address, RequestHandlerClass) + # WSDHttpRequestHandler is a BaseHTTPRequestHandler. Passing to the parent constructor is therefore safe and + # we can ignore the type error reported by mypy + super().__init__(mch.listen_address, WSDHttpRequestHandler) # type: ignore - def server_bind(self): - if self.address_family == socket.AF_INET6: + def server_bind(self) -> None: + if self.mch.address.family == socket.AF_INET6: self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) super().server_bind() - def server_activate(self): + def server_activate(self) -> None: super().server_activate() self.aio_loop.add_reader(self.fileno(), self.handle_request) self.registered = True - def server_close(self): + def server_close(self) -> None: if self.registered: self.aio_loop.remove_reader(self.fileno()) super().server_close() @@ -927,10 +1072,10 @@ class WSDHttpRequestHandler(http.server.BaseHTTPRequestHandler): """Class for handling WSD requests coming over HTTP""" - def log_message(self, fmt, *args): + def log_message(self, fmt, *args) -> None: logger.info("{} - - ".format(self.address_string()) + fmt % args) - def do_POST(self): + def do_POST(self) -> None: if self.path != '/' + str(args.uuid): self.send_error(http.HTTPStatus.NOT_FOUND) @@ -941,37 +1086,42 @@ content_length = int(self.headers['Content-Length']) body = self.rfile.read(content_length) - response = self.server.wsd_handler.handle_message(body, None, None) + response = self.server.wsd_handler.handle_message(body) # type: ignore if response: self.send_response(http.HTTPStatus.OK) self.send_header('Content-Type', MIME_TYPE_SOAP_XML) self.end_headers() - self.wfile.write(response) + self.wfile.write(response.encode('utf-8')) else: self.send_error(http.HTTPStatus.BAD_REQUEST) class ApiServer: - def __init__(self, aio_loop, listen_address, address_monitor): + address_monitor: 'NetworkAddressMonitor' + + def __init__(self, aio_loop: asyncio.AbstractEventLoop, listen_address: bytes, + address_monitor: 'NetworkAddressMonitor') -> None: self.server = None self.address_monitor = address_monitor # defer server creation self.create_task = aio_loop.create_task(self.create_server(aio_loop, listen_address)) - async def create_server(self, aio_loop, listen_address): + async def create_server(self, aio_loop: asyncio.AbstractEventLoop, listen_address: Any) -> None: + # It appears mypy is not able to check the argument to create_task and the return value of start_server + # correctly. The docs say start_server returns a coroutine and the create_task takes a coro. And: It works. + # Thus, we ignore type errors here. if isinstance(listen_address, int) or listen_address.isnumeric(): - self.server = await aio_loop.create_task(asyncio.start_server(self.on_connect, host='localhost', - port=int(listen_address), loop=aio_loop, reuse_address=True, - reuse_port=True)) + self.server = await aio_loop.create_task(asyncio.start_server( # type: ignore + self.on_connect, host='localhost', port=int(listen_address), reuse_address=True, + reuse_port=True)) else: - self.server = await aio_loop.create_task(asyncio.start_unix_server(self.on_connect, path=listen_address, - loop=aio_loop)) - - async def on_connect(self, read_stream, write_stream): + self.server = await aio_loop.create_task(asyncio.start_unix_server( # type: ignore + self.on_connect, path=listen_address)) + async def on_connect(self, read_stream: asyncio.StreamReader, write_stream: asyncio.StreamWriter) -> None: while True: try: line = await read_stream.readline() @@ -989,7 +1139,7 @@ write_stream.close() return - def handle_command(self, line, write_stream): + def handle_command(self, line: str, write_stream: asyncio.StreamWriter) -> None: words = line.split() if len(words) == 0: return @@ -1015,15 +1165,15 @@ else: logger.debug('could not handle API request: {}'.format(line)) - def get_clients_by_interface(self, interface): - return [c for c in WSDClient.instances if c.mch.interface.name == interface or not interface] + def get_clients_by_interface(self, interface: Optional[str]) -> List[WSDClient]: + return [c for c in WSDClient.instances if c.mch.address.interface.name == interface or not interface] - def get_list_reply(self): + def get_list_reply(self) -> str: retval = '' for dev_uuid, dev in WSDDiscoveredDevice.instances.items(): addrs_str = [] - for mci, addrs in dev.addresses.items(): - addrs_str.append(', '.join(['{}%{}'.format(a, mci.interface.name) for a in addrs])) + for addrs in dev.addresses.items(): + addrs_str.append(', '.join(['{}'.format(a) for a in addrs])) retval = retval + '{}\t{}\t{}\t{}\t{}\n'.format( dev_uuid, @@ -1035,7 +1185,7 @@ retval += '.\n' return retval - async def cleanup(self): + async def cleanup(self) -> None: # ensure the server is not created after we have teared down await self.create_task if self.server: @@ -1043,13 +1193,6 @@ await self.server.wait_closed() -class NetworkInterface: - - def __init__(self, name, scope): - self.name = name - self.scope = scope - - class MetaEnumAfterInit(type): def __call__(cls, *cargs, **kwargs): @@ -1067,9 +1210,16 @@ done in subclasses. This class is used as a singleton """ - instance = None + instance: ClassVar[object] = None + + interfaces: Dict[int, NetworkInterface] + aio_loop: asyncio.AbstractEventLoop + mchs: List[MulticastHandler] + http_servers: List[WSDHttpServer] + teardown_tasks: List[asyncio.Task] + active: bool - def __init__(self, aio_loop): + def __init__(self, aio_loop: asyncio.AbstractEventLoop) -> None: if NetworkAddressMonitor.instance is not None: raise RuntimeError('Instance of NetworkAddressMonitor already created') @@ -1085,7 +1235,7 @@ self.active = False - def enumerate(self): + def enumerate(self) -> None: """ Performs an initial enumeration of addresses and sets up everything for observing future changes. @@ -1096,88 +1246,77 @@ self.active = True self.do_enumerate() - def do_enumerate(self): + def do_enumerate(self) -> None: pass - def handle_request(self): + def handle_change(self) -> None: """ handle network change message """ pass - def add_interface(self, name, idx, scope): - if idx in self.interfaces: - self.interfaces[idx].name = name + def add_interface(self, interface: NetworkInterface) -> NetworkInterface: + # TODO: Cleanup + if interface.index in self.interfaces: + pass + # self.interfaces[idx].name = name else: - self.interfaces[idx] = NetworkInterface(name, scope) + self.interfaces[interface.index] = interface - return self.interfaces[idx] + return self.interfaces[interface.index] - def is_address_handled(self, addr, addr_family, interface): - """ - Check if we should handle that address. - Address must be provided as raw address, i.e. byte array - """ + def is_address_handled(self, address: NetworkAddress) -> bool: # do not handle anything when we are not active if not self.active: return False # filter out address families we are not interested in - if args.ipv4only and addr_family != socket.AF_INET: + if args.ipv4only and address.family != socket.AF_INET: return False - if args.ipv6only and addr_family != socket.AF_INET6: + if args.ipv6only and address.family != socket.AF_INET6: return False - # Nah, this check is not optimal but there are no local flags for - # addresses, but it should be safe for IPv4 anyways - # (https://tools.ietf.org/html/rfc5735#page-3) - if (addr_family == socket.AF_INET) and (addr[0] == 127): + if address.is_multicastable: return False - if (addr_family == socket.AF_INET6) and (addr[0:2] != b'\xfe\x80'): - return False - - addr_str = socket.inet_ntop(addr_family, addr) - if (args.interface) and (interface.name not in args.interface) and (addr_str not in args.interface): + # Use interface only if it's in the list of user-provided interface names + if ((args.interface) and (address.interface.name not in args.interface) + and (address.address_str not in args.interface)): return False return True - def handle_new_address(self, raw_addr, addr_family, interface): - addr = socket.inet_ntop(addr_family, raw_addr) - logger.debug('new address {} on {}'.format(addr, interface.name)) + def handle_new_address(self, address: NetworkAddress) -> None: + logger.debug('new address {}'.format(address)) - if not self.is_address_handled(raw_addr, addr_family, interface): - logger.debug('ignoring that address on {}'.format(interface.name)) + if not self.is_address_handled(address): + logger.debug('ignoring that address on {}'.format(address.interface)) return # filter out what is not wanted # Ignore addresses or interfaces we already handle. There can only be # one multicast handler per address family and network interface for mch in self.mchs: - if mch.handles(addr_family, addr, interface): + if mch.handles_address(address): return - logger.debug('handling traffic for {} on {}'.format( - addr, interface.name)) - mch = MulticastHandler(addr_family, addr, interface, self.aio_loop) + logger.debug('handling traffic for {}'.format(address)) + mch = MulticastHandler(address, self.aio_loop) self.mchs.append(mch) if not args.no_host: WSDHost(mch) if not args.no_http: - self.http_servers.append(WSDHttpServer( - mch, WSDHttpRequestHandler, mch.family, self.aio_loop)) + self.http_servers.append(WSDHttpServer(mch, self.aio_loop)) if args.discovery: WSDClient(mch) - def handle_deleted_address(self, raw_addr, addr_family, interface): - addr = socket.inet_ntop(addr_family, raw_addr) - logger.info('deleted address {} on {}'.format(addr, interface.name)) + def handle_deleted_address(self, address: NetworkAddress) -> None: + logger.info('deleted address {}'.format(address)) - if not self.is_address_handled(raw_addr, addr_family, interface): + if not self.is_address_handled(address): return - mch = self.get_mch_by_address(addr_family, addr, interface) + mch: Optional[MulticastHandler] = self.get_mch_by_address(address) if mch is None: return @@ -1199,7 +1338,7 @@ mch.cleanup() self.mchs.remove(mch) - def teardown(self): + def teardown(self) -> None: if not self.active: return @@ -1231,7 +1370,7 @@ for t in self.teardown_tasks: t.add_done_callback(self.mch_teardown) - def mch_teardown(self, task): + def mch_teardown(self, task) -> None: if any([not t.done() for t in self.teardown_tasks]): return @@ -1241,56 +1380,56 @@ mch.cleanup() self.mchs.clear() - def cleanup(self): + def cleanup(self) -> None: self.teardown() - def get_mch_by_address(self, family, address, interface): + def get_mch_by_address(self, address: NetworkAddress) -> Optional[MulticastHandler]: """ Get the MCI for the address, its family and the interface. adress must be given as a string. """ for retval in self.mchs: - if retval.handles(family, address, interface): + if retval.handles_address(address): return retval return None # from rtnetlink.h -RTMGRP_LINK = 1 -RTMGRP_IPV4_IFADDR = 0x10 -RTMGRP_IPV6_IFADDR = 0x100 - -# from netlink.h -NLM_HDR_LEN = 16 - -NLM_F_REQUEST = 0x01 -NLM_F_ROOT = 0x100 -NLM_F_MATCH = 0x200 -NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH +RTMGRP_LINK: int = 1 +RTMGRP_IPV4_IFADDR: int = 0x10 +RTMGRP_IPV6_IFADDR: int = 0x100 + +# from netlink.h (struct nlmsghdr) +NLM_HDR_DEF: str = '@IHHII' + +NLM_F_REQUEST: int = 0x01 +NLM_F_ROOT: int = 0x100 +NLM_F_MATCH: int = 0x200 +NLM_F_DUMP: int = NLM_F_ROOT | NLM_F_MATCH # self defines -NLM_HDR_LEN = 16 -NLM_HDR_ALIGNTO = 4 +NLM_HDR_ALIGNTO: int = 4 # ifa flags -IFA_F_DADFAILED = 0x08 -IFA_F_HOMEADDRESS = 0x10 -IFA_F_DEPRECATED = 0x20 -IFA_F_TENTATIVE = 0x40 - -# from if_addr.h -IFA_ADDRESS = 1 -IFA_LOCAL = 2 -IFA_LABEL = 3 -IFA_FLAGS = 8 -IFA_MSG_LEN = 8 +IFA_F_DADFAILED: int = 0x08 +IFA_F_HOMEADDRESS: int = 0x10 +IFA_F_DEPRECATED: int = 0x20 +IFA_F_TENTATIVE: int = 0x40 + +# from if_addr.h (struct ifaddrmsg) +IFADDR_MSG_DEF: str = '@BBBBI' +IFA_ADDRESS: int = 1 +IFA_LOCAL: int = 2 +IFA_LABEL: int = 3 +IFA_FLAGS: int = 8 +IFA_MSG_LEN: int = 8 -RTA_ALIGNTO = 4 -RTA_LEN = 4 +RTA_ALIGNTO: int = 4 +RTA_LEN: int = 4 -def align_to(x, n): +def align_to(x: int, n: int) -> int: return ((x + n - 1) // n) * n @@ -1299,11 +1438,13 @@ Implementation of the AddressMonitor for Netlink sockets, i.e. Linux """ - RTM_NEWADDR = 20 - RTM_DELADDR = 21 - RTM_GETADDR = 22 + RTM_NEWADDR: int = 20 + RTM_DELADDR: int = 21 + RTM_GETADDR: int = 22 + + socket: socket.socket - def __init__(self, aio_loop): + def __init__(self, aio_loop: asyncio.AbstractEventLoop) -> None: super().__init__(aio_loop) rtm_groups = RTMGRP_LINK @@ -1314,28 +1455,31 @@ self.socket = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE) self.socket.bind((0, rtm_groups)) - self.aio_loop.add_reader(self.socket.fileno(), self.handle_request) + self.aio_loop.add_reader(self.socket.fileno(), self.handle_change) + + self.NLM_HDR_LEN = struct.calcsize(NLM_HDR_DEF) - def do_enumerate(self): + def do_enumerate(self) -> None: super().do_enumerate() kernel = (0, 0) - req = struct.pack('@IHHIIB', NLM_HDR_LEN + 1, self.RTM_GETADDR, + # Append an unsigned byte to the header for the request. + req = struct.pack(NLM_HDR_DEF + 'B', self.NLM_HDR_LEN + 1, self.RTM_GETADDR, NLM_F_REQUEST | NLM_F_DUMP, 1, 0, socket.AF_PACKET) self.socket.sendto(req, kernel) - def handle_request(self): - super().handle_request() + def handle_change(self) -> None: + super().handle_change() buf, src = self.socket.recvfrom(4096) logger.debug('netlink message with {} bytes'.format(len(buf))) offset = 0 while offset < len(buf): - h_len, h_type, _, _, _ = struct.unpack_from('@IHHII', buf, offset) - offset += NLM_HDR_LEN + h_len, h_type, _, _, _ = struct.unpack_from(NLM_HDR_DEF, buf, offset) + offset += self.NLM_HDR_LEN - msg_len = h_len - NLM_HDR_LEN + msg_len = h_len - self.NLM_HDR_LEN if msg_len < 0: break @@ -1344,8 +1488,8 @@ offset += align_to(msg_len, NLM_HDR_ALIGNTO) continue - # decode ifaddrmsg as in rtnetlink.h - ifa_family, _, ifa_flags, ifa_scope, ifa_idx = struct.unpack_from('@BBBBI', buf, offset) + # decode ifaddrmsg as in if_addr.h + ifa_family, _, ifa_flags, ifa_scope, ifa_idx = struct.unpack_from(IFADDR_MSG_DEF, buf, offset) if ((ifa_flags & IFA_F_DADFAILED) or (ifa_flags & IFA_F_HOMEADDRESS) or (ifa_flags & IFA_F_DEPRECATED) or (ifa_flags & IFA_F_TENTATIVE)): logger.debug('ignore address with invalid state {}'.format(hex(ifa_flags))) @@ -1366,7 +1510,7 @@ if attr_type == IFA_LABEL: name, = struct.unpack_from(str(attr_len - 4 - 1) + 's', buf, i + 4) - self.add_interface(name.decode(), ifa_idx, ifa_scope) + self.add_interface(NetworkInterface(name.decode(), ifa_scope, ifa_idx)) elif attr_type == IFA_LOCAL and ifa_family == socket.AF_INET: addr = buf[i + 4:i + 4 + 4] elif attr_type == IFA_ADDRESS and ifa_family == socket.AF_INET6: @@ -1386,7 +1530,7 @@ try: logger.debug('unknown interface name for idx {}. resolving manually'.format(ifa_idx)) if_name = socket.if_indextoname(ifa_idx) - self.add_interface(if_name, ifa_idx, ifa_scope) + self.add_interface(NetworkInterface(if_name, ifa_scope, ifa_idx)) except OSError: logger.exception('interface detection failed') # accept this exception (which should not occur) @@ -1395,66 +1539,85 @@ # In case really strange things happen and we could not find out the # interface name for the returned ifa_idx, we... log a message. if ifa_idx in self.interfaces: - iface = self.interfaces[ifa_idx] + address = NetworkAddress(ifa_family, addr, self.interfaces[ifa_idx]) if h_type == self.RTM_NEWADDR: - self.handle_new_address(addr, ifa_family, iface) + self.handle_new_address(address) elif h_type == self.RTM_DELADDR: - self.handle_deleted_address(addr, ifa_family, iface) + self.handle_deleted_address(address) else: logger.debug('unknown interface index: {}'.format(ifa_idx)) offset += align_to(msg_len, NLM_HDR_ALIGNTO) - def cleanup(self): + def cleanup(self) -> None: self.aio_loop.remove_reader(self.socket.fileno()) self.socket.close() super().cleanup() # from sys/net/route.h -RTA_IFA = 0x20 +RTA_IFA: int = 0x20 # from sys/socket.h -CTL_NET = 4 -NET_RT_IFLIST = 3 +CTL_NET: int = 4 +NET_RT_IFLIST: int = 3 # from sys/net/if.h -IFF_LOOPBACK = 0x8 -IFF_MULTICAST = 0x800 +IFF_LOOPBACK: int = 0x8 +IFF_MULTICAST: int = 0x800 # sys/netinet6/in6_var.h -IN6_IFF_TENTATIVE = 0x02 -IN6_IFF_DUPLICATED = 0x04 -IN6_IFF_NOTREADY = IN6_IFF_TENTATIVE | IN6_IFF_DUPLICATED +IN6_IFF_TENTATIVE: int = 0x02 +IN6_IFF_DUPLICATED: int = 0x04 +IN6_IFF_NOTREADY: int = IN6_IFF_TENTATIVE | IN6_IFF_DUPLICATED -SA_ALIGNTO = ctypes.sizeof(ctypes.c_long) +SA_ALIGNTO: int = ctypes.sizeof(ctypes.c_long) if platform.system() != "Darwin" else ctypes.sizeof(ctypes.c_uint32) class RouteSocketAddressMonitor(NetworkAddressMonitor): """ - Implementation of the AddressMonitor for FreeBSD using route sockets + Implementation of the AddressMonitor for FreeBSD and Darwin using route sockets """ - # from sys/net/route.h - RTM_NEWADDR = 0xC - RTM_DELADDR = 0xD - RTM_IFINFO = 0xE + # Common definition for beginning part of if(m?a)?_msghdr structs (see net/if.h). + IF_COMMON_HDR_DEF: str = '@HBBii' + RTM_NEWADDR: int = 0xC + RTM_DELADDR: int = 0xD + RTM_IFINFO: int = 0xE + + # from net/if.h (struct ifa_msghdr) + IFA_MSGHDR_DEF: str = IF_COMMON_HDR_DEF + 'hi' + IFA_MSGHDR_SIZE: int = struct.calcsize(IFA_MSGHDR_DEF) + + IF_MSGHDR_DEF_BASE: str = IF_COMMON_HDR_DEF + 'h' + # The struct package does not allow to specify those, thus we hard code them as chars (x4). + IF_DATA_DEFS: Dict[str, str] = { + # if_data in if_msghdr is prepended with an u_short _ifm_spare1, thus the 'H' a the beginning) + 'FreeBSD': 'H6c2c8c8c104c8c16c', + # There are 8 bytes and 22 uint32_t in the if_data struct (22 x 4 Bytes + 8 = 96 Bytes) + # It is also aligned on 4-byte boundary necessitating 2 bytes padding inside if_msghdr + 'Darwin': '2c8c22I' + } + + socket: socket.socket + intf_blacklist: List[str] - def __init__(self, selector): - super().__init__(selector) + def __init__(self, aio_loop: asyncio.AbstractEventLoop) -> None: + super().__init__(aio_loop) self.intf_blacklist = [] # Create routing socket to get notified about future changes. - # Do this before fetching the current routing information to avoid - # race condition. + # Do this before fetching the current routing information to avoid race condition. self.socket = socket.socket(socket.AF_ROUTE, socket.SOCK_RAW, socket.AF_UNSPEC) - self.aio_loop.add_reader(self.socket.fileno(), self.handle_request) + self.aio_loop.add_reader(self.socket.fileno(), self.handle_change) + + self.IF_MSGHDR_SIZE = struct.calcsize(self.IF_MSGHDR_DEF_BASE + self.IF_DATA_DEFS[platform.system()]) - def do_enumerate(self): + def do_enumerate(self) -> None: super().do_enumerate() mib = [CTL_NET, socket.AF_ROUTE, 0, 0, NET_RT_IFLIST, 0] rt_mib = (ctypes.c_int * len(mib))() - rt_mib[:] = mib[:] + rt_mib[:] = [ctypes.c_int(m) for m in mib] libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True) @@ -1470,31 +1633,28 @@ self.parse_route_socket_response(rt_buf.raw, True) - def handle_request(self): - super().handle_request() + def handle_change(self) -> None: + super().handle_change() self.parse_route_socket_response(self.socket.recv(4096), False) - def parse_route_socket_response(self, buf, keep_intf): + def parse_route_socket_response(self, buf: bytes, keep_intf: bool) -> None: offset = 0 intf = None intf_flags = 0 while offset < len(buf): - rtm_len, _, rtm_type = struct.unpack_from('@HBB', buf, offset) - # addr_mask has same offset in if_msghdr and ifs_msghdr - addr_mask, flags = struct.unpack_from('ii', buf, offset + 4) + # unpack route message response + rtm_len, _, rtm_type, addr_mask, flags = struct.unpack_from(self.IF_COMMON_HDR_DEF, buf, offset) - msg_types = [self.RTM_NEWADDR, self.RTM_DELADDR, self.RTM_IFINFO] - if rtm_type not in msg_types: + if rtm_type not in [self.RTM_NEWADDR, self.RTM_DELADDR, self.RTM_IFINFO]: offset += rtm_len continue if rtm_type == self.RTM_IFINFO: intf_flags = flags - # those offset may unfortunately be architecture dependent - sa_offset = offset + ((16 + 152) if rtm_type == self.RTM_IFINFO else 20) + sa_offset = offset + (self.IF_MSGHDR_SIZE if rtm_type == self.RTM_IFINFO else self.IFA_MSGHDR_SIZE) # For a route socket message, and different to a sysctl response, # the link info is stored inside the same rtm message, so it has to @@ -1507,11 +1667,20 @@ offset += rtm_len - def parse_addrs(self, buf, offset, limit, intf, addr_mask, rtm_type, - flags): + def clear_addr_scope(self, raw_addr: bytes) -> bytes: + addr: bytearray = bytearray(raw_addr) + # adapted from in6_clearscope BSD/Mac kernel method (see scope6.c) + if addr[0] == 0xfe and (addr[1] & 0xc0) == 0x80: + addr[2] = 0 + addr[3] = 0 + + return bytes(addr) + + def parse_addrs(self, buf: bytes, offset: int, limit: int, intf: Optional[NetworkInterface], addr_mask: int, + rtm_type: int, flags: int) -> Optional[NetworkInterface]: addr_type_idx = 1 addr = None - addr_family = None + addr_family: int = socket.AF_UNSPEC while offset < limit: while not (addr_type_idx & addr_mask) and (addr_type_idx <= addr_mask): addr_type_idx = addr_type_idx << 1 @@ -1520,15 +1689,17 @@ if sa_fam in [socket.AF_INET, socket.AF_INET6] and addr_type_idx == RTA_IFA: addr_family = sa_fam addr_offset = 4 if sa_fam == socket.AF_INET else 8 - addr_length = 16 if sa_fam == socket.AF_INET6 else 4 + addr_length = 4 if sa_fam == socket.AF_INET else 16 addr_start = offset + addr_offset addr = buf[addr_start:addr_start + addr_length] + if sa_fam == socket.AF_INET6: + addr = self.clear_addr_scope(addr) elif sa_fam == socket.AF_LINK: idx, _, name_len = struct.unpack_from('@HBB', buf, offset + 2) if idx > 0: off_name = offset + 8 if_name = (buf[off_name:off_name + name_len]).decode() - intf = self.add_interface(if_name, idx, idx) + intf = self.add_interface(NetworkInterface(if_name, idx, idx)) offset += align_to(sa_len, SA_ALIGNTO) if sa_len > 0 else SA_ALIGNTO addr_type_idx = addr_type_idx << 1 @@ -1536,35 +1707,36 @@ if rtm_type == self.RTM_IFINFO and intf is not None: if flags & IFF_LOOPBACK or not flags & IFF_MULTICAST: self.intf_blacklist.append(intf.name) - elif intf in self.intf_blacklist: + elif intf.name in self.intf_blacklist: self.intf_blacklist.remove(intf.name) if intf is None or intf.name in self.intf_blacklist or addr is None: return intf + address = NetworkAddress(addr_family, addr, intf) if rtm_type == self.RTM_DELADDR: - self.handle_deleted_address(addr, addr_family, intf) + self.handle_deleted_address(address) else: # Too bad, the address may be unuseable (tentative, e.g.) here # but we won't get any further notifcation about the address being # available for use. Thus, we try and may fail here - self.handle_new_address(addr, addr_family, intf) + self.handle_new_address(address) return intf - def cleanup(self): + def cleanup(self) -> None: self.aio_loop.remove_reader(self.socket.fileno()) self.socket.close() super().cleanup() -def sigterm_handler(): +def sigterm_handler() -> None: logger.info('received termination/interrupt signal, tearing down') # implictely raise SystemExit to cleanup properly sys.exit(0) -def parse_args(): +def parse_args() -> None: global args, logger parser = argparse.ArgumentParser() @@ -1684,7 +1856,7 @@ ElementTree.register_namespace(prefix, uri) -def chroot(root): +def chroot(root: str) -> bool: """ Chroot into a separate directory to isolate ourself for increased security. """ @@ -1702,9 +1874,9 @@ return True -def get_ids_from_userspec(user_spec): - uid = None - gid = None +def get_ids_from_userspec(user_spec: str) -> Tuple[int, int]: + uid: int + gid: int try: user, _, group = user_spec.partition(':') @@ -1714,13 +1886,12 @@ if group: gid = grp.getgrnam(group).gr_gid except Exception as e: - logger.error('could not get uid/gid for {}: {}'.format(user_spec, e)) - return False + raise RuntimeError('could not get uid/gid for {}: {}'.format(user_spec, e)) return (uid, gid) -def drop_privileges(uid, gid): +def drop_privileges(uid: int, gid: int) -> bool: try: if gid is not None: os.setgid(gid) @@ -1740,7 +1911,18 @@ return True -def main(): +def create_address_monitor(system: str, aio_loop: asyncio.AbstractEventLoop) -> NetworkAddressMonitor: + if system == 'Linux': + return NetlinkAddressMonitor(aio_loop) + elif system in ['FreeBSD', 'Darwin']: + return RouteSocketAddressMonitor(aio_loop) + else: + raise NotImplementedError('unsupported OS: ' + system) + + +def main() -> int: + global logger, args + parse_args() if args.ipv4only and args.ipv6only: @@ -1748,12 +1930,7 @@ return 4 aio_loop = asyncio.new_event_loop() - if platform.system() == 'Linux': - nm = NetlinkAddressMonitor(aio_loop) - elif platform.system() == 'FreeBSD': - nm = RouteSocketAddressMonitor(aio_loop) - else: - raise NotImplementedError('unsupported OS') + nm = create_address_monitor(platform.system(), aio_loop) api_server = None if args.listen: diff -Nru wsdd-0.7.0/test/linting/mypy.sh wsdd-0.7.1/test/linting/mypy.sh --- wsdd-0.7.0/test/linting/mypy.sh 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/test/linting/mypy.sh 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,9 @@ +#!/bin/bash + +root_dir="$(realpath $(dirname $0)/../..)" + +for version in 3.7 3.8 3.9 3.10 3.11; do + echo -n "checking for Python ${version}..." + mypy --python-version=${version} ${root_dir}/src/wsdd.py + echo +done diff -Nru wsdd-0.7.0/test/regressions/01_asyncio_changes_python310/run_tcp.sh wsdd-0.7.1/test/regressions/01_asyncio_changes_python310/run_tcp.sh --- wsdd-0.7.0/test/regressions/01_asyncio_changes_python310/run_tcp.sh 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/test/regressions/01_asyncio_changes_python310/run_tcp.sh 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,49 @@ +#!/bin/bash + +# Test if TCP server socket for API can be created with different Python versions +# The API for ´start_server´ was schanged in 3.10. +# @see related Github issue #162 + +python_versions=('3.7' '3.8' '3.9.' '3.10' '3.11') + +socket_port="3333" + +wsdd_script_args=("--no-autostart" "--no-http" "--discovery" "--listen" "${socket_port}") + +return_code=0 +python_found=0 + +# Use netcat to connec to UNIX Domain Socket +netcat="nc" +netcat_args=("-N", "127.0.0.1:${socket_port}") + +for version in "${python_versions[@]}"; do + python_ver_name="python${version}" + + if command -v "${python_ver_name}" >/dev/null 2>&1; then + python_found=1 + "${python_ver_name}" "${WSDD_SCRIPT}" "${wsdd_script_args[@]}" & + wsdd_pid=$! + + # wait until socket should be ready + sleep 2 + + # connect to socket and send a simple command + ${netcat} "${netcat_args[@]}" <<< "list" + + # terminate and get exit status + kill -INT ${wsdd_pid} + wait ${wsdd_pid} + status=$? + + if [ ! ${status} -eq 0 ]; then + return_code=1 + fi + fi +done + +if [ $python_found -eq 1 ]; then + exit ${return_code} +else + exit 2 +fi diff -Nru wsdd-0.7.0/test/regressions/01_asyncio_changes_python310/run_unix.sh wsdd-0.7.1/test/regressions/01_asyncio_changes_python310/run_unix.sh --- wsdd-0.7.0/test/regressions/01_asyncio_changes_python310/run_unix.sh 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/test/regressions/01_asyncio_changes_python310/run_unix.sh 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,55 @@ +#!/bin/bash + +# Test if Unix Domain Socket for API can be created with different Python versions +# The API for ´start_unix_server´ was schanged in 3.10. +# @see Github issue #162 + +python_versions=('3.7' '3.8' '3.9.' '3.10' '3.11') + +socket_dir="$(mktemp -d)" +socket_filename="${socket_dir}/wsdd.sock" + +wsdd_script_args=("--no-autostart" "--no-http" "--discovery" "--listen" "${socket_filename}") + +return_code=0 +python_found=0 + +# Use netcat to connec to UNIX Domain Socket +netcat="nc" +netcat_args=("-U" "${socket_filename}" "-N") + +for version in "${python_versions[@]}"; do + python_ver_name="python${version}" + + if command -v "${python_ver_name}" >/dev/null 2>&1; then + python_found=1 + rm -f "${socket_filename}" + "${python_ver_name}" "${WSDD_SCRIPT}" "${wsdd_script_args[@]}" & + wsdd_pid=$! + + # wait until socket should be ready + sleep 2 + + # connect to socket and send a simple command + ${netcat} "${netcat_args[@]}" <<< "list" + + # terminate and get exit status + kill -INT ${wsdd_pid} + wait ${wsdd_pid} + status=$? + + if [ ! ${status} -eq 0 ]; then + return_code=1 + fi + + rm -f "${socket_filename}" + fi +done + +find "${socket_dir}" -delete + +if [ $python_found -eq 1 ]; then + exit ${return_code} +else + exit 2 +fi diff -Nru wsdd-0.7.0/test/regressions/run-regressions.sh wsdd-0.7.1/test/regressions/run-regressions.sh --- wsdd-0.7.0/test/regressions/run-regressions.sh 1970-01-01 00:00:00.000000000 +0000 +++ wsdd-0.7.1/test/regressions/run-regressions.sh 2023-03-04 11:21:05.000000000 +0000 @@ -0,0 +1,49 @@ +#!/bin/bash + +# Run regression tests found as executable .sh scripts in level-1 subdirectories. + +basedir="$(realpath "$(dirname "$0")")" + +export WSDD_ROOT_DIR="$(realpath "${basedir}/../..")" +export WSDD_SCRIPT="${WSDD_ROOT_DIR}/src/wsdd.py" + +test_files=() + +for script in "${basedir}"/*/*.sh; do + if [ -x ${script} ]; then + test_files+=("${script}") + fi +done + +total_tests="${#test_files[@]}" + +[ ${total_tests} -eq 0 ] && exit 0 + +echo "Running ${total_tests} tests..." + +test_number=1 + +num_succeeded=0 +num_failed=0 + +for test_case in "${test_files[@]}"; do + # change later to capture test output + log_target="/dev/null" + + echo -n "[${test_number}/${total_tests}] $(basename $(dirname "$test_case")) -> $(basename "${test_case}")... " + + if "${test_case}" > ${log_target} 2>&1; then + echo "OK" + num_succeeded=$((num_succeeded + 1)) + else + echo "FAILED" + num_failed=$((num_failed + 1)) + fi + + test_number=$(($test_number + 1)) +done + +echo "------------------------------------------" +echo "${num_succeeded} succeeded, ${num_failed} failed." + +exit $((${total_tests} - ${num_succeeded})) diff -Nru wsdd-0.7.0/test/routesocket_monitor.py wsdd-0.7.1/test/routesocket_monitor.py --- wsdd-0.7.0/test/routesocket_monitor.py 2021-11-20 20:11:29.000000000 +0000 +++ wsdd-0.7.1/test/routesocket_monitor.py 2023-03-04 11:21:05.000000000 +0000 @@ -34,9 +34,9 @@ link = None print(len(buf)) while offset < len(buf): - rtm_len, _, rtm_type = struct.unpack_from('@HBB', buf, offset) # mask(addrs) has same offset in if_msghdr and ifs_msghdr - addr_mask, flags = struct.unpack_from('ii', buf, offset + 4) + rtm_len, _, rtm_type, addr_mask, flags = struct.unpack_from( + '@HBBii', buf, offset) msg_type = '' if rtm_type not in [RTM_NEWADDR, RTM_DELADDR, RTM_IFINFO]: @@ -44,6 +44,7 @@ continue # those offset may unfortunately be architecture dependent + # (152 is FreeBSD-specific) sa_offset = offset + ((16 + 152) if rtm_type == RTM_IFINFO else 20) if rtm_type in [RTM_NEWADDR, RTM_IFINFO]: