A Python UPnP Media Server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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