Skip to content

Commit bad86c9

Browse files
Added S2Pairing implementation
1 parent 9af14ff commit bad86c9

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

dev-requirements.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,17 @@ wheel==0.44.0
239239
# via pip-tools
240240
zipp==3.20.1
241241
# via importlib-metadata
242+
jwskate==0.11.1
243+
binapy==0.8.0
244+
# via jwskate
245+
cffi==1.17.1
246+
# via jwskate
247+
cryptography==44.0.2
248+
# via jwskate
249+
pycparser==2.22
250+
# via jwskate
251+
requests==2.32.3
252+
242253

243254
# The following packages are considered to be unsafe in a requirements file:
244255
# pip

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ install_requires =
4343
pytz
4444
click
4545
websockets~=13.1
46+
jwskate~=0.11
47+
requests~=2.32.3
4648

4749
[options.packages.find]
4850
where = src
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# generated by datamodel-codegen:
2+
# filename: s2-over-ip-pairing
3+
# timestamp: 2025-02-28T14:52:45+00:00
4+
5+
from __future__ import annotations
6+
7+
from enum import Enum
8+
from typing import List
9+
10+
from pydantic import BaseModel, ConfigDict, Field
11+
from s2python.common import EnergyManagementRole as S2Role
12+
13+
14+
15+
class Deployment(Enum):
16+
WAN = 'WAN'
17+
LAN = 'LAN'
18+
19+
20+
class Protocols(Enum):
21+
WebSocketSecure = 'WebSocketSecure'
22+
23+
24+
class PairingInfo(BaseModel):
25+
model_config = ConfigDict(
26+
extra='forbid',
27+
)
28+
pairingUri: str
29+
token: str
30+
validUntil: str
31+
32+
33+
class S2NodeDescription(BaseModel):
34+
model_config = ConfigDict(
35+
extra='forbid',
36+
)
37+
brand: str
38+
logoUri: str
39+
type: str
40+
modelName: str
41+
userDefinedName: str
42+
role: S2Role
43+
deployment: Deployment
44+
45+
46+
class PairingRequest(BaseModel):
47+
model_config = ConfigDict(
48+
extra='forbid',
49+
)
50+
token: str
51+
publicKey: str
52+
s2ClientNodeId: str
53+
s2ClientNodeDescription: str
54+
supportedProtocols: List[Protocols]
55+
56+
57+
class PairingResponse(BaseModel):
58+
model_config = ConfigDict(
59+
extra='forbid',
60+
)
61+
s2ServerNodeId: str
62+
serverNodeDescription: str
63+
requestConnectionUri: str
64+
65+
66+
class ConnectionRequest(BaseModel):
67+
model_config = ConfigDict(
68+
extra='forbid',
69+
)
70+
s2ClientNodeId: str
71+
supportedProtocols: List[Protocols]
72+
73+
74+
class ConnectionDetails(BaseModel):
75+
model_config = ConfigDict(
76+
extra='forbid',
77+
)
78+
selectedProtocol: Protocols
79+
challenge: str
80+
connectionUri: str

src/s2python/s2_pairing.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import logging
2+
import uuid
3+
from typing import Tuple
4+
import requests
5+
6+
from jwskate import JweCompact
7+
from jwskate.jwk.rsa import RSAJwk
8+
from binapy.binapy import BinaPy
9+
10+
from s2python.generated.gen_s2_pairing import (Protocols,
11+
PairingRequest,
12+
S2NodeDescription,
13+
PairingResponse,
14+
ConnectionRequest,
15+
ConnectionDetails)
16+
17+
18+
logger = logging.getLogger("s2python")
19+
20+
class S2Pairing: # pylint: disable=too-many-instance-attributes
21+
paired: bool
22+
s2_server_node_id: str
23+
server_node_description: str
24+
selected_protocol: Protocols
25+
connection_uri: str
26+
challenge: BinaPy
27+
28+
_request_pairing_endpoint: str
29+
_token: str
30+
_s2_client_node_description: S2NodeDescription
31+
_verify_certificate: bool | str # pylint: disable=E1131
32+
_client_node_id: str
33+
_supported_protocols: Tuple[Protocols]
34+
_rsa_key_pair: RSAJwk
35+
def __init__( # pylint: disable=too-many-arguments
36+
self,
37+
request_pairing_endpoint: str,
38+
token: str,
39+
s2_client_node_description: S2NodeDescription,
40+
verify_certificate: bool | str = False, # pylint: disable=E1131
41+
client_node_id: str = str(uuid.uuid4()),
42+
supported_protocols: Tuple[Protocols] = (Protocols.WebSocketSecure, )
43+
) -> None:
44+
self.paired = False
45+
46+
self._request_pairing_endpoint = request_pairing_endpoint
47+
self._token = token
48+
self._s2_client_node_description = s2_client_node_description
49+
self._verify_certificate = verify_certificate
50+
self._client_node_id = client_node_id
51+
self._supported_protocols = supported_protocols
52+
self._rsa_key_pair = RSAJwk(self._rsa_key_pair)
53+
54+
def pair(self) -> bool:
55+
self.paired = False
56+
pairing_request: PairingRequest = PairingRequest(token=self._token,
57+
publicKey=self._rsa_key_pair.public_jwk().to_pem(),
58+
s2ClientNodeId=self._client_node_id,
59+
s2ClientNodeDescription=self._s2_client_node_description,
60+
supportedProtocols=self._supported_protocols)
61+
62+
response = requests.post(self._request_pairing_endpoint,
63+
json=pairing_request.model_dump_json(),
64+
timeout=10,
65+
verify = self._verify_certificate)
66+
response.raise_for_status()
67+
pairing_response: PairingResponse = PairingResponse.parse_raw(response.json())
68+
self.s2_server_node_id = pairing_response.s2ServerNodeId
69+
self.server_node_description = pairing_response.serverNodeDescription
70+
71+
connection_request: ConnectionRequest = ConnectionRequest(s2ClientNodeId=self._client_node_id,
72+
supportedProtocols=self._supported_protocols)
73+
74+
response = requests.post(pairing_response.requestConnectionUri,
75+
json=connection_request.model_dump_json(),
76+
timeout=10,
77+
verify = self._verify_certificate)
78+
response.raise_for_status()
79+
connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json())
80+
81+
self.selected_protocol = connection_details.selectedProtocol
82+
self.connection_uri = connection_details.connectionUri
83+
self.challenge = JweCompact(connection_details.challenge).decrypt(self._rsa_key_pair)
84+
85+
self.paired = True
86+
return self.paired

0 commit comments

Comments
 (0)