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.

205 lines
6.1 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-2007 John-Mark Gurney <jmg@funkthat.com>
  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. maxage = 7 * 24 * 60 * 60
  32. def __init__(self):
  33. # XXX - no init?
  34. #DatagramProtocol.__init__(self)
  35. pass
  36. def startProtocol(self):
  37. self.transport.joinGroup(SSDP_ADDR)
  38. # so we don't get our own sends
  39. self.transport.setLoopbackMode(0)
  40. def doStop(self):
  41. '''Make sure we send out the byebye notifications.'''
  42. for st in list(self.known.keys()):
  43. self.doByebye(st)
  44. del self.known[st]
  45. DatagramProtocol.doStop(self)
  46. def datagramReceived(self, data, hostporttup):
  47. """Handle a received multicast datagram."""
  48. host, port = hostporttup
  49. data = data.decode('ascii')
  50. header, payload = data.split('\r\n\r\n')
  51. lines = header.split('\r\n')
  52. cmd = lines[0].split(' ')
  53. lines = [x.replace(': ', ':', 1) for x in lines[1:]]
  54. lines = [x for x in lines if len(x) > 0]
  55. headers = [x.split(':', 1) for x in lines]
  56. headers = dict([(x[0].lower(), x[1]) for x in headers])
  57. if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
  58. # SSDP discovery
  59. self.discoveryRequest(headers, (host, port))
  60. elif cmd[0] == 'NOTIFY' and cmd[1] == '*':
  61. # SSDP presence
  62. self.notifyReceived(headers, (host, port))
  63. else:
  64. log.msg('Unknown SSDP command %s %s' % cmd)
  65. def discoveryRequest(self, headers, hostporttup):
  66. """Process a discovery request. The response must be sent to
  67. the address specified by (host, port)."""
  68. host, port = hostporttup
  69. log.msg('Discovery request for %s' % headers['st'])
  70. # Do we know about this service?
  71. if headers['st'] == 'ssdp:all':
  72. for i in self.known:
  73. hcopy = dict(headers.items())
  74. hcopy['st'] = i
  75. self.discoveryRequest(hcopy, (host, port))
  76. return
  77. if headers['st'] not in self.known:
  78. return
  79. #print 'responding'
  80. # Generate a response
  81. response = []
  82. response.append('HTTP/1.1 200 OK')
  83. for k, v in self.known[headers['st']].items():
  84. response.append('%s: %s' % (k, v))
  85. response.extend(('', ''))
  86. delay = random.randint(0, int(headers['mx']))
  87. reactor.callLater(delay, self.transport.write,
  88. b'\r\n'.join((bytes(x, 'ascii') for x in response)), (host, port))
  89. def register(self, usn, st, location):
  90. """Register a service or device that this SSDP server will
  91. respond to."""
  92. log.msg('Registering %s' % st)
  93. self.known[st] = {}
  94. self.known[st]['USN'] = usn
  95. self.known[st]['LOCATION'] = location
  96. self.known[st]['ST'] = st
  97. self.known[st]['EXT'] = ''
  98. self.known[st]['SERVER'] = 'Twisted, UPnP/1.0, python-upnp'
  99. self.known[st]['CACHE-CONTROL'] = 'max-age=%d' % self.maxage
  100. self.doNotifySchedule(st)
  101. reactor.callLater(random.uniform(.5, 1), lambda: self.doNotify(st))
  102. reactor.callLater(random.uniform(1, 5), lambda: self.doNotify(st))
  103. def doNotifySchedule(self, st):
  104. self.doNotify(st)
  105. reactor.callLater(random.uniform(self.maxage / 3,
  106. self.maxage / 2), lambda: self.doNotifySchedule(st))
  107. def doByebye(self, st):
  108. """Do byebye"""
  109. log.msg('Sending byebye notification for %s' % st)
  110. resp = [ 'NOTIFY * HTTP/1.1',
  111. 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT),
  112. 'NTS: ssdp:byebye',
  113. ]
  114. stcpy = dict(self.known[st].items())
  115. stcpy['NT'] = stcpy['ST']
  116. del stcpy['ST']
  117. resp.extend([': '.join(x) for x in stcpy.items()])
  118. resp.extend(('', ''))
  119. resp = b'\r\n'.join(bytes(x, 'ascii') for x in resp)
  120. self.transport.write(resp, (SSDP_ADDR, SSDP_PORT))
  121. self.transport.write(resp, (SSDP_ADDR, SSDP_PORT))
  122. def doNotify(self, st):
  123. """Do notification"""
  124. log.msg('Sending alive notification for %s' % st)
  125. resp = [ 'NOTIFY * HTTP/1.1',
  126. 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT),
  127. 'NTS: ssdp:alive',
  128. ]
  129. stcpy = dict(self.known[st].items())
  130. stcpy['NT'] = stcpy['ST']
  131. del stcpy['ST']
  132. resp.extend([': '.join(x) for x in stcpy.items()])
  133. resp.extend(('', ''))
  134. self.transport.write(b'\r\n'.join(bytes(x, 'ascii') for x in resp), (SSDP_ADDR, SSDP_PORT))
  135. def notifyReceived(self, headers, hostporttup):
  136. """Process a presence announcement. We just remember the
  137. details of the SSDP service announced."""
  138. host, port = hostporttup
  139. if headers['nts'] == 'ssdp:alive':
  140. if headers['nt'] not in self.elements:
  141. # Register device/service
  142. self.elements[headers['nt']] = {}
  143. self.elements[headers['nt']]['USN'] = headers['usn']
  144. self.elements[headers['nt']]['host'] = (host, port)
  145. log.msg('Detected presence of %s' % headers['nt'])
  146. #log.msg('headers: %s' % `headers`)
  147. elif headers['nts'] == 'ssdp:byebye':
  148. if headers['nt'] in self.elements:
  149. # Unregister device/service
  150. del(self.elements[headers['nt']])
  151. log.msg('Detected absence for %s' % headers['nt'])
  152. else:
  153. log.msg('Unknown subtype %s for notification type %s' %
  154. (headers['nts'], headers['nt']))
  155. def findService(self, name):
  156. """Return information about a service registered over SSDP."""
  157. # TODO: Implement me.
  158. # TODO: Send out a discovery request if we haven't registered
  159. # a presence announcement.
  160. def findDevice(self, name):
  161. """Return information about a device registered over SSDP."""
  162. # TODO: Implement me.
  163. # TODO: Send out a discovery request if we haven't registered
  164. # a presence announcement.