4
4
under compatible GNU GPL3 license.
5
5
"""
6
6
7
- import asyncio
8
7
import base64
9
8
import hashlib
10
9
import logging
11
10
import time
12
- from typing import Optional
11
+ from typing import Optional , Union
13
12
14
13
import httpx
15
14
from cryptography .hazmat .primitives import padding , serialization
@@ -56,60 +55,51 @@ def __init__(
56
55
) -> None :
57
56
super ().__init__ (host = host )
58
57
59
- self .credentials = (
60
- credentials
61
- if credentials and credentials .username and credentials .password
62
- else Credentials (username = "" , password = "" )
63
- )
64
-
65
- self ._local_seed : Optional [bytes ] = None
66
- self .kasa_setup_auth_hash = None
67
- self .blank_auth_hash = None
68
- self .handshake_lock = asyncio .Lock ()
58
+ self ._credentials = credentials or Credentials (username = "" , password = "" )
69
59
70
- self .handshake_done = False
60
+ self ._handshake_done = False
71
61
72
- self .encryption_session : Optional [AesEncyptionSession ] = None
73
- self .session_expire_at : Optional [float ] = None
62
+ self ._encryption_session : Optional [AesEncyptionSession ] = None
63
+ self ._session_expire_at : Optional [float ] = None
74
64
75
- self .timeout = timeout if timeout else self .DEFAULT_TIMEOUT
76
- self .session_cookie = None
65
+ self ._timeout = timeout if timeout else self .DEFAULT_TIMEOUT
66
+ self ._session_cookie = None
77
67
78
- self .http_client : httpx .AsyncClient = httpx .AsyncClient ()
79
- self .login_token = None
68
+ self ._http_client : httpx .AsyncClient = httpx .AsyncClient ()
69
+ self ._login_token = None
80
70
81
71
_LOGGER .debug ("Created AES object for %s" , self .host )
82
72
83
- def hash_credentials (self , credentials , try_login_version2 ):
73
+ def hash_credentials (self , login_v2 ):
84
74
"""Hash the credentials."""
85
- if try_login_version2 :
75
+ if login_v2 :
86
76
un = base64 .b64encode (
87
- _sha1 (credentials .username .encode ()).encode ()
77
+ _sha1 (self . _credentials .username .encode ()).encode ()
88
78
).decode ()
89
79
pw = base64 .b64encode (
90
- _sha1 (credentials .password .encode ()).encode ()
80
+ _sha1 (self . _credentials .password .encode ()).encode ()
91
81
).decode ()
92
82
else :
93
83
un = base64 .b64encode (
94
- _sha1 (credentials .username .encode ()).encode ()
84
+ _sha1 (self . _credentials .username .encode ()).encode ()
95
85
).decode ()
96
- pw = base64 .b64encode (credentials .password .encode ()).decode ()
86
+ pw = base64 .b64encode (self . _credentials .password .encode ()).decode ()
97
87
return un , pw
98
88
99
89
async def client_post (self , url , params = None , data = None , json = None , headers = None ):
100
90
"""Send an http post request to the device."""
101
91
response_data = None
102
92
cookies = None
103
- if self .session_cookie :
93
+ if self ._session_cookie :
104
94
cookies = httpx .Cookies ()
105
- cookies .set (self .SESSION_COOKIE_NAME , self .session_cookie )
106
- self .http_client .cookies .clear ()
107
- resp = await self .http_client .post (
95
+ cookies .set (self .SESSION_COOKIE_NAME , self ._session_cookie )
96
+ self ._http_client .cookies .clear ()
97
+ resp = await self ._http_client .post (
108
98
url ,
109
99
params = params ,
110
100
data = data ,
111
101
json = json ,
112
- timeout = self .timeout ,
102
+ timeout = self ._timeout ,
113
103
cookies = cookies ,
114
104
headers = self .COMMON_HEADERS ,
115
105
)
@@ -121,63 +111,67 @@ async def client_post(self, url, params=None, data=None, json=None, headers=None
121
111
async def send_secure_passthrough (self , request : str ):
122
112
"""Send encrypted message as passthrough."""
123
113
url = f"http://{ self .host } /app"
124
- if self .login_token :
125
- url += f"?token={ self .login_token } "
114
+ if self ._login_token :
115
+ url += f"?token={ self ._login_token } "
126
116
127
- encrypted_payload = self .encryption_session .encrypt (request .encode ()) # type: ignore
117
+ encrypted_payload = self ._encryption_session .encrypt (request .encode ()) # type: ignore
128
118
passthrough_request = {
129
119
"method" : "securePassthrough" ,
130
120
"params" : {"request" : encrypted_payload .decode ()},
131
121
}
132
122
status_code , resp_dict = await self .client_post (url , json = passthrough_request )
133
123
_LOGGER .debug (f"secure_passthrough response is { status_code } : { resp_dict } " )
134
124
if status_code == 200 and resp_dict ["error_code" ] == 0 :
135
- response = self .encryption_session .decrypt ( # type: ignore
125
+ response = self ._encryption_session .decrypt ( # type: ignore
136
126
resp_dict ["result" ]["response" ].encode ()
137
127
)
138
128
_LOGGER .debug (f"decrypted secure_passthrough response is { response } " )
139
129
resp_dict = json_loads (response )
140
130
return resp_dict
141
131
else :
142
- self .handshake_done = False
143
- self .login_token = None
132
+ self ._handshake_done = False
133
+ self ._login_token = None
144
134
raise AuthenticationException ("Could not complete send" )
145
135
146
- async def perform_login (self , login_request , login_v2 ):
136
+ async def perform_login (self , login_request : Union [ str , dict ], * , login_v2 : bool ):
147
137
"""Login to the device."""
148
- self .login_token = None
138
+ self ._login_token = None
149
139
150
140
if isinstance (login_request , str ):
151
- login_request = json_loads (login_request )
141
+ login_request_dict : dict = json_loads (login_request )
142
+ else :
143
+ login_request_dict = login_request
152
144
153
- un , pw = self .hash_credentials (self . credentials , login_v2 )
154
- login_request ["params" ] = {"password" : pw , "username" : un }
155
- request = json_dumps (login_request )
145
+ un , pw = self .hash_credentials (login_v2 )
146
+ login_request_dict ["params" ] = {"password" : pw , "username" : un }
147
+ request = json_dumps (login_request_dict )
156
148
try :
157
149
resp_dict = await self .send_secure_passthrough (request )
158
150
except SmartDeviceException as ex :
159
151
raise AuthenticationException (ex ) from ex
160
- self .login_token = resp_dict ["result" ]["token" ]
152
+ self ._login_token = resp_dict ["result" ]["token" ]
161
153
154
+ @property
162
155
def needs_login (self ) -> bool :
163
156
"""Return true if the transport needs to do a login."""
164
- return self .login_token is None
157
+ return self ._login_token is None
165
158
166
159
async def login (self , request : str ) -> None :
167
160
"""Login to the device."""
168
161
try :
169
- if self .needs_handshake () :
162
+ if self .needs_handshake :
170
163
raise SmartDeviceException (
171
164
"Handshake must be complete before trying to login"
172
165
)
173
- await self .perform_login (request , False )
166
+ await self .perform_login (request , login_v2 = False )
174
167
except AuthenticationException :
175
168
await self .perform_handshake ()
176
- await self .perform_login (request , True )
169
+ await self .perform_login (request , login_v2 = True )
177
170
171
+ @property
178
172
def needs_handshake (self ) -> bool :
179
173
"""Return true if the transport needs to do a handshake."""
180
- return not self .handshake_done or self .handshake_session_expired ()
174
+ return not self ._handshake_done or self ._handshake_session_expired ()
181
175
182
176
async def handshake (self ) -> None :
183
177
"""Perform the encryption handshake."""
@@ -188,9 +182,9 @@ async def perform_handshake(self):
188
182
_LOGGER .debug ("Will perform handshaking..." )
189
183
_LOGGER .debug ("Generating keypair" )
190
184
191
- self .handshake_done = False
192
- self .session_expire_at = None
193
- self .session_cookie = None
185
+ self ._handshake_done = False
186
+ self ._session_expire_at = None
187
+ self ._session_cookie = None
194
188
195
189
url = f"http://{ self .host } /app"
196
190
key_pair = KeyPair .create_key_pair ()
@@ -215,54 +209,55 @@ async def perform_handshake(self):
215
209
_LOGGER .debug ("Decoding handshake key..." )
216
210
handshake_key = resp_dict ["result" ]["key" ]
217
211
218
- self .session_cookie = self .http_client .cookies .get ( # type: ignore
212
+ self ._session_cookie = self ._http_client .cookies .get ( # type: ignore
219
213
self .SESSION_COOKIE_NAME
220
214
)
221
- if not self .session_cookie :
222
- self .session_cookie = self .http_client .cookies .get ( # type: ignore
215
+ if not self ._session_cookie :
216
+ self ._session_cookie = self ._http_client .cookies .get ( # type: ignore
223
217
"SESSIONID"
224
218
)
225
219
226
- self .session_expire_at = time .time () + 86400
227
- self .encryption_session = AesEncyptionSession .create_from_keypair (
220
+ self ._session_expire_at = time .time () + 86400
221
+ self ._encryption_session = AesEncyptionSession .create_from_keypair (
228
222
handshake_key , key_pair
229
223
)
230
224
231
- self .handshake_done = True
225
+ self ._handshake_done = True
232
226
233
227
_LOGGER .debug ("Handshake with %s complete" , self .host )
234
228
235
229
else :
236
230
raise AuthenticationException ("Could not complete handshake" )
237
231
238
- def handshake_session_expired (self ):
232
+ def _handshake_session_expired (self ):
239
233
"""Return true if session has expired."""
240
234
return (
241
- self .session_expire_at is None or self .session_expire_at - time .time () <= 0
235
+ self ._session_expire_at is None
236
+ or self ._session_expire_at - time .time () <= 0
242
237
)
243
238
244
239
async def send (self , request : str ):
245
240
"""Send the request."""
246
- if self .needs_handshake () :
241
+ if self .needs_handshake :
247
242
raise SmartDeviceException (
248
243
"Handshake must be complete before trying to send"
249
244
)
250
- if self .needs_login () :
245
+ if self .needs_login :
251
246
raise SmartDeviceException ("Login must be complete before trying to send" )
252
247
253
248
resp_dict = await self .send_secure_passthrough (request )
254
249
if resp_dict ["error_code" ] != 0 :
255
- self .handshake_done = False
256
- self .login_token = None
250
+ self ._handshake_done = False
251
+ self ._login_token = None
257
252
raise SmartDeviceException (
258
253
f"Could not complete send, response was { resp_dict } " ,
259
254
)
260
255
return resp_dict
261
256
262
257
async def close (self ) -> None :
263
258
"""Close the protocol."""
264
- client = self .http_client
265
- self .http_client = None
259
+ client = self ._http_client
260
+ self ._http_client = None
266
261
if client :
267
262
await client .aclose ()
268
263
0 commit comments