diff --git a/ssdp_upnp/lib/ssdp.py b/ssdp_upnp/lib/ssdp.py new file mode 100644 index 0000000..25581cf --- /dev/null +++ b/ssdp_upnp/lib/ssdp.py @@ -0,0 +1,229 @@ +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2005, Tim Potter +# Copyright 2006 John-Mark Gurney +# Copyright (C) 2006 Fluendo, S.A. (www.fluendo.com). +# Copyright 2006,2007,2008,2009 Frank Scholz +# Copyright 2016 Erwan Martin +# +# Implementation of a SSDP server. +# + +import random +import time +import socket +import logging +from email.utils import formatdate +from errno import ENOPROTOOPT + +SSDP_PORT = 1900 +SSDP_ADDR = '239.255.255.250' +SERVER_ID = 'Raspberry Pi SSDP Server' + + +#logger = logging.getLogger() + + +class SSDPServer: + """A class implementing a SSDP server. The notify_received and + searchReceived methods are called when the appropriate type of + datagram is received by the server.""" + known = {} + + def __init__(self): + self.sock = None + + def run(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if hasattr(socket, "SO_REUSEPORT"): + try: + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except socket.error as le: + # RHEL6 defines SO_REUSEPORT but it doesn't work + if le.errno == ENOPROTOOPT: + pass + else: + raise + + addr = socket.inet_aton(SSDP_ADDR) + interface = socket.inet_aton('0.0.0.0') + cmd = socket.IP_ADD_MEMBERSHIP + self.sock.setsockopt(socket.IPPROTO_IP, cmd, addr + interface) + self.sock.bind(('0.0.0.0', SSDP_PORT)) + self.sock.settimeout(1) + + while True: + try: + data, addr = self.sock.recvfrom(1024) + self.datagram_received(data, addr) + except socket.timeout: + continue + self.shutdown() + + def shutdown(self): + for st in self.known: + if self.known[st]['MANIFESTATION'] == 'local': + self.do_byebye(st) + + def datagram_received(self, data, host_port): + """Handle a received multicast datagram.""" + + (host, port) = host_port + + try: + header, payload = data.decode().split('\r\n\r\n')[:2] + except ValueError as err: + print(err) + return + + lines = header.split('\r\n') + cmd = lines[0].split(' ') + lines = map(lambda x: x.replace(': ', ':', 1), lines[1:]) + lines = filter(lambda x: len(x) > 0, lines) + + headers = [x.split(':', 1) for x in lines] + headers = dict(map(lambda x: (x[0].lower(), x[1]), headers)) + + print('SSDP command %s %s - from %s:%d' % (cmd[0], cmd[1], host, port)) + #print('with headers: {}.'.format(headers)) + if cmd[0] == 'M-SEARCH' and cmd[1] == '*': + print ('SSDP discovery') + self.discovery_request(headers, (host, port)) + elif cmd[0] == 'NOTIFY' and cmd[1] == '*': + # SSDP presence + print('NOTIFY *') + else: + print('Unknown SSDP command %s %s' % (cmd[0], cmd[1])) + + def register(self, manifestation, usn, st, location, server=SERVER_ID, cache_control='max-age=1800', silent=False, + host=None): + """Register a service or device that this SSDP server will + respond to.""" + + print('Registering %s (%s)' % (st, location)) + + self.known[usn] = {} + self.known[usn]['USN'] = usn + self.known[usn]['LOCATION'] = location + self.known[usn]['ST'] = st + self.known[usn]['EXT'] = '' + self.known[usn]['SERVER'] = server + self.known[usn]['CACHE-CONTROL'] = cache_control + + self.known[usn]['MANIFESTATION'] = manifestation + self.known[usn]['SILENT'] = silent + self.known[usn]['HOST'] = host + self.known[usn]['last-seen'] = time.time() + + if manifestation == 'local' and self.sock: + self.do_notify(usn) + + def unregister(self, usn): + print("Un-registering %s" % usn) + del self.known[usn] + + def is_known(self, usn): + return usn in self.known + + def send_it(self, response, destination, delay, usn): + #print('send discovery response delayed by %ds for %s to %r' % (delay, usn, destination)) + try: + self.sock.sendto(response.encode(), destination) + except (AttributeError, socket.error) as msg: + print("failure sending out byebye notification: %r" % msg) + + def discovery_request(self, headers, host_port): + """Process a discovery request. The response must be sent to + the address specified by (host, port).""" + + (host, port) = host_port + + print('Discovery request from (%s,%d) for %s' % (host, port, headers['st'])) + if headers['st'] == "urn:schemas-upnp-org:device:AirMentorPro2:1": + print('Discovery request for %s' % headers['st']) + print self.known.values() + + # Do we know about this service? + for i in self.known.values(): + if i['MANIFESTATION'] == 'remote': + continue + if headers['st'] == 'ssdp:all' and i['SILENT']: + continue + if i['ST'] == headers['st'] or headers['st'] == 'ssdp:all': + print "HTTP/1.1 200 OK" + response = ['HTTP/1.1 200 OK'] + + usn = None + for k, v in i.items(): + if k == 'USN': + usn = v + if k not in ('MANIFESTATION', 'SILENT', 'HOST'): + response.append('%s: %s' % (k, v)) + + if usn: + response.append('DATE: %s' % formatdate(timeval=None, localtime=False, usegmt=True)) + + response.extend(('', '')) + delay = random.randint(0, int(headers['mx'])) + + self.send_it('\r\n'.join(response), (host, port), delay, usn) + + def do_notify(self, usn): + """Do notification""" + + if self.known[usn]['SILENT']: + return + # print('Sending alive notification for %s' % usn) + + resp = [ + 'NOTIFY * HTTP/1.1', + 'HOST: %s:%d' % (SSDP_ADDR, SSDP_PORT), + 'NTS: ssdp:alive', + ] + stcpy = dict(self.known[usn].items()) + stcpy['NT'] = stcpy['ST'] + del stcpy['ST'] + del stcpy['MANIFESTATION'] + del stcpy['SILENT'] + del stcpy['HOST'] + del stcpy['last-seen'] + + resp.extend(map(lambda x: ': '.join(x), stcpy.items())) + resp.extend(('', '')) + # print('do_notify content', resp) + try: + self.sock.sendto('\r\n'.join(resp).encode(), (SSDP_ADDR, SSDP_PORT)) + self.sock.sendto('\r\n'.join(resp).encode(), (SSDP_ADDR, SSDP_PORT)) + except (AttributeError, socket.error) as msg: + print("failure sending out alive notification: %r" % msg) + + def do_byebye(self, usn): + """Do byebye""" + + # print('Sending byebye notification for %s' % usn) + + resp = [ + 'NOTIFY * HTTP/1.1', + 'HOST: %s:%d' % (SSDP_ADDR, SSDP_PORT), + 'NTS: ssdp:byebye', + ] + try: + stcpy = dict(self.known[usn].items()) + stcpy['NT'] = stcpy['ST'] + del stcpy['ST'] + del stcpy['MANIFESTATION'] + del stcpy['SILENT'] + del stcpy['HOST'] + del stcpy['last-seen'] + resp.extend(map(lambda x: ': '.join(x), stcpy.items())) + resp.extend(('', '')) + # print('do_byebye content', resp) + if self.sock: + try: + self.sock.sendto('\r\n'.join(resp), (SSDP_ADDR, SSDP_PORT)) + except (AttributeError, socket.error) as msg: + print("failure sending out byebye notification: %r" % msg) + except KeyError as msg: + print("error building byebye notification: %r" % msg) diff --git a/ssdp_upnp/lib/upnp_http_server.py b/ssdp_upnp/lib/upnp_http_server.py new file mode 100644 index 0000000..00ca6ed --- /dev/null +++ b/ssdp_upnp/lib/upnp_http_server.py @@ -0,0 +1,133 @@ + +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +import threading + +PORT_NUMBER = 8090 + + +class UPNPHTTPServerHandler(BaseHTTPRequestHandler): + """ + A HTTP handler that serves the UPnP XML files. + """ + + # Handler for the GET requests + def do_GET(self): + print "===============",self.path + + if self.path == '/airmentor_wsd.xml': + self.send_response(200) + self.send_header('Content-type', 'application/xml') + self.end_headers() + self.wfile.write(self.get_wsd_xml().encode()) + return + if self.path == '/airmentorpro2.xml': + self.send_response(200) + self.send_header('Content-type', 'application/xml') + self.end_headers() + self.wfile.write(self.get_device_xml().encode()) + return + else: + self.send_response(404) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b"Not found.") + return + + def get_device_xml(self): + """ + Get the main device descriptor xml file. + """ + xml = """ + + 1 + 0 + + + urn:schemas-upnp-org:device:AirMentorPro2:1 + {friendly_name} + {manufacturer} + {manufacturer_url} + {model_description} + {model_name} + {model_number} + {model_url} + {serial_number} + uuid:{uuid} + + + https://www.air-mentor.com/web2017/product/air_mentor_8096ap + urn:airmentor:service:AirMentorPro2:1 + urn:airmentor:serviceId:AirMentorPro2 + /AirMentorPro2 + + /AirMentorPro2_wsd.xml + + + {presentation_url} + +""" + return xml.format(friendly_name=self.server.friendly_name, + manufacturer=self.server.manufacturer, + manufacturer_url=self.server.manufacturer_url, + model_description=self.server.model_description, + model_name=self.server.model_name, + model_number=self.server.model_number, + model_url=self.server.model_url, + serial_number=self.server.serial_number, + uuid=self.server.uuid, + presentation_url=self.server.presentation_url) + + @staticmethod + def get_wsd_xml(): + """ + Get the device WSD file. + """ + return """ + +1 +0 + +""" + + +class UPNPHTTPServerBase(HTTPServer): + """ + A simple HTTP server that knows the information about a UPnP device. + """ + def __init__(self, server_address, request_handler_class): + HTTPServer.__init__(self, server_address, request_handler_class) + self.port = None + self.friendly_name = None + self.manufacturer = None + self.manufacturer_url = None + self.model_description = None + self.model_name = None + self.model_url = None + self.serial_number = None + self.uuid = None + self.presentation_url = None + + +class UPNPHTTPServer(threading.Thread): + """ + A thread that runs UPNPHTTPServerBase. + """ + + def __init__(self, port, friendly_name, manufacturer, manufacturer_url, model_description, model_name, + model_number, model_url, serial_number, uuid, presentation_url): + threading.Thread.__init__(self) + self.server = UPNPHTTPServerBase(('', port), UPNPHTTPServerHandler) + self.server.port = port + self.server.friendly_name = friendly_name + self.server.manufacturer = manufacturer + self.server.manufacturer_url = manufacturer_url + self.server.model_description = model_description + self.server.model_name = model_name + self.server.model_number = model_number + self.server.model_url = model_url + self.server.serial_number = serial_number + self.server.uuid = uuid + self.server.presentation_url = presentation_url + + def run(self): + self.server.serve_forever()