Compare commits

..

13 commits

Author SHA1 Message Date
1abbc5bb11 Aggiorna README.md 2025-01-16 21:22:20 +08:00
bd04b77d2f Aggiorna Dockerfile 2025-01-16 21:15:11 +08:00
c0c7adc7cb improved with multi hosts 2025-01-16 19:27:48 +08:00
612f8822e0 Aggiorna README.md 2025-01-16 15:39:22 +08:00
5f79b77461 Aggiorna README.md 2025-01-16 15:38:45 +08:00
061eec7d7e Aggiorna README.md 2025-01-16 15:36:46 +08:00
038a399e70 Aggiorna README.md 2025-01-16 15:35:25 +08:00
95fed5f3f3 Aggiorna README.md 2025-01-16 15:29:54 +08:00
Jonas Pedersen
8db71452a8
Merge pull request #1 from shimikano/master
Introduce WSDD_ARGS to directly pass wsdd arguments.
2022-06-07 21:30:02 +02:00
Jonas Pedersen
227e219f39
Merge branch 'master' into master 2022-06-07 21:25:05 +02:00
Jonas Pedersen
121dc9a59b
Merge pull request #2 from edzius/master
Bugfix wsdd args concat
2022-06-07 21:21:29 +02:00
Edvinas
5846cd7ef0
Bugfix wsdd args concat
Using brackets in string concatenation has special meaning
for for bash thus later argument never gets appended. Use
simple strings concat instead fixes problem.
2020-05-20 23:54:14 +03:00
shimikano
4ace370a40 Introduce WSDD_ARGS to directly pass wsdd arguments. 2020-02-19 17:22:24 +01:00
4 changed files with 839 additions and 24 deletions

View file

@ -1,9 +1,10 @@
FROM python:3-alpine
WORKDIR /usr/src/app
RUN apk add --no-cache curl bash && curl https://raw.githubusercontent.com/christgau/wsdd/v0.7.0/src/wsdd.py -o wsdd.py && apk del curl
copy docker-cmd.sh .
RUN apk update
RUN apk upgrade
RUN apk add bash
COPY wsdd.py .
COPY docker-cmd.sh .
CMD [ "./docker-cmd.sh"]

View file

@ -1,14 +1,40 @@
# 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
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.
## 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
### From command line

View file

@ -1,23 +1,24 @@
#!/usr/bin/env bash
args=( )
args=
if [ ! -z "${WSDD_ARGS}" ]; then
args=${WSDD_ARGS}
else
if [ ! -z "${HOSTNAME}" ]; then
args+=( "-n $HOSTNAME ")
args+="-n $HOSTNAME "
else
echo "HOSTNAME environment variable must be set."
exit 1
fi
if [ ! -z "${WORKGROUP}" ]; then
args+=( "-w $WORKGROUP " )
args+="-w $WORKGROUP "
fi
if [ ! -z "${DOMAIN}" ]; then
args+=( "-d $DOMAIN " )
args+="-d $DOMAIN "
fi
fi
exec python wsdd.py ${args}

787
wsdd.py Normal file
View file

@ -0,0 +1,787 @@
#!/usr/bin/env python3
# Implements a target service according to the Web Service Discovery
# specification.
#
# The purpose is to enable non-Windows devices to be found by the 'Network
# (Neighborhood)' from Windows machines.
#
# see http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf and
# related documents for details (look at README for more references)
#
# (c) Steffen Christgau, 2017-2019
import sys
import signal
import socket
import selectors
import struct
import argparse
import uuid
import time
import random
import logging
import platform
import ctypes.util
import collections
import xml.etree.ElementTree as ElementTree
import http.server
# sockaddr C type, with a larger data field to capture IPv6 addresses
# unfortunately, the structures differ on Linux and FreeBSD
if platform.system() == 'Linux':
class sockaddr(ctypes.Structure):
_fields_ = [('family', ctypes.c_uint16),
('data', ctypes.c_uint8 * 24)]
else:
class sockaddr(ctypes.Structure):
_fields_ = [('length', ctypes.c_uint8),
('family', ctypes.c_uint8),
('data', ctypes.c_uint8 * 24)]
class if_addrs(ctypes.Structure):
pass
if_addrs._fields_ = [('next', ctypes.POINTER(if_addrs)),
('name', ctypes.c_char_p),
('flags', ctypes.c_uint),
('addr', ctypes.POINTER(sockaddr)),
('netmask', ctypes.POINTER(sockaddr))]
class HTTPv6Server(http.server.HTTPServer):
"""Simple HTTP server with IPv6 support"""
address_family = socket.AF_INET6
def server_bind(self):
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
super().server_bind()
class MulticastInterface:
"""
A class for handling multicast traffic on a given interface for a
given address family. It provides multicast sender and receiver sockets
"""
def __init__(self, family, address, intf_name):
self.address = address
self.family = family
self.interface = intf_name
self.recv_socket = socket.socket(self.family, socket.SOCK_DGRAM)
self.recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.send_socket = socket.socket(self.family, socket.SOCK_DGRAM)
self.transport_address = address
self.multicast_address = None
self.listen_address = None
if family == socket.AF_INET:
self.init_v4()
elif family == socket.AF_INET6:
self.init_v6()
logger.info('joined multicast group {0} on {2}%{1}'.format(
self.multicast_address, self.interface, self.address))
logger.debug('transport address on {0} is {1}'.format(
self.interface, self.transport_address))
logger.debug('will listen for HTTP traffic on address {0}'.format(
self.listen_address))
def init_v6(self):
idx = socket.if_nametoindex(self.interface)
self.multicast_address = (WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0x575C, idx)
# v6: member_request = { multicast_addr, intf_idx }
mreq = (
socket.inet_pton(self.family, WSD_MCAST_GRP_V6) +
struct.pack('@I', idx))
self.recv_socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
self.recv_socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
# bind to network interface, i.e. scope and handle OS differences,
# see Stevens: Unix Network Programming, Section 21.6, last paragraph
try:
self.recv_socket.bind((WSD_MCAST_GRP_V6, WSD_UDP_PORT, 0, idx))
except OSError:
self.recv_socket.bind(('::', 0, 0, idx))
self.send_socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0)
self.send_socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, args.hoplimit)
self.send_socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
self.transport_address = '[{0}]'.format(self.address)
self.listen_address = (self.address, WSD_HTTP_PORT, 0, idx)
def init_v4(self):
idx = socket.if_nametoindex(self.interface)
self.multicast_address = (WSD_MCAST_GRP_V4, WSD_UDP_PORT)
# v4: member_request (ip_mreqn) = { multicast_addr, intf_addr, idx }
mreq = (
socket.inet_pton(self.family, WSD_MCAST_GRP_V4) +
socket.inet_pton(self.family, self.address) +
struct.pack('@I', idx))
self.recv_socket.setsockopt(
socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
try:
self.recv_socket.bind((WSD_MCAST_GRP_V4, WSD_UDP_PORT))
except OSError:
self.recv_socket.bind(('', WSD_UDP_PORT))
self.send_socket.setsockopt(
socket.IPPROTO_IP, socket.IP_MULTICAST_IF, mreq)
self.send_socket.setsockopt(
socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0)
self.send_socket.setsockopt(
socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, args.hoplimit)
self.listen_address = (self.address, WSD_HTTP_PORT)
# constants for WSD XML/SOAP parsing
WSA_URI = 'http://schemas.xmlsoap.org/ws/2004/08/addressing'
WSD_URI = 'http://schemas.xmlsoap.org/ws/2005/04/discovery'
WSDP_URI = 'http://schemas.xmlsoap.org/ws/2006/02/devprof'
namespaces = {
'soap': 'http://www.w3.org/2003/05/soap-envelope',
'wsa': WSA_URI,
'wsd': WSD_URI,
'wsx': 'http://schemas.xmlsoap.org/ws/2004/09/mex',
'wsdp': WSDP_URI,
'pnpx': 'http://schemas.microsoft.com/windows/pnpx/2005/10',
'pub': 'http://schemas.microsoft.com/windows/pub/2005/07'
}
WSD_MAX_KNOWN_MESSAGES = 10
WSD_PROBE = WSD_URI + '/Probe'
WSD_PROBE_MATCH = WSD_URI + '/ProbeMatches'
WSD_RESOLVE = WSD_URI + '/Resolve'
WSD_RESOLVE_MATCH = WSD_URI + '/ResolveMatches'
WSD_HELLO = WSD_URI + '/Hello'
WSD_BYE = WSD_URI + '/Bye'
WSD_GET = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Get'
WSD_GET_RESPONSE = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse'
WSD_TYPE_DEVICE = 'wsdp:Device'
PUB_COMPUTER = 'pub:Computer'
WSD_TYPE_DEVICE_COMPUTER = '{0} {1}'.format(WSD_TYPE_DEVICE, PUB_COMPUTER)
WSD_MCAST_GRP_V4 = '239.255.255.250'
WSD_MCAST_GRP_V6 = 'ff02::c' # link-local
WSA_ANON = WSA_URI + '/role/anonymous'
WSA_DISCOVERY = 'urn:schemas-xmlsoap-org:ws:2005:04:discovery'
# protocol assignments (WSD spec/Section 2.4)
WSD_UDP_PORT = 3702
WSD_HTTP_PORT = 5357
WSD_MAX_LEN = 32767
# SOAP/UDP transmission constants
MULTICAST_UDP_REPEAT = 4
UNICAST_UDP_REPEAT = 2
UDP_MIN_DELAY = 50
UDP_MAX_DELAY = 250
UDP_UPPER_DELAY = 500
# some globals
wsd_known_messages = collections.deque([])
wsd_message_number = 1
wsd_instance_id = int(time.time())
send_queue = []
args = None
logger = None
# shortcuts for building WSD responses
def wsd_add_metadata_version(parent):
meta_data = ElementTree.SubElement(parent, 'wsd:MetadataVersion')
meta_data.text = '1'
def wsd_add_types(parent):
dev_type = ElementTree.SubElement(parent, 'wsd:Types')
dev_type.text = WSD_TYPE_DEVICE_COMPUTER
def wsd_add_endpoint_reference(uuid_, parent):
endpoint = ElementTree.SubElement(parent, 'wsa:EndpointReference')
address = ElementTree.SubElement(endpoint, 'wsa:Address')
address.text = uuid_.urn
def wsd_add_xaddr(uuid_, parent, transport_addr):
if transport_addr:
item = ElementTree.SubElement(parent, 'wsd:XAddrs')
item.text = 'http://{0}:{1}/{2}'.format(
transport_addr, WSD_HTTP_PORT, uuid_)
def wsd_build_message(to_addr, action_str, request_header, response):
"""
Build a WSD message with a given action string including SOAP header.
The message can be constructed based on a response to another
message (given by its header) and with a optional response that
serves as the message's body
"""
global wsd_message_number
env = ElementTree.Element('soap:Envelope')
header = ElementTree.SubElement(env, 'soap:Header')
to = ElementTree.SubElement(header, 'wsa:To')
to.text = to_addr
action = ElementTree.SubElement(header, 'wsa:Action')
action.text = action_str
msg_id = ElementTree.SubElement(header, 'wsa:MessageID')
msg_id.text = uuid.uuid1().urn
if request_header:
req_msg_id = request_header.find('./wsa:MessageID', namespaces)
if req_msg_id is not None:
relates_to = ElementTree.SubElement(header, 'wsa:RelatesTo')
relates_to.text = req_msg_id.text
seq = ElementTree.SubElement(header, 'wsd:AppSequence', {
'InstanceId': str(wsd_instance_id),
'SequenceId': uuid.uuid1().urn,
'MessageNumber': str(wsd_message_number)})
wsd_message_number = wsd_message_number + 1
body = ElementTree.SubElement(env, 'soap:Body')
body.append(response)
for prefix, uri in namespaces.items():
env.attrib['xmlns:' + prefix] = uri
xml = b'<?xml 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())