pyown 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyown/__init__.py +1 -0
- pyown/auth/__init__.py +3 -0
- pyown/auth/enum.py +46 -0
- pyown/auth/hmac.py +169 -0
- pyown/auth/open.py +66 -0
- pyown/client/__init__.py +3 -0
- pyown/client/base.py +267 -0
- pyown/client/client.py +157 -0
- pyown/client/session.py +31 -0
- pyown/exceptions.py +100 -0
- pyown/items/__init__.py +1 -0
- pyown/items/automation/__init__.py +1 -0
- pyown/items/automation/automation.py +108 -0
- pyown/items/base.py +300 -0
- pyown/items/gateway/__init__.py +0 -0
- pyown/items/lighting/__init__.py +2 -0
- pyown/items/lighting/base.py +272 -0
- pyown/items/lighting/dimmer.py +258 -0
- pyown/items/lighting/light.py +74 -0
- pyown/items/temperature/__init__.py +0 -0
- pyown/items/utils.py +17 -0
- pyown/messages/__init__.py +6 -0
- pyown/messages/ack.py +46 -0
- pyown/messages/base.py +225 -0
- pyown/messages/dimension.py +120 -0
- pyown/messages/nack.py +46 -0
- pyown/messages/normal.py +52 -0
- pyown/messages/status.py +47 -0
- pyown/protocol/__init__.py +1 -0
- pyown/protocol/protocol.py +135 -0
- pyown/tags/__init__.py +5 -0
- pyown/tags/base.py +122 -0
- pyown/tags/dimension.py +18 -0
- pyown/tags/what.py +15 -0
- pyown/tags/where.py +64 -0
- pyown/tags/who.py +95 -0
- pyown-0.1.0.dist-info/LICENSE +674 -0
- pyown-0.1.0.dist-info/METADATA +78 -0
- pyown-0.1.0.dist-info/RECORD +40 -0
- pyown-0.1.0.dist-info/WHEEL +4 -0
pyown/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .client import Client
|
pyown/auth/__init__.py
ADDED
pyown/auth/enum.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
from ..messages import GenericMessage
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AuthAlgorithm",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthAlgorithm(IntEnum):
|
|
11
|
+
"""
|
|
12
|
+
Represents the all allowed hashing algorithms when using the HMAC authentication algorithm.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
SHA1: The SHA1 hashing algorithm.
|
|
16
|
+
SHA256: The SHA256 hashing algorithm
|
|
17
|
+
"""
|
|
18
|
+
SHA1 = 1
|
|
19
|
+
SHA256 = 2
|
|
20
|
+
|
|
21
|
+
def to_message(self) -> GenericMessage:
|
|
22
|
+
"""
|
|
23
|
+
Converts the AuthAlgorithm to a message.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
GenericMessage: The message.
|
|
27
|
+
"""
|
|
28
|
+
return GenericMessage(["98", str(self.value)])
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_string(cls, value: str) -> "AuthAlgorithm":
|
|
32
|
+
"""
|
|
33
|
+
Converts a string to an AuthAlgorithm.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
value: The string to convert.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
AuthAlgorithm: The corresponding AuthAlgorithm.
|
|
40
|
+
"""
|
|
41
|
+
if value == "1":
|
|
42
|
+
return cls.SHA1
|
|
43
|
+
elif value == "2":
|
|
44
|
+
return cls.SHA256
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError("Invalid hash algorithm")
|
pyown/auth/hmac.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from hashlib import sha1, sha256
|
|
2
|
+
from hmac import HMAC, compare_digest
|
|
3
|
+
from secrets import token_bytes
|
|
4
|
+
|
|
5
|
+
from .enum import AuthAlgorithm
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"client_hmac",
|
|
9
|
+
"server_hmac",
|
|
10
|
+
"compare_hmac",
|
|
11
|
+
"create_key",
|
|
12
|
+
"hex_to_digits",
|
|
13
|
+
"digits_to_hex",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def client_hmac(
|
|
18
|
+
server_key: str,
|
|
19
|
+
client_key: str,
|
|
20
|
+
password: str,
|
|
21
|
+
client_identity: str = "736F70653E",
|
|
22
|
+
server_identity: str = "636F70653E",
|
|
23
|
+
hash_algorithm: AuthAlgorithm = AuthAlgorithm.SHA256,
|
|
24
|
+
) -> bytes:
|
|
25
|
+
"""
|
|
26
|
+
Generates the HMAC authentication for the client.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
server_key: The key sent by the server (Ra)
|
|
30
|
+
client_key: The key generated by the client (Rb)
|
|
31
|
+
password: The open password of the server (Kab = sha(kab))
|
|
32
|
+
client_identity: string used to identify the client (A)
|
|
33
|
+
server_identity: string used to identify the server (B)
|
|
34
|
+
hash_algorithm: The hash function to use for the hmac calculation (can be sha1 or sha256)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: the client authentication string in bytes
|
|
38
|
+
"""
|
|
39
|
+
if hash_algorithm == AuthAlgorithm.SHA1:
|
|
40
|
+
hash_function = sha1
|
|
41
|
+
elif hash_algorithm == AuthAlgorithm.SHA256:
|
|
42
|
+
hash_function = sha256
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError("Invalid hash algorithm")
|
|
45
|
+
|
|
46
|
+
kab = hash_function(password.encode()).hexdigest()
|
|
47
|
+
hmac_message = f"{server_key}{client_key}{client_identity}{server_identity}{kab}"
|
|
48
|
+
|
|
49
|
+
hmac = HMAC(
|
|
50
|
+
key=server_key.encode(),
|
|
51
|
+
msg=hmac_message.encode(),
|
|
52
|
+
digestmod=hash_function,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return hmac.digest()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def server_hmac(
|
|
59
|
+
server_key: str,
|
|
60
|
+
client_key: str,
|
|
61
|
+
password: str,
|
|
62
|
+
hash_algorithm: AuthAlgorithm = AuthAlgorithm.SHA256,
|
|
63
|
+
) -> bytes:
|
|
64
|
+
"""
|
|
65
|
+
Generates the HMAC authentication for the server.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
server_key: The key sent by the server (Ra)
|
|
69
|
+
client_key: The key generated by the client (Rb)
|
|
70
|
+
password: The open password of the server (Kab = sha(kab))
|
|
71
|
+
hash_algorithm: The hash function to use for the hmac calculation (can be sha1 or sha256)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: the server confirmation string in bytes
|
|
75
|
+
"""
|
|
76
|
+
if hash_algorithm == AuthAlgorithm.SHA1:
|
|
77
|
+
hash_function = sha1
|
|
78
|
+
elif hash_algorithm == AuthAlgorithm.SHA256:
|
|
79
|
+
hash_function = sha256
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError("Invalid hash algorithm")
|
|
82
|
+
|
|
83
|
+
kab = hash_function(password.encode()).hexdigest()
|
|
84
|
+
hmac_message = f"{client_key}{server_key}{kab}"
|
|
85
|
+
|
|
86
|
+
hmac = HMAC(
|
|
87
|
+
key=server_key.encode(),
|
|
88
|
+
msg=hmac_message.encode(),
|
|
89
|
+
digestmod=hash_function,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return hmac.digest()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def compare_hmac(
|
|
96
|
+
hmac1: bytes,
|
|
97
|
+
hmac2: bytes,
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
Compares two hmacs in constant time.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
hmac1: The first hmac
|
|
104
|
+
hmac2: The second hmac
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
bool: True if the hmacs are equal, False otherwise
|
|
108
|
+
"""
|
|
109
|
+
return compare_digest(hmac1, hmac2)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def create_key(
|
|
113
|
+
hash_algorithm: AuthAlgorithm = AuthAlgorithm.SHA256,
|
|
114
|
+
) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Creates a random key for the hmac.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
hash_algorithm: The hash function to use for the hmac calculation (can be sha1 or sha256)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
str: the key in hex format
|
|
123
|
+
"""
|
|
124
|
+
if hash_algorithm == AuthAlgorithm.SHA1:
|
|
125
|
+
hash_function = sha1
|
|
126
|
+
elif hash_algorithm == AuthAlgorithm.SHA256:
|
|
127
|
+
hash_function = sha256
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError("Invalid hash algorithm")
|
|
130
|
+
|
|
131
|
+
return hash_function(token_bytes(32)).hexdigest()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def hex_to_digits(
|
|
135
|
+
hex_string: str,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Converts a hex string to digits.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
hex_string: The hex string
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
str: the digit string
|
|
145
|
+
"""
|
|
146
|
+
out = ""
|
|
147
|
+
for c in hex_string:
|
|
148
|
+
value = int(c, 16)
|
|
149
|
+
out += str(value // 10) + str(value % 10)
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def digits_to_hex(
|
|
154
|
+
digits_string: str,
|
|
155
|
+
) -> str:
|
|
156
|
+
"""
|
|
157
|
+
Converts a digit string to hex.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
digits_string: The digit string
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
str: the hex string
|
|
164
|
+
"""
|
|
165
|
+
out = ""
|
|
166
|
+
for i in range(0, len(digits_string), 2):
|
|
167
|
+
value = int(digits_string[i]) * 10 + int(digits_string[i + 1])
|
|
168
|
+
out += hex(value)[2:]
|
|
169
|
+
return out
|
pyown/auth/open.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
__all__ = ["own_calc_pass"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def own_calc_pass(password: str | int, nonce: str | int) -> str: # noqa: C901
|
|
5
|
+
"""
|
|
6
|
+
Encodes the password using the OPEN algorithm.
|
|
7
|
+
Source: https://rosettacode.org/wiki/OpenWebNet_password#Python
|
|
8
|
+
|
|
9
|
+
Parameters:
|
|
10
|
+
password (str | int): The password to encode must be composed of only digits.
|
|
11
|
+
nonce (str): The nonce received from the gateway.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
str: The encoded password.
|
|
15
|
+
"""
|
|
16
|
+
start = True
|
|
17
|
+
num1 = 0
|
|
18
|
+
num2 = 0
|
|
19
|
+
|
|
20
|
+
if isinstance(password, str):
|
|
21
|
+
password = int(password)
|
|
22
|
+
|
|
23
|
+
if isinstance(nonce, int):
|
|
24
|
+
nonce = str(nonce)
|
|
25
|
+
|
|
26
|
+
for c in nonce:
|
|
27
|
+
if c != "0":
|
|
28
|
+
if start:
|
|
29
|
+
num2 = password
|
|
30
|
+
start = False
|
|
31
|
+
if c == '1':
|
|
32
|
+
num1 = (num2 & 0xFFFFFF80) >> 7
|
|
33
|
+
num2 = num2 << 25
|
|
34
|
+
elif c == '2':
|
|
35
|
+
num1 = (num2 & 0xFFFFFFF0) >> 4
|
|
36
|
+
num2 = num2 << 28
|
|
37
|
+
elif c == '3':
|
|
38
|
+
num1 = (num2 & 0xFFFFFFF8) >> 3
|
|
39
|
+
num2 = num2 << 29
|
|
40
|
+
elif c == '4':
|
|
41
|
+
num1 = num2 << 1
|
|
42
|
+
num2 = num2 >> 31
|
|
43
|
+
elif c == '5':
|
|
44
|
+
num1 = num2 << 5
|
|
45
|
+
num2 = num2 >> 27
|
|
46
|
+
elif c == '6':
|
|
47
|
+
num1 = num2 << 12
|
|
48
|
+
num2 = num2 >> 20
|
|
49
|
+
elif c == '7':
|
|
50
|
+
num1 = num2 & 0x0000FF00 | ((num2 & 0x000000FF) << 24) | ((num2 & 0x00FF0000) >> 16)
|
|
51
|
+
num2 = (num2 & 0xFF000000) >> 8
|
|
52
|
+
elif c == '8':
|
|
53
|
+
num1 = (num2 & 0x0000FFFF) << 16 | (num2 >> 24)
|
|
54
|
+
num2 = (num2 & 0x00FF0000) >> 8
|
|
55
|
+
elif c == '9':
|
|
56
|
+
num1 = ~num2
|
|
57
|
+
else:
|
|
58
|
+
num1 = num2
|
|
59
|
+
|
|
60
|
+
num1 &= 0xFFFFFFFF
|
|
61
|
+
num2 &= 0xFFFFFFFF
|
|
62
|
+
if c not in "09":
|
|
63
|
+
num1 |= num2
|
|
64
|
+
num2 = num1
|
|
65
|
+
|
|
66
|
+
return str(num1)
|
pyown/client/__init__.py
ADDED
pyown/client/base.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from asyncio import AbstractEventLoop, Transport, Future
|
|
5
|
+
from typing import Optional, Any
|
|
6
|
+
|
|
7
|
+
from .session import SessionType
|
|
8
|
+
from ..auth import AuthAlgorithm
|
|
9
|
+
from ..auth.hmac import server_hmac, client_hmac, hex_to_digits, compare_hmac, create_key
|
|
10
|
+
from ..auth.open import own_calc_pass
|
|
11
|
+
from ..exceptions import InvalidAuthentication, InvalidSession
|
|
12
|
+
from ..messages import BaseMessage, MessageType, GenericMessage, NACK, ACK
|
|
13
|
+
from ..protocol import OWNProtocol
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BaseClient",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger("pyown.client")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseClient(ABC):
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
host: str,
|
|
26
|
+
port: int,
|
|
27
|
+
password: str,
|
|
28
|
+
session_type: SessionType = SessionType.CommandSession,
|
|
29
|
+
*,
|
|
30
|
+
loop: Optional[AbstractEventLoop] = None
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
BaseClient constructor
|
|
34
|
+
This class should not be instantiated directly, use the Client class instead
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
host (str): The host to connect to (ip address)
|
|
38
|
+
port (int): The port to connect to
|
|
39
|
+
password (str): The password to authenticate with
|
|
40
|
+
session_type (SessionType): The session type to use
|
|
41
|
+
loop (Optional[AbstractEventLoop]): The event loop to use
|
|
42
|
+
"""
|
|
43
|
+
self._host = host
|
|
44
|
+
self._port = port
|
|
45
|
+
self._password = password
|
|
46
|
+
self._session_type = session_type
|
|
47
|
+
|
|
48
|
+
self._transport: Optional[Transport] = None
|
|
49
|
+
self._protocol: Optional[OWNProtocol] = None
|
|
50
|
+
|
|
51
|
+
self._loop = loop or asyncio.get_event_loop()
|
|
52
|
+
self._loop.set_exception_handler(self._exception_handler)
|
|
53
|
+
|
|
54
|
+
self._on_connection_start: Future[Transport] = self._loop.create_future()
|
|
55
|
+
self._on_connection_end: Future[Exception | None] = self._loop.create_future()
|
|
56
|
+
|
|
57
|
+
def is_cmd_session(self) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Checks if the session is a command session
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
bool: True if the session is a command session, False otherwise
|
|
63
|
+
"""
|
|
64
|
+
return self._session_type == SessionType.CommandSession or self._session_type == SessionType.OldCommandSession
|
|
65
|
+
|
|
66
|
+
async def start(self) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Creates a connection with the gateway and does the initial handshake
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
TimeoutError: if the server does not respond
|
|
72
|
+
InvalidSession: if the server requires an unknown authentication algorithm
|
|
73
|
+
"""
|
|
74
|
+
self._transport, self._protocol = await self._loop.create_connection(
|
|
75
|
+
lambda: OWNProtocol(
|
|
76
|
+
on_connection_start=self._on_connection_start,
|
|
77
|
+
on_connection_end=self._on_connection_end,
|
|
78
|
+
),
|
|
79
|
+
self._host,
|
|
80
|
+
self._port
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Wait for the connection to start
|
|
84
|
+
await self._on_connection_start
|
|
85
|
+
|
|
86
|
+
log.debug("Connection started")
|
|
87
|
+
|
|
88
|
+
# Handshake
|
|
89
|
+
# The first packet is from the server, and it's an ACK packet
|
|
90
|
+
# The second packet is from the client and set the session type
|
|
91
|
+
# Wait for the first packet
|
|
92
|
+
async with asyncio.timeout(5):
|
|
93
|
+
message = await self.read_message()
|
|
94
|
+
|
|
95
|
+
if message.type != MessageType.ACK:
|
|
96
|
+
raise InvalidAuthentication("Expected ACK message")
|
|
97
|
+
|
|
98
|
+
log.debug("Starting handshake")
|
|
99
|
+
|
|
100
|
+
# Send the session type
|
|
101
|
+
await self.send_message(self._session_type.to_message(), force=True)
|
|
102
|
+
resp = await self.read_message()
|
|
103
|
+
|
|
104
|
+
# Authentication
|
|
105
|
+
# if the next message is an ACK, the server does not require authentication
|
|
106
|
+
# if it's a message with only a number, the server requires the open authentication algorithm
|
|
107
|
+
# if it's a ∗98∗## open command, the server requires the hmac authentication algorithm
|
|
108
|
+
if resp.type == MessageType.ACK:
|
|
109
|
+
log.info("No authentication required")
|
|
110
|
+
elif len(resp.tags) == 1:
|
|
111
|
+
log.info("Using open authentication")
|
|
112
|
+
await self._authenticate_open(nonce=resp.tags[0])
|
|
113
|
+
elif resp.tags[0] == "98":
|
|
114
|
+
log.info("Using hmac authentication")
|
|
115
|
+
tag = resp.tags[1]
|
|
116
|
+
await self._authenticate_hmac(
|
|
117
|
+
hash_algorithm=tag,
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
raise InvalidAuthentication("Invalid authentication response")
|
|
121
|
+
|
|
122
|
+
log.info("Client ready")
|
|
123
|
+
|
|
124
|
+
async def _authenticate_open(self, nonce: str) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Authenticates the client using the open authentication algorithm
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
nonce (str): The nonce sent by the server
|
|
130
|
+
"""
|
|
131
|
+
enc = own_calc_pass(self._password, nonce)
|
|
132
|
+
|
|
133
|
+
await self.send_message(GenericMessage(["#" + enc]), force=True)
|
|
134
|
+
resp = await self.read_message()
|
|
135
|
+
|
|
136
|
+
if resp.type != MessageType.ACK:
|
|
137
|
+
raise InvalidAuthentication("Invalid password")
|
|
138
|
+
|
|
139
|
+
async def _authenticate_hmac(self, hash_algorithm: AuthAlgorithm | str) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Authenticates the client using the hmac authentication algorithm
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
hash_algorithm (AuthAlgorithm | str): The hash algorithm to use
|
|
145
|
+
"""
|
|
146
|
+
# TODO: Check with a real device if the handshake is implemented correctly
|
|
147
|
+
if isinstance(hash_algorithm, str):
|
|
148
|
+
try:
|
|
149
|
+
hash_algorithm = AuthAlgorithm.from_string(hash_algorithm)
|
|
150
|
+
except ValueError:
|
|
151
|
+
# Close the connection
|
|
152
|
+
await self.send_message(NACK(), force=True)
|
|
153
|
+
raise InvalidAuthentication("Invalid hash algorithm")
|
|
154
|
+
|
|
155
|
+
# Send an ACK to accept the algorithm and wait for the server key
|
|
156
|
+
await self.send_message(ACK(), force=True)
|
|
157
|
+
resp = await self.read_message()
|
|
158
|
+
server_key = resp.tags[0]
|
|
159
|
+
|
|
160
|
+
# Generate the client key
|
|
161
|
+
client_key = create_key(hash_algorithm)
|
|
162
|
+
|
|
163
|
+
# Generate the two authentication strings
|
|
164
|
+
client_auth = client_hmac(
|
|
165
|
+
server_key=server_key,
|
|
166
|
+
client_key=client_key,
|
|
167
|
+
password=self._password,
|
|
168
|
+
hash_algorithm=hash_algorithm
|
|
169
|
+
)
|
|
170
|
+
server_auth = server_hmac(
|
|
171
|
+
server_key=server_key,
|
|
172
|
+
client_key=client_key,
|
|
173
|
+
password=self._password,
|
|
174
|
+
hash_algorithm=hash_algorithm
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Send the client authentication string
|
|
178
|
+
await self.send_message(
|
|
179
|
+
GenericMessage([hex_to_digits(client_key), hex_to_digits(client_auth.hex())]),
|
|
180
|
+
force=True
|
|
181
|
+
)
|
|
182
|
+
resp = await self.read_message()
|
|
183
|
+
|
|
184
|
+
if resp.type == MessageType.NACK:
|
|
185
|
+
raise InvalidAuthentication("Invalid password")
|
|
186
|
+
|
|
187
|
+
# Check the server authentication string with the one generated
|
|
188
|
+
if not compare_hmac(
|
|
189
|
+
server_auth,
|
|
190
|
+
bytes.fromhex(hex_to_digits(resp.tags[0]))
|
|
191
|
+
):
|
|
192
|
+
raise InvalidAuthentication("Invalid password")
|
|
193
|
+
else:
|
|
194
|
+
await self.send_message(ACK(), force=True)
|
|
195
|
+
|
|
196
|
+
async def send_message(self, message: BaseMessage, *, force: bool = False) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Sends a message to the server
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
message (BaseMessage): send to the server a subclass of BaseMessage
|
|
202
|
+
force (bool): if True, the message will be sent even if the client is set as an event session
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
InvalidSession: if the client is set as an event session or if the client is not started
|
|
206
|
+
"""
|
|
207
|
+
if not self.is_cmd_session() and not force:
|
|
208
|
+
raise InvalidSession("Cannot send messages in an event session")
|
|
209
|
+
if self._protocol is None:
|
|
210
|
+
raise InvalidSession("Client not started")
|
|
211
|
+
|
|
212
|
+
await self._protocol.send_message(message)
|
|
213
|
+
|
|
214
|
+
async def read_message(self, timeout: int | None = 5) -> BaseMessage:
|
|
215
|
+
"""
|
|
216
|
+
Awaits a message from the server and returns it.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
BaseMessage: the message from the server, it will be a subclass of BaseMessage.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
TimeoutError: if the server does not respond in the given time.
|
|
223
|
+
InvalidSession: if the client is not started.
|
|
224
|
+
"""
|
|
225
|
+
if self._protocol is None:
|
|
226
|
+
raise InvalidSession("Client not started")
|
|
227
|
+
|
|
228
|
+
async with asyncio.timeout(timeout):
|
|
229
|
+
message = await self._protocol.receive_messages()
|
|
230
|
+
|
|
231
|
+
return message
|
|
232
|
+
|
|
233
|
+
async def close(self) -> None:
|
|
234
|
+
"""
|
|
235
|
+
Close the client
|
|
236
|
+
"""
|
|
237
|
+
if self._transport is not None:
|
|
238
|
+
self._transport.close()
|
|
239
|
+
|
|
240
|
+
self._protocol = None
|
|
241
|
+
self._transport = None
|
|
242
|
+
|
|
243
|
+
@abstractmethod
|
|
244
|
+
async def loop(self):
|
|
245
|
+
"""
|
|
246
|
+
Runs the client loop.
|
|
247
|
+
This is a loop that runs the entire event system for the client, it will read messages from the gateway and
|
|
248
|
+
dispatch them to the correct callbacks.
|
|
249
|
+
"""
|
|
250
|
+
raise NotImplementedError
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _exception_handler(_: AbstractEventLoop, context: dict[str, Any]):
|
|
254
|
+
exception = context.get("exception")
|
|
255
|
+
message = context.get("message")
|
|
256
|
+
|
|
257
|
+
if exception is not None:
|
|
258
|
+
log.error(f"Exception occurred: {message} ({exception})")
|
|
259
|
+
else:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
if (task := context.get("task")) is not None:
|
|
263
|
+
task.print_stack()
|
|
264
|
+
elif (protocol := context.get("protocol")) is not None:
|
|
265
|
+
log.error(f"Protocol error: {protocol}")
|
|
266
|
+
elif (transport := context.get("transport")) is not None:
|
|
267
|
+
log.error(f"Transport error: {transport}")
|