A Python UPnP Media Server

197 lines
5.6 KiB

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