Compare commits
No commits in common. "master" and "v0.7.0" have entirely different histories.
4 changed files with 24 additions and 839 deletions
|
@ -1,10 +1,9 @@
|
||||||
FROM python:3-alpine
|
FROM python:3-alpine
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
RUN apk update
|
|
||||||
RUN apk upgrade
|
RUN apk add --no-cache curl bash && curl https://raw.githubusercontent.com/christgau/wsdd/v0.7.0/src/wsdd.py -o wsdd.py && apk del curl
|
||||||
RUN apk add bash
|
|
||||||
COPY wsdd.py .
|
copy docker-cmd.sh .
|
||||||
COPY docker-cmd.sh .
|
|
||||||
|
|
||||||
CMD [ "./docker-cmd.sh"]
|
CMD [ "./docker-cmd.sh"]
|
||||||
|
|
32
README.md
32
README.md
|
@ -1,40 +1,14 @@
|
||||||
# FABIO note
|
|
||||||
clonare git ed entrare nel folder
|
|
||||||
```
|
|
||||||
git clone https://forgit.patachina.duckdns.org/Fabio/wsdd-docker.git
|
|
||||||
cd wsdd-docker
|
|
||||||
```
|
|
||||||
creare l'immagine docker wsdd con il comando
|
|
||||||
```
|
|
||||||
sudo docker build -t wsdd .
|
|
||||||
```
|
|
||||||
per far partire il docker usare il seguente docker compose
|
|
||||||
in HOSTNAME= mettere il nome che si vuole visualizzare nella rete
|
|
||||||
|
|
||||||
``` docker compose
|
|
||||||
services:
|
|
||||||
wsdd:
|
|
||||||
image: wsdd:latest
|
|
||||||
network_mode: host
|
|
||||||
environment:
|
|
||||||
- 'HOSTNAME=OrangePiSMB'
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
original git https://github.com/JonasPed/wsdd-docker e https://github.com/nitmir/wsdd (multiple hosts)
|
|
||||||
|
|
||||||
# wsdd-docker
|
# wsdd-docker
|
||||||
Docker image for wsdd.py.
|
Docker image for wsdd.py.
|
||||||
|
|
||||||
wsdd implements a Web Service Discovery host daemon. This enables (Samba) hosts, like your local NAS device or Linux server, to be found by Web Service Discovery Clients like Windows.
|
wsdd implements a Web Service Discovery host daemon. This enables (Samba) hosts, like your local NAS device or Linux server, to be found by Web Service Discovery Clients like Windows.
|
||||||
|
|
||||||
## Supported environment variables
|
## Supported environment variables
|
||||||
`HOSTNAME`: Samba Netbios name to report.
|
HOSTNAME: Samba Netbios name to report.
|
||||||
|
|
||||||
`WORKGROUP`: Workgroup name
|
WORKGROUP: Workgroup name
|
||||||
|
|
||||||
`DOMAIN`: Report being a member of an AD DOMAIN. Disables `WORKGROUP` if set.
|
DOMAIN: Report being a member of an AD DOMAIN. Disables WORKGROUP if set.
|
||||||
|
|
||||||
Alternatively, you can pass all desired wsdd arguments in the environment variable `WSDD_ARGS`. In this case, the arguments are passed as-is and all other environment variables are ignored.
|
|
||||||
|
|
||||||
## Running container
|
## Running container
|
||||||
### From command line
|
### From command line
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
args=
|
args=( )
|
||||||
|
|
||||||
if [ ! -z "${WSDD_ARGS}" ]; then
|
if [ ! -z "${HOSTNAME}" ]; then
|
||||||
args=${WSDD_ARGS}
|
args+=( "-n $HOSTNAME ")
|
||||||
else
|
else
|
||||||
if [ ! -z "${HOSTNAME}" ]; then
|
echo "HOSTNAME environment variable must be set."
|
||||||
args+="-n $HOSTNAME "
|
exit 1
|
||||||
else
|
|
||||||
echo "HOSTNAME environment variable must be set."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -z "${WORKGROUP}" ]; then
|
|
||||||
args+="-w $WORKGROUP "
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -z "${DOMAIN}" ]; then
|
|
||||||
args+="-d $DOMAIN "
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "${WORKGROUP}" ]; then
|
||||||
|
args+=( "-w $WORKGROUP " )
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "${DOMAIN}" ]; then
|
||||||
|
args+=( "-d $DOMAIN " )
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
exec python wsdd.py ${args}
|
exec python wsdd.py ${args}
|
||||||
|
|
||||||
|
|
787
wsdd.py
787
wsdd.py
|
@ -1,787 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Implements a target service according to the Web Service Discovery
|
|
||||||
# specification.
|
|
||||||
#
|
|
||||||
# The purpose is to enable non-Windows devices to be found by the 'Network
|
|
||||||
# (Neighborhood)' from Windows machines.
|
|
||||||
#
|
|
||||||
# see http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf and
|
|
||||||
# related documents for details (look at README for more references)
|
|
||||||
#
|
|
||||||
# (c) Steffen Christgau, 2017-2019
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import signal
|
|
||||||
import socket
|
|
||||||
import selectors
|
|
||||||
import struct
|
|
||||||
import argparse
|
|
||||||
import uuid
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import logging
|
|
||||||
import platform
|
|
||||||
import ctypes.util
|
|
||||||
import collections
|
|
||||||
import xml.etree.ElementTree as ElementTree
|
|
||||||
import http.server
|
|
||||||
|
|
||||||
|
|
||||||
# sockaddr C type, with a larger data field to capture IPv6 addresses
|
|
||||||
# unfortunately, the structures differ on Linux and FreeBSD
|
|
||||||
if platform.system() == 'Linux':
|
|
||||||
class sockaddr(ctypes.Structure):
|
|
||||||
_fields_ = [('family', ctypes.c_uint16),
|
|
||||||
('data', ctypes.c_uint8 * 24)]
|
|
||||||
else:
|
|
||||||
class sockaddr(ctypes.Structure):
|
|
||||||
_fields_ = [('length', ctypes.c_uint8),
|
|
||||||
('family', ctypes.c_uint8),
|
|
||||||
('data', ctypes.c_uint8 * 24)]
|
|
||||||
|
|
||||||
|
|
||||||
class if_addrs(ctypes.Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if_addrs._fields_ = [('next', ctypes.POINTER(if_addrs)),
|
|
||||||
('name', ctypes.c_char_p),
|
|
||||||
('flags', ctypes.c_uint),
|
|
||||||
('addr', ctypes.POINTER(sockaddr)),
|
|
||||||
('netmask', ctypes.POINTER(sockaddr))]
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPv6Server(http.server.HTTPServer):
|
|
||||||
"""Simple HTTP server with IPv6 support"""
|
|
||||||
address_family = socket.AF_INET6
|
|
||||||
|
|
||||||
def server_bind(self):
|
|
||||||
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
||||||
super().server_bind()
|
|
||||||
|
|
||||||
|
|
||||||
class MulticastInterface:
|
|
||||||
"""
|
|
||||||
A class for handling multicast traffic on a given interface for a
|
|
||||||
given address family. It provides multicast sender and receiver sockets
|
|
||||||
"""
|
|
||||||
def __init__(self, family, address, intf_name):
|
|
||||||
self.address = address
|
|
||||||
self.family = family
|
|
||||||
self.interface = intf_name
|
|
||||||
self.recv_socket = socket.socket(self.family, socket.SOCK_DGRAM)
|
|
||||||
self.recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
self.send_socket = socket.socket(self.family, socket.SOCK_DGRAM)
|
|
||||||
self.transport_address = address
|
|
||||||
self.multicast_address = None
|
|
||||||
self.listen_address = None
|
|
||||||
|
|
||||||
if family == socket.AF_INET:
|
|
||||||
self.init_v4()
|
|
||||||
elif family == socket.AF_INET6:
|
|
||||||
self.init_v6()
|
|
||||||
|
|
||||||
logger.info('joined multicast group {0} on {2}%{1}'.format(
|
|
||||||
self.multicast_address, self.interface, self.address))
|
|
||||||
logger.debug('transport address on {0} is {1}'.format(
|
|
||||||
self.interface, self.transport_address))
|
|
||||||
logger.debug('will listen for HTTP traffic on address {0}'.format(
|
|
||||||
self.listen_address))
|
|
||||||
|
|
||||||
def init_v6(self):
|
|
||||||
idx = socket.if_nametoindex(self.interface)
|
|
||||||
self.multicast_address = (WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0x575C, idx)
|
|
||||||
|
|
||||||
# v6: member_request = { multicast_addr, intf_idx }
|
|
||||||
mreq = (
|
|
||||||
socket.inet_pton(self.family, WSD_MCAST_GRP_V6) +
|
|
||||||
struct.pack('@I', idx))
|
|
||||||
self.recv_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
|
|
||||||
self.recv_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
||||||
|
|
||||||
# bind to network interface, i.e. scope and handle OS differences,
|
|
||||||
# see Stevens: Unix Network Programming, Section 21.6, last paragraph
|
|
||||||
try:
|
|
||||||
self.recv_socket.bind((WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0, idx))
|
|
||||||
except OSError:
|
|
||||||
self.recv_socket.bind(('::', 0, 0, idx))
|
|
||||||
|
|
||||||
self.send_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0)
|
|
||||||
self.send_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, args.hoplimit)
|
|
||||||
self.send_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
|
|
||||||
|
|
||||||
self.transport_address = '[{0}]'.format(self.address)
|
|
||||||
self.listen_address = (self.address, WSD_HTTP_PORT, 0, idx)
|
|
||||||
|
|
||||||
def init_v4(self):
|
|
||||||
idx = socket.if_nametoindex(self.interface)
|
|
||||||
self.multicast_address = (WSD_MCAST_GRP_V4, WSD_UDP_PORT)
|
|
||||||
|
|
||||||
# v4: member_request (ip_mreqn) = { multicast_addr, intf_addr, idx }
|
|
||||||
mreq = (
|
|
||||||
socket.inet_pton(self.family, WSD_MCAST_GRP_V4) +
|
|
||||||
socket.inet_pton(self.family, self.address) +
|
|
||||||
struct.pack('@I', idx))
|
|
||||||
self.recv_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.recv_socket.bind((WSD_MCAST_GRP_V4, WSD_UDP_PORT))
|
|
||||||
except OSError:
|
|
||||||
self.recv_socket.bind(('', WSD_UDP_PORT))
|
|
||||||
|
|
||||||
self.send_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IP, socket.IP_MULTICAST_IF, mreq)
|
|
||||||
self.send_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0)
|
|
||||||
self.send_socket.setsockopt(
|
|
||||||
socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, args.hoplimit)
|
|
||||||
|
|
||||||
self.listen_address = (self.address, WSD_HTTP_PORT)
|
|
||||||
|
|
||||||
|
|
||||||
# constants for WSD XML/SOAP parsing
|
|
||||||
WSA_URI = 'http://schemas.xmlsoap.org/ws/2004/08/addressing'
|
|
||||||
WSD_URI = 'http://schemas.xmlsoap.org/ws/2005/04/discovery'
|
|
||||||
WSDP_URI = 'http://schemas.xmlsoap.org/ws/2006/02/devprof'
|
|
||||||
|
|
||||||
namespaces = {
|
|
||||||
'soap': 'http://www.w3.org/2003/05/soap-envelope',
|
|
||||||
'wsa': WSA_URI,
|
|
||||||
'wsd': WSD_URI,
|
|
||||||
'wsx': 'http://schemas.xmlsoap.org/ws/2004/09/mex',
|
|
||||||
'wsdp': WSDP_URI,
|
|
||||||
'pnpx': 'http://schemas.microsoft.com/windows/pnpx/2005/10',
|
|
||||||
'pub': 'http://schemas.microsoft.com/windows/pub/2005/07'
|
|
||||||
}
|
|
||||||
|
|
||||||
WSD_MAX_KNOWN_MESSAGES = 10
|
|
||||||
|
|
||||||
WSD_PROBE = WSD_URI + '/Probe'
|
|
||||||
WSD_PROBE_MATCH = WSD_URI + '/ProbeMatches'
|
|
||||||
WSD_RESOLVE = WSD_URI + '/Resolve'
|
|
||||||
WSD_RESOLVE_MATCH = WSD_URI + '/ResolveMatches'
|
|
||||||
WSD_HELLO = WSD_URI + '/Hello'
|
|
||||||
WSD_BYE = WSD_URI + '/Bye'
|
|
||||||
WSD_GET = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Get'
|
|
||||||
WSD_GET_RESPONSE = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse'
|
|
||||||
|
|
||||||
WSD_TYPE_DEVICE = 'wsdp:Device'
|
|
||||||
PUB_COMPUTER = 'pub:Computer'
|
|
||||||
WSD_TYPE_DEVICE_COMPUTER = '{0} {1}'.format(WSD_TYPE_DEVICE, PUB_COMPUTER)
|
|
||||||
|
|
||||||
WSD_MCAST_GRP_V4 = '239.255.255.250'
|
|
||||||
WSD_MCAST_GRP_V6 = 'ff02::c' # link-local
|
|
||||||
|
|
||||||
WSA_ANON = WSA_URI + '/role/anonymous'
|
|
||||||
WSA_DISCOVERY = 'urn:schemas-xmlsoap-org:ws:2005:04:discovery'
|
|
||||||
|
|
||||||
# protocol assignments (WSD spec/Section 2.4)
|
|
||||||
WSD_UDP_PORT = 3702
|
|
||||||
WSD_HTTP_PORT = 5357
|
|
||||||
WSD_MAX_LEN = 32767
|
|
||||||
|
|
||||||
# SOAP/UDP transmission constants
|
|
||||||
MULTICAST_UDP_REPEAT = 4
|
|
||||||
UNICAST_UDP_REPEAT = 2
|
|
||||||
UDP_MIN_DELAY = 50
|
|
||||||
UDP_MAX_DELAY = 250
|
|
||||||
UDP_UPPER_DELAY = 500
|
|
||||||
|
|
||||||
# some globals
|
|
||||||
wsd_known_messages = collections.deque([])
|
|
||||||
wsd_message_number = 1
|
|
||||||
wsd_instance_id = int(time.time())
|
|
||||||
send_queue = []
|
|
||||||
|
|
||||||
args = None
|
|
||||||
logger = None
|
|
||||||
|
|
||||||
|
|
||||||
# shortcuts for building WSD responses
|
|
||||||
def wsd_add_metadata_version(parent):
|
|
||||||
meta_data = ElementTree.SubElement(parent, 'wsd:MetadataVersion')
|
|
||||||
meta_data.text = '1'
|
|
||||||
|
|
||||||
|
|
||||||
def wsd_add_types(parent):
|
|
||||||
dev_type = ElementTree.SubElement(parent, 'wsd:Types')
|
|
||||||
dev_type.text = WSD_TYPE_DEVICE_COMPUTER
|
|
||||||
|
|
||||||
|
|
||||||
def wsd_add_endpoint_reference(uuid_, parent):
|
|
||||||
endpoint = ElementTree.SubElement(parent, 'wsa:EndpointReference')
|
|
||||||
address = ElementTree.SubElement(endpoint, 'wsa:Address')
|
|
||||||
address.text = uuid_.urn
|
|
||||||
|
|
||||||
|
|
||||||
def wsd_add_xaddr(uuid_, parent, transport_addr):
|
|
||||||
if transport_addr:
|
|
||||||
item = ElementTree.SubElement(parent, 'wsd:XAddrs')
|
|
||||||
item.text = 'http://{0}:{1}/{2}'.format(
|
|
||||||
transport_addr, WSD_HTTP_PORT, uuid_)
|
|
||||||
|
|
||||||
|
|
||||||
def wsd_build_message(to_addr, action_str, request_header, response):
|
|
||||||
"""
|
|
||||||
Build a WSD message with a given action string including SOAP header.
|
|
||||||
|
|
||||||
The message can be constructed based on a response to another
|
|
||||||
message (given by its header) and with a optional response that
|
|
||||||
serves as the message's body
|
|
||||||
"""
|
|
||||||
global wsd_message_number
|
|
||||||
|
|
||||||
env = ElementTree.Element('soap:Envelope')
|
|
||||||
header = ElementTree.SubElement(env, 'soap:Header')
|
|
||||||
|
|
||||||
to = ElementTree.SubElement(header, 'wsa:To')
|
|
||||||
to.text = to_addr
|
|
||||||
|
|
||||||
action = ElementTree.SubElement(header, 'wsa:Action')
|
|
||||||
action.text = action_str
|
|
||||||
|
|
||||||
msg_id = ElementTree.SubElement(header, 'wsa:MessageID')
|
|
||||||
msg_id.text = uuid.uuid1().urn
|
|
||||||
|
|
||||||
if request_header:
|
|
||||||
req_msg_id = request_header.find('./wsa:MessageID', namespaces)
|
|
||||||
if req_msg_id is not None:
|
|
||||||
relates_to = ElementTree.SubElement(header, 'wsa:RelatesTo')
|
|
||||||
relates_to.text = req_msg_id.text
|
|
||||||
|
|
||||||
seq = ElementTree.SubElement(header, 'wsd:AppSequence', {
|
|
||||||
'InstanceId': str(wsd_instance_id),
|
|
||||||
'SequenceId': uuid.uuid1().urn,
|
|
||||||
'MessageNumber': str(wsd_message_number)})
|
|
||||||
wsd_message_number = wsd_message_number + 1
|
|
||||||
|
|
||||||
body = ElementTree.SubElement(env, 'soap:Body')
|
|
||||||
body.append(response)
|
|
||||||
|
|
||||||
for prefix, uri in namespaces.items():
|
|
||||||
env.attrib['xmlns:' + prefix] = uri
|
|
||||||
|
|
||||||
xml = b'<?xml version="1.0" encoding="utf-8"?>'
|
|
||||||
xml = xml + ElementTree.tostring(env, encoding='utf-8')
|
|
||||||
|
|
||||||
logger.debug('constructed xml for WSD message: {0}'.format(xml))
|
|
||||||
|
|
||||||
return xml
|
|
||||||
|
|
||||||
|
|
||||||
# WSD message type handling
|
|
||||||
def wsd_handle_probe(uuid_, probe):
|
|
||||||
scopes = probe.find('./wsd:Scopes', namespaces)
|
|
||||||
|
|
||||||
if scopes:
|
|
||||||
# THINK: send fault message (see p. 21 in WSD)
|
|
||||||
logger.warn('Scopes are not supported but were probed ({}).'.format(
|
|
||||||
scopes))
|
|
||||||
return None
|
|
||||||
|
|
||||||
types_elem = probe.find('./wsd:Types', namespaces)
|
|
||||||
if types_elem is None:
|
|
||||||
logger.debug('Probe message lacks wsd:Types element. Ignored.')
|
|
||||||
return None
|
|
||||||
|
|
||||||
types = types_elem.text
|
|
||||||
if not types == WSD_TYPE_DEVICE:
|
|
||||||
logger.debug('unknown discovery type ({0}) during probe'.format(types))
|
|
||||||
return None
|
|
||||||
|
|
||||||
matches = ElementTree.Element('wsd:ProbeMatches')
|
|
||||||
match = ElementTree.SubElement(matches, 'wsd:ProbeMatch')
|
|
||||||
wsd_add_endpoint_reference(uuid_, match)
|
|
||||||
wsd_add_types(match)
|
|
||||||
wsd_add_metadata_version(match)
|
|
||||||
|
|
||||||
return matches
|
|
||||||
|
|
||||||
|
|
||||||
def wsd_handle_resolve(resolve, xaddr):
|
|
||||||
addr = resolve.find('./wsa:EndpointReference/wsa:Address', namespaces)
|
|
||||||
if addr is None:
|
|
||||||
logger.debug('invalid resolve request: missing endpoint address')
|
|
||||||
return None
|
|
||||||
|
|
||||||
if addr.text not in args.urn2uuid:
|
|
||||||
logger.debug(('invalid resolve request: address ({}) does not match '
|
|
||||||
'own one ({})').format(addr.text, args.uuid))
|
|
||||||
return None
|
|
||||||
uuid_ = args.urn2uuid[addr.text]
|
|
||||||
matches = ElementTree.Element('wsd:ResolveMatches')
|
|
||||||
match = ElementTree.SubElement(matches, 'wsd:ResolveMatch')
|
|
||||||
wsd_add_endpoint_reference(uuid_, match)
|
|
||||||
wsd_add_types(match)
|
|
||||||
wsd_add_xaddr(uuid_, match, xaddr)
|
|
||||||
wsd_add_metadata_version(match)
|
|
||||||
|
|
||||||
return matches
|
|
||||||
|
|
||||||
|
|
||||||
def wsd_handle_get(uuid_, hostname):
|
|
||||||
# see https://msdn.microsoft.com/en-us/library/hh441784.aspx for an example
|
|
||||||
metadata = ElementTree.Element('wsx:Metadata')
|
|
||||||
section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {
|
|
||||||
'Dialect': WSDP_URI + '/ThisDevice'})
|
|
||||||
device = ElementTree.SubElement(section, 'wsdp:ThisDevice')
|
|
||||||
ElementTree.SubElement(device, 'wsdp:FriendlyName').text = (
|
|
||||||
'WSD Device {0}'.format(hostname))
|
|
||||||
ElementTree.SubElement(device, 'wsdp:FirmwareVersion').text = '1.0'
|
|
||||||
ElementTree.SubElement(device, 'wsdp:SerialNumber').text = '1'
|
|
||||||
|
|
||||||
section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {
|
|
||||||
'Dialect': WSDP_URI + '/ThisModel'})
|
|
||||||
model = ElementTree.SubElement(section, 'wsdp:ThisModel')
|
|
||||||
ElementTree.SubElement(model, 'wsdp:Manufacturer').text = 'wsdd'
|
|
||||||
ElementTree.SubElement(model, 'wsdp:ModelName').text = 'wsdd'
|
|
||||||
ElementTree.SubElement(model, 'pnpx:DeviceCategory').text = 'Computers'
|
|
||||||
|
|
||||||
section = ElementTree.SubElement(metadata, 'wsx:MetadataSection', {
|
|
||||||
'Dialect': WSDP_URI + '/Relationship'})
|
|
||||||
rel = ElementTree.SubElement(section, 'wsdp:Relationship', {
|
|
||||||
'Type': WSDP_URI + '/host'})
|
|
||||||
host = ElementTree.SubElement(rel, 'wsdp:Host')
|
|
||||||
wsd_add_endpoint_reference(uuid_, host)
|
|
||||||
ElementTree.SubElement(host, 'wsdp:Types').text = PUB_COMPUTER
|
|
||||||
ElementTree.SubElement(host, 'wsdp:ServiceId').text = uuid_.urn
|
|
||||||
if args.domain:
|
|
||||||
ElementTree.SubElement(host, PUB_COMPUTER).text = (
|
|
||||||
'{0}/Domain:{1}'.format(
|
|
||||||
hostname if args.preserve_case else hostname.lower(),
|
|
||||||
args.domain))
|
|
||||||
else:
|
|
||||||
ElementTree.SubElement(host, PUB_COMPUTER).text = (
|
|
||||||
'{0}/Workgroup:{1}'.format(
|
|
||||||
hostname if args.preserve_case else hostname.upper(),
|
|
||||||
args.workgroup.upper()))
|
|
||||||
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
|
|
||||||
def wsd_is_duplicated_msg(msg_id):
|
|
||||||
"""
|
|
||||||
Check for a duplicated message.
|
|
||||||
|
|
||||||
Implements SOAP-over-UDP Appendix II Item 2
|
|
||||||
"""
|
|
||||||
if msg_id in wsd_known_messages:
|
|
||||||
return True
|
|
||||||
|
|
||||||
wsd_known_messages.append(msg_id)
|
|
||||||
if len(wsd_known_messages) > WSD_MAX_KNOWN_MESSAGES:
|
|
||||||
wsd_known_messages.popleft()
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def wsd_handle_message(data, interface, src_address):
|
|
||||||
"""
|
|
||||||
handle a WSD message that might be received by a MulticastInterface class
|
|
||||||
"""
|
|
||||||
tree = ElementTree.fromstring(data)
|
|
||||||
header = tree.find('./soap:Header', namespaces)
|
|
||||||
msg_id = header.find('./wsa:MessageID', namespaces).text
|
|
||||||
urn = header.find('./wsa:To', namespaces).text
|
|
||||||
try:
|
|
||||||
uuid_ = args.urn2uuid[urn]
|
|
||||||
except KeyError:
|
|
||||||
if urn in ['urn:schemas-xmlsoap-org:ws:2005:04:discovery']:
|
|
||||||
urn = None
|
|
||||||
else:
|
|
||||||
logger.debug('Go unknown wsa:To: {}'.format(data))
|
|
||||||
|
|
||||||
# if message came over multicast interface, check for duplicates
|
|
||||||
if interface and wsd_is_duplicated_msg(msg_id):
|
|
||||||
logger.debug('known message ({0}): dropping it'.format(msg_id))
|
|
||||||
return []
|
|
||||||
|
|
||||||
response = None
|
|
||||||
action = header.find('./wsa:Action', namespaces).text
|
|
||||||
body = tree.find('./soap:Body', namespaces)
|
|
||||||
_, _, action_method = action.rpartition('/')
|
|
||||||
|
|
||||||
if interface:
|
|
||||||
logger.info('{}:{}({}) - - "{} {} UDP" - -'.format(
|
|
||||||
src_address[0], src_address[1], interface.interface,
|
|
||||||
action_method, msg_id
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
# http logging is already done by according server
|
|
||||||
logger.debug('processing WSD {} message ({})'.format(
|
|
||||||
action_method, msg_id))
|
|
||||||
|
|
||||||
logger.debug('incoming message content is {0}'.format(data))
|
|
||||||
if action == WSD_PROBE:
|
|
||||||
probe = body.find('./wsd:Probe', namespaces)
|
|
||||||
responses = []
|
|
||||||
for uuid_ in args.uuid:
|
|
||||||
response = wsd_handle_probe(uuid_, probe)
|
|
||||||
if response:
|
|
||||||
responses.append(
|
|
||||||
wsd_build_message(WSA_ANON, WSD_PROBE_MATCH, header, response)
|
|
||||||
)
|
|
||||||
return responses
|
|
||||||
elif action == WSD_RESOLVE:
|
|
||||||
resolve = body.find('./wsd:Resolve', namespaces)
|
|
||||||
response = wsd_handle_resolve(resolve, interface.transport_address)
|
|
||||||
if response:
|
|
||||||
return [wsd_build_message(WSA_ANON, WSD_RESOLVE_MATCH, header, response)]
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
elif action == WSD_GET:
|
|
||||||
uuid_ = args.urn2uuid[urn]
|
|
||||||
hostname = args.uuid2hostname[str(uuid_)]
|
|
||||||
return [wsd_build_message(
|
|
||||||
WSA_ANON,
|
|
||||||
WSD_GET_RESPONSE,
|
|
||||||
header,
|
|
||||||
wsd_handle_get(uuid_, hostname))]
|
|
||||||
else:
|
|
||||||
logger.debug('unhandled action {0}/{1}'.format(action, msg_id))
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class WSDUdpRequestHandler():
|
|
||||||
"""Class for handling WSD requests coming from UDP datagrams."""
|
|
||||||
def __init__(self, interface):
|
|
||||||
self.interface = interface
|
|
||||||
|
|
||||||
def handle_request(self):
|
|
||||||
msg, address = self.interface.recv_socket.recvfrom(WSD_MAX_LEN)
|
|
||||||
msgs = wsd_handle_message(msg, self.interface, address)
|
|
||||||
for msg in msgs:
|
|
||||||
self.enqueue_datagram(msg, address=address)
|
|
||||||
|
|
||||||
def send_hello(self, uuid_):
|
|
||||||
"""WS-Discovery, Section 4.1, Hello message"""
|
|
||||||
hello = ElementTree.Element('wsd:Hello')
|
|
||||||
wsd_add_endpoint_reference(uuid_, hello)
|
|
||||||
# THINK: Microsoft does not send the transport address here due
|
|
||||||
# to privacy reasons. Could make this optional.
|
|
||||||
wsd_add_xaddr(uuid_, hello, self.interface.transport_address)
|
|
||||||
wsd_add_metadata_version(hello)
|
|
||||||
|
|
||||||
msg = wsd_build_message(WSA_DISCOVERY, WSD_HELLO, None, hello)
|
|
||||||
self.enqueue_datagram(msg, msg_type='Hello')
|
|
||||||
|
|
||||||
def send_bye(self, uuid_):
|
|
||||||
"""WS-Discovery, Section 4.2, Bye message"""
|
|
||||||
bye = ElementTree.Element('wsd:Bye')
|
|
||||||
wsd_add_endpoint_reference(uuid_, bye)
|
|
||||||
|
|
||||||
msg = wsd_build_message(WSA_DISCOVERY, WSD_BYE, None, bye)
|
|
||||||
self.enqueue_datagram(msg, msg_type='Bye')
|
|
||||||
|
|
||||||
def enqueue_datagram(self, msg, address=None, msg_type=None):
|
|
||||||
"""
|
|
||||||
Add an outgoing WSD (SOAP) message to the queue of outstanding messages
|
|
||||||
|
|
||||||
Implements SOAP over UDP, Appendix I.
|
|
||||||
"""
|
|
||||||
if not address:
|
|
||||||
address = self.interface.multicast_address
|
|
||||||
|
|
||||||
if msg_type:
|
|
||||||
logger.debug('scheduling {0} message via {1} to {2}'.format(
|
|
||||||
msg_type, self.interface.interface, address))
|
|
||||||
|
|
||||||
msg_count = (
|
|
||||||
MULTICAST_UDP_REPEAT
|
|
||||||
if address == self.interface.multicast_address
|
|
||||||
else UNICAST_UDP_REPEAT)
|
|
||||||
|
|
||||||
due_time = time.time()
|
|
||||||
t = random.randint(UDP_MIN_DELAY, UDP_MAX_DELAY)
|
|
||||||
for i in range(msg_count):
|
|
||||||
send_queue.append([due_time, self.interface, address, msg])
|
|
||||||
due_time += t / 1000
|
|
||||||
t = min(t * 2, UDP_UPPER_DELAY)
|
|
||||||
|
|
||||||
|
|
||||||
class WSDHttpRequestHandler(http.server.BaseHTTPRequestHandler):
|
|
||||||
"""Class for handling WSD requests coming over HTTP"""
|
|
||||||
def log_message(self, fmt, *args):
|
|
||||||
logger.info("{} - - ".format(self.address_string()) + fmt % args)
|
|
||||||
|
|
||||||
def do_POST(s):
|
|
||||||
uuid_ = s.path.strip('/')
|
|
||||||
if not uuid_ in args.uuid2hostname:
|
|
||||||
s.send_error(404)
|
|
||||||
|
|
||||||
if s.headers['Content-Type'] != 'application/soap+xml':
|
|
||||||
s.send_error(400, 'Invalid Content-Type')
|
|
||||||
|
|
||||||
content_length = int(s.headers['Content-Length'])
|
|
||||||
body = s.rfile.read(content_length)
|
|
||||||
|
|
||||||
responses = wsd_handle_message(body, None, None)
|
|
||||||
if len(responses) == 1:
|
|
||||||
s.send_response(200)
|
|
||||||
s.send_header('Content-Type', 'application/soap+xml')
|
|
||||||
s.end_headers()
|
|
||||||
s.wfile.write(responses[0])
|
|
||||||
else:
|
|
||||||
s.send_error(500)
|
|
||||||
|
|
||||||
|
|
||||||
def enumerate_host_interfaces():
|
|
||||||
"""Get all addresses of all installed interfaces, except loopback"""
|
|
||||||
libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
|
|
||||||
addr = ctypes.POINTER(if_addrs)()
|
|
||||||
retval = libc.getifaddrs(ctypes.byref(addr))
|
|
||||||
if retval:
|
|
||||||
raise OSError(ctypes.get_errno())
|
|
||||||
|
|
||||||
addrs = []
|
|
||||||
ptr = addr
|
|
||||||
while ptr:
|
|
||||||
deref = ptr[0]
|
|
||||||
family = deref.addr[0].family if deref.addr else None
|
|
||||||
if family == socket.AF_INET:
|
|
||||||
addrs.append((
|
|
||||||
deref.name.decode(), family,
|
|
||||||
socket.inet_ntop(family, bytes(deref.addr[0].data[2:6]))))
|
|
||||||
elif family == socket.AF_INET6:
|
|
||||||
if bytes(deref.addr[0].data[6:8]) == b'\xfe\x80':
|
|
||||||
addrs.append((
|
|
||||||
deref.name.decode(), family,
|
|
||||||
socket.inet_ntop(family, bytes(deref.addr[0].data[6:22]))))
|
|
||||||
|
|
||||||
ptr = deref.next
|
|
||||||
|
|
||||||
libc.freeifaddrs(addr)
|
|
||||||
|
|
||||||
# filter detected addresses by command line arguments,
|
|
||||||
# always exclude 'lo' interface
|
|
||||||
if args.ipv4only:
|
|
||||||
addrs = [x for x in addrs if x[1] == socket.AF_INET]
|
|
||||||
|
|
||||||
if args.ipv6only:
|
|
||||||
addrs = [x for x in addrs if x[1] == socket.AF_INET6]
|
|
||||||
|
|
||||||
addrs = [x for x in addrs if not x[0].startswith('lo')]
|
|
||||||
if args.interface:
|
|
||||||
addrs = [x for x in addrs if x[0] in args.interface]
|
|
||||||
|
|
||||||
return addrs
|
|
||||||
|
|
||||||
|
|
||||||
def sigterm_handler(signum, frame):
|
|
||||||
if signum == signal.SIGTERM:
|
|
||||||
logger.info('received SIGTERM, tearing down')
|
|
||||||
# implictely raise SystemExit to cleanup properly
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
|
||||||
global args, logger
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'-i', '--interface',
|
|
||||||
help='interface address to use',
|
|
||||||
action='append', default=[])
|
|
||||||
parser.add_argument(
|
|
||||||
'-H', '--hoplimit',
|
|
||||||
help='hop limit for multicast packets (default = 1)', type=int,
|
|
||||||
default=1)
|
|
||||||
parser.add_argument(
|
|
||||||
'-u', '--uuid',
|
|
||||||
help='UUID for the target device',
|
|
||||||
default=None,
|
|
||||||
nargs='*'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-v', '--verbose',
|
|
||||||
help='increase verbosity',
|
|
||||||
action='count', default=0)
|
|
||||||
parser.add_argument(
|
|
||||||
'-d', '--domain',
|
|
||||||
help='set domain name (disables workgroup)',
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-n', '--hostname',
|
|
||||||
help='override (NetBIOS) hostname to be used (default hostname)',
|
|
||||||
nargs='*')
|
|
||||||
parser.add_argument(
|
|
||||||
'-w', '--workgroup',
|
|
||||||
help='set workgroup name (default WORKGROUP)',
|
|
||||||
default='WORKGROUP')
|
|
||||||
parser.add_argument(
|
|
||||||
'-t', '--nohttp',
|
|
||||||
help='disable http service (for debugging, e.g.)',
|
|
||||||
action='store_true')
|
|
||||||
parser.add_argument(
|
|
||||||
'-4', '--ipv4only',
|
|
||||||
help='use only IPv4 (default = off)',
|
|
||||||
action='store_true')
|
|
||||||
parser.add_argument(
|
|
||||||
'-6', '--ipv6only',
|
|
||||||
help='use IPv6 (default = off)',
|
|
||||||
action='store_true')
|
|
||||||
parser.add_argument(
|
|
||||||
'-s', '--shortlog',
|
|
||||||
help='log only level and message',
|
|
||||||
action='store_true')
|
|
||||||
parser.add_argument(
|
|
||||||
'-p', '--preserve-case',
|
|
||||||
help='preserve case of the provided/detected hostname',
|
|
||||||
action='store_true')
|
|
||||||
|
|
||||||
args = parser.parse_args(sys.argv[1:])
|
|
||||||
|
|
||||||
if args.verbose == 1:
|
|
||||||
log_level = logging.INFO
|
|
||||||
elif args.verbose > 1:
|
|
||||||
log_level = logging.DEBUG
|
|
||||||
else:
|
|
||||||
log_level = logging.WARNING
|
|
||||||
|
|
||||||
if args.shortlog:
|
|
||||||
fmt = '%(levelname)s: %(message)s'
|
|
||||||
else:
|
|
||||||
fmt = ('%(asctime)s:%(name)s %(levelname)s(pid %(process)d): '
|
|
||||||
'%(message)s')
|
|
||||||
|
|
||||||
logging.basicConfig(level=log_level, format=fmt)
|
|
||||||
logger = logging.getLogger('wsdd')
|
|
||||||
|
|
||||||
if not args.interface:
|
|
||||||
logger.warning('no interface given, using all interfaces')
|
|
||||||
|
|
||||||
if not args.hostname:
|
|
||||||
args.hostname.append(socket.gethostname().partition('.')[0])
|
|
||||||
|
|
||||||
if not args.uuid:
|
|
||||||
args.uuid = [uuid.uuid5(uuid.NAMESPACE_DNS, hostname) for hostname in args.hostname]
|
|
||||||
logger.info('using pre-defined UUID {0}'.format(str(args.uuid)))
|
|
||||||
else:
|
|
||||||
if len(args.uuid) != len(args.hostname):
|
|
||||||
raise ValueError("please give the same number of uuid and hostname")
|
|
||||||
args.uuid = [uuid.UUID(uuid_) for uuid_ in args.uuid]
|
|
||||||
logger.info('user-supplied device UUID is {0}'.format(str(args.uuid)))
|
|
||||||
args.uuid2hostname = {str(uuid_): hostname for uuid_, hostname in zip(args.uuid, args.hostname)}
|
|
||||||
args.urn2uuid = {uuid_.urn: uuid_ for uuid_ in args.uuid}
|
|
||||||
args.hostname2uuid = {hostname: uuid_ for uuid_, hostname in zip(args.uuid, args.hostname)}
|
|
||||||
|
|
||||||
for prefix, uri in namespaces.items():
|
|
||||||
ElementTree.register_namespace(prefix, uri)
|
|
||||||
|
|
||||||
|
|
||||||
def send_outstanding_messages(block=False):
|
|
||||||
"""
|
|
||||||
Send all queued datagrams for which the timeout has been reached. If block
|
|
||||||
is true then all queued messages will be sent but with their according
|
|
||||||
delay.
|
|
||||||
"""
|
|
||||||
if len(send_queue) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# reverse ordering for faster removal of the last element
|
|
||||||
send_queue.sort(key=lambda x: x[0], reverse=True)
|
|
||||||
|
|
||||||
# Emit every message that is "too late". Note that in case the system
|
|
||||||
# time jumps forward, multiple outstanding message which have a
|
|
||||||
# delay between them are sent out without that delay.
|
|
||||||
now = time.time()
|
|
||||||
while len(send_queue) > 0 and (send_queue[-1][0] <= now or block):
|
|
||||||
interface = send_queue[-1][1]
|
|
||||||
addr = send_queue[-1][2]
|
|
||||||
msg = send_queue[-1][3]
|
|
||||||
try:
|
|
||||||
interface.send_socket.sendto(msg, addr)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error('error while sending packet on {}: {}'.format(
|
|
||||||
interface.interface, e))
|
|
||||||
|
|
||||||
del send_queue[-1]
|
|
||||||
if block and len(send_queue) > 0:
|
|
||||||
delay = send_queue[-1][0] - now
|
|
||||||
if delay > 0:
|
|
||||||
time.sleep(delay)
|
|
||||||
now = time.time()
|
|
||||||
|
|
||||||
if len(send_queue) > 0 and not block:
|
|
||||||
return send_queue[-1][0] - now
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def serve_wsd_requests(addresses):
|
|
||||||
"""
|
|
||||||
Multicast handling: send Hello message on startup, receive from multicast
|
|
||||||
sockets and handle the messages, and emit Bye message when process gets
|
|
||||||
terminated by signal
|
|
||||||
"""
|
|
||||||
s = selectors.DefaultSelector()
|
|
||||||
udp_srvs = []
|
|
||||||
|
|
||||||
for address in addresses:
|
|
||||||
interface = MulticastInterface(address[1], address[2], address[0])
|
|
||||||
udp_srv = WSDUdpRequestHandler(interface)
|
|
||||||
udp_srvs.append(udp_srv)
|
|
||||||
s.register(interface.recv_socket, selectors.EVENT_READ, udp_srv)
|
|
||||||
|
|
||||||
if not args.nohttp:
|
|
||||||
klass = (
|
|
||||||
http.server.HTTPServer
|
|
||||||
if interface.family == socket.AF_INET
|
|
||||||
else HTTPv6Server)
|
|
||||||
http_srv = klass(interface.listen_address, WSDHttpRequestHandler)
|
|
||||||
s.register(http_srv.fileno(), selectors.EVENT_READ, http_srv)
|
|
||||||
|
|
||||||
# everything is set up, announce ourself and serve requests
|
|
||||||
try:
|
|
||||||
for srv in udp_srvs:
|
|
||||||
for uuid_ in args.uuid:
|
|
||||||
srv.send_hello(uuid_)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
timeout = send_outstanding_messages()
|
|
||||||
events = s.select(timeout)
|
|
||||||
for key, mask in events:
|
|
||||||
key.data.handle_request()
|
|
||||||
except (SystemExit, KeyboardInterrupt):
|
|
||||||
# silently exit the loop
|
|
||||||
logger.debug('got termination signal')
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
logger.exception('error in main loop')
|
|
||||||
finally:
|
|
||||||
logger.info('shutting down gracefully...')
|
|
||||||
|
|
||||||
# say goodbye
|
|
||||||
for srv in udp_srvs:
|
|
||||||
for uuid_ in args.uuid:
|
|
||||||
srv.send_bye(uuid_)
|
|
||||||
|
|
||||||
send_outstanding_messages(True)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parse_args()
|
|
||||||
|
|
||||||
addresses = enumerate_host_interfaces()
|
|
||||||
if not addresses:
|
|
||||||
logger.error("No multicast addresses available. Exiting.")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, sigterm_handler)
|
|
||||||
serve_wsd_requests(addresses)
|
|
||||||
logger.info('Done.')
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main())
|
|
Loading…
Reference in a new issue