# Licensed under the MIT license # http://opensource.org/licenses/mit-license.php # (c) 2005, Tim Potter # # Implementation of SSDP server under Twisted Python. # import string from twisted.python import log from twisted.internet.protocol import DatagramProtocol # 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.""" elements = {} known = {} def datagramReceived(self, data, (host, port)): """Handle a received multicast datagram.""" # 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) cmd = string.split(lines[0], ' ') 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 if cmd[0] == 'M-SEARCH' and cmd[1] == '*': self.discoveryRequest(headers, (host, port)) # SSDP presence if cmd[0] == 'NOTIFY' and cmd[1] == '*': self.notifyReceived(headers, (host, port)) 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']) # Do we know about this service? if not self.known.has_key(headers['ST']): return # 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)) log.msg('responding with: %s' % response) self.transport.write( string.join(response, '\r\n') + '\r\n\r\n', (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=1800' 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 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) log.msg('Detected presence for %s' % headers['NT']) elif headers['NTS'] == 'ssdp:byebye': if self.elements.has_key(headers['NT']): # 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.