-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
91f90f5
commit 7fc6d7d
Showing
2 changed files
with
362 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
# Licensed under the MIT license | ||
# http://opensource.org/licenses/mit-license.php | ||
|
||
# Copyright 2005, Tim Potter <[email protected]> | ||
# Copyright 2006 John-Mark Gurney <[email protected]> | ||
# Copyright (C) 2006 Fluendo, S.A. (www.fluendo.com). | ||
# Copyright 2006,2007,2008,2009 Frank Scholz <[email protected]> | ||
# Copyright 2016 Erwan Martin <[email protected]> | ||
# | ||
# 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = """<root> | ||
<specVersion> | ||
<major>1</major> | ||
<minor>0</minor> | ||
</specVersion> | ||
<device> | ||
<deviceType>urn:schemas-upnp-org:device:AirMentorPro2:1</deviceType> | ||
<friendlyName>{friendly_name}</friendlyName> | ||
<manufacturer>{manufacturer}</manufacturer> | ||
<manufacturerURL>{manufacturer_url}</manufacturerURL> | ||
<modelDescription>{model_description}</modelDescription> | ||
<modelName>{model_name}</modelName> | ||
<modelNumber>{model_number}</modelNumber> | ||
<modelURL>{model_url}</modelURL> | ||
<serialNumber>{serial_number}</serialNumber> | ||
<UDN>uuid:{uuid}</UDN> | ||
<serviceList> | ||
<service> | ||
<URLBase>https://www.air-mentor.com/web2017/product/air_mentor_8096ap</URLBase> | ||
<serviceType>urn:airmentor:service:AirMentorPro2:1</serviceType> | ||
<serviceId>urn:airmentor:serviceId:AirMentorPro2</serviceId> | ||
<controlURL>/AirMentorPro2</controlURL> | ||
<eventSubURL/> | ||
<SCPDURL>/AirMentorPro2_wsd.xml</SCPDURL> | ||
</service> | ||
</serviceList> | ||
<presentationURL>{presentation_url}</presentationURL> | ||
</device> | ||
</root>""" | ||
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 """<scpd xmlns="urn:schemas-upnp-org:service-1-0"> | ||
<specVersion> | ||
<major>1</major> | ||
<minor>0</minor> | ||
</specVersion> | ||
</scpd>""" | ||
|
||
|
||
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() |