def _dispatchSoapRequest(self, request): try: try: envelope = XML(request.soap_data) body = envelope.find("{http://schemas.xmlsoap.org/soap/envelope/}Body") # determine UPnP action action = body.find("{%s}%s" % (request.soap_ns, request.soap_action)) # look up the action in the service upnp_action = self.service._actions[request.soap_action] # build a list of the action arguments in_args = {} for arg in action: in_args[arg.tag] = arg.text # execute the UPnP action logger.log_debug("executing %s#%s" % (self.service.serviceID, request.soap_action)) out_args = upnp_action(request, self.service, in_args) # return the action response env = Element("s:Envelope") env.attrib['xmlns:s'] = "http://schemas.xmlsoap.org/soap/envelope/" env.attrib['s:encodingStyle'] = "http://schemas.xmlsoap.org/soap/encoding/" env.attrib['xmlns:i'] = "http://www.w3.org/1999/XMLSchema-instance" body = SubElement(env, "s:Body") resp = SubElement(body, "u:%sResponse" % request.soap_action) resp.attrib['xmlns:u'] = request.soap_ns for (name,type,value) in out_args: arg = SubElement(resp, name) arg.attrib["i:type"] = type arg.text = value output = xmlprint(env) return HttpResponse(200, headers={'EXT': ''}, stream=output) except UPNPError, e: raise e except Exception, e: logger.log_error("caught unhandled exception: %s" % e) raise UPNPError(500, "Internal server error")
def _notify(self, statevars): # create the request body propset = Element("e:propertyset") propset.attrib['xmlns:e'] = "urn:schemas-upnp-org:event-1-0" prop = SubElement(propset, "e:property") # add each evented statevar to the property set for statevar in statevars: if statevar.sendEvents: SubElement(prop, statevar.name).text = statevar.text_value else: raise Exception("StateVar '%s' is not evented" % statevar.name) postData = xmlprint(propset) logger.log_debug("NOTIFY property set:\n" + postData) # send the NOTIFY request to each callback for url,urlparts in self.callbacks: # set the NOTIFY headers headers = { 'Host': urlparts.netloc, 'Content-Type': MimeType('text', 'xml'), 'NT': 'upnp:event', 'NTS': 'upnp:propchange', 'SID': self.id, 'SEQ': self.seqid } # creator = protocol.ClientCreator(reactor, HTTPClientProtocol) request = ClientRequest("NOTIFY", urlparts.path, headers, postData) d = creator.connectTCP(urlparts.hostname, urlparts.port) d.addCallback(self._sendNotifyRequest, request) logger.log_debug("sending NOTIFY to %s" % url) self.seqid = self.seqid + 1
def _renew(self, sid, timeout): try: s = self._subscribers[sid] s.handle.reset(timeout) logger.log_debug("service %s renewed subscription %s for %i seconds" % (self.serviceID, sid, timeout)) except Exception, e: logger.log_debug("service %s failed to renew %s: %s" % (self.serviceID, sid, str(e)))
def _subscribe(self, callbacks, timeout=1800): s = Subscription(callbacks, timeout) self._subscribers[s.id] = s logger.log_debug("service %s created new subscription %s" % (self.serviceID,s.id)) s.handle = reactor.callLater(timeout, self._unsubscribe, s.id) s._notify([sv for sv in self._stateVars.values() if sv.sendEvents]) return s
def _unsubscribe(self, sid): try: s = self._subscribers[sid] s.handle.cancel() del self._subscribers[sid] logger.log_debug("service %s unsubscribed %s" % (self.serviceID, sid)) except Exception, e: logger.log_debug("service %s failed to unsubscribe %s: %s" % (self.serviceID, sid, str(e)))
def _sendResponses(self, host, port, responses, delayMax): self.transport.write(responses.pop(0) + '\r\n', (host,port)) logger.log_debug("wrote ssdp response to %s:%i" % (host,port)) if len(responses) > 0: reactor.callLater(random.randint(0, delayMax), self._sendResponses, host, port, responses, delayMax)
def _sendByebye(self, usn, nt): resp = [ 'NOTIFY * HTTP/1.1', 'HOST: 239.255.255.250:1900', 'NTS: ssdp:byebye', 'NT: %s' % nt, 'USN: %s' % usn ] self.transport.write('\r\n'.join(resp) + '\r\n\r\n', (SSDP_MULTICAST_GROUP, 1900)) logger.log_debug("sent ssdp:byebye for %s on %s" % (usn, self.interface.address))
def start(self): self.servers = [] for addr,iface in self.interfaces.items(): protocol = SSDPFactory(iface) #listener = reactor.listenMulticast(1900, protocol, addr, listenMultiple=True) listener = reactor.listenMulticast(1900, protocol, listenMultiple=True) listener.joinGroup('239.255.255.250', addr) listener.setOutgoingInterface(addr) listener.setTTL(1) listener.setLoopbackMode(0) self.servers.append((addr,protocol,listener)) logger.log_debug("SSDP Server listening on %s:1900" % addr)
def unregisterDevice(self, device): if not device.UDN in self.devices: raise Exception("%s is not a registered device" % device) # advertise the device self._stopAdvertising("uuid:%s::upnp:rootdevice" % device.UDN) self._stopAdvertising("uuid:%s" % device.UDN) self._stopAdvertising("uuid:%s::%s" % (device.UDN, device.deviceType)) # advertise each service on the device for svc in device._services.values(): self._stopAdvertising("uuid:%s::%s" % (device.UDN, svc.serviceType)) logger.log_debug("unregistered device %s" % device.UDN) del self.devices[device.UDN]
def _parseTimeout(header): """Returns the event timeout""" try: unused,timeout = header.split('-') if timeout.lower() == 'infinite': return -1 timeout = int(timeout) if timeout < 1800: timeout = 1800 return timeout except Exception, e: logger.log_debug("failed to parse SUBSCRIBE timeout: %s" % str(e)) return 1800
def __call__(self, request, service, arguments): # arguments is a dict. key is the argument name, value is # the argument value as a string. a = self.in_args[:] parsed_args = [] while not a == []: arg = a.pop(0) try: arg_value = arguments[arg.name] parsed_value = arg.parse(arg_value) logger.log_debug("parsed %s => '%s'" % (arg.name, arg_value)) parsed_args.append(parsed_value) except KeyError: raise Exception("missing required InArgument %s" % arg.name) except Exception, e: raise e
def _sendAlive(self, usn, nt, udn): if not usn in self.advertisements: logger.log_warning("no registered advertisement for %s" % usn) return adv = self.advertisements[usn] resp = [ 'NOTIFY * HTTP/1.1', 'HOST: 239.255.255.250:1900', 'NTS: ssdp:alive', 'NT: %s' % nt, 'USN: %s' % usn, 'LOCATION: http://%s:%d/%s' % (self.interface.address, 1900, udn.replace(':','_')), 'CACHE-CONTROL: max-age=%d' % self.expires, 'SERVER: Twisted, UPnP/1.0, Higgins' ] self.transport.write('\r\n'.join(resp) + '\r\n\r\n', (SSDP_MULTICAST_GROUP, 1900)) logger.log_debug("sent ssdp:alive for %s on %s" % (usn, self.interface.address)) adv.delayed = reactor.callLater(self.expires, self._sendAlive, usn, nt, udn)
def registerDevice(self, device): if device.UDN in self.devices: raise Exception("%s is already a registered device" % device) # advertise the device self._startAdvertising("uuid:%s::upnp:rootdevice" % device.UDN, "upnp:rootdevice", device.UDN) self._startAdvertising("uuid:%s" % device.UDN, "uuid:%s" % device.UDN, device.UDN) self._startAdvertising("uuid:%s::%s" % (device.UDN, device.deviceType), device.deviceType, device.UDN) # advertise each service on the device for svc in device._services.values(): self._startAdvertising("uuid:%s::%s" % (device.UDN, svc.serviceType), svc.serviceType, device.UDN) self.devices[device.UDN] = device logger.log_debug("registered device %s" % device.UDN)
def locateChild(self, request, segments): segments = [part for part in segments if part != ''] logger.log_debug("%s" % '/' + '/'.join(segments)) # we need the Host header if request.host == None: logger.log_warning("can't determine host from request, ignoring") return None, [] # this is a fix for coherence (seen on v0.6.2), which doesn't send the # port number as part of the Host header. urlparts = urlparse('http://' + request.host) host = '%s:1901' % urlparts.netloc # / returns 404 if segments == []: return None, [] # the first segment is the device UDN with ':' escaped as '_' device_id = segments[0].replace('_',':') try: device = self.server.devices[device_id] except: logger.log_warning("device '%s' doesn't exist" % device_id) return None, [] # if the next segment is 'root-device.xml', return the device description segments = segments[1:] if segments == []: return StaticResource(device.getDescription(host, True), 'text/xml'), [] # otherwise the next segment is the service ID service_id = segments[0].replace('_',':') try: service = device._services[service_id] except: logger.log_warning("service '%s' doesn't exist" % service_id) return None, [] segments = segments[1:] if segments == []: return StaticResource(service.getDescription(), 'text/xml'), [] if segments[0] == 'control': return ControlResource(service), [] if segments[0] == 'event': return EventResource(service), [] return None, []
def __new__(cls, name, bases, attrs): # TODO: verify required service attributes # create UDN if needed udn_conf_key = "UPNP_" + name + "_UDN" udn = conf.get(udn_conf_key) if udn == None: udn = "".join(map(lambda x: random.choice(string.letters), xrange(20))) conf[udn_conf_key] = udn attrs["UDN"] = udn logger.log_debug("UDN for %s is %s" % (name, udn)) # load services services = {} for key, svc in attrs.items(): if isinstance(svc, UPNPDeviceService): # add the service back-reference for each StateVar and Action for statevar in svc._stateVars.values(): statevar.service = svc for action in svc._actions.values(): action.service = svc services[svc.serviceID] = svc attrs["_services"] = services return super(DeviceDeclarativeParser, cls).__new__(cls, name, bases, attrs)
def start(self): from higgins.http.server import Site from higgins.http.channel import HTTPFactory self.site = Site(RootResource(self)) self.listener = reactor.listenTCP(1901, HTTPFactory(self.site)) logger.log_debug("UPnP Server listening on port 1901")
def stop(self): for addr,protocol,listener in self.servers: protocol.sendAllByebyes() listener.stopListening() logger.log_debug("SSDP server stopped listening on %s:1900" % addr)
parsed_value = arg.parse(arg_value) logger.log_debug("parsed %s => '%s'" % (arg.name, arg_value)) parsed_args.append(parsed_value) except KeyError: raise Exception("missing required InArgument %s" % arg.name) except Exception, e: raise e try: out_args = self.action(service, request, *parsed_args) except UPNPError, e: raise e except Exception, e: logger.log_error("caught exception executing %s: %s" % (arg.name, e)) raise UPNPError(500, "Internal server error") a = self.out_args[:] parsed_args = [] while not a == []: arg = a.pop(0) try: arg_value = out_args[arg.name] parsed_value = arg.write(arg_value) logger.log_debug("wrote %s => %s" % (arg.name,parsed_value)) parsed_args.append((arg.name, arg.type, parsed_value)) except KeyError: raise Exception("missing required OutArgument %s" % arg.name) except Exception, e: raise e return parsed_args __all__ = ['Action', 'InArgument', 'OutArgument']
def _startAdvertising(self, usn, nt, udn): adv = Advertisement(usn, nt, udn) self.advertisements[usn] = adv reactor.callLater(1, self._sendByebye, usn, nt) adv.delayed = reactor.callLater(2, self._sendAlive, usn, nt, udn) logger.log_debug("registered advertisement for %s" % usn)
body = SubElement(env, "s:Body") resp = SubElement(body, "u:%sResponse" % request.soap_action) resp.attrib['xmlns:u'] = request.soap_ns for (name,type,value) in out_args: arg = SubElement(resp, name) arg.attrib["i:type"] = type arg.text = value output = xmlprint(env) return HttpResponse(200, headers={'EXT': ''}, stream=output) except UPNPError, e: raise e except Exception, e: logger.log_error("caught unhandled exception: %s" % e) raise UPNPError(500, "Internal server error") except UPNPError, e: logger.log_debug("failed to execute %s#%s: %s" % (self.service.serviceID, request.soap_action, e)) return HttpResponse(500, headers={'EXT': ''}, stream="""<?xml version="1.0"?> <u:Envelope xmlns:u="http://schemas.xmlsoap.org/soap/envelope" u:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" <u:Body> <u:Fault> <faultcode>u:Client</faultcode> <faultstring>UPnPError</faultstring> <detail> <UPnPError xmlns="urn:schemas-upnp-org:control-1-0"> <errorCode>%i</errorCode> <errorDescription>%s</errorDescription> </UPnPError>
def unregisterDevice(self, device): del self.devices[device.UDN] logger.log_debug("unregistered device %s with UPnP server" % device.UDN)
def discoveryRequest(self, headers, (host, port)): def makeResponse(st, usn, location): resp = [ 'HTTP/1.1 200 OK', 'DATE: %s' % strftime('%a, %d %B 20%y %H:%M:%S GMT', gmtime()), 'EXT: ', 'LOCATION: %s' % location, 'SERVER: Twisted, UPnP/1.0, Higgins', 'ST: %s' % st, 'USN: %s' % usn, 'CACHE-CONTROL: max-age=%d' % self.expires ] return '\r\n'.join(resp) + '\r\n' # if the MAN header is present, make sure its ssdp:discover if not headers.get('MAN', '') == '"ssdp:discover"': logger.log_debug("MAN header for discovery request is not 'ssdp:discover', ignoring") logger.log_debug2("SSDP data:\n%s" % '\n'.join(headers)) return logger.log_debug('received discovery request from %s:%d for %s' % (host, port, headers['ST'])) # Generate a response iface = self.interface.address responses = [] # return all devices and services if headers['ST'] == 'ssdp:all': for udn,device in self.devices.items(): # advertise the device responses.append(makeResponse("upnp:rootdevice", "uuid:%s::upnp:rootdevice" % udn, "http://%s:1901/%s" % (iface,udn.replace(':','_')))) responses.append(makeResponse("uuid:%s" % udn, "uuid:%s" % udn,
def _notifySuccess(self, response): readAndDiscard(response.stream) logger.log_debug("NOTIFY succeeded")
def registerDevice(self, device): self.devices[device.UDN] = device logger.log_debug("registered device %s with UPnP server" % device.UDN)