# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php
# Copyright 2005, Tim Potter <tpot@samba.org>
# Copyright 2006 John-Mark Gurney <gurney_j@resnet.uroegon.edu>
# Copyright (C) 2006 Fluendo, S.A. (www.fluendo.com).
# Copyright 2006,2007,2008,2009 Frank Scholz <coherence@beebits.net>
#
# Implementation of a SSDP server under Twisted Python.
#
import random
import socket
import time
from twisted.internet import reactor
from twisted.internet import task
from twisted.internet.protocol import DatagramProtocol
from twisted.web.http import datetimeToString
from twisted.test import proto_helpers
import coherence.extern.louie as louie
from coherence import log, SERVER_ID
SSDP_PORT = 1900
SSDP_ADDR = '239.255.255.250'
[docs]class SSDPServer(DatagramProtocol, log.LogAble):
"""A class implementing a SSDP server. The notifyReceived and
searchReceived methods are called when the appropriate type of
datagram is received by the server."""
logCategory = 'ssdp'
def __init__(self, test=False, interface=''):
# Create SSDP server
log.LogAble.__init__(self)
self.known = {}
self._callbacks = {}
self.test = test
if not self.test:
self.port = reactor.listenMulticast(
SSDP_PORT, self,
listenMultiple=True,
interface=interface)
self.port.joinGroup(SSDP_ADDR, interface=interface)
self.resend_notify_loop = task.LoopingCall(self.resendNotify)
self.resend_notify_loop.start(777.0, now=False)
self.check_valid_loop = task.LoopingCall(self.check_valid)
self.check_valid_loop.start(333.0, now=False)
self.active_calls = []
[docs] def shutdown(self):
for call in reactor.getDelayedCalls():
if call.func == self.send_it:
call.cancel()
if not self.test:
if self.resend_notify_loop.running:
self.resend_notify_loop.stop()
if self.check_valid_loop.running:
self.check_valid_loop.stop()
'''Make sure we send out the byebye notifications.'''
for st in self.known:
if self.known[st]['MANIFESTATION'] == 'local':
self.doByebye(st)
[docs] def datagramReceived(self, data, xxx_todo_changeme):
"""Handle a received multicast datagram."""
self.debug('datagramReceived: {}'.format(data))
(host, port) = xxx_todo_changeme
if isinstance(data, bytes):
data = data.decode('utf-8')
try:
header, payload = data.split('\r\n\r\n')[:2]
except ValueError as err:
print(err)
print('Arggg,', data)
import pdb
pdb.set_trace()
lines = header.split('\r\n')
cmd = lines[0].split(' ')
lines = [x.replace(': ', ':', 1) for x in lines[1:]]
lines = [x for x in lines if len(x) > 0]
# TODO: Find and fix where some of the header's keys are quoted.
# This hack, allows to fix the quoted keys for the headers, introduced
# at some point of the source code. I notice that the issue appears
# when using FSStore plugin. But where?
def fix_string(s, to_lower=True):
for q in ["'", "\""]:
while s.startswith(q):
s = s[1:]
for q in ["'", "\""]:
while s.endswith(q):
s = s[:-1]
if to_lower:
s = s.lower()
return s
headers = [x.split(':', 1) for x in lines]
headers = \
dict([(fix_string(x[0]),
fix_string(x[1], to_lower=False)) for x in headers])
self.msg('SSDP command {} {} - from {}:{}'.format(
cmd[0], cmd[1], host, port))
self.debug('with headers: {}'.format(headers))
if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
# SSDP discovery
self.discoveryRequest(headers, (host, port))
elif cmd[0] == 'NOTIFY' and cmd[1] == '*':
# SSDP presence
self.notifyReceived(headers, (host, port))
else:
self.warning('Unknown SSDP command {} {}'.format(cmd[0], cmd[1]))
# make raw data available
# send out the signal after we had a chance to register the device
louie.send('UPnP.SSDP.datagram_received', None, data, host, port)
[docs] def register(self, manifestation, usn, st, location,
server=SERVER_ID,
cache_control='max-age=1800',
silent=False,
host=None):
"""Register a service or device that this SSDP server will
respond to."""
self.info('Registering {} ({}) -> {}'.format(
st, location, manifestation))
self.debug('\t-searching usn: {}'.format(usn))
try:
self.known[usn] = {}
self.known[usn]['USN'] = usn
self.known[usn]['LOCATION'] = location
self.known[usn]['ST'] = st
self.known[usn]['EXT'] = ''
self.known[usn]['SERVER'] = server
self.known[usn]['CACHE-CONTROL'] = cache_control
self.known[usn]['MANIFESTATION'] = manifestation
self.known[usn]['SILENT'] = silent
self.known[usn]['HOST'] = host
self.known[usn]['last-seen'] = time.time()
self.msg(self.known[usn])
self.debug('\t-self.known: {}'.format(self.known))
if manifestation == 'local':
self.doNotify(usn)
if st == 'upnp:rootdevice':
louie.send(
'Coherence.UPnP.SSDP.new_device',
None, device_type=st, infos=self.known[usn])
# self.callback("new_device", st, self.known[usn])
# print('\t - ok all')
except Exception as err:
self.error('\t -> Error on registering service: '
'{} [error: "{}"]'.format(manifestation, err))
[docs] def unRegister(self, usn):
self.msg("Un-registering {}".format(usn))
st = self.known[usn]['ST']
if st == 'upnp:rootdevice':
louie.send(
'Coherence.UPnP.SSDP.removed_device',
None, device_type=st, infos=self.known[usn])
# self.callback("removed_device", st, self.known[usn])
del self.known[usn]
[docs] def isKnown(self, usn):
return usn in self.known
[docs] def notifyReceived(self, headers, xxx_todo_changeme1):
"""Process a presence announcement. We just remember the
details of the SSDP service announced."""
(host, port) = xxx_todo_changeme1
self.info('Notification from ({},{}) for {}'.format(
host, port, headers['nt']))
self.debug('Notification headers: {}'.format(headers))
if headers['nts'] == 'ssdp:alive':
try:
self.known[headers['usn']]['last-seen'] = time.time()
self.debug('updating last-seen for {}'.format(headers['usn']))
except KeyError:
self.register('remote', headers['usn'], headers['nt'],
headers['location'],
headers['server'], headers['cache-control'],
host=host)
elif headers['nts'] == 'ssdp:byebye':
if self.isKnown(headers['usn']):
self.unRegister(headers['usn'])
else:
self.warning('Unknown subtype {} for notification type {}'.format(
headers['nts'], headers['nt']))
louie.send('Coherence.UPnP.Log', None, 'SSDP', host,
'Notify %s for %s' % (headers['nts'], headers['usn']))
[docs] def send_it(self, response, destination, delay, usn):
self.info('send discovery response delayed by '
'{} for {} to {}'.format(delay, usn, destination))
r = response if isinstance(response, bytes) else \
response.encode('ascii')
d = destination if isinstance(destination, bytes) else \
destination.encode('ascii')
try:
self.transport.write(r, d)
except (AttributeError, socket.error) as msg:
self.info('failure sending out byebye notification: '
'{}'.format(msg))
[docs] def discoveryRequest(self, headers, xxx_todo_changeme2):
"""Process a discovery request. The response must be sent to
the address specified by (host, port)."""
(host, port) = xxx_todo_changeme2
self.info('Discovery request from ({},{}) for {}'.format(
host, port, headers['st']))
self.info('Discovery request for {}'.format(headers['st']))
louie.send(
'Coherence.UPnP.Log',
None, 'SSDP', host, 'M-Search for %s' % headers['st'])
# Do we know about this service?
for i in list(self.known.values()):
if i['MANIFESTATION'] == 'remote':
continue
if (headers['st'] == 'ssdp:all' and
i['SILENT'] is True):
continue
if (i['ST'] == headers['st'] or
headers['st'] == 'ssdp:all'):
response = []
response.append(b'HTTP/1.1 200 OK')
for k, v in list(i.items()):
if k == 'USN':
usn = v
if k not in ('MANIFESTATION', 'SILENT', 'HOST'):
response.append(b'%r: %r' % (k, v))
response.append(b'DATE: %r' % datetimeToString())
response.extend((b'', b''))
delay = random.randint(0, int(headers['mx']))
reactor.callLater(
delay, self.send_it, b'\r\n'.join(response),
(host, port), delay, usn)
[docs] def doNotify(self, usn):
"""Do notification"""
if self.known[usn]['SILENT'] is True:
return
self.info('Sending alive notification for {}'.format(usn))
# self.info('\t - self.known[usn]: {}'.format(self.known[usn]))
resp = ['NOTIFY * HTTP/1.1',
'HOST: %s:%d' % (SSDP_ADDR, SSDP_PORT),
'NTS: ssdp:alive',
]
stcpy = dict(iter(self.known[usn].items()))
stcpy['NT'] = stcpy['ST']
del stcpy['ST']
del stcpy['MANIFESTATION']
del stcpy['SILENT']
del stcpy['HOST']
del stcpy['last-seen']
resp.extend([
'%r: %r' % (k, v) for k, v in stcpy.items()])
resp.extend(('', ''))
r = '\r\n'.join(resp).encode('ascii')
self.debug('doNotify content {} [transport is: {}]'.format(
r, self.transport))
if not self.transport:
try:
self.warning('transport not initialized...'
'trying to initialize a FakeDatagramTransport')
self.transport = proto_helpers.FakeDatagramTransport()
except Exception as er:
self.error('Cannot initialize transport: {}'.format(er))
try:
self.transport.write(r, (SSDP_ADDR, SSDP_PORT))
except (AttributeError, socket.error) as msg:
self.info('failure sending out alive notification: {}'.format(msg))
[docs] def doByebye(self, usn):
"""Do byebye"""
self.info('Sending byebye notification for %s', usn)
resp = ['NOTIFY * HTTP/1.1',
'HOST: %r:%r' % (SSDP_ADDR, SSDP_PORT),
'NTS: ssdp:byebye',
]
try:
stcpy = dict(iter(self.known[usn].items()))
stcpy['NT'] = stcpy['ST']
del stcpy['ST']
del stcpy['MANIFESTATION']
del stcpy['SILENT']
del stcpy['HOST']
del stcpy['last-seen']
resp.extend([
'%r: %r' % (k, v) for k, v in stcpy.items()])
resp.extend(('', ''))
r = '\r\n'.join(resp).encode('ascii')
self.debug('doByebye content %s', resp)
if not self.transport:
self.warning('transport not initialized...'
'trying to initialize a FakeDatagramTransport')
self.transport = proto_helpers.FakeDatagramTransport()
self.makeConnection(self.transport)
try:
self.transport.write(r, (SSDP_ADDR, SSDP_PORT))
except (AttributeError, socket.error) as msg:
self.info(
"failure sending out byebye notification: %r", msg)
except KeyError as msg:
self.debug("error building byebye notification: %r", msg)
[docs] def resendNotify(self):
for usn in self.known:
if self.known[usn]['MANIFESTATION'] == 'local':
self.doNotify(usn)
[docs] def check_valid(self):
""" check if the discovered devices are still ok, or
if we haven't received a new discovery response
"""
self.debug("Checking devices/services are still valid")
removable = []
for usn in self.known:
if self.known[usn]['MANIFESTATION'] != 'local':
_, expiry = self.known[usn]['CACHE-CONTROL'].split('=')
expiry = int(expiry)
now = time.time()
last_seen = self.known[usn]['last-seen']
self.debug('Checking if {} is still valid - last seen '
'{} (+{}), now {}'.format(
self.known[usn]['USN'], last_seen, expiry, now))
if last_seen + expiry + 30 < now:
self.debug('Expiring: {}'.format(self.known[usn]))
if self.known[usn]['ST'] == 'upnp:rootdevice':
louie.send(
'Coherence.UPnP.SSDP.removed_device',
None, device_type=self.known[usn]['ST'],
infos=self.known[usn])
removable.append(usn)
while len(removable) > 0:
usn = removable.pop(0)
del self.known[usn]
[docs] def subscribe(self, name, callback):
self._callbacks.setdefault(name, []).append(callback)
[docs] def unsubscribe(self, name, callback):
callbacks = self._callbacks.get(name, [])
if callback in callbacks:
callbacks.remove(callback)
self._callbacks[name] = callbacks
[docs] def callback(self, name, *args):
for callback in self._callbacks.get(name, []):
callback(*args)