diff --git a/SSDP.py b/SSDP.py index f1ad76b..9ead2d8 100644 --- a/SSDP.py +++ b/SSDP.py @@ -27,7 +27,9 @@ 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 = {} @@ -36,37 +38,40 @@ class SSDPServer(DatagramProtocol): # Break up message in to command and headers - data = string.replace(data, '\r', '') - data = string.replace(data, ': ', ':') - lines = string.split(data, '\n') - lines = filter(lambda x: len(x) > 0, lines) + log.msg('respond to: %s:%d' % (host, port)) + header, payload = data.split('\r\n\r\n') + lines = header.split('\r\n') cmd = string.split(lines[0], ' ') + lines = map(lambda x: x.replace(': ', ':', 1), lines[1:]) + lines = filter(lambda x: len(x) > 0, lines) - headers = dict([string.split(x, ':', 1) for x in lines[1:]]) - - # TODO: datagram may contain a payload, i.e Content-Length - # header is > 0. Maybe use some twisted HTTP object to do the - # parsing for us. - - # SSDP discovery + headers = [string.split(x, ':', 1) for x in lines[1:]] + headers = dict(map(lambda x: (x[0].lower(), x[1]), headers)) if cmd[0] == 'M-SEARCH' and cmd[1] == '*': + # SSDP discovery self.discoveryRequest(headers, (host, port)) - - # SSDP presence - - if cmd[0] == 'NOTIFY' and cmd[1] == '*': + 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, (host, port)): """Process a discovery request. The response must be sent to the address specified by (host, port).""" - log.msg('Discovery request for %s' % headers['ST']) + log.msg('Discovery request for %s' % headers['st']) # Do we know about this service? - if not self.known.has_key(headers['ST']): + if headers['st'] == 'ssdp:all': + for i in self.known: + hcopy = dict(headers.iteritems()) + hcopy['st'] = i + self.discoveryRequest(hcopy, (host, post)) + return + if not self.known.has_key(headers['st']): return # Generate a response @@ -74,11 +79,12 @@ class SSDPServer(DatagramProtocol): response = [] response.append('HTTP/1.1 200 OK') - for k, v in self.known[headers['ST']].items(): + for k, v in self.known[headers['st']].items(): response.append('%s: %s' % (k, v)) log.msg('responding with: %s' % response) + # TODO: we should wait random(headers['mx']) self.transport.write( string.join(response, '\r\n') + '\r\n\r\n', (host, port)) @@ -95,36 +101,54 @@ class SSDPServer(DatagramProtocol): self.known[st]['EXT'] = '' self.known[st]['SERVER'] = 'Twisted, UPnP/1.0, python-upnp' self.known[st]['CACHE-CONTROL'] = 'max-age=1800' + self.doNotify(st) + + 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].iteritems()) + stcpy['NT'] = stcpy['ST'] + del stcpy['ST'] + resp.extend(map(lambda x: ': '.join(x), stcpy.iteritems())) + log.msg(repr(resp)) + self.transport.write( + string.join(resp, '\r\n') + '\r\n\r\n', (SSDP_ADDR, SSDP_PORT)) def notifyReceived(self, headers, (host, port)): """Process a presence announcement. We just remember the details of the SSDP service announced.""" - if headers['NTS'] == 'ssdp:alive': + if headers['nts'] == 'ssdp:alive': - if not self.elements.has_key(headers['NT']): + if not self.elements.has_key(headers['nt']): # Register device/service - self.elements[headers['NT']] = {} - self.elements[headers['NT']]['USN'] = headers['USN'] - self.elements[headers['NT']]['host'] = (host, port) + self.elements[headers['nt']] = {} + self.elements[headers['nt']]['USN'] = headers['usn'] + self.elements[headers['nt']]['host'] = (host, port) - log.msg('Detected presence for %s' % headers['NT']) + log.msg('Detected presence for %s' % headers['nt']) - elif headers['NTS'] == 'ssdp:byebye': + elif headers['nts'] == 'ssdp:byebye': - if self.elements.has_key(headers['NT']): + if self.elements.has_key(headers['nt']): # Unregister device/service - del(self.elements[headers['NT']]) + del(self.elements[headers['nt']]) - log.msg('Detected absence for %s' % headers['NT']) + log.msg('Detected absence for %s' % headers['nt']) else: log.msg('Unknown subtype %s for notification type %s' % - (headers['NTS'], headers['NT'])) + (headers['nts'], headers['nt'])) def findService(self, name): """Return information about a service registered over SSDP.""" diff --git a/pymediaserv b/pymediaserv index b590f45..e64ce59 100755 --- a/pymediaserv +++ b/pymediaserv @@ -1,15 +1,28 @@ -#!/usr/bin/python +#!/usr/bin/env python # Licensed under the MIT license # http://opensource.org/licenses/mit-license.php # (c) 2005, Tim Potter +import random +import string import sys from twisted.python import log from twisted.internet import reactor +def generateuuid(): + if False: + return 'asdflkjewoifjslkdfj' + return ''.join(map(lambda x: random.choice(string.letters), xrange(20))) + listenAddr = sys.argv[1] +if len(sys.argv) > 2: + listenPort = int(sys.argv[2]) + if listenPort < 1024 or listenPort > 65535: + raise ValueError, 'port out of range' +else: + listenPort = 8080 log.startLogging(sys.stdout) @@ -19,30 +32,11 @@ from SSDP import SSDPServer, SSDP_PORT, SSDP_ADDR s = SSDPServer() -port = reactor.listenMulticast(SSDP_PORT, s, SSDP_ADDR) -port.joinGroup(SSDP_ADDR, listenAddr) - -uuid = 'uuid:XVKKBUKYRDLGJQDTPOT' - -s.register('%s::upnp:rootdevice' % uuid, - 'upnp:rootdevice', - 'http://%s:8080/root-device.xml' % listenAddr) - -s.register(uuid, - uuid, - 'http://%s:8080/root-device.xml' % listenAddr) - -s.register('%s::urn:schemas-upnp-org:device:MediaServer:1' % uuid, - 'urn:schemas-upnp-org:device:MediaServer:1', - 'http://%s:8080/root-device.xml' % listenAddr) - -s.register('%s::urn:schemas-upnp-org:service:ConnectionManager:1' % uuid, - 'urn:schemas-upnp-org:device:ConnectionManager:1', - 'http://%s:8080/root-device.xml' % listenAddr) +port = reactor.listenMulticast(SSDP_PORT, s) +port.joinGroup(SSDP_ADDR) +port.setLoopbackMode(0) # don't get our own sends -s.register('%s::urn:schemas-upnp-org:service:ContentDirectory:1' % uuid, - 'urn:schemas-upnp-org:device:ContentDirectory:1', - 'http://%s:8080/root-device.xml' % listenAddr) +uuid = 'uuid:' + generateuuid() # Create SOAP server @@ -70,7 +64,28 @@ from MediaServer import MediaServer root.putChild('media', static.File('media')) site = server.Site(root) -reactor.listenTCP(8080, site) +reactor.listenTCP(listenPort, site) + +# we need to do this after the children are there, since we send notifies +s.register('%s::upnp:rootdevice' % uuid, + 'upnp:rootdevice', + 'http://%s:%d/root-device.xml' % (listenAddr, listenPort)) + +s.register(uuid, + uuid, + 'http://%s:%d/root-device.xml' % (listenAddr, listenPort)) + +s.register('%s::urn:schemas-upnp-org:device:MediaServer:1' % uuid, + 'urn:schemas-upnp-org:device:MediaServer:1', + 'http://%s:%d/root-device.xml' % (listenAddr, listenPort)) + +s.register('%s::urn:schemas-upnp-org:service:ConnectionManager:1' % uuid, + 'urn:schemas-upnp-org:device:ConnectionManager:1', + 'http://%s:%d/root-device.xml' % (listenAddr, listenPort)) + +s.register('%s::urn:schemas-upnp-org:service:ContentDirectory:1' % uuid, + 'urn:schemas-upnp-org:device:ContentDirectory:1', + 'http://%s:%d/root-device.xml' % (listenAddr, listenPort)) # Main loop