From 7f0ad937ab733608d28769e27f09d20b66736a92 Mon Sep 17 00:00:00 2001 From: cclecle Date: Thu, 6 Jan 2022 13:31:56 +0100 Subject: [PATCH 1/5] Add AllowSelfSignedSSL feature to prepare UPnP-gw-DeviceProtection-V1-Service compliance. --- README.md | 23 +++++++++++++++++++++++ upnpclient/soap.py | 8 ++++++-- upnpclient/ssdp.py | 4 ++-- upnpclient/upnp.py | 21 ++++++++++++++------- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4924f68..c0f049f 100644 --- a/README.md +++ b/README.md @@ -180,3 +180,26 @@ device.Layer3Forwarding1.GetDefaultConnectionService( If you've set either at `Device` level, they can be overridden per-call by setting them to `None`. + + +#### HTTPS Certificate + +UPnP DeviceProtection:1 Standardized secured SSL connection to Devices: +[UPnP-gw-DeviceProtection-V1-Service](http://upnp.org/specs/gw/UPnP-gw-DeviceProtection-V1-Service.pdf) +from §1.1.2: `Devices and Control Points will generate their own CA certificates` + +To be able to connect to those protected Devices, you must add `AllowSelfSignedSSL` kwargs: + +```python +device = upnpclient.Device( + "https://192.168.1.1:5000/rootDesc.xml" + AllowSelfSignedSSL=True +) +``` +Or + +```python +devices = upnpclient.discover(AllowSelfSignedSSL=True) +``` + +Note1: At the moment, upnpclient will not try to access the SSL URL (described in §2.3.1 as `SECURELOCATION.UPNP.ORG` header extentson) \ No newline at end of file diff --git a/upnpclient/soap.py b/upnpclient/soap.py index e35ec43..0906922 100644 --- a/upnpclient/soap.py +++ b/upnpclient/soap.py @@ -26,7 +26,7 @@ class SOAP(object): This class defines a simple SOAP client. """ - def __init__(self, url, service_type): + def __init__(self, url, service_type,**kwargs): self.url = url self.service_type = service_type # FIXME: Use urlparse for this: @@ -34,6 +34,10 @@ def __init__(self, url, service_type): 0 ] # Get hostname portion of url self._log = _getLogger("SOAP") + + self.AllowSelfSignedSSL = False + if "AllowSelfSignedSSL" in kwargs: + self.AllowSelfSignedSSL = kwargs["AllowSelfSignedSSL"] def _extract_upnperror(self, err_xml): """ @@ -105,7 +109,7 @@ def call(self, action_name, arg_in=None, http_auth=None, http_headers=None): try: resp = requests.post( - self.url, body, headers=headers, timeout=SOAP_TIMEOUT, auth=http_auth + self.url, body, headers=headers, timeout=SOAP_TIMEOUT, auth=http_auth, verify=not(self.AllowSelfSignedSSL) ) resp.raise_for_status() except requests.exceptions.HTTPError as exc: diff --git a/upnpclient/ssdp.py b/upnpclient/ssdp.py index 2d9e82c..42486e3 100644 --- a/upnpclient/ssdp.py +++ b/upnpclient/ssdp.py @@ -109,7 +109,7 @@ def get_addresses_ipv4(): ) -def discover(timeout=5): +def discover(timeout=5,**kwargs): """ Convenience method to discover UPnP devices on the network. Returns a list of `upnp.Device` instances. Any invalid servers are silently @@ -120,7 +120,7 @@ def discover(timeout=5): if entry.location in devices: continue try: - devices[entry.location] = Device(entry.location) + devices[entry.location] = Device(entry.location,**kwargs) except Exception as exc: log = _getLogger("ssdp") log.error("Error '%s' for %s", exc, entry) diff --git a/upnpclient/upnp.py b/upnpclient/upnp.py index 0f9974b..58b0459 100644 --- a/upnpclient/upnp.py +++ b/upnpclient/upnp.py @@ -94,6 +94,7 @@ def __init__( ignore_urlbase=False, http_auth=None, http_headers=None, + **kwargs ): """ Create a new Device instance. `location` is an URL to an XML file @@ -107,9 +108,14 @@ def __init__( self.http_auth = http_auth self.http_headers = http_headers + + + self.AllowSelfSignedSSL = False + if "AllowSelfSignedSSL" in kwargs: + self.AllowSelfSignedSSL = kwargs["AllowSelfSignedSSL"] resp = requests.get( - location, timeout=HTTP_TIMEOUT, auth=self.http_auth, headers=self.http_headers + location, timeout=HTTP_TIMEOUT, auth=self.http_auth, headers=self.http_headers, verify=not(self.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -136,7 +142,7 @@ def __init__( self._find = partial(root.find, namespaces=root.nsmap) self._findall = partial(root.findall, namespaces=root.nsmap) self._read_services() - + pass def __repr__(self): return "" % (self.friendly_name) @@ -191,7 +197,7 @@ def _read_services(self): ) self.services.append(svc) self.service_map[svc.name] = svc - + pass def find_action(self, action_name): """Find an action by name. Convenience method that searches through all the services offered by @@ -247,6 +253,7 @@ def __init__( timeout=HTTP_TIMEOUT, auth=self.device.http_auth, headers=self.device.http_headers, + verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() self.scpd_xml = etree.fromstring(resp.content) @@ -399,7 +406,7 @@ def subscribe(self, callback_url, timeout=None): if timeout is not None: headers["TIMEOUT"] = "Second-%s" % timeout resp = requests.request( - "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth + "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() return Service.validate_subscription_response(resp) @@ -413,7 +420,7 @@ def renew_subscription(self, sid, timeout=None): if timeout is not None: headers["TIMEOUT"] = "Second-%s" % timeout resp = requests.request( - "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth + "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() return Service.validate_subscription_renewal_response(resp) @@ -425,7 +432,7 @@ def cancel_subscription(self, sid): url = urljoin(self._url_base, self._event_sub_url) headers = dict(HOST=urlparse(url).netloc, SID=sid) resp = requests.request( - "UNSUBSCRIBE", url, headers=headers, auth=self.device.http_auth + "UNSUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -468,7 +475,7 @@ def __call__(self, http_auth=None, http_headers=None, **kwargs): # Make the actual call self._log.debug(">> %s (%s)", self.name, call_kwargs) - soap_client = SOAP(self.url, self.service_type) + soap_client = SOAP(self.url, self.service_type,AllowSelfSignedSSL=self.service.device.AllowSelfSignedSSL) soap_response = soap_client.call( self.name, From 79380e0aaeabeb66e8e9327ffeb5d583ab53bc5a Mon Sep 17 00:00:00 2001 From: cclecle Date: Fri, 7 Jan 2022 14:14:16 +0100 Subject: [PATCH 2/5] - add ability to bind a custom port to handle ssdp request responses - add certificate option - update Readme --- README.md | 47 +++++++++++++++++++++++++++++++++++++++------- upnpclient/soap.py | 6 +++++- upnpclient/ssdp.py | 12 ++++++++---- upnpclient/upnp.py | 15 ++++++++++----- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c0f049f..aee87a0 100644 --- a/README.md +++ b/README.md @@ -184,22 +184,55 @@ setting them to `None`. #### HTTPS Certificate -UPnP DeviceProtection:1 Standardized secured SSL connection to Devices: +UPnP DeviceProtection:1 Standardized secured SSL connection to Devices (server): [UPnP-gw-DeviceProtection-V1-Service](http://upnp.org/specs/gw/UPnP-gw-DeviceProtection-V1-Service.pdf) -from §1.1.2: `Devices and Control Points will generate their own CA certificates` +From §1.1.2: `Devices and Control Points will generate their own CA certificates`. +This means two things: +- your control-point (client) must accept Device (server) certificate, which might not be signed by trusted autorithy - eg self-signed. +- your control-point (client) must provide a certificate to the Device (server), wich also can be self-signed. -To be able to connect to those protected Devices, you must add `AllowSelfSignedSSL` kwargs: +In order to do that, two paramters have been added to kwargs: +- `AllowSelfSignedSSL`: a boolean allowing upnpclient to connect to not-trusted devices +- `cert`: to allow user-provided certificate to be used for connection ```python +mycert = ("C:\\fooo.crt", "C:\\fooo.key") device = upnpclient.Device( - "https://192.168.1.1:5000/rootDesc.xml" - AllowSelfSignedSSL=True + "https://192.168.1.1:5000/rootDesc.xml", + AllowSelfSignedSSL = True, + cert = mycert, ) ``` + +Or + +```python +devices = upnpclient.discover(AllowSelfSignedSSL=True,AllowSelfSignedSSL = True,cert = mycert) +``` + +Note: At the moment, upnpclient will not try to access the SSL URL in discover mode (described in §2.3.1 as `SECURELOCATION.UPNP.ORG` header extension) + + +#### Custom SSDP inbound port + +SSDP protocol is not well supported by firewalls (like netfilter/conntrack) so if you run this control-point client on a critical device, you may have problems setting filter rules. +Main problem is the defaut SSDP behavior which use random inbound UDP port to receive SSDP responses. + +To address that problem, we add a workaround option that let you fix this udp input port: + +```python +device = upnpclient.Device( + "https://192.168.1.1:5000/rootDesc.xml", + SSDPInPort=20000 +) +``` + Or ```python -devices = upnpclient.discover(AllowSelfSignedSSL=True) +devices = upnpclient.discover(AllowSelfSignedSSL=True,SSDPInPort=30000) ``` -Note1: At the moment, upnpclient will not try to access the SSL URL (described in §2.3.1 as `SECURELOCATION.UPNP.ORG` header extentson) \ No newline at end of file +Then you can allow this path in your firewall configuration. +Example for iptables: +```iptables -A INPUT [-i <>] -d <> -p udp --dport <> -j ACCEPT``` \ No newline at end of file diff --git a/upnpclient/soap.py b/upnpclient/soap.py index 0906922..27850a0 100644 --- a/upnpclient/soap.py +++ b/upnpclient/soap.py @@ -35,6 +35,10 @@ def __init__(self, url, service_type,**kwargs): ] # Get hostname portion of url self._log = _getLogger("SOAP") + self.ClientCert = None + if "ClientCert" in kwargs: + self.ClientCert = kwargs["ClientCert"] + self.AllowSelfSignedSSL = False if "AllowSelfSignedSSL" in kwargs: self.AllowSelfSignedSSL = kwargs["AllowSelfSignedSSL"] @@ -109,7 +113,7 @@ def call(self, action_name, arg_in=None, http_auth=None, http_headers=None): try: resp = requests.post( - self.url, body, headers=headers, timeout=SOAP_TIMEOUT, auth=http_auth, verify=not(self.AllowSelfSignedSSL) + self.url, body, headers=headers, timeout=SOAP_TIMEOUT, auth=http_auth,cert=self.ClientCert, verify=not(self.AllowSelfSignedSSL) ) resp.raise_for_status() except requests.exceptions.HTTPError as exc: diff --git a/upnpclient/ssdp.py b/upnpclient/ssdp.py index 42486e3..ad177a7 100644 --- a/upnpclient/ssdp.py +++ b/upnpclient/ssdp.py @@ -33,17 +33,21 @@ def ssdp_request(ssdp_st, ssdp_mx=SSDP_MX): ).encode("utf-8") -def scan(timeout=5): +def scan(timeout=5,**kwargs): urls = [] sockets = [] ssdp_requests = [ssdp_request(ST_ALL), ssdp_request(ST_ROOTDEVICE)] stop_wait = datetime.now() + timedelta(seconds=timeout) + + SSDPInPort = 0 + if "SSDPInPort" in kwargs: + SSDPInPort = kwargs["SSDPInPort"] for addr in get_addresses_ipv4(): try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, SSDP_MX) - sock.bind((addr, 0)) + sock.bind((addr, SSDPInPort)) sockets.append(sock) except socket.error: pass @@ -114,9 +118,9 @@ def discover(timeout=5,**kwargs): Convenience method to discover UPnP devices on the network. Returns a list of `upnp.Device` instances. Any invalid servers are silently ignored. - """ + """ devices = {} - for entry in scan(timeout): + for entry in scan(timeout,**kwargs): if entry.location in devices: continue try: diff --git a/upnpclient/upnp.py b/upnpclient/upnp.py index 58b0459..863ddc4 100644 --- a/upnpclient/upnp.py +++ b/upnpclient/upnp.py @@ -110,12 +110,16 @@ def __init__( self.http_headers = http_headers + self.ClientCert = None + if "ClientCert" in kwargs: + self.ClientCert = kwargs["ClientCert"] + self.AllowSelfSignedSSL = False if "AllowSelfSignedSSL" in kwargs: self.AllowSelfSignedSSL = kwargs["AllowSelfSignedSSL"] - + print(self.ClientCert) resp = requests.get( - location, timeout=HTTP_TIMEOUT, auth=self.http_auth, headers=self.http_headers, verify=not(self.AllowSelfSignedSSL) + location, timeout=HTTP_TIMEOUT, auth=self.http_auth, headers=self.http_headers,cert=self.ClientCert, verify=not(self.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -253,6 +257,7 @@ def __init__( timeout=HTTP_TIMEOUT, auth=self.device.http_auth, headers=self.device.http_headers, + cert=self.device.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -406,7 +411,7 @@ def subscribe(self, callback_url, timeout=None): if timeout is not None: headers["TIMEOUT"] = "Second-%s" % timeout resp = requests.request( - "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) + "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() return Service.validate_subscription_response(resp) @@ -420,7 +425,7 @@ def renew_subscription(self, sid, timeout=None): if timeout is not None: headers["TIMEOUT"] = "Second-%s" % timeout resp = requests.request( - "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) + "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() return Service.validate_subscription_renewal_response(resp) @@ -432,7 +437,7 @@ def cancel_subscription(self, sid): url = urljoin(self._url_base, self._event_sub_url) headers = dict(HOST=urlparse(url).netloc, SID=sid) resp = requests.request( - "UNSUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) + "UNSUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() From 638ea2aae611164564c6552fa31ef9d8874b7ab0 Mon Sep 17 00:00:00 2001 From: cclecle Date: Fri, 7 Jan 2022 14:14:16 +0100 Subject: [PATCH 3/5] - add ability to bind a custom port to handle ssdp request responses - add certificate option - update Readme --- README.md | 52 +++++++++++++++++++++++++++++++++++++++------- upnpclient/soap.py | 6 +++++- upnpclient/ssdp.py | 12 +++++++---- upnpclient/upnp.py | 15 ++++++++----- 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c0f049f..6ecaddd 100644 --- a/README.md +++ b/README.md @@ -184,22 +184,60 @@ setting them to `None`. #### HTTPS Certificate -UPnP DeviceProtection:1 Standardized secured SSL connection to Devices: +UPnP DeviceProtection:1 Standardized secured SSL connection to Devices (server): [UPnP-gw-DeviceProtection-V1-Service](http://upnp.org/specs/gw/UPnP-gw-DeviceProtection-V1-Service.pdf) -from §1.1.2: `Devices and Control Points will generate their own CA certificates` -To be able to connect to those protected Devices, you must add `AllowSelfSignedSSL` kwargs: +From §1.1.2: `Devices and Control Points will generate their own CA certificates`. + +This means two things: +- your control-point (client) must accept Device (server) certificate, which might not be signed by trusted autorithy - eg self-signed. +- your control-point (client) must provide a certificate to the Device (server), wich also can be self-signed. + +In order to do that, two paramters have been added to kwargs: +- `AllowSelfSignedSSL`: a boolean allowing upnpclient to connect to not-trusted devices +- `cert`: to allow user-provided certificate to be used for connection ```python +mycert = ("C:\\fooo.crt", "C:\\fooo.key") device = upnpclient.Device( - "https://192.168.1.1:5000/rootDesc.xml" - AllowSelfSignedSSL=True + "https://192.168.1.1:5000/rootDesc.xml", + AllowSelfSignedSSL = True, + cert = mycert, ) ``` + Or ```python -devices = upnpclient.discover(AllowSelfSignedSSL=True) +devices = upnpclient.discover(AllowSelfSignedSSL=True,AllowSelfSignedSSL = True,cert = mycert) ``` -Note1: At the moment, upnpclient will not try to access the SSL URL (described in §2.3.1 as `SECURELOCATION.UPNP.ORG` header extentson) \ No newline at end of file +Note: At the moment, upnpclient will not try to access the SSL URL in discover mode (described in §2.3.1 as `SECURELOCATION.UPNP.ORG` header extension) + + +#### Custom SSDP inbound port + +SSDP protocol is not well supported by firewalls (like netfilter/conntrack) so if you run this control-point client on a critical device, you may have problems setting filter rules. + +Main problem is the defaut SSDP behavior which use random inbound UDP port to receive SSDP responses. + +To address that problem, we add a workaround option that let you fix this udp input port: + +```python +device = upnpclient.Device( + "https://192.168.1.1:5000/rootDesc.xml", + SSDPInPort=20000 +) +``` + +Or + +```python +devices = upnpclient.discover(AllowSelfSignedSSL=True,SSDPInPort=30000) +``` + +Then you can allow this path in your firewall configuration. + +Example for iptables: + +```iptables -A INPUT [-i <>] -d <> -p udp --dport <> -j ACCEPT``` \ No newline at end of file diff --git a/upnpclient/soap.py b/upnpclient/soap.py index 0906922..27850a0 100644 --- a/upnpclient/soap.py +++ b/upnpclient/soap.py @@ -35,6 +35,10 @@ def __init__(self, url, service_type,**kwargs): ] # Get hostname portion of url self._log = _getLogger("SOAP") + self.ClientCert = None + if "ClientCert" in kwargs: + self.ClientCert = kwargs["ClientCert"] + self.AllowSelfSignedSSL = False if "AllowSelfSignedSSL" in kwargs: self.AllowSelfSignedSSL = kwargs["AllowSelfSignedSSL"] @@ -109,7 +113,7 @@ def call(self, action_name, arg_in=None, http_auth=None, http_headers=None): try: resp = requests.post( - self.url, body, headers=headers, timeout=SOAP_TIMEOUT, auth=http_auth, verify=not(self.AllowSelfSignedSSL) + self.url, body, headers=headers, timeout=SOAP_TIMEOUT, auth=http_auth,cert=self.ClientCert, verify=not(self.AllowSelfSignedSSL) ) resp.raise_for_status() except requests.exceptions.HTTPError as exc: diff --git a/upnpclient/ssdp.py b/upnpclient/ssdp.py index 42486e3..ad177a7 100644 --- a/upnpclient/ssdp.py +++ b/upnpclient/ssdp.py @@ -33,17 +33,21 @@ def ssdp_request(ssdp_st, ssdp_mx=SSDP_MX): ).encode("utf-8") -def scan(timeout=5): +def scan(timeout=5,**kwargs): urls = [] sockets = [] ssdp_requests = [ssdp_request(ST_ALL), ssdp_request(ST_ROOTDEVICE)] stop_wait = datetime.now() + timedelta(seconds=timeout) + + SSDPInPort = 0 + if "SSDPInPort" in kwargs: + SSDPInPort = kwargs["SSDPInPort"] for addr in get_addresses_ipv4(): try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, SSDP_MX) - sock.bind((addr, 0)) + sock.bind((addr, SSDPInPort)) sockets.append(sock) except socket.error: pass @@ -114,9 +118,9 @@ def discover(timeout=5,**kwargs): Convenience method to discover UPnP devices on the network. Returns a list of `upnp.Device` instances. Any invalid servers are silently ignored. - """ + """ devices = {} - for entry in scan(timeout): + for entry in scan(timeout,**kwargs): if entry.location in devices: continue try: diff --git a/upnpclient/upnp.py b/upnpclient/upnp.py index 58b0459..863ddc4 100644 --- a/upnpclient/upnp.py +++ b/upnpclient/upnp.py @@ -110,12 +110,16 @@ def __init__( self.http_headers = http_headers + self.ClientCert = None + if "ClientCert" in kwargs: + self.ClientCert = kwargs["ClientCert"] + self.AllowSelfSignedSSL = False if "AllowSelfSignedSSL" in kwargs: self.AllowSelfSignedSSL = kwargs["AllowSelfSignedSSL"] - + print(self.ClientCert) resp = requests.get( - location, timeout=HTTP_TIMEOUT, auth=self.http_auth, headers=self.http_headers, verify=not(self.AllowSelfSignedSSL) + location, timeout=HTTP_TIMEOUT, auth=self.http_auth, headers=self.http_headers,cert=self.ClientCert, verify=not(self.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -253,6 +257,7 @@ def __init__( timeout=HTTP_TIMEOUT, auth=self.device.http_auth, headers=self.device.http_headers, + cert=self.device.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -406,7 +411,7 @@ def subscribe(self, callback_url, timeout=None): if timeout is not None: headers["TIMEOUT"] = "Second-%s" % timeout resp = requests.request( - "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) + "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() return Service.validate_subscription_response(resp) @@ -420,7 +425,7 @@ def renew_subscription(self, sid, timeout=None): if timeout is not None: headers["TIMEOUT"] = "Second-%s" % timeout resp = requests.request( - "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) + "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() return Service.validate_subscription_renewal_response(resp) @@ -432,7 +437,7 @@ def cancel_subscription(self, sid): url = urljoin(self._url_base, self._event_sub_url) headers = dict(HOST=urlparse(url).netloc, SID=sid) resp = requests.request( - "UNSUBSCRIBE", url, headers=headers, auth=self.device.http_auth, verify=not(self.device.AllowSelfSignedSSL) + "UNSUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() From 520b0c6b9ef149491b88a1ac15e2252cbcf9ce4f Mon Sep 17 00:00:00 2001 From: cclecle Date: Fri, 7 Jan 2022 14:19:26 +0100 Subject: [PATCH 4/5] readme: tab -> spaces --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6ecaddd..76556a5 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ mycert = ("C:\\fooo.crt", "C:\\fooo.key") device = upnpclient.Device( "https://192.168.1.1:5000/rootDesc.xml", AllowSelfSignedSSL = True, - cert = mycert, + cert = mycert, ) ``` @@ -226,7 +226,7 @@ To address that problem, we add a workaround option that let you fix this udp in ```python device = upnpclient.Device( "https://192.168.1.1:5000/rootDesc.xml", - SSDPInPort=20000 + SSDPInPort=20000 ) ``` From c6f6a7dd291b7b1fb901c084fef5af79e521d672 Mon Sep 17 00:00:00 2001 From: cclecle Date: Sun, 9 Jan 2022 15:30:46 +0100 Subject: [PATCH 5/5] - switch requests to session mode - Subclassing HTTP / requests to get peer_certificate back from lower levels (WIP) --- upnpclient/soap.py | 5 ++- upnpclient/upnp.py | 101 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 12 deletions(-) diff --git a/upnpclient/soap.py b/upnpclient/soap.py index 27850a0..ef34236 100644 --- a/upnpclient/soap.py +++ b/upnpclient/soap.py @@ -26,7 +26,8 @@ class SOAP(object): This class defines a simple SOAP client. """ - def __init__(self, url, service_type,**kwargs): + def __init__(self,action, url, service_type,**kwargs): + self.action = action self.url = url self.service_type = service_type # FIXME: Use urlparse for this: @@ -112,7 +113,7 @@ def call(self, action_name, arg_in=None, http_auth=None, http_headers=None): headers.update(http_headers or {}) try: - resp = requests.post( + resp = self.action.service.device.session.post( self.url, body, headers=headers, timeout=SOAP_TIMEOUT, auth=http_auth,cert=self.ClientCert, verify=not(self.AllowSelfSignedSSL) ) resp.raise_for_status() diff --git a/upnpclient/upnp.py b/upnpclient/upnp.py index 863ddc4..2144f4b 100644 --- a/upnpclient/upnp.py +++ b/upnpclient/upnp.py @@ -7,7 +7,7 @@ from collections import OrderedDict import six -import requests +from requests import Session from requests.compat import urljoin, urlparse from dateutil.parser import parse as parse_date from lxml import etree @@ -18,6 +18,83 @@ from .marshal import marshal_value + + +""" +Subclassing HTTP / requests to get peer_certificate back from lower levels +""" +from typing import Optional, Mapping, Any +from http.client import HTTPSConnection +from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK +from urllib3.poolmanager import PoolManager,key_fn_by_scheme +from urllib3.connectionpool import HTTPSConnectionPool,HTTPConnectionPool +from urllib3.connection import HTTPSConnection,HTTPConnection +from urllib3.response import HTTPResponse as URLLIB3_HTTPResponse + +#force urllib3 to use pyopenssl +import urllib3.contrib.pyopenssl +urllib3.contrib.pyopenssl.inject_into_urllib3() + +class HTTPSConnection_withcert(HTTPSConnection): + def __init__(self, *args, **kw): + self.peer_certificate = None + super().__init__(*args, **kw) + def connect(self): + res = super().connect() + self.peer_certificate = self.sock.connection.get_peer_certificate() + return res + +class HTTPResponse_withcert(URLLIB3_HTTPResponse): + def __init__(self, *args, **kwargs): + self.peer_certificate = None + res = super().__init__( *args, **kwargs) + self.peer_certificate = self._connection.peer_certificate + return res + +class HTTPSConnectionPool_withcert(HTTPSConnectionPool): + ConnectionCls = HTTPSConnection_withcert + ResponseCls = HTTPResponse_withcert + +class PoolManager_withcert(PoolManager): + def __init__( + self, + num_pools: int = 10, + headers: Optional[Mapping[str, str]] = None, + **connection_pool_kw: Any, + ) -> None: + super().__init__(num_pools,headers,**connection_pool_kw) + self.pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool_withcert} + self.key_fn_by_scheme = key_fn_by_scheme.copy() + +class HTTPAdapter_withcert(HTTPAdapter): + _clsHTTPResponse = HTTPResponse_withcert + def build_response(self, request, resp): + response = super().build_response( request, resp) + response.peer_certificate = resp.peer_certificate + return response + + def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs): + #do not call super() to not initialize PoolManager twice + # save these values for pickling + self._pool_connections = connections + self._pool_maxsize = maxsize + self._pool_block = block + + self.poolmanager = PoolManager_withcert(num_pools=connections, + maxsize=maxsize, + block=block, + strict=True, + **pool_kwargs) + +class Session_withcert(Session): + def __init__(self): + super().__init__() + self.mount('https://', HTTPAdapter_withcert()) + + + + + class UPNPError(Exception): """ Exception class for UPnP errors. @@ -86,7 +163,6 @@ class Device(CallActionMixin): urn:upnp-org:serviceId:wandsllc:pvc_Internet urn:upnp-org:serviceId:wanipc:Internet """ - def __init__( self, location, @@ -109,7 +185,6 @@ def __init__( self.http_auth = http_auth self.http_headers = http_headers - self.ClientCert = None if "ClientCert" in kwargs: self.ClientCert = kwargs["ClientCert"] @@ -118,9 +193,14 @@ def __init__( if "AllowSelfSignedSSL" in kwargs: self.AllowSelfSignedSSL = kwargs["AllowSelfSignedSSL"] print(self.ClientCert) - resp = requests.get( + + self.session = Session_withcert() + resp = self.session.get( location, timeout=HTTP_TIMEOUT, auth=self.http_auth, headers=self.http_headers,cert=self.ClientCert, verify=not(self.AllowSelfSignedSSL) ) + + print(resp.peer_certificate.get_subject()) + resp.raise_for_status() root = etree.fromstring(resp.content) @@ -146,6 +226,7 @@ def __init__( self._find = partial(root.find, namespaces=root.nsmap) self._findall = partial(root.findall, namespaces=root.nsmap) self._read_services() + print(self._url_base) pass def __repr__(self): return "" % (self.friendly_name) @@ -249,10 +330,10 @@ def __init__( self._log.debug("%s SCPDURL: %s", self.service_id, self.scpd_url) self._log.debug("%s controlURL: %s", self.service_id, self._control_url) self._log.debug("%s eventSubURL: %s", self.service_id, self._event_sub_url) - + print(self._url_base) url = urljoin(self._url_base, self.scpd_url) self._log.debug("Reading %s", url) - resp = requests.get( + resp = self.device.session.get( url, timeout=HTTP_TIMEOUT, auth=self.device.http_auth, @@ -410,7 +491,7 @@ def subscribe(self, callback_url, timeout=None): ) if timeout is not None: headers["TIMEOUT"] = "Second-%s" % timeout - resp = requests.request( + resp = self.device.session.post( "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -424,7 +505,7 @@ def renew_subscription(self, sid, timeout=None): headers = dict(HOST=urlparse(url).netloc, SID=sid) if timeout is not None: headers["TIMEOUT"] = "Second-%s" % timeout - resp = requests.request( + resp = self.device.session.post( "SUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -436,7 +517,7 @@ def cancel_subscription(self, sid): """ url = urljoin(self._url_base, self._event_sub_url) headers = dict(HOST=urlparse(url).netloc, SID=sid) - resp = requests.request( + resp = self.device.session.post( "UNSUBSCRIBE", url, headers=headers, auth=self.device.http_auth,cert=self.ClientCert, verify=not(self.device.AllowSelfSignedSSL) ) resp.raise_for_status() @@ -480,7 +561,7 @@ def __call__(self, http_auth=None, http_headers=None, **kwargs): # Make the actual call self._log.debug(">> %s (%s)", self.name, call_kwargs) - soap_client = SOAP(self.url, self.service_type,AllowSelfSignedSSL=self.service.device.AllowSelfSignedSSL) + soap_client = SOAP(self,self.url, self.service_type,AllowSelfSignedSSL=self.service.device.AllowSelfSignedSSL,ClientCert=self.service.device.ClientCert) soap_response = soap_client.call( self.name,