From bad86c998e3b08b60b2d0990d969d8f154a3a6f7 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 4 Mar 2025 15:27:00 +0100 Subject: [PATCH 01/17] Added S2Pairing implementation --- dev-requirements.txt | 11 +++ setup.cfg | 2 + src/s2python/generated/gen_s2_pairing.py | 80 ++++++++++++++++++++++ src/s2python/s2_pairing.py | 86 ++++++++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 src/s2python/generated/gen_s2_pairing.py create mode 100644 src/s2python/s2_pairing.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 183afa3..31e60b9 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -239,6 +239,17 @@ wheel==0.44.0 # via pip-tools zipp==3.20.1 # via importlib-metadata +jwskate==0.11.1 +binapy==0.8.0 + # via jwskate +cffi==1.17.1 + # via jwskate +cryptography==44.0.2 + # via jwskate +pycparser==2.22 + # via jwskate +requests==2.32.3 + # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/setup.cfg b/setup.cfg index b23fdfb..2395f6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,8 @@ install_requires = pytz click websockets~=13.1 + jwskate~=0.11 + requests~=2.32.3 [options.packages.find] where = src diff --git a/src/s2python/generated/gen_s2_pairing.py b/src/s2python/generated/gen_s2_pairing.py new file mode 100644 index 0000000..7ed825d --- /dev/null +++ b/src/s2python/generated/gen_s2_pairing.py @@ -0,0 +1,80 @@ +# generated by datamodel-codegen: +# filename: s2-over-ip-pairing +# timestamp: 2025-02-28T14:52:45+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import List + +from pydantic import BaseModel, ConfigDict, Field +from s2python.common import EnergyManagementRole as S2Role + + + +class Deployment(Enum): + WAN = 'WAN' + LAN = 'LAN' + + +class Protocols(Enum): + WebSocketSecure = 'WebSocketSecure' + + +class PairingInfo(BaseModel): + model_config = ConfigDict( + extra='forbid', + ) + pairingUri: str + token: str + validUntil: str + + +class S2NodeDescription(BaseModel): + model_config = ConfigDict( + extra='forbid', + ) + brand: str + logoUri: str + type: str + modelName: str + userDefinedName: str + role: S2Role + deployment: Deployment + + +class PairingRequest(BaseModel): + model_config = ConfigDict( + extra='forbid', + ) + token: str + publicKey: str + s2ClientNodeId: str + s2ClientNodeDescription: str + supportedProtocols: List[Protocols] + + +class PairingResponse(BaseModel): + model_config = ConfigDict( + extra='forbid', + ) + s2ServerNodeId: str + serverNodeDescription: str + requestConnectionUri: str + + +class ConnectionRequest(BaseModel): + model_config = ConfigDict( + extra='forbid', + ) + s2ClientNodeId: str + supportedProtocols: List[Protocols] + + +class ConnectionDetails(BaseModel): + model_config = ConfigDict( + extra='forbid', + ) + selectedProtocol: Protocols + challenge: str + connectionUri: str diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py new file mode 100644 index 0000000..6d62095 --- /dev/null +++ b/src/s2python/s2_pairing.py @@ -0,0 +1,86 @@ +import logging +import uuid +from typing import Tuple +import requests + +from jwskate import JweCompact +from jwskate.jwk.rsa import RSAJwk +from binapy.binapy import BinaPy + +from s2python.generated.gen_s2_pairing import (Protocols, + PairingRequest, + S2NodeDescription, + PairingResponse, + ConnectionRequest, + ConnectionDetails) + + +logger = logging.getLogger("s2python") + +class S2Pairing: # pylint: disable=too-many-instance-attributes + paired: bool + s2_server_node_id: str + server_node_description: str + selected_protocol: Protocols + connection_uri: str + challenge: BinaPy + + _request_pairing_endpoint: str + _token: str + _s2_client_node_description: S2NodeDescription + _verify_certificate: bool | str # pylint: disable=E1131 + _client_node_id: str + _supported_protocols: Tuple[Protocols] + _rsa_key_pair: RSAJwk + def __init__( # pylint: disable=too-many-arguments + self, + request_pairing_endpoint: str, + token: str, + s2_client_node_description: S2NodeDescription, + verify_certificate: bool | str = False, # pylint: disable=E1131 + client_node_id: str = str(uuid.uuid4()), + supported_protocols: Tuple[Protocols] = (Protocols.WebSocketSecure, ) + ) -> None: + self.paired = False + + self._request_pairing_endpoint = request_pairing_endpoint + self._token = token + self._s2_client_node_description = s2_client_node_description + self._verify_certificate = verify_certificate + self._client_node_id = client_node_id + self._supported_protocols = supported_protocols + self._rsa_key_pair = RSAJwk(self._rsa_key_pair) + + def pair(self) -> bool: + self.paired = False + pairing_request: PairingRequest = PairingRequest(token=self._token, + publicKey=self._rsa_key_pair.public_jwk().to_pem(), + s2ClientNodeId=self._client_node_id, + s2ClientNodeDescription=self._s2_client_node_description, + supportedProtocols=self._supported_protocols) + + response = requests.post(self._request_pairing_endpoint, + json=pairing_request.model_dump_json(), + timeout=10, + verify = self._verify_certificate) + response.raise_for_status() + pairing_response: PairingResponse = PairingResponse.parse_raw(response.json()) + self.s2_server_node_id = pairing_response.s2ServerNodeId + self.server_node_description = pairing_response.serverNodeDescription + + connection_request: ConnectionRequest = ConnectionRequest(s2ClientNodeId=self._client_node_id, + supportedProtocols=self._supported_protocols) + + response = requests.post(pairing_response.requestConnectionUri, + json=connection_request.model_dump_json(), + timeout=10, + verify = self._verify_certificate) + response.raise_for_status() + connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json()) + + self.selected_protocol = connection_details.selectedProtocol + self.connection_uri = connection_details.connectionUri + self.challenge = JweCompact(connection_details.challenge).decrypt(self._rsa_key_pair) + + self.paired = True + return self.paired From 7b251cd2feeb49e903d2f9d5c984213ca0b181cf Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 4 Mar 2025 15:32:22 +0100 Subject: [PATCH 02/17] added types-requests to dev-requirements for typetesting of S2Pairing --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 31e60b9..87282f6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -249,6 +249,7 @@ cryptography==44.0.2 pycparser==2.22 # via jwskate requests==2.32.3 +types-requests==2.32.0.20250301 # The following packages are considered to be unsafe in a requirements file: From 54c2b95756cdb6fe90c98ec541c121d7e5a9c7ab Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 4 Mar 2025 15:44:46 +0100 Subject: [PATCH 03/17] adjuested types-requests version to play nicely with linting --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 87282f6..9426c39 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -249,7 +249,7 @@ cryptography==44.0.2 pycparser==2.22 # via jwskate requests==2.32.3 -types-requests==2.32.0.20250301 +types-requests==2.32.0.20241016 # The following packages are considered to be unsafe in a requirements file: From dfe01ae0c202341d14366bd71c5739a89f1e8b55 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 4 Mar 2025 15:57:20 +0100 Subject: [PATCH 04/17] fix typing error for python 3.8 and 3.9 --- .../Initial/WindowsCommandPrompt.pkgx | Bin 0 -> 2089 bytes src/s2python/s2_pairing.py | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 %LOCALAPPDATA%/Microsoft/UEV/%COMPUTERNAME%/WindowsCommandPrompt/Initial/WindowsCommandPrompt.pkgx diff --git a/%LOCALAPPDATA%/Microsoft/UEV/%COMPUTERNAME%/WindowsCommandPrompt/Initial/WindowsCommandPrompt.pkgx b/%LOCALAPPDATA%/Microsoft/UEV/%COMPUTERNAME%/WindowsCommandPrompt/Initial/WindowsCommandPrompt.pkgx new file mode 100644 index 0000000000000000000000000000000000000000..6f8614dab01d7787610e4cf3a879a3f0c983830b GIT binary patch literal 2089 zcmah~3pg8C7LI4A($b<;j8yGMJ)%mzimf!pvrVKzy@kl6B0++xmZn~*AX}Y!mn~Iq zO^ruTLD;m6*L0K`p}tU6R6J`awNf(M-P!&2-0wU0+*;y-+y5aBBHtgiQR#T z32+B|BT~DzPZ%0U1ncoNn)dSnmi}RAtVYlHrJxJRhVsT@4bE0->@(6iz-+ZOUa!55 zUQ#sA)G^kLgLtm_OxoKT@mlTWT*y!3whYI~CvhDE1s}@7u!}f#s=<0z#M=QIm6A-Rr~o!3hf!N|C#N< z-2ft*M8e`RL_HK1|IW=R=m`91%RP5R1*;3%lw_BSq8~F|v?)W!%GvItS+-h3ScAG> zY9;M*V$@EakzNwk7Q~gn^ugLgPneFc0&UHB z56y{vExf*4kxfs^!>}7^_K_QUTpBgZYWj^6F}Ax~yW;5tPcGDHsIESUY7|-d;}#LJm$XB23iomu{}_R!F`uXqnUTM6gZdt2;l%{ zCI)KNIYz!d>qFU1$T&!DRtTjzgK79#g7irFxgB9om9;3GECcS`Lz|QvZnEkzJ;qp>!;KmUS=J$WmFAdAh6g z(MrKL>bLRJdD#_^SgNlB&5CMK#`8`8P~d6Vv^IRMO?fH>w~x!4S6$RI)hYPDJ$?OCd209Os1B|OHzq1?VX|7; zQkpT?8a!40L%WahkDZkgr72sXsvkJyJQhxK&J=kS`kemp#Yk&&N>}wV_S}dFedzE9 z>nVmJMv0r7Z`JpjGo|ui(MEI#8`mJ#IG8kM;q^3n`sIcwv0}lt8Z0Fa#JG< zAvb-FY&7C)DKezL9yOhhXYVA&uia^L52gnW*WQ#5q(mUgs`W>dYKoYZfVqd$Y3^%Y zlTl}vPggY4COI$qn*u-TVKvwsXdt}hi=|t_BNp`@I|`#8Nw3$Oca%OotkzZuN&$Y5 zm`2TL80P$C%>|rmdVeW5Ydt5v0paWI@!TdciQ$?Svei+D>AkpB62BAI<~=g;R)&xS z_2DGVr*dsk_1YMw%r?~ZQi`1CcR*;u40=uW&EwL<2!v4|N+F-VIHvr%@r(c5Z__>j zmc|29!-RQaw)RR){TW)nzi>3t+Oq#9col__BAa>OibN4VN&Bf8!F#piUuI9240>8v6gjC3oY7AYd$5w+G(vnW_zOV+hmYSr!r zVd)d9{JdVDx8s9`;sL5WYaKQBk1HGJJ;0w7`Q%jPc{bQXbdQw8wf`SE9oY@=fDrH` zG@j&hB`O3>^wPT?jJrkqS>_I(l8dve$c0k=2Wbg3aND6$bUfQ2_&57Sp6=gI_Gib-VV@jX8$2ygu%K#g2sa&`&nJS-=MyfcLX*3C z8Q$?GO&J~_ULlm(Rim4kDC}&@B8hZwQH+-c$Pd{J?^>BUV1!#+m&(tOqY&2K&=A;2+!fY$|}f@qk-zk~A2EdV|EC(v(spN}eNh@e3W`>_9s%1=jt zdgM>2|D|Dka6#UJCsW1t&|wbZ;yU{m@#`TFk$pojOYGN;EWYR1JqP4lcfV=?fbWJm zh=~gxCYUV&(CZ`{EDjAJgyTsT`sVvfTR-9tyDMijkwggl!+8ZA9D?&Bp^@k?B9?%+ HIAi`#Z1H#g literal 0 HcmV?d00001 diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index 6d62095..a503e46 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -1,6 +1,6 @@ import logging import uuid -from typing import Tuple +from typing import Tuple, Union import requests from jwskate import JweCompact @@ -28,7 +28,7 @@ class S2Pairing: # pylint: disable=too-many-instance-attributes _request_pairing_endpoint: str _token: str _s2_client_node_description: S2NodeDescription - _verify_certificate: bool | str # pylint: disable=E1131 + _verify_certificate: Union[bool, str] _client_node_id: str _supported_protocols: Tuple[Protocols] _rsa_key_pair: RSAJwk @@ -37,7 +37,7 @@ def __init__( # pylint: disable=too-many-arguments request_pairing_endpoint: str, token: str, s2_client_node_description: S2NodeDescription, - verify_certificate: bool | str = False, # pylint: disable=E1131 + verify_certificate: Union[bool, str] = False, client_node_id: str = str(uuid.uuid4()), supported_protocols: Tuple[Protocols] = (Protocols.WebSocketSecure, ) ) -> None: From 8565d8e35cdebcbf06ce310deb2ccc011e46ce7e Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 4 Mar 2025 17:16:14 +0100 Subject: [PATCH 05/17] added facility to update chellange if timeout has happened --- src/s2python/s2_pairing.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index a503e46..15f5105 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -1,5 +1,6 @@ import logging import uuid +import datetime from typing import Tuple, Union import requests @@ -17,14 +18,17 @@ logger = logging.getLogger("s2python") +REQTEST_TIMEOUT = 10 +CHALLANGE_TIMEOUT = datetime.timedelta(minutes=5) + class S2Pairing: # pylint: disable=too-many-instance-attributes - paired: bool s2_server_node_id: str server_node_description: str selected_protocol: Protocols connection_uri: str - challenge: BinaPy + _challenge: BinaPy + _paring_timestamp: datetime.datetime _request_pairing_endpoint: str _token: str _s2_client_node_description: S2NodeDescription @@ -41,8 +45,7 @@ def __init__( # pylint: disable=too-many-arguments client_node_id: str = str(uuid.uuid4()), supported_protocols: Tuple[Protocols] = (Protocols.WebSocketSecure, ) ) -> None: - self.paired = False - + self._paring_timestamp = datetime.datetime(year = datetime.MINYEAR, month = 1, day = 1) self._request_pairing_endpoint = request_pairing_endpoint self._token = token self._s2_client_node_description = s2_client_node_description @@ -51,8 +54,12 @@ def __init__( # pylint: disable=too-many-arguments self._supported_protocols = supported_protocols self._rsa_key_pair = RSAJwk(self._rsa_key_pair) - def pair(self) -> bool: - self.paired = False + def get_challenge(self) -> BinaPy: + # If pairing was done within the timeout, the existing chellange can be returned + if (self._paring_timestamp + CHALLANGE_TIMEOUT) < datetime.datetime.now(): + return self._challenge + + self._paring_timestamp = datetime.datetime.now() pairing_request: PairingRequest = PairingRequest(token=self._token, publicKey=self._rsa_key_pair.public_jwk().to_pem(), s2ClientNodeId=self._client_node_id, @@ -61,7 +68,7 @@ def pair(self) -> bool: response = requests.post(self._request_pairing_endpoint, json=pairing_request.model_dump_json(), - timeout=10, + timeout=REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() pairing_response: PairingResponse = PairingResponse.parse_raw(response.json()) @@ -73,14 +80,13 @@ def pair(self) -> bool: response = requests.post(pairing_response.requestConnectionUri, json=connection_request.model_dump_json(), - timeout=10, + timeout=REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json()) self.selected_protocol = connection_details.selectedProtocol self.connection_uri = connection_details.connectionUri - self.challenge = JweCompact(connection_details.challenge).decrypt(self._rsa_key_pair) + self._challenge = JweCompact(connection_details.challenge).decrypt(self._rsa_key_pair) - self.paired = True - return self.paired + return self._challenge From 1ed93421f19eb28531037ac3257adb86b076116c Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 4 Mar 2025 17:21:46 +0100 Subject: [PATCH 06/17] changed get_challange to property for S2Pairing --- src/s2python/s2_pairing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index 15f5105..e20b217 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -54,7 +54,8 @@ def __init__( # pylint: disable=too-many-arguments self._supported_protocols = supported_protocols self._rsa_key_pair = RSAJwk(self._rsa_key_pair) - def get_challenge(self) -> BinaPy: + @property + def challenge(self) -> BinaPy: # If pairing was done within the timeout, the existing chellange can be returned if (self._paring_timestamp + CHALLANGE_TIMEOUT) < datetime.datetime.now(): return self._challenge From c05abec4bf423e967415d5b7db52625522c78760 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Wed, 5 Mar 2025 11:00:25 +0100 Subject: [PATCH 07/17] Added doctsrings --- src/s2python/s2_pairing.py | 59 +++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index e20b217..11cd436 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -1,6 +1,7 @@ import logging import uuid import datetime +from dataclasses import dataclass from typing import Tuple, Union import requests @@ -18,16 +19,24 @@ logger = logging.getLogger("s2python") + REQTEST_TIMEOUT = 10 -CHALLANGE_TIMEOUT = datetime.timedelta(minutes=5) +PAIRING_TIMEOUT = datetime.timedelta(minutes=5) + +@dataclass +class PairingDetails: + """The result of an S2 pairing + :param pairing_response: Details about the server. + :param connection_details: Details about how to connect. + :param supported_protocols: The decrypted challenge needed as bearer token.""" + pairing_response: PairingResponse + connection_details: ConnectionDetails + decrypted_challenge: BinaPy class S2Pairing: # pylint: disable=too-many-instance-attributes - s2_server_node_id: str - server_node_description: str - selected_protocol: Protocols - connection_uri: str + _pairing_response: PairingResponse + _connection_details: ConnectionDetails _challenge: BinaPy - _paring_timestamp: datetime.datetime _request_pairing_endpoint: str _token: str @@ -45,6 +54,15 @@ def __init__( # pylint: disable=too-many-arguments client_node_id: str = str(uuid.uuid4()), supported_protocols: Tuple[Protocols] = (Protocols.WebSocketSecure, ) ) -> None: + """Creates an S2 pairing for the device and holds the challenge needed to be provided as bearer token + when setting up an S2 (websockets) communication session + :param request_pairing_endpoint: The full uri endpoint to request pairing from. + :param token: The token that needs to be provided to the server in teh pairing process. + :param s2_client_node_description: The descriptin ofr the client as a S2NodeDescription. + :param verify_certificate: Either a boolean whether or not to verify the server's SSL certificate + (defaults to False), or a path to a certificate file to use for verification purposes. + :param client_node_id: UUID for the client. If none is given, one will be generated. + :param supported_protocols: The protocols supported by the client (defaults: Protocols.WebSocketSecure).""" self._paring_timestamp = datetime.datetime(year = datetime.MINYEAR, month = 1, day = 1) self._request_pairing_endpoint = request_pairing_endpoint self._token = token @@ -54,11 +72,11 @@ def __init__( # pylint: disable=too-many-arguments self._supported_protocols = supported_protocols self._rsa_key_pair = RSAJwk(self._rsa_key_pair) - @property - def challenge(self) -> BinaPy: - # If pairing was done within the timeout, the existing chellange can be returned - if (self._paring_timestamp + CHALLANGE_TIMEOUT) < datetime.datetime.now(): - return self._challenge + def _pair(self) -> None: + """Private method establishing pairing""" + # If pairing has been established recently we don't need to do it again + if (self._paring_timestamp + PAIRING_TIMEOUT) > datetime.datetime.now(): + return self._paring_timestamp = datetime.datetime.now() pairing_request: PairingRequest = PairingRequest(token=self._token, @@ -72,22 +90,23 @@ def challenge(self) -> BinaPy: timeout=REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() - pairing_response: PairingResponse = PairingResponse.parse_raw(response.json()) - self.s2_server_node_id = pairing_response.s2ServerNodeId - self.server_node_description = pairing_response.serverNodeDescription + self._pairing_response: PairingResponse = PairingResponse.parse_raw(response.json()) connection_request: ConnectionRequest = ConnectionRequest(s2ClientNodeId=self._client_node_id, supportedProtocols=self._supported_protocols) - response = requests.post(pairing_response.requestConnectionUri, + response = requests.post(self._pairing_response.requestConnectionUri, json=connection_request.model_dump_json(), timeout=REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() - connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json()) + self._connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json()) + self._challenge = JweCompact(self._connection_details.challenge).decrypt(self._rsa_key_pair) - self.selected_protocol = connection_details.selectedProtocol - self.connection_uri = connection_details.connectionUri - self._challenge = JweCompact(connection_details.challenge).decrypt(self._rsa_key_pair) - return self._challenge + @property + def pairing_details(self) -> PairingDetails: + """:raises: requests.exceptions.HTTPError, requests.exceptions.JSONDecodeError + :return: PairingDetails object that's the result of the latest pairing.""" + self._pair() + return PairingDetails(self._pairing_response, self._connection_details, self._challenge) From d172b4c902f591a1b23debe6b6ee27eeaaed03c9 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Wed, 5 Mar 2025 11:14:04 +0100 Subject: [PATCH 08/17] moved key generation into pairning method (new keys every time pairing is done) --- src/s2python/s2_pairing.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index 11cd436..2965421 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -5,8 +5,7 @@ from typing import Tuple, Union import requests -from jwskate import JweCompact -from jwskate.jwk.rsa import RSAJwk +from jwskate import JweCompact, Jwk from binapy.binapy import BinaPy from s2python.generated.gen_s2_pairing import (Protocols, @@ -22,8 +21,9 @@ REQTEST_TIMEOUT = 10 PAIRING_TIMEOUT = datetime.timedelta(minutes=5) +KEY_ALGORITHM = "RSA-OAEP-256" -@dataclass +@dataclass(frozen=True) class PairingDetails: """The result of an S2 pairing :param pairing_response: Details about the server. @@ -34,9 +34,7 @@ class PairingDetails: decrypted_challenge: BinaPy class S2Pairing: # pylint: disable=too-many-instance-attributes - _pairing_response: PairingResponse - _connection_details: ConnectionDetails - _challenge: BinaPy + _pairing_details: PairingDetails _paring_timestamp: datetime.datetime _request_pairing_endpoint: str _token: str @@ -44,7 +42,6 @@ class S2Pairing: # pylint: disable=too-many-instance-attributes _verify_certificate: Union[bool, str] _client_node_id: str _supported_protocols: Tuple[Protocols] - _rsa_key_pair: RSAJwk def __init__( # pylint: disable=too-many-arguments self, request_pairing_endpoint: str, @@ -70,17 +67,19 @@ def __init__( # pylint: disable=too-many-arguments self._verify_certificate = verify_certificate self._client_node_id = client_node_id self._supported_protocols = supported_protocols - self._rsa_key_pair = RSAJwk(self._rsa_key_pair) def _pair(self) -> None: """Private method establishing pairing""" # If pairing has been established recently we don't need to do it again - if (self._paring_timestamp + PAIRING_TIMEOUT) > datetime.datetime.now(): + if datetime.datetime.now() < (self._paring_timestamp + PAIRING_TIMEOUT): return self._paring_timestamp = datetime.datetime.now() + + rsa_key_pair = Jwk.generate_for_alg(KEY_ALGORITHM).with_kid_thumbprint() + pairing_request: PairingRequest = PairingRequest(token=self._token, - publicKey=self._rsa_key_pair.public_jwk().to_pem(), + publicKey=rsa_key_pair.public_jwk().to_pem(), s2ClientNodeId=self._client_node_id, s2ClientNodeDescription=self._s2_client_node_description, supportedProtocols=self._supported_protocols) @@ -90,18 +89,19 @@ def _pair(self) -> None: timeout=REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() - self._pairing_response: PairingResponse = PairingResponse.parse_raw(response.json()) + pairing_response: PairingResponse = PairingResponse.parse_raw(response.json()) connection_request: ConnectionRequest = ConnectionRequest(s2ClientNodeId=self._client_node_id, supportedProtocols=self._supported_protocols) - response = requests.post(self._pairing_response.requestConnectionUri, + response = requests.post(pairing_response.requestConnectionUri, json=connection_request.model_dump_json(), timeout=REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() - self._connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json()) - self._challenge = JweCompact(self._connection_details.challenge).decrypt(self._rsa_key_pair) + connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json()) + challenge = JweCompact(connection_details.challenge).decrypt(rsa_key_pair) + self._pairing_details = PairingDetails(pairing_response, connection_details, challenge) @property @@ -109,4 +109,4 @@ def pairing_details(self) -> PairingDetails: """:raises: requests.exceptions.HTTPError, requests.exceptions.JSONDecodeError :return: PairingDetails object that's the result of the latest pairing.""" self._pair() - return PairingDetails(self._pairing_response, self._connection_details, self._challenge) + return self._pairing_details From 81cbb062ce4fa640bf00010e8146771e33cf2133 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 11 Mar 2025 12:07:23 +0100 Subject: [PATCH 09/17] for pairing do not assume we get requestConnectionUri --- src/s2python/s2_pairing.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index 2965421..c28f8fd 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -94,7 +94,14 @@ def _pair(self) -> None: connection_request: ConnectionRequest = ConnectionRequest(s2ClientNodeId=self._client_node_id, supportedProtocols=self._supported_protocols) - response = requests.post(pairing_response.requestConnectionUri, + + restest_pairing_uri: str = \ + pairing_response.requestConnectionUri if hasattr(pairing_response, 'requestConnectionUri') \ + and pairing_response.requestConnectionUri is not None \ + else self._request_pairing_endpoint.replace('requestPairing', + 'requestConnection') + + response = requests.post(restest_pairing_uri, json=connection_request.model_dump_json(), timeout=REQTEST_TIMEOUT, verify = self._verify_certificate) From 531387adf7a90212e2710321f675e64cf4b15cf3 Mon Sep 17 00:00:00 2001 From: Dr Maurice Hendrix Date: Fri, 14 Mar 2025 10:37:33 +0100 Subject: [PATCH 10/17] Delete %LOCALAPPDATA%/Microsoft/UEV/%COMPUTERNAME%/WindowsCommandPrompt/Initial directory --- .../Initial/WindowsCommandPrompt.pkgx | Bin 2089 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 %LOCALAPPDATA%/Microsoft/UEV/%COMPUTERNAME%/WindowsCommandPrompt/Initial/WindowsCommandPrompt.pkgx diff --git a/%LOCALAPPDATA%/Microsoft/UEV/%COMPUTERNAME%/WindowsCommandPrompt/Initial/WindowsCommandPrompt.pkgx b/%LOCALAPPDATA%/Microsoft/UEV/%COMPUTERNAME%/WindowsCommandPrompt/Initial/WindowsCommandPrompt.pkgx deleted file mode 100644 index 6f8614dab01d7787610e4cf3a879a3f0c983830b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2089 zcmah~3pg8C7LI4A($b<;j8yGMJ)%mzimf!pvrVKzy@kl6B0++xmZn~*AX}Y!mn~Iq zO^ruTLD;m6*L0K`p}tU6R6J`awNf(M-P!&2-0wU0+*;y-+y5aBBHtgiQR#T z32+B|BT~DzPZ%0U1ncoNn)dSnmi}RAtVYlHrJxJRhVsT@4bE0->@(6iz-+ZOUa!55 zUQ#sA)G^kLgLtm_OxoKT@mlTWT*y!3whYI~CvhDE1s}@7u!}f#s=<0z#M=QIm6A-Rr~o!3hf!N|C#N< z-2ft*M8e`RL_HK1|IW=R=m`91%RP5R1*;3%lw_BSq8~F|v?)W!%GvItS+-h3ScAG> zY9;M*V$@EakzNwk7Q~gn^ugLgPneFc0&UHB z56y{vExf*4kxfs^!>}7^_K_QUTpBgZYWj^6F}Ax~yW;5tPcGDHsIESUY7|-d;}#LJm$XB23iomu{}_R!F`uXqnUTM6gZdt2;l%{ zCI)KNIYz!d>qFU1$T&!DRtTjzgK79#g7irFxgB9om9;3GECcS`Lz|QvZnEkzJ;qp>!;KmUS=J$WmFAdAh6g z(MrKL>bLRJdD#_^SgNlB&5CMK#`8`8P~d6Vv^IRMO?fH>w~x!4S6$RI)hYPDJ$?OCd209Os1B|OHzq1?VX|7; zQkpT?8a!40L%WahkDZkgr72sXsvkJyJQhxK&J=kS`kemp#Yk&&N>}wV_S}dFedzE9 z>nVmJMv0r7Z`JpjGo|ui(MEI#8`mJ#IG8kM;q^3n`sIcwv0}lt8Z0Fa#JG< zAvb-FY&7C)DKezL9yOhhXYVA&uia^L52gnW*WQ#5q(mUgs`W>dYKoYZfVqd$Y3^%Y zlTl}vPggY4COI$qn*u-TVKvwsXdt}hi=|t_BNp`@I|`#8Nw3$Oca%OotkzZuN&$Y5 zm`2TL80P$C%>|rmdVeW5Ydt5v0paWI@!TdciQ$?Svei+D>AkpB62BAI<~=g;R)&xS z_2DGVr*dsk_1YMw%r?~ZQi`1CcR*;u40=uW&EwL<2!v4|N+F-VIHvr%@r(c5Z__>j zmc|29!-RQaw)RR){TW)nzi>3t+Oq#9col__BAa>OibN4VN&Bf8!F#piUuI9240>8v6gjC3oY7AYd$5w+G(vnW_zOV+hmYSr!r zVd)d9{JdVDx8s9`;sL5WYaKQBk1HGJJ;0w7`Q%jPc{bQXbdQw8wf`SE9oY@=fDrH` zG@j&hB`O3>^wPT?jJrkqS>_I(l8dve$c0k=2Wbg3aND6$bUfQ2_&57Sp6=gI_Gib-VV@jX8$2ygu%K#g2sa&`&nJS-=MyfcLX*3C z8Q$?GO&J~_ULlm(Rim4kDC}&@B8hZwQH+-c$Pd{J?^>BUV1!#+m&(tOqY&2K&=A;2+!fY$|}f@qk-zk~A2EdV|EC(v(spN}eNh@e3W`>_9s%1=jt zdgM>2|D|Dka6#UJCsW1t&|wbZ;yU{m@#`TFk$pojOYGN;EWYR1JqP4lcfV=?fbWJm zh=~gxCYUV&(CZ`{EDjAJgyTsT`sVvfTR-9tyDMijkwggl!+8ZA9D?&Bp^@k?B9?%+ HIAi`#Z1H#g From 2a34578816af641c6084d641c4f6ba442f06b2fa Mon Sep 17 00:00:00 2001 From: Dr Maurice Hendrix Date: Fri, 14 Mar 2025 12:21:07 +0100 Subject: [PATCH 11/17] S2 pairing frbc example (#93) added example using first paring and then using the details, connection uri and token in the S2 websocket connection --- examples/example_frbc_rm.py | 12 ++++--- examples/example_with_pairing_frbc_rm.py | 41 ++++++++++++++++++++++++ src/s2python/generated/gen_s2_pairing.py | 14 ++++---- src/s2python/s2_pairing.py | 28 ++++++++-------- src/s2python/validate_values_mixin.py | 5 ++- 5 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 examples/example_with_pairing_frbc_rm.py diff --git a/examples/example_frbc_rm.py b/examples/example_frbc_rm.py index 36d8fb7..ff10fd4 100644 --- a/examples/example_frbc_rm.py +++ b/examples/example_frbc_rm.py @@ -1,5 +1,4 @@ import argparse -import re from functools import partial import logging import sys @@ -157,7 +156,7 @@ def stop(s2_connection, signal_num, _current_stack_frame): print(f"Received signal {signal_num}. Will stop S2 connection.") s2_connection.stop() -def start_s2_session(url, client_node_id=str(uuid.uuid4())): +def start_s2_session(url, client_node_id=str(uuid.uuid4()), bearer_token=None): s2_conn = S2Connection( url=url, role=EnergyManagementRole.RM, @@ -172,7 +171,8 @@ def start_s2_session(url, client_node_id=str(uuid.uuid4())): provides_power_measurements=[CommodityQuantity.ELECTRIC_POWER_L1] ), reconnect=True, - verify_certificate=False + verify_certificate=False, + bearer_token=bearer_token ) signal.signal(signal.SIGINT, partial(stop, s2_conn)) signal.signal(signal.SIGTERM, partial(stop, s2_conn)) @@ -181,7 +181,11 @@ def start_s2_session(url, client_node_id=str(uuid.uuid4())): if __name__ == "__main__": parser = argparse.ArgumentParser(description="A simple S2 reseource manager example.") - parser.add_argument('endpoint', type=str, help="WebSocket endpoint uri for the server (CEM) e.g. ws://localhost:8080/backend/rm/s2python-frbc/cem/dummy_model/ws") + parser.add_argument( + 'endpoint', + type=str, + help="WebSocket endpoint uri for the server (CEM) e.h. ws://localhost:8080/websocket/s2/my-first-websocket-rm" + ) args = parser.parse_args() start_s2_session(args.endpoint) diff --git a/examples/example_with_pairing_frbc_rm.py b/examples/example_with_pairing_frbc_rm.py new file mode 100644 index 0000000..74a31e1 --- /dev/null +++ b/examples/example_with_pairing_frbc_rm.py @@ -0,0 +1,41 @@ +import argparse +import uuid +import logging + +from example_frbc_rm import start_s2_session +from s2python.s2_pairing import S2Pairing +from s2python.generated.gen_s2_pairing import S2NodeDescription, Deployment +from s2python.generated.gen_s2 import EnergyManagementRole + +logger = logging.getLogger("s2python") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="A simple S2 reseource manager example.") + parser.add_argument('endpoint', + type=str, + help="Rest endpoint to start S2 pairing. E.g. https://localhost/requestPairing") + parser.add_argument('pairing_token', + type=str, + help="The pairing toekn for teh endpoint. You should get this from the S2 server e.g. ca14fda4") + args = parser.parse_args() + + nodeDescription: S2NodeDescription = \ + S2NodeDescription(brand="TNO", + logoUri = "https://www.tno.nl/publish/pages/5604/tno-logo-1484x835_003_.jpg", + type = "demo frbc example", + modelName = "S2 pairing example stub", + userDefinedName = "TNO S2 pairing example for frbc", + role = EnergyManagementRole.RM, + deployment = Deployment.LAN) + client_node_id: str = str(uuid.uuid4()) + + pairing: S2Pairing = S2Pairing(request_pairing_endpoint = args.endpoint, + token = args.pairing_token, + s2_client_node_description = nodeDescription, + client_node_id = client_node_id) + + logger.info("Pairing details: \n%s", pairing.pairing_details) + + start_s2_session(pairing.pairing_details.connection_details.connectionUri, + bearer_token=pairing.pairing_details.decrypted_challenge) diff --git a/src/s2python/generated/gen_s2_pairing.py b/src/s2python/generated/gen_s2_pairing.py index 7ed825d..df45a08 100644 --- a/src/s2python/generated/gen_s2_pairing.py +++ b/src/s2python/generated/gen_s2_pairing.py @@ -8,16 +8,18 @@ from typing import List from pydantic import BaseModel, ConfigDict, Field -from s2python.common import EnergyManagementRole as S2Role - -class Deployment(Enum): +class S2Role(str, Enum): + CEM = 'CEM' + RM = 'RM' + +class Deployment(str, Enum): WAN = 'WAN' LAN = 'LAN' -class Protocols(Enum): +class Protocols(str, Enum): WebSocketSecure = 'WebSocketSecure' @@ -50,7 +52,7 @@ class PairingRequest(BaseModel): token: str publicKey: str s2ClientNodeId: str - s2ClientNodeDescription: str + s2ClientNodeDescription: S2NodeDescription supportedProtocols: List[Protocols] @@ -59,7 +61,7 @@ class PairingResponse(BaseModel): extra='forbid', ) s2ServerNodeId: str - serverNodeDescription: str + serverNodeDescription: S2NodeDescription requestConnectionUri: str diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index c28f8fd..2675746 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -2,11 +2,11 @@ import uuid import datetime from dataclasses import dataclass -from typing import Tuple, Union +from typing import Tuple, Union, Mapping, Any +import json import requests -from jwskate import JweCompact, Jwk -from binapy.binapy import BinaPy +from jwskate import JweCompact, Jwk, Jwt, SignedJwt from s2python.generated.gen_s2_pairing import (Protocols, PairingRequest, @@ -28,10 +28,10 @@ class PairingDetails: """The result of an S2 pairing :param pairing_response: Details about the server. :param connection_details: Details about how to connect. - :param supported_protocols: The decrypted challenge needed as bearer token.""" + :param decrypted_challenge: The decrypted challenge needed as bearer token.""" pairing_response: PairingResponse connection_details: ConnectionDetails - decrypted_challenge: BinaPy + decrypted_challenge: str class S2Pairing: # pylint: disable=too-many-instance-attributes _pairing_details: PairingDetails @@ -77,7 +77,6 @@ def _pair(self) -> None: self._paring_timestamp = datetime.datetime.now() rsa_key_pair = Jwk.generate_for_alg(KEY_ALGORITHM).with_kid_thumbprint() - pairing_request: PairingRequest = PairingRequest(token=self._token, publicKey=rsa_key_pair.public_jwk().to_pem(), s2ClientNodeId=self._client_node_id, @@ -85,11 +84,11 @@ def _pair(self) -> None: supportedProtocols=self._supported_protocols) response = requests.post(self._request_pairing_endpoint, - json=pairing_request.model_dump_json(), - timeout=REQTEST_TIMEOUT, + json = pairing_request.dict(), + timeout = REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() - pairing_response: PairingResponse = PairingResponse.parse_raw(response.json()) + pairing_response: PairingResponse = PairingResponse.parse_raw(response.text) connection_request: ConnectionRequest = ConnectionRequest(s2ClientNodeId=self._client_node_id, supportedProtocols=self._supported_protocols) @@ -102,13 +101,14 @@ def _pair(self) -> None: 'requestConnection') response = requests.post(restest_pairing_uri, - json=connection_request.model_dump_json(), - timeout=REQTEST_TIMEOUT, + json = connection_request.dict(), + timeout = REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() - connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json()) - challenge = JweCompact(connection_details.challenge).decrypt(rsa_key_pair) - self._pairing_details = PairingDetails(pairing_response, connection_details, challenge) + connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.text) + challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) + decrypted_challenge_token: SignedJwt = Jwt.unprotected(challenge) + self._pairing_details = PairingDetails(pairing_response, connection_details, str(decrypted_challenge_token)) @property diff --git a/src/s2python/validate_values_mixin.py b/src/s2python/validate_values_mixin.py index cc9c6fd..fa4a8d7 100644 --- a/src/s2python/validate_values_mixin.py +++ b/src/s2python/validate_values_mixin.py @@ -59,7 +59,10 @@ def inner(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: return inner -def catch_and_convert_exceptions(input_class: Type[S2MessageComponent[B_co]]) -> Type[S2MessageComponent[B_co]]: +S = TypeVar("S", bound=S2MessageComponent) + + +def catch_and_convert_exceptions(input_class: Type[S]) -> Type[S]: input_class.__init__ = convert_to_s2exception(input_class.__init__) # type: ignore[method-assign] input_class.__setattr__ = convert_to_s2exception(input_class.__setattr__) # type: ignore[method-assign] input_class.model_validate_json = convert_to_s2exception( # type: ignore[method-assign] From 89c3536ab8573ff70174a594dd4c95dd611d9568 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Mon, 17 Mar 2025 23:41:41 +0100 Subject: [PATCH 12/17] added base 64 encoding to token (decrypted challenge) for pairing --- examples/example_with_pairing_frbc_rm.py | 2 +- src/s2python/s2_pairing.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/example_with_pairing_frbc_rm.py b/examples/example_with_pairing_frbc_rm.py index 74a31e1..aef4de4 100644 --- a/examples/example_with_pairing_frbc_rm.py +++ b/examples/example_with_pairing_frbc_rm.py @@ -38,4 +38,4 @@ logger.info("Pairing details: \n%s", pairing.pairing_details) start_s2_session(pairing.pairing_details.connection_details.connectionUri, - bearer_token=pairing.pairing_details.decrypted_challenge) + bearer_token=pairing.pairing_details.decrypted_challenge_base64) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index 2675746..7e615aa 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -1,3 +1,4 @@ +import base64 import logging import uuid import datetime @@ -28,10 +29,10 @@ class PairingDetails: """The result of an S2 pairing :param pairing_response: Details about the server. :param connection_details: Details about how to connect. - :param decrypted_challenge: The decrypted challenge needed as bearer token.""" + :param decrypted_challenge_base64: The decrypted challenge needed as bearer token.""" pairing_response: PairingResponse connection_details: ConnectionDetails - decrypted_challenge: str + decrypted_challenge_base64: str class S2Pairing: # pylint: disable=too-many-instance-attributes _pairing_details: PairingDetails @@ -108,7 +109,8 @@ def _pair(self) -> None: connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.text) challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) decrypted_challenge_token: SignedJwt = Jwt.unprotected(challenge) - self._pairing_details = PairingDetails(pairing_response, connection_details, str(decrypted_challenge_token)) + decrypted_challenge_str: str = base64.b64encode(bytes(decrypted_challenge_token)).decode('utf-8') + self._pairing_details = PairingDetails(pairing_response, connection_details, decrypted_challenge_str) @property From eea996261828d41808c3b0c4ffa0c7aebc3fc077 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 18 Mar 2025 17:37:47 +0100 Subject: [PATCH 13/17] deal with getting just a baseurl returned --- src/s2python/s2_pairing.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index 7e615aa..e289af6 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -98,6 +98,7 @@ def _pair(self) -> None: restest_pairing_uri: str = \ pairing_response.requestConnectionUri if hasattr(pairing_response, 'requestConnectionUri') \ and pairing_response.requestConnectionUri is not None \ + and pairing_response.requestConnectionUri != "" \ else self._request_pairing_endpoint.replace('requestPairing', 'requestConnection') @@ -107,6 +108,14 @@ def _pair(self) -> None: verify = self._verify_certificate) response.raise_for_status() connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.text) + # if websocket address doesn't start with ws:// or wss:// assume it's relative to the requestPairing + if not connection_details.connectionUri.startswith('ws://') \ + and not connection_details.connectionUri.startswith('wss://'): + connection_details.connectionUri = self._request_pairing_endpoint.replace('http://', 'ws://') \ + .replace('https://', 'wss://') \ + .replace('requestPairing', '') \ + + '/' + connection_details.connectionUri + challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) decrypted_challenge_token: SignedJwt = Jwt.unprotected(challenge) decrypted_challenge_str: str = base64.b64encode(bytes(decrypted_challenge_token)).decode('utf-8') From e7db42b32eec5ad22294dbb2ab3146b102196e2f Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 18 Mar 2025 18:34:23 +0100 Subject: [PATCH 14/17] added logging around connection uri for pairning --- src/s2python/s2_pairing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index e289af6..a7830c5 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -102,6 +102,8 @@ def _pair(self) -> None: else self._request_pairing_endpoint.replace('requestPairing', 'requestConnection') + logger.info('requestConnectionUri %s ', connection_details.connectionUri) + response = requests.post(restest_pairing_uri, json = connection_request.dict(), timeout = REQTEST_TIMEOUT, @@ -115,6 +117,7 @@ def _pair(self) -> None: .replace('https://', 'wss://') \ .replace('requestPairing', '') \ + '/' + connection_details.connectionUri + logger.info('connectionUri %s ', connection_details.connectionUri) challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) decrypted_challenge_token: SignedJwt = Jwt.unprotected(challenge) From a703b1d8fa32030dee893bec109513e5bc457df4 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 18 Mar 2025 19:55:33 +0100 Subject: [PATCH 15/17] remove dubble / in ws uri --- src/s2python/s2_pairing.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index a7830c5..9e73a3a 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -102,7 +102,7 @@ def _pair(self) -> None: else self._request_pairing_endpoint.replace('requestPairing', 'requestConnection') - logger.info('requestConnectionUri %s ', connection_details.connectionUri) + logger.info('restest_pairing_uri %s ', restest_pairing_uri) response = requests.post(restest_pairing_uri, json = connection_request.dict(), @@ -113,10 +113,12 @@ def _pair(self) -> None: # if websocket address doesn't start with ws:// or wss:// assume it's relative to the requestPairing if not connection_details.connectionUri.startswith('ws://') \ and not connection_details.connectionUri.startswith('wss://'): - connection_details.connectionUri = self._request_pairing_endpoint.replace('http://', 'ws://') \ - .replace('https://', 'wss://') \ - .replace('requestPairing', '') \ - + '/' + connection_details.connectionUri + connection_details.connectionUri = \ + self._request_pairing_endpoint.replace('http://', 'ws://') \ + .replace('https://', 'wss://') \ + .replace('requestPairing', '') \ + .removesuffix('/') \ + + '/' + connection_details.connectionUri.removeprefix('/') logger.info('connectionUri %s ', connection_details.connectionUri) challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) From 7522b216cc908c5d15f8045cf7bf5da681e72df7 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 18 Mar 2025 20:08:28 +0100 Subject: [PATCH 16/17] lstrip/rstrip for python backward compatability --- src/s2python/s2_pairing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index 9e73a3a..ed808cc 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -117,8 +117,8 @@ def _pair(self) -> None: self._request_pairing_endpoint.replace('http://', 'ws://') \ .replace('https://', 'wss://') \ .replace('requestPairing', '') \ - .removesuffix('/') \ - + '/' + connection_details.connectionUri.removeprefix('/') + .rstrip('/') \ + + '/' + connection_details.connectionUri).lstrip('/') logger.info('connectionUri %s ', connection_details.connectionUri) challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) From 6f43d0c065dfd1e337b61947637faf56dc85aacc Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Tue, 18 Mar 2025 20:12:26 +0100 Subject: [PATCH 17/17] linting error fix --- src/s2python/s2_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index ed808cc..1b9c413 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -118,7 +118,7 @@ def _pair(self) -> None: .replace('https://', 'wss://') \ .replace('requestPairing', '') \ .rstrip('/') \ - + '/' + connection_details.connectionUri).lstrip('/') + + '/' + connection_details.connectionUri.lstrip('/') logger.info('connectionUri %s ', connection_details.connectionUri) challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair))