# Licensed under the MIT license # http://opensource.org/licenses/mit-license.php # Copyright 2005, Tim Potter # Copyright 2006-2007 John-Mark Gurney __version__ = '$Change$' # $Id$ # # Implementation of SSDP server under Twisted Python. # import random import string from twisted.python import log from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor, task # TODO: Is there a better way of hooking the SSDPServer into a reactor # without having to know the default SSDP port and multicast address? # There must be a twisted idiom for doing this. SSDP_PORT = 1900 SSDP_ADDR = '239.255.255.250' # TODO: Break out into a HTTPOverUDP class and implement # process_SEARCH(), process_NOTIFY() methods. Create UPNP specific # class to handle services etc. class SSDPServer(DatagramProtocol): """A class implementing a SSDP server. The notifyReceived and searchReceived methods are called when the appropriate type of datagram is received by the server.""" # not used yet stdheaders = [ ('Server', 'Twisted, UPnP/1.0, python-upnp'), ] elements = {} known = {} maxage = 7 * 24 * 60 * 60 def __init__(self): # XXX - no init? #DatagramProtocol.__init__(self) pass def startProtocol(self): self.transport.joinGroup(SSDP_ADDR) # so we don't get our own sends self.transport.setLoopbackMode(0) def doStop(self): '''Make sure we send out the byebye notifications.''' for st in list(self.known.keys()): self.doByebye(st) del self.known[st] DatagramProtocol.doStop(self) def datagramReceived(self, data, hostporttup): """Handle a received multicast datagram.""" host, port = hostporttup data = data.decode('ascii') header, payload = data.split('\r\n\r\n') 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] headers = [x.split(':', 1) for x in lines] headers = dict([(x[0].lower(), x[1]) for x in 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: log.msg('Unknown SSDP command %s %s' % cmd) def discoveryRequest(self, headers, hostporttup): """Process a discovery request. The response must be sent to the address specified by (host, port).""" host, port = hostporttup log.msg('Discovery request for %s' % headers['st']) # Do we know about this service? if headers['st'] == 'ssdp:all': for i in self.known: hcopy = dict(headers.items()) hcopy['st'] = i self.discoveryRequest(hcopy, (host, port)) return if headers['st'] not in self.known: return #print 'responding' # Generate a response response = [] response.append('HTTP/1.1 200 OK') for k, v in self.known[headers['st']].items(): response.append('%s: %s' % (k, v)) response.extend(('', '')) delay = random.randint(0, int(headers['mx'])) reactor.callLater(delay, self.transport.write, b'\r\n'.join((bytes(x, 'ascii') for x in response)), (host, port)) def register(self, usn, st, location): """Register a service or device that this SSDP server will respond to.""" log.msg('Registering %s' % st) self.known[st] = {} self.known[st]['USN'] = usn self.known[st]['LOCATION'] = location self.known[st]['ST'] = st self.known[st]['EXT'] = '' self.known[st]['SERVER'] = 'Twisted, UPnP/1.0, python-upnp' self.known[st]['CACHE-CONTROL'] = 'max-age=%d' % self.maxage self.doNotifySchedule(st) reactor.callLater(random.uniform(.5, 1), lambda: self.doNotify(st)) reactor.callLater(random.uniform(1, 5), lambda: self.doNotify(st)) def doNotifySchedule(self, st): self.doNotify(st) reactor.callLater(random.uniform(self.maxage / 3, self.maxage / 2), lambda: self.doNotifySchedule(st)) def doByebye(self, st): """Do byebye""" log.msg('Sending byebye notification for %s' % st) resp = [ 'NOTIFY * HTTP/1.1', 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT), 'NTS: ssdp:byebye', ] stcpy = dict(self.known[st].items()) stcpy['NT'] = stcpy['ST'] del stcpy['ST'] resp.extend([': '.join(x) for x in stcpy.items()]) resp.extend(('', '')) resp = b'\r\n'.join(bytes(x, 'ascii') for x in resp) self.transport.write(resp, (SSDP_ADDR, SSDP_PORT)) self.transport.write(resp, (SSDP_ADDR, SSDP_PORT)) def doNotify(self, st): """Do notification""" log.msg('Sending alive notification for %s' % st) resp = [ 'NOTIFY * HTTP/1.1', 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT), 'NTS: ssdp:alive', ] stcpy = dict(self.known[st].items()) stcpy['NT'] = stcpy['ST'] del stcpy['ST'] resp.extend([': '.join(x) for x in stcpy.items()]) resp.extend(('', '')) self.transport.write(b'\r\n'.join(bytes(x, 'ascii') for x in resp), (SSDP_ADDR, SSDP_PORT)) def notifyReceived(self, headers, hostporttup): """Process a presence announcement. We just remember the details of the SSDP service announced.""" host, port = hostporttup if headers['nts'] == 'ssdp:alive': if headers['nt'] not in self.elements: # Register device/service self.elements[headers['nt']] = {} self.elements[headers['nt']]['USN'] = headers['usn'] self.elements[headers['nt']]['host'] = (host, port) log.msg('Detected presence of %s' % headers['nt']) #log.msg('headers: %s' % `headers`) elif headers['nts'] == 'ssdp:byebye': if headers['nt'] in self.elements: # Unregister device/service del(self.elements[headers['nt']]) log.msg('Detected absence for %s' % headers['nt']) else: log.msg('Unknown subtype %s for notification type %s' % (headers['nts'], headers['nt'])) def findService(self, name): """Return information about a service registered over SSDP.""" # TODO: Implement me. # TODO: Send out a discovery request if we haven't registered # a presence announcement. def findDevice(self, name): """Return information about a device registered over SSDP.""" # TODO: Implement me. # TODO: Send out a discovery request if we haven't registered # a presence announcement.