# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php
# Copyright 2005, Tim Potter <tpot@samba.org>
# Copyright 2006-2007 John-Mark Gurney <jmg@funkthat.com>

__version__ = '$Change$'
# $Id$

#
# Implementation of SSDP server under Twisted Python.
#

import random
import string

from twisted.python import log
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor, task

# 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."""

	# not used yet
	stdheaders = [ ('Server', 'Twisted, UPnP/1.0, python-upnp'), ]
	elements = {}
	known = {}
	maxage = 7 * 24 * 60 * 60

	def __init__(self):
		# XXX - no init?
		#DatagramProtocol.__init__(self)
		pass

	def startProtocol(self):
		self.transport.joinGroup(SSDP_ADDR)
		# so we don't get our own sends
		self.transport.setLoopbackMode(0)

	def doStop(self):
		'''Make sure we send out the byebye notifications.'''

		for st in list(self.known.keys()):
			self.doByebye(st)
			del self.known[st]

		DatagramProtocol.doStop(self)

	def datagramReceived(self, data, hostporttup):
		"""Handle a received multicast datagram."""
		host, port = hostporttup
		data = data.decode('ascii')
		header, payload = data.split('\r\n\r\n')
		lines = header.split('\r\n')
		cmd = lines[0].split(' ')
		lines = [x.replace(': ', ':', 1) for x in lines[1:]]
		lines = [x for x in lines if len(x) > 0]

		headers = [x.split(':', 1) for x in lines]
		headers = dict([(x[0].lower(), x[1]) for x in headers])

		if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
			# SSDP discovery
			self.discoveryRequest(headers, (host, port))
		elif cmd[0] == 'NOTIFY' and cmd[1] == '*':
			# SSDP presence
			self.notifyReceived(headers, (host, port))
		else:
			log.msg('Unknown SSDP command %s %s' % cmd)

	def discoveryRequest(self, headers, hostporttup):
		"""Process a discovery request.  The response must be sent to
		the address specified by (host, port)."""
		host, port = hostporttup
		log.msg('Discovery request for %s' % headers['st'])

		# Do we know about this service?
		if headers['st'] == 'ssdp:all':
			for i in self.known:
				hcopy = dict(headers.items())
				hcopy['st'] = i
				self.discoveryRequest(hcopy, (host, port))
			return
		if headers['st'] not in self.known:
			return

		#print 'responding'
		# 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))

		response.extend(('', ''))
		delay = random.randint(0, int(headers['mx']))
		reactor.callLater(delay, self.transport.write,
		    b'\r\n'.join((bytes(x, 'ascii') for x in response)), (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=%d' % self.maxage
		self.doNotifySchedule(st)

		reactor.callLater(random.uniform(.5, 1), lambda: self.doNotify(st))
		reactor.callLater(random.uniform(1, 5), lambda: self.doNotify(st))

	def doNotifySchedule(self, st):
		self.doNotify(st)
		reactor.callLater(random.uniform(self.maxage / 3,
		    self.maxage / 2), lambda: self.doNotifySchedule(st))

	def doByebye(self, st):
		"""Do byebye"""

		log.msg('Sending byebye notification for %s' % st)

		resp = [ 'NOTIFY * HTTP/1.1',
			 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT),
			 'NTS: ssdp:byebye',
		    ]
		stcpy = dict(self.known[st].items())
		stcpy['NT'] = stcpy['ST']
		del stcpy['ST']
		resp.extend([': '.join(x) for x in stcpy.items()])
		resp.extend(('', ''))
		resp = b'\r\n'.join(bytes(x, 'ascii') for x in resp)
		self.transport.write(resp, (SSDP_ADDR, SSDP_PORT))
		self.transport.write(resp, (SSDP_ADDR, SSDP_PORT))

	def doNotify(self, st):
		"""Do notification"""

		log.msg('Sending alive notification for %s' % st)

		resp = [ 'NOTIFY * HTTP/1.1',
			'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT),
			'NTS: ssdp:alive',
			]
		stcpy = dict(self.known[st].items())
		stcpy['NT'] = stcpy['ST']
		del stcpy['ST']
		resp.extend([': '.join(x) for x in stcpy.items()])
		resp.extend(('', ''))
		self.transport.write(b'\r\n'.join(bytes(x, 'ascii') for x in resp), (SSDP_ADDR, SSDP_PORT))

	def notifyReceived(self, headers, hostporttup):
		"""Process a presence announcement.  We just remember the
		details of the SSDP service announced."""
		host, port = hostporttup
		if headers['nts'] == 'ssdp:alive':
			if headers['nt'] not in self.elements:
				# 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 of %s' % headers['nt'])
				#log.msg('headers: %s' % `headers`)
		elif headers['nts'] == 'ssdp:byebye':
			if headers['nt'] in self.elements:
				# 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.