A Python UPnP Media Server

192 lines
5.8 KiB

  1. # Licensed under the MIT license
  2. # http://opensource.org/licenses/mit-license.php
  3. # (c) 2005, Tim Potter <tpot@samba.org>
  4. #
  5. # Implementation of SSDP server under Twisted Python.
  6. #
  7. import random
  8. import string
  9. from twisted.python import log
  10. from twisted.internet.protocol import DatagramProtocol
  11. from twisted.internet import reactor
  12. # TODO: Is there a better way of hooking the SSDPServer into a reactor
  13. # without having to know the default SSDP port and multicast address?
  14. # There must be a twisted idiom for doing this.
  15. SSDP_PORT = 1900
  16. SSDP_ADDR = '239.255.255.250'
  17. # TODO: Break out into a HTTPOverUDP class and implement
  18. # process_SEARCH(), process_NOTIFY() methods. Create UPNP specific
  19. # class to handle services etc.
  20. class SSDPServer(DatagramProtocol):
  21. """A class implementing a SSDP server. The notifyReceived and
  22. searchReceived methods are called when the appropriate type of
  23. datagram is received by the server."""
  24. # not used yet
  25. stdheaders = [ ('Server', 'Twisted, UPnP/1.0, python-upnp'), ]
  26. elements = {}
  27. known = {}
  28. def doStop(self):
  29. '''Make sure we send out the byebye notifications.'''
  30. self.transport.write('foobar', (SSDP_ADDR, SSDP_PORT))
  31. for st in self.known:
  32. self.doByebye(st)
  33. DatagramProtocol.doStop(self)
  34. def datagramReceived(self, data, (host, port)):
  35. """Handle a received multicast datagram."""
  36. # Break up message in to command and headers
  37. log.msg('respond to: %s:%d' % (host, port))
  38. header, payload = data.split('\r\n\r\n')
  39. lines = header.split('\r\n')
  40. cmd = string.split(lines[0], ' ')
  41. lines = map(lambda x: x.replace(': ', ':', 1), lines[1:])
  42. lines = filter(lambda x: len(x) > 0, lines)
  43. headers = [string.split(x, ':', 1) for x in lines]
  44. headers = dict(map(lambda x: (x[0].lower(), x[1]), headers))
  45. if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
  46. # SSDP discovery
  47. self.discoveryRequest(headers, (host, port))
  48. elif cmd[0] == 'NOTIFY' and cmd[1] == '*':
  49. # SSDP presence
  50. self.notifyReceived(headers, (host, port))
  51. else:
  52. log.msg('Unknown SSDP command %s %s' % cmd)
  53. def discoveryRequest(self, headers, (host, port)):
  54. """Process a discovery request. The response must be sent to
  55. the address specified by (host, port)."""
  56. log.msg('Discovery request for %s' % headers['st'])
  57. # Do we know about this service?
  58. if headers['st'] == 'ssdp:all':
  59. for i in self.known:
  60. hcopy = dict(headers.iteritems())
  61. hcopy['st'] = i
  62. self.discoveryRequest(hcopy, (host, post))
  63. return
  64. if not self.known.has_key(headers['st']):
  65. return
  66. # Generate a response
  67. response = []
  68. response.append('HTTP/1.1 200 OK')
  69. for k, v in self.known[headers['st']].items():
  70. response.append('%s: %s' % (k, v))
  71. response.extend(('', ''))
  72. delay = random.randint(0, int(headers['mx']))
  73. reactor.callLater(delay, self.transport.write,
  74. '\r\n'.join(response), (host, port))
  75. def register(self, usn, st, location):
  76. """Register a service or device that this SSDP server will
  77. respond to."""
  78. log.msg('Registering %s' % st)
  79. self.known[st] = {}
  80. self.known[st]['USN'] = usn
  81. self.known[st]['LOCATION'] = location
  82. self.known[st]['ST'] = st
  83. self.known[st]['EXT'] = ''
  84. self.known[st]['SERVER'] = 'Twisted, UPnP/1.0, python-upnp'
  85. self.known[st]['CACHE-CONTROL'] = 'max-age=1800'
  86. self.doNotify(st)
  87. def doByebye(self, st):
  88. """Do byebye"""
  89. log.msg('Sending byebye notification for %s' % st)
  90. resp = [ 'NOTIFY * HTTP/1.1',
  91. 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT),
  92. 'NTS: ssdp:byebye',
  93. ]
  94. stcpy = dict(self.known[st].iteritems())
  95. stcpy['NT'] = stcpy['ST']
  96. del stcpy['ST']
  97. resp.extend(map(lambda x: ': '.join(x), stcpy.iteritems()))
  98. resp.extend(('', ''))
  99. self.transport.write('\r\n'.join(resp), (SSDP_ADDR, SSDP_PORT))
  100. def doNotify(self, st):
  101. """Do notification"""
  102. log.msg('Sending alive notification for %s' % st)
  103. resp = [ 'NOTIFY * HTTP/1.1',
  104. 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT),
  105. 'NTS: ssdp:alive',
  106. ]
  107. stcpy = dict(self.known[st].iteritems())
  108. stcpy['NT'] = stcpy['ST']
  109. del stcpy['ST']
  110. resp.extend(map(lambda x: ': '.join(x), stcpy.iteritems()))
  111. resp.extend(('', ''))
  112. self.transport.write('\r\n'.join(resp), (SSDP_ADDR, SSDP_PORT))
  113. def notifyReceived(self, headers, (host, port)):
  114. """Process a presence announcement. We just remember the
  115. details of the SSDP service announced."""
  116. if headers['nts'] == 'ssdp:alive':
  117. if not self.elements.has_key(headers['nt']):
  118. # Register device/service
  119. self.elements[headers['nt']] = {}
  120. self.elements[headers['nt']]['USN'] = headers['usn']
  121. self.elements[headers['nt']]['host'] = (host, port)
  122. log.msg('Detected presence for %s' % headers['nt'])
  123. elif headers['nts'] == 'ssdp:byebye':
  124. if self.elements.has_key(headers['nt']):
  125. # Unregister device/service
  126. del(self.elements[headers['nt']])
  127. log.msg('Detected absence for %s' % headers['nt'])
  128. else:
  129. log.msg('Unknown subtype %s for notification type %s' %
  130. (headers['nts'], headers['nt']))
  131. def findService(self, name):
  132. """Return information about a service registered over SSDP."""
  133. # TODO: Implement me.
  134. # TODO: Send out a discovery request if we haven't registered
  135. # a presence announcement.
  136. def findDevice(self, name):
  137. """Return information about a device registered over SSDP."""
  138. # TODO: Implement me.
  139. # TODO: Send out a discovery request if we haven't registered
  140. # a presence announcement.