tplinkrouterc6u 5.0.3__py3-none-any.whl → 5.2.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.
- test/{test_client.py → test_client_c6u.py} +2 -1
- test/test_client_ex.py +339 -0
- test/test_client_mr.py +365 -0
- test/test_client_xdr.py +536 -0
- tplinkrouterc6u/__init__.py +16 -12
- tplinkrouterc6u/client/__init__.py +1 -0
- tplinkrouterc6u/client/c1200.py +102 -0
- tplinkrouterc6u/client/c5400x.py +109 -0
- tplinkrouterc6u/client/c6u.py +436 -0
- tplinkrouterc6u/client/c6v4.py +38 -0
- tplinkrouterc6u/client/deco.py +177 -0
- tplinkrouterc6u/client/ex.py +295 -0
- tplinkrouterc6u/client/mr.py +712 -0
- tplinkrouterc6u/client/xdr.py +263 -0
- tplinkrouterc6u/client_abstract.py +48 -0
- tplinkrouterc6u/common/__init__.py +1 -0
- tplinkrouterc6u/{dataclass.py → common/dataclass.py} +40 -1
- tplinkrouterc6u/{package_enum.py → common/package_enum.py} +5 -0
- tplinkrouterc6u/provider.py +39 -0
- {tplinkrouterc6u-5.0.3.dist-info → tplinkrouterc6u-5.2.0.dist-info}/METADATA +52 -2
- tplinkrouterc6u-5.2.0.dist-info/RECORD +30 -0
- {tplinkrouterc6u-5.0.3.dist-info → tplinkrouterc6u-5.2.0.dist-info}/WHEEL +1 -1
- tplinkrouterc6u/client.py +0 -1451
- tplinkrouterc6u-5.0.3.dist-info/RECORD +0 -17
- /tplinkrouterc6u/{encryption.py → common/encryption.py} +0 -0
- /tplinkrouterc6u/{exception.py → common/exception.py} +0 -0
- /tplinkrouterc6u/{helper.py → common/helper.py} +0 -0
- {tplinkrouterc6u-5.0.3.dist-info → tplinkrouterc6u-5.2.0.dist-info}/LICENSE +0 -0
- {tplinkrouterc6u-5.0.3.dist-info → tplinkrouterc6u-5.2.0.dist-info}/top_level.txt +0 -0
tplinkrouterc6u/client.py
DELETED
|
@@ -1,1451 +0,0 @@
|
|
|
1
|
-
from base64 import b64decode
|
|
2
|
-
from hashlib import md5
|
|
3
|
-
from re import search
|
|
4
|
-
from json import dumps, loads
|
|
5
|
-
from time import time, sleep
|
|
6
|
-
from urllib.parse import quote
|
|
7
|
-
from requests.packages import urllib3
|
|
8
|
-
from requests import post, Response, Session
|
|
9
|
-
from datetime import timedelta
|
|
10
|
-
from macaddress import EUI48
|
|
11
|
-
from ipaddress import IPv4Address
|
|
12
|
-
from logging import Logger
|
|
13
|
-
from tplinkrouterc6u.helper import get_ip, get_mac
|
|
14
|
-
from tplinkrouterc6u.encryption import EncryptionWrapper, EncryptionWrapperMR
|
|
15
|
-
from tplinkrouterc6u.package_enum import Connection
|
|
16
|
-
from tplinkrouterc6u.dataclass import Firmware, Status, Device, IPv4Reservation, IPv4DHCPLease, IPv4Status
|
|
17
|
-
from tplinkrouterc6u.exception import ClientException, ClientError, AuthorizeError
|
|
18
|
-
from abc import ABC, abstractmethod
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class AbstractRouter(ABC):
|
|
22
|
-
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
23
|
-
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
24
|
-
self.username = username
|
|
25
|
-
self.password = password
|
|
26
|
-
self.timeout = timeout
|
|
27
|
-
self._logger = logger
|
|
28
|
-
self.host = host
|
|
29
|
-
if not (self.host.startswith('http://') or self.host.startswith('https://')):
|
|
30
|
-
self.host = "http://{}".format(self.host)
|
|
31
|
-
self._verify_ssl = verify_ssl
|
|
32
|
-
if self._verify_ssl is False:
|
|
33
|
-
urllib3.disable_warnings()
|
|
34
|
-
|
|
35
|
-
@abstractmethod
|
|
36
|
-
def supports(self) -> bool:
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
@abstractmethod
|
|
40
|
-
def authorize(self) -> None:
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
@abstractmethod
|
|
44
|
-
def logout(self) -> None:
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
@abstractmethod
|
|
48
|
-
def get_firmware(self) -> Firmware:
|
|
49
|
-
pass
|
|
50
|
-
|
|
51
|
-
@abstractmethod
|
|
52
|
-
def get_status(self) -> Status:
|
|
53
|
-
pass
|
|
54
|
-
|
|
55
|
-
@abstractmethod
|
|
56
|
-
def reboot(self) -> None:
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
@abstractmethod
|
|
60
|
-
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
61
|
-
pass
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class TplinkRequest:
|
|
65
|
-
host = ''
|
|
66
|
-
_stok = ''
|
|
67
|
-
timeout = 10
|
|
68
|
-
_logged = False
|
|
69
|
-
_sysauth = None
|
|
70
|
-
_verify_ssl = False
|
|
71
|
-
_logger = None
|
|
72
|
-
_headers_request = {}
|
|
73
|
-
_headers_login = {}
|
|
74
|
-
_data_block = 'data'
|
|
75
|
-
|
|
76
|
-
def request(self, path: str, data: str, ignore_response: bool = False, ignore_errors: bool = False) -> dict | None:
|
|
77
|
-
if self._logged is False:
|
|
78
|
-
raise Exception('Not authorised')
|
|
79
|
-
url = '{}/cgi-bin/luci/;stok={}/{}'.format(self.host, self._stok, path)
|
|
80
|
-
|
|
81
|
-
response = post(
|
|
82
|
-
url,
|
|
83
|
-
data=self._prepare_data(data),
|
|
84
|
-
headers=self._headers_request,
|
|
85
|
-
cookies={'sysauth': self._sysauth},
|
|
86
|
-
timeout=self.timeout,
|
|
87
|
-
verify=self._verify_ssl,
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
if ignore_response:
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
data = response.text
|
|
94
|
-
error = ''
|
|
95
|
-
try:
|
|
96
|
-
data = response.json()
|
|
97
|
-
if 'data' not in data:
|
|
98
|
-
raise Exception("Router didn't respond with JSON")
|
|
99
|
-
data = self._decrypt_response(data)
|
|
100
|
-
|
|
101
|
-
if self._is_valid_response(data):
|
|
102
|
-
return data.get(self._data_block)
|
|
103
|
-
elif ignore_errors:
|
|
104
|
-
return data
|
|
105
|
-
except Exception as e:
|
|
106
|
-
error = ('TplinkRouter - {} - An unknown response - {}; Request {} - Response {}'
|
|
107
|
-
.format(self.__class__.__name__, e, path, data))
|
|
108
|
-
error = ('TplinkRouter - {} - Response with error; Request {} - Response {}'
|
|
109
|
-
.format(self.__class__.__name__, path, data)) if not error else error
|
|
110
|
-
if self._logger:
|
|
111
|
-
self._logger.debug(error)
|
|
112
|
-
raise ClientError(error)
|
|
113
|
-
|
|
114
|
-
def _is_valid_response(self, data: dict) -> bool:
|
|
115
|
-
return 'success' in data and data['success'] and self._data_block in data
|
|
116
|
-
|
|
117
|
-
def _prepare_data(self, data: str):
|
|
118
|
-
return data
|
|
119
|
-
|
|
120
|
-
def _decrypt_response(self, data: dict) -> dict:
|
|
121
|
-
return data
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
class TplinkEncryption(TplinkRequest):
|
|
125
|
-
username = ''
|
|
126
|
-
password = ''
|
|
127
|
-
nn = ''
|
|
128
|
-
ee = ''
|
|
129
|
-
_seq = ''
|
|
130
|
-
_pwdNN = ''
|
|
131
|
-
_pwdEE = ''
|
|
132
|
-
_encryption = EncryptionWrapper()
|
|
133
|
-
|
|
134
|
-
def supports(self) -> bool:
|
|
135
|
-
if len(self.password) > 125:
|
|
136
|
-
return False
|
|
137
|
-
|
|
138
|
-
try:
|
|
139
|
-
self._request_pwd()
|
|
140
|
-
return True
|
|
141
|
-
except ClientException:
|
|
142
|
-
return False
|
|
143
|
-
|
|
144
|
-
def authorize(self) -> None:
|
|
145
|
-
if self._pwdNN == '':
|
|
146
|
-
self._request_pwd()
|
|
147
|
-
|
|
148
|
-
if self._seq == '':
|
|
149
|
-
self._request_seq()
|
|
150
|
-
|
|
151
|
-
response = self._try_login()
|
|
152
|
-
|
|
153
|
-
is_valid_json = False
|
|
154
|
-
try:
|
|
155
|
-
response.json()
|
|
156
|
-
is_valid_json = True
|
|
157
|
-
except BaseException:
|
|
158
|
-
"""Ignore"""
|
|
159
|
-
|
|
160
|
-
if is_valid_json is False or response.status_code == 403:
|
|
161
|
-
self._logged = False
|
|
162
|
-
self._request_pwd()
|
|
163
|
-
self._request_seq()
|
|
164
|
-
response = self._try_login()
|
|
165
|
-
|
|
166
|
-
data = response.text
|
|
167
|
-
try:
|
|
168
|
-
data = response.json()
|
|
169
|
-
data = self._decrypt_response(data)
|
|
170
|
-
|
|
171
|
-
self._stok = data[self._data_block]['stok']
|
|
172
|
-
regex_result = search(
|
|
173
|
-
'sysauth=(.*);', response.headers['set-cookie'])
|
|
174
|
-
self._sysauth = regex_result.group(1)
|
|
175
|
-
self._logged = True
|
|
176
|
-
|
|
177
|
-
except Exception as e:
|
|
178
|
-
error = ("TplinkRouter - {} - Cannot authorize! Error - {}; Response - {}"
|
|
179
|
-
.format(self.__class__.__name__, e, data))
|
|
180
|
-
if self._logger:
|
|
181
|
-
self._logger.debug(error)
|
|
182
|
-
raise ClientException(error)
|
|
183
|
-
|
|
184
|
-
def _request_pwd(self) -> None:
|
|
185
|
-
url = '{}/cgi-bin/luci/;stok=/login?form=keys'.format(self.host)
|
|
186
|
-
|
|
187
|
-
# If possible implement RSA encryption of password here.
|
|
188
|
-
response = post(
|
|
189
|
-
url, params={'operation': 'read'},
|
|
190
|
-
timeout=self.timeout,
|
|
191
|
-
verify=self._verify_ssl,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
try:
|
|
195
|
-
data = response.json()
|
|
196
|
-
|
|
197
|
-
args = data[self._data_block]['password']
|
|
198
|
-
|
|
199
|
-
self._pwdNN = args[0]
|
|
200
|
-
self._pwdEE = args[1]
|
|
201
|
-
|
|
202
|
-
except Exception as e:
|
|
203
|
-
error = ('TplinkRouter - {} - Unknown error for pwd! Error - {}; Response - {}'
|
|
204
|
-
.format(self.__class__.__name__, e, response.text))
|
|
205
|
-
if self._logger:
|
|
206
|
-
self._logger.debug(error)
|
|
207
|
-
raise ClientException(error)
|
|
208
|
-
|
|
209
|
-
def _request_seq(self) -> None:
|
|
210
|
-
url = '{}/cgi-bin/luci/;stok=/login?form=auth'.format(self.host)
|
|
211
|
-
|
|
212
|
-
# If possible implement RSA encryption of password here.
|
|
213
|
-
response = post(
|
|
214
|
-
url,
|
|
215
|
-
params={'operation': 'read'},
|
|
216
|
-
timeout=self.timeout,
|
|
217
|
-
verify=self._verify_ssl,
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
try:
|
|
221
|
-
data = response.json()
|
|
222
|
-
|
|
223
|
-
self._seq = data[self._data_block]['seq']
|
|
224
|
-
args = data[self._data_block]['key']
|
|
225
|
-
|
|
226
|
-
self.nn = args[0]
|
|
227
|
-
self.ee = args[1]
|
|
228
|
-
|
|
229
|
-
except Exception as e:
|
|
230
|
-
error = ('TplinkRouter - {} - Unknown error for seq! Error - {}; Response - {}'
|
|
231
|
-
.format(self.__class__.__name__, e, response.text))
|
|
232
|
-
if self._logger:
|
|
233
|
-
self._logger.debug(error)
|
|
234
|
-
raise ClientException(error)
|
|
235
|
-
|
|
236
|
-
def _try_login(self) -> Response:
|
|
237
|
-
url = '{}/cgi-bin/luci/;stok=/login?form=login'.format(self.host)
|
|
238
|
-
|
|
239
|
-
crypted_pwd = self._encryption.rsa_encrypt(self.password, self._pwdNN, self._pwdEE)
|
|
240
|
-
|
|
241
|
-
body = self._prepare_data(self._get_login_data(crypted_pwd))
|
|
242
|
-
|
|
243
|
-
return post(
|
|
244
|
-
url,
|
|
245
|
-
data=body,
|
|
246
|
-
headers=self._headers_login,
|
|
247
|
-
timeout=self.timeout,
|
|
248
|
-
verify=self._verify_ssl,
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
@staticmethod
|
|
252
|
-
def _get_login_data(crypted_pwd: str) -> str:
|
|
253
|
-
return 'operation=login&password={}&confirm=true'.format(crypted_pwd)
|
|
254
|
-
|
|
255
|
-
def _prepare_data(self, data: str) -> dict:
|
|
256
|
-
encrypted_data = self._encryption.aes_encrypt(data)
|
|
257
|
-
data_len = len(encrypted_data)
|
|
258
|
-
hash = md5((self.username + self.password).encode()).hexdigest()
|
|
259
|
-
|
|
260
|
-
sign = self._encryption.get_signature(int(self._seq) + data_len,
|
|
261
|
-
True if self._logged is False else False,
|
|
262
|
-
hash, self.nn, self.ee)
|
|
263
|
-
|
|
264
|
-
return {'sign': sign, 'data': encrypted_data}
|
|
265
|
-
|
|
266
|
-
def _decrypt_response(self, data: dict) -> dict:
|
|
267
|
-
return loads(self._encryption.aes_decrypt(data['data']))
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
class TplinkBaseRouter(AbstractRouter, TplinkRequest):
|
|
271
|
-
_smart_network = True
|
|
272
|
-
_perf_status = True
|
|
273
|
-
|
|
274
|
-
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
275
|
-
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
276
|
-
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
277
|
-
|
|
278
|
-
self._url_firmware = 'admin/firmware?form=upgrade&operation=read'
|
|
279
|
-
self._url_ipv4_reservations = 'admin/dhcps?form=reservation&operation=load'
|
|
280
|
-
self._url_ipv4_dhcp_leases = 'admin/dhcps?form=client&operation=load'
|
|
281
|
-
referer = '{}/webpages/index.html'.format(self.host)
|
|
282
|
-
self._headers_request = {'Referer': referer}
|
|
283
|
-
self._headers_login = {'Referer': referer, 'Content-Type': 'application/x-www-form-urlencoded'}
|
|
284
|
-
|
|
285
|
-
@abstractmethod
|
|
286
|
-
def authorize(self) -> bool:
|
|
287
|
-
pass
|
|
288
|
-
|
|
289
|
-
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
290
|
-
values = {
|
|
291
|
-
Connection.HOST_2G: 'wireless_2g',
|
|
292
|
-
Connection.HOST_5G: 'wireless_5g',
|
|
293
|
-
Connection.HOST_6G: 'wireless_6g',
|
|
294
|
-
Connection.GUEST_2G: 'guest_2g',
|
|
295
|
-
Connection.GUEST_5G: 'guest_5g',
|
|
296
|
-
Connection.GUEST_6G: 'guest_6g',
|
|
297
|
-
Connection.IOT_2G: 'iot_2g',
|
|
298
|
-
Connection.IOT_5G: 'iot_5g',
|
|
299
|
-
Connection.IOT_6G: 'iot_6g',
|
|
300
|
-
}
|
|
301
|
-
value = values.get(wifi)
|
|
302
|
-
path = f"admin/wireless?&form=guest&form={value}"
|
|
303
|
-
data = f"operation=write&{value}_enable={'on' if enable else 'off'}"
|
|
304
|
-
self.request(path, data)
|
|
305
|
-
|
|
306
|
-
def reboot(self) -> None:
|
|
307
|
-
self.request('admin/system?form=reboot', 'operation=write', True)
|
|
308
|
-
|
|
309
|
-
def logout(self) -> None:
|
|
310
|
-
self.request('admin/system?form=logout', 'operation=write', True)
|
|
311
|
-
self._stok = ''
|
|
312
|
-
self._sysauth = ''
|
|
313
|
-
self._logged = False
|
|
314
|
-
|
|
315
|
-
def get_firmware(self) -> Firmware:
|
|
316
|
-
data = self.request(self._url_firmware, 'operation=read')
|
|
317
|
-
firmware = Firmware(data.get('hardware_version', ''), data.get('model', ''), data.get('firmware_version', ''))
|
|
318
|
-
|
|
319
|
-
return firmware
|
|
320
|
-
|
|
321
|
-
def get_status(self) -> Status:
|
|
322
|
-
data = self.request('admin/status?form=all&operation=read', 'operation=read')
|
|
323
|
-
|
|
324
|
-
status = Status()
|
|
325
|
-
status._wan_macaddr = EUI48(data['wan_macaddr']) if 'wan_macaddr' in data else None
|
|
326
|
-
status._lan_macaddr = EUI48(data['lan_macaddr'])
|
|
327
|
-
status._wan_ipv4_addr = IPv4Address(data['wan_ipv4_ipaddr']) if 'wan_ipv4_ipaddr' in data else None
|
|
328
|
-
status._lan_ipv4_addr = IPv4Address(data['lan_ipv4_ipaddr']) if 'lan_ipv4_ipaddr' in data else None
|
|
329
|
-
status._wan_ipv4_gateway = IPv4Address(
|
|
330
|
-
data['wan_ipv4_gateway']) if 'wan_ipv4_gateway' in data else None
|
|
331
|
-
status.wan_ipv4_uptime = data.get('wan_ipv4_uptime')
|
|
332
|
-
status.mem_usage = data.get('mem_usage')
|
|
333
|
-
status.cpu_usage = data.get('cpu_usage')
|
|
334
|
-
status.wired_total = len(data.get('access_devices_wired', []))
|
|
335
|
-
status.wifi_clients_total = len(data.get('access_devices_wireless_host', []))
|
|
336
|
-
status.guest_clients_total = len(data.get('access_devices_wireless_guest', []))
|
|
337
|
-
status.guest_2g_enable = self._str2bool(data.get('guest_2g_enable'))
|
|
338
|
-
status.guest_5g_enable = self._str2bool(data.get('guest_5g_enable'))
|
|
339
|
-
status.guest_6g_enable = self._str2bool(data.get('guest_6g_enable'))
|
|
340
|
-
status.iot_2g_enable = self._str2bool(data.get('iot_2g_enable'))
|
|
341
|
-
status.iot_5g_enable = self._str2bool(data.get('iot_5g_enable'))
|
|
342
|
-
status.iot_6g_enable = self._str2bool(data.get('iot_6g_enable'))
|
|
343
|
-
status.wifi_2g_enable = self._str2bool(data.get('wireless_2g_enable'))
|
|
344
|
-
status.wifi_5g_enable = self._str2bool(data.get('wireless_5g_enable'))
|
|
345
|
-
status.wifi_6g_enable = self._str2bool(data.get('wireless_6g_enable'))
|
|
346
|
-
|
|
347
|
-
if (status.mem_usage is None or status.mem_usage is None) and self._perf_status:
|
|
348
|
-
try:
|
|
349
|
-
performance = self.request('admin/status?form=perf&operation=read', 'operation=read')
|
|
350
|
-
status.mem_usage = performance.get('mem_usage')
|
|
351
|
-
status.cpu_usage = performance.get('cpu_usage')
|
|
352
|
-
except BaseException:
|
|
353
|
-
self._perf_status = False
|
|
354
|
-
|
|
355
|
-
devices = {}
|
|
356
|
-
|
|
357
|
-
def _add_device(conn: Connection, item: dict) -> None:
|
|
358
|
-
devices[item['macaddr']] = Device(conn, get_mac(item.get('macaddr', '00:00:00:00:00:00')),
|
|
359
|
-
get_ip(item['ipaddr']),
|
|
360
|
-
item['hostname'])
|
|
361
|
-
|
|
362
|
-
for item in data.get('access_devices_wired', []):
|
|
363
|
-
type = self._map_wire_type(item.get('wire_type'))
|
|
364
|
-
_add_device(type, item)
|
|
365
|
-
|
|
366
|
-
for item in data.get('access_devices_wireless_host', []):
|
|
367
|
-
type = self._map_wire_type(item.get('wire_type'))
|
|
368
|
-
_add_device(type, item)
|
|
369
|
-
|
|
370
|
-
for item in data.get('access_devices_wireless_guest', []):
|
|
371
|
-
type = self._map_wire_type(item.get('wire_type'), False)
|
|
372
|
-
_add_device(type, item)
|
|
373
|
-
|
|
374
|
-
smart_network = None
|
|
375
|
-
if self._smart_network:
|
|
376
|
-
try:
|
|
377
|
-
smart_network = self.request('admin/smart_network?form=game_accelerator', 'operation=loadDevice')
|
|
378
|
-
except Exception:
|
|
379
|
-
self._smart_network = False
|
|
380
|
-
|
|
381
|
-
if smart_network:
|
|
382
|
-
for item in smart_network:
|
|
383
|
-
if item['mac'] not in devices:
|
|
384
|
-
conn = self._map_wire_type(item.get('deviceTag'), not item.get('isGuest'))
|
|
385
|
-
devices[item['mac']] = Device(conn, get_mac(item.get('mac', '00:00:00:00:00:00')),
|
|
386
|
-
get_ip(item['ip']), item['deviceName'])
|
|
387
|
-
if conn.is_iot():
|
|
388
|
-
if status.iot_clients_total is None:
|
|
389
|
-
status.iot_clients_total = 0
|
|
390
|
-
status.iot_clients_total += 1
|
|
391
|
-
|
|
392
|
-
devices[item['mac']].down_speed = item.get('downloadSpeed')
|
|
393
|
-
devices[item['mac']].up_speed = item.get('uploadSpeed')
|
|
394
|
-
|
|
395
|
-
for item in self.request('admin/wireless?form=statistics', 'operation=load'):
|
|
396
|
-
if item['mac'] not in devices:
|
|
397
|
-
status.wifi_clients_total += 1
|
|
398
|
-
type = self._map_wire_type(item.get('type'))
|
|
399
|
-
devices[item['mac']] = Device(type, EUI48(item['mac']), IPv4Address('0.0.0.0'),
|
|
400
|
-
'')
|
|
401
|
-
devices[item['mac']].packets_sent = item.get('txpkts')
|
|
402
|
-
devices[item['mac']].packets_received = item.get('rxpkts')
|
|
403
|
-
|
|
404
|
-
status.devices = list(devices.values())
|
|
405
|
-
status.clients_total = status.wired_total + status.wifi_clients_total + status.guest_clients_total
|
|
406
|
-
|
|
407
|
-
return status
|
|
408
|
-
|
|
409
|
-
def get_ipv4_status(self) -> IPv4Status:
|
|
410
|
-
ipv4_status = IPv4Status()
|
|
411
|
-
data = self.request('admin/network?form=status_ipv4&operation=read', 'operation=read')
|
|
412
|
-
ipv4_status._wan_macaddr = EUI48(data['wan_macaddr'])
|
|
413
|
-
ipv4_status._wan_ipv4_ipaddr = IPv4Address(data['wan_ipv4_ipaddr'])
|
|
414
|
-
ipv4_status._wan_ipv4_gateway = IPv4Address(data['wan_ipv4_gateway'])
|
|
415
|
-
ipv4_status.wan_ipv4_conntype = data['wan_ipv4_conntype']
|
|
416
|
-
ipv4_status._wan_ipv4_netmask = IPv4Address(data['wan_ipv4_netmask'])
|
|
417
|
-
ipv4_status._wan_ipv4_pridns = IPv4Address(data['wan_ipv4_pridns'])
|
|
418
|
-
ipv4_status._wan_ipv4_snddns = IPv4Address(data['wan_ipv4_snddns'])
|
|
419
|
-
ipv4_status._lan_macaddr = EUI48(data['lan_macaddr'])
|
|
420
|
-
ipv4_status._lan_ipv4_ipaddr = IPv4Address(data['lan_ipv4_ipaddr'])
|
|
421
|
-
ipv4_status.lan_ipv4_dhcp_enable = self._str2bool(data['lan_ipv4_dhcp_enable'])
|
|
422
|
-
ipv4_status._lan_ipv4_netmask = IPv4Address(data['lan_ipv4_netmask'])
|
|
423
|
-
ipv4_status.remote = self._str2bool(data.get('remote'))
|
|
424
|
-
|
|
425
|
-
return ipv4_status
|
|
426
|
-
|
|
427
|
-
def get_ipv4_reservations(self) -> [IPv4Reservation]:
|
|
428
|
-
ipv4_reservations = []
|
|
429
|
-
data = self.request(self._url_ipv4_reservations, 'operation=load')
|
|
430
|
-
|
|
431
|
-
for item in data:
|
|
432
|
-
ipv4_reservations.append(
|
|
433
|
-
IPv4Reservation(EUI48(item['mac']), IPv4Address(item['ip']), item['comment'],
|
|
434
|
-
self._str2bool(item['enable'])))
|
|
435
|
-
|
|
436
|
-
return ipv4_reservations
|
|
437
|
-
|
|
438
|
-
def get_ipv4_dhcp_leases(self) -> [IPv4DHCPLease]:
|
|
439
|
-
dhcp_leases = []
|
|
440
|
-
data = self.request(self._url_ipv4_dhcp_leases, 'operation=load')
|
|
441
|
-
|
|
442
|
-
for item in data:
|
|
443
|
-
dhcp_leases.append(
|
|
444
|
-
IPv4DHCPLease(EUI48(item['macaddr']), IPv4Address(item['ipaddr']), item['name'],
|
|
445
|
-
item['leasetime']))
|
|
446
|
-
|
|
447
|
-
return dhcp_leases
|
|
448
|
-
|
|
449
|
-
@staticmethod
|
|
450
|
-
def _str2bool(v) -> bool | None:
|
|
451
|
-
return str(v).lower() in ("yes", "true", "on") if v is not None else None
|
|
452
|
-
|
|
453
|
-
@staticmethod
|
|
454
|
-
def _map_wire_type(data: str | None, host: bool = True) -> Connection:
|
|
455
|
-
result = Connection.UNKNOWN
|
|
456
|
-
if data is None:
|
|
457
|
-
return result
|
|
458
|
-
if data == 'wired':
|
|
459
|
-
result = Connection.WIRED
|
|
460
|
-
if data.startswith('2.4'):
|
|
461
|
-
result = Connection.HOST_2G if host else Connection.GUEST_2G
|
|
462
|
-
elif data.startswith('5'):
|
|
463
|
-
result = Connection.HOST_5G if host else Connection.GUEST_5G
|
|
464
|
-
elif data.startswith('6'):
|
|
465
|
-
result = Connection.HOST_6G if host else Connection.GUEST_6G
|
|
466
|
-
elif data.startswith('iot_2'):
|
|
467
|
-
result = Connection.IOT_2G
|
|
468
|
-
elif data.startswith('iot_5'):
|
|
469
|
-
result = Connection.IOT_5G
|
|
470
|
-
elif data.startswith('iot_6'):
|
|
471
|
-
result = Connection.IOT_6G
|
|
472
|
-
return result
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
class TplinkRouter(TplinkEncryption, TplinkBaseRouter):
|
|
476
|
-
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
477
|
-
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
478
|
-
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
479
|
-
|
|
480
|
-
self._url_firmware = 'admin/firmware?form=upgrade'
|
|
481
|
-
self._url_ipv4_reservations = 'admin/dhcps?form=reservation'
|
|
482
|
-
self._url_ipv4_dhcp_leases = 'admin/dhcps?form=client'
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
class TPLinkDecoClient(TplinkEncryption, AbstractRouter):
|
|
486
|
-
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
487
|
-
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
488
|
-
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
489
|
-
|
|
490
|
-
self._headers_request = {'Content-Type': 'application/json'}
|
|
491
|
-
self._headers_login = {'Content-Type': 'application/json'}
|
|
492
|
-
self._data_block = 'result'
|
|
493
|
-
self.devices = []
|
|
494
|
-
|
|
495
|
-
def logout(self) -> None:
|
|
496
|
-
self.request('admin/system?form=logout', dumps({'operation': 'logout'}), True)
|
|
497
|
-
self._stok = ''
|
|
498
|
-
self._sysauth = ''
|
|
499
|
-
self._logged = False
|
|
500
|
-
|
|
501
|
-
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
502
|
-
en = {'enable': enable}
|
|
503
|
-
if Connection.HOST_2G == wifi:
|
|
504
|
-
params = {'band2_4': {'host': en}}
|
|
505
|
-
elif Connection.HOST_5G == wifi:
|
|
506
|
-
params = {'band5_1': {'host': en}}
|
|
507
|
-
elif Connection.GUEST_5G == wifi:
|
|
508
|
-
params = {'band5_1': {'guest': en}}
|
|
509
|
-
elif Connection.HOST_6G == wifi:
|
|
510
|
-
params = {'band6': {'host': en}}
|
|
511
|
-
elif Connection.GUEST_6G == wifi:
|
|
512
|
-
params = {'band6': {'guest': en}}
|
|
513
|
-
else:
|
|
514
|
-
params = {'band2_4': {'guest': en}}
|
|
515
|
-
|
|
516
|
-
self.request('admin/wireless?form=wlan', dumps({'operation': 'write', 'params': params}))
|
|
517
|
-
|
|
518
|
-
def reboot(self) -> None:
|
|
519
|
-
if not self.devices:
|
|
520
|
-
self.get_firmware()
|
|
521
|
-
self.request('admin/device?form=system', dumps({
|
|
522
|
-
'operation': 'reboot',
|
|
523
|
-
'params': {'mac_list': [{"mac": item['mac']} for item in self.devices]}}))
|
|
524
|
-
|
|
525
|
-
def get_firmware(self) -> Firmware:
|
|
526
|
-
self.devices = self.request('admin/device?form=device_list', dumps({"operation": "read"})).get(
|
|
527
|
-
'device_list', [])
|
|
528
|
-
|
|
529
|
-
for item in self.devices:
|
|
530
|
-
if item.get('role') != 'master' and len(self.devices) != 1:
|
|
531
|
-
continue
|
|
532
|
-
firmware = Firmware(item.get('hardware_ver', ''),
|
|
533
|
-
item.get('device_model', ''),
|
|
534
|
-
item.get('software_ver', ''))
|
|
535
|
-
|
|
536
|
-
return firmware
|
|
537
|
-
|
|
538
|
-
def get_status(self) -> Status:
|
|
539
|
-
data = self.request('admin/network?form=wan_ipv4', dumps({'operation': 'read'}))
|
|
540
|
-
|
|
541
|
-
status = Status()
|
|
542
|
-
element = self._get_value(data, ['wan', 'ip_info', 'mac'])
|
|
543
|
-
status._wan_macaddr = EUI48(element) if element else None
|
|
544
|
-
status._lan_macaddr = EUI48(self._get_value(data, ['lan', 'ip_info', 'mac']))
|
|
545
|
-
element = self._get_value(data, ['wan', 'ip_info', 'ip'])
|
|
546
|
-
status._wan_ipv4_addr = IPv4Address(element) if element else None
|
|
547
|
-
element = self._get_value(data, ['lan', 'ip_info', 'ip'])
|
|
548
|
-
status._lan_ipv4_addr = IPv4Address(element) if element else None
|
|
549
|
-
element = self._get_value(data, ['wan', 'ip_info', 'gateway'])
|
|
550
|
-
status._wan_ipv4_gateway = IPv4Address(element) if element else None
|
|
551
|
-
|
|
552
|
-
data = self.request('admin/network?form=performance', dumps({"operation": "read"}))
|
|
553
|
-
status.mem_usage = data.get('mem_usage')
|
|
554
|
-
status.cpu_usage = data.get('cpu_usage')
|
|
555
|
-
|
|
556
|
-
data = self.request('admin/wireless?form=wlan', dumps({'operation': 'read'}))
|
|
557
|
-
status.wifi_2g_enable = self._get_value(data, ['band2_4', 'host', 'enable'])
|
|
558
|
-
status.guest_2g_enable = self._get_value(data, ['band2_4', 'guest', 'enable'])
|
|
559
|
-
status.wifi_5g_enable = self._get_value(data, ['band5_1', 'host', 'enable'])
|
|
560
|
-
status.guest_5g_enable = self._get_value(data, ['band5_1', 'guest', 'enable'])
|
|
561
|
-
status.wifi_6g_enable = self._get_value(data, ['band6', 'host', 'enable'])
|
|
562
|
-
status.guest_6g_enable = self._get_value(data, ['band6', 'guest', 'enable'])
|
|
563
|
-
|
|
564
|
-
devices = []
|
|
565
|
-
data = self.request('admin/client?form=client_list', dumps(
|
|
566
|
-
{"operation": "read", "params": {"device_mac": "default"}})).get('client_list', [])
|
|
567
|
-
|
|
568
|
-
for item in data:
|
|
569
|
-
if not item.get('online'):
|
|
570
|
-
continue
|
|
571
|
-
conn = self._map_wire_type(item)
|
|
572
|
-
if conn == Connection.WIRED:
|
|
573
|
-
status.wired_total += 1
|
|
574
|
-
elif conn.is_host_wifi():
|
|
575
|
-
status.wifi_clients_total += 1
|
|
576
|
-
elif conn.is_guest_wifi():
|
|
577
|
-
status.guest_clients_total += 1
|
|
578
|
-
elif conn.is_iot():
|
|
579
|
-
if status.iot_clients_total is None:
|
|
580
|
-
status.iot_clients_total = 0
|
|
581
|
-
status.iot_clients_total += 1
|
|
582
|
-
|
|
583
|
-
device = Device(conn,
|
|
584
|
-
get_mac(item.get('mac', '00:00:00:00:00:00')),
|
|
585
|
-
get_ip(item.get('ip', '0.0.0.0')),
|
|
586
|
-
b64decode(item['name']).decode())
|
|
587
|
-
device.down_speed = item.get('down_speed')
|
|
588
|
-
device.up_speed = item.get('up_speed')
|
|
589
|
-
devices.append(device)
|
|
590
|
-
|
|
591
|
-
status.clients_total = (status.wired_total + status.wifi_clients_total + status.guest_clients_total
|
|
592
|
-
+ (0 if status.iot_clients_total is None else status.iot_clients_total))
|
|
593
|
-
status.devices = devices
|
|
594
|
-
|
|
595
|
-
return status
|
|
596
|
-
|
|
597
|
-
def get_ipv4_status(self) -> IPv4Status:
|
|
598
|
-
ipv4_status = IPv4Status()
|
|
599
|
-
data = self.request('admin/network?form=wan_ipv4', dumps({'operation': 'read'}))
|
|
600
|
-
ipv4_status._wan_macaddr = EUI48(self._get_value(data, ['wan', 'ip_info', 'mac']))
|
|
601
|
-
element = self._get_value(data, ['wan', 'ip_info', 'ip'])
|
|
602
|
-
ipv4_status._wan_ipv4_ipaddr = IPv4Address(element) if element else None
|
|
603
|
-
element = self._get_value(data, ['wan', 'ip_info', 'gateway'])
|
|
604
|
-
ipv4_status._wan_ipv4_gateway = IPv4Address(element) if element else None
|
|
605
|
-
ipv4_status.wan_ipv4_conntype = self._get_value(data, ['wan', 'dial_type'])
|
|
606
|
-
element = self._get_value(data, ['wan', 'ip_info', 'mask'])
|
|
607
|
-
ipv4_status._wan_ipv4_netmask = IPv4Address(element) if element else None
|
|
608
|
-
ipv4_status._wan_ipv4_pridns = IPv4Address(self._get_value(data, ['wan', 'ip_info', 'dns1']))
|
|
609
|
-
ipv4_status._wan_ipv4_snddns = IPv4Address(self._get_value(data, ['wan', 'ip_info', 'dns2']))
|
|
610
|
-
ipv4_status._lan_macaddr = EUI48(self._get_value(data, ['lan', 'ip_info', 'mac']))
|
|
611
|
-
ipv4_status._lan_ipv4_ipaddr = IPv4Address(self._get_value(data, ['lan', 'ip_info', 'ip']))
|
|
612
|
-
ipv4_status.lan_ipv4_dhcp_enable = False
|
|
613
|
-
ipv4_status._lan_ipv4_netmask = IPv4Address(self._get_value(data, ['lan', 'ip_info', 'mask']))
|
|
614
|
-
|
|
615
|
-
return ipv4_status
|
|
616
|
-
|
|
617
|
-
@staticmethod
|
|
618
|
-
def _get_value(dictionary: dict, keys: list):
|
|
619
|
-
nested_dict = dictionary
|
|
620
|
-
|
|
621
|
-
for key in keys:
|
|
622
|
-
try:
|
|
623
|
-
nested_dict = nested_dict[key]
|
|
624
|
-
except Exception:
|
|
625
|
-
return None
|
|
626
|
-
return nested_dict
|
|
627
|
-
|
|
628
|
-
def _map_wire_type(self, data: dict) -> Connection:
|
|
629
|
-
if data.get('wire_type') == 'wired':
|
|
630
|
-
return Connection.WIRED
|
|
631
|
-
mapping = {'band2_4': {'main': Connection.HOST_2G, 'guest': Connection.GUEST_2G, 'iot': Connection.IOT_2G},
|
|
632
|
-
'band5': {'main': Connection.HOST_5G, 'guest': Connection.GUEST_5G, 'iot': Connection.IOT_5G},
|
|
633
|
-
'band6': {'main': Connection.HOST_6G, 'guest': Connection.GUEST_6G, 'iot': Connection.IOT_6G}
|
|
634
|
-
}
|
|
635
|
-
result = self._get_value(mapping, [data.get('connection_type'), data.get('interface')])
|
|
636
|
-
|
|
637
|
-
return result if result else Connection.UNKNOWN
|
|
638
|
-
|
|
639
|
-
@staticmethod
|
|
640
|
-
def _get_login_data(crypted_pwd: str) -> str:
|
|
641
|
-
data = {
|
|
642
|
-
"params": {"password": crypted_pwd},
|
|
643
|
-
"operation": "login",
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
return dumps(data)
|
|
647
|
-
|
|
648
|
-
def _is_valid_response(self, data: dict) -> bool:
|
|
649
|
-
return 'error_code' in data and data['error_code'] == 0
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
class TplinkC6V4Router(AbstractRouter):
|
|
653
|
-
def supports(self) -> bool:
|
|
654
|
-
url = '{}/?code=2&asyn=1'.format(self.host)
|
|
655
|
-
try:
|
|
656
|
-
response = post(url, timeout=self.timeout, verify=self._verify_ssl)
|
|
657
|
-
except BaseException:
|
|
658
|
-
return False
|
|
659
|
-
if response.status_code == 401 and response.text.startswith('00'):
|
|
660
|
-
raise ClientException(('Your router is not supported. Please add your router support to '
|
|
661
|
-
'https://github.com/AlexandrErohin/TP-Link-Archer-C6U '
|
|
662
|
-
'by implementing methods for TplinkC6V4Router class'
|
|
663
|
-
))
|
|
664
|
-
return False
|
|
665
|
-
|
|
666
|
-
def authorize(self) -> None:
|
|
667
|
-
raise ClientException('Not Implemented')
|
|
668
|
-
|
|
669
|
-
def logout(self) -> None:
|
|
670
|
-
raise ClientException('Not Implemented')
|
|
671
|
-
|
|
672
|
-
def get_firmware(self) -> Firmware:
|
|
673
|
-
raise ClientException('Not Implemented')
|
|
674
|
-
|
|
675
|
-
def get_status(self) -> Status:
|
|
676
|
-
raise ClientException('Not Implemented')
|
|
677
|
-
|
|
678
|
-
def reboot(self) -> None:
|
|
679
|
-
raise ClientException('Not Implemented')
|
|
680
|
-
|
|
681
|
-
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
682
|
-
raise ClientException('Not Implemented')
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
class TplinkC5400XRouter(TplinkBaseRouter):
|
|
686
|
-
def supports(self) -> bool:
|
|
687
|
-
return len(self.password) >= 200
|
|
688
|
-
|
|
689
|
-
def authorize(self) -> None:
|
|
690
|
-
if len(self.password) < 200:
|
|
691
|
-
raise Exception('You need to use web encrypted password instead. Check the documentation!')
|
|
692
|
-
|
|
693
|
-
url = '{}/cgi-bin/luci/;stok=/login?form=login'.format(self.host)
|
|
694
|
-
|
|
695
|
-
response = post(
|
|
696
|
-
url,
|
|
697
|
-
params={'operation': 'login', 'username': self.username, 'password': self.password},
|
|
698
|
-
headers=self._headers_login,
|
|
699
|
-
timeout=self.timeout,
|
|
700
|
-
verify=self._verify_ssl,
|
|
701
|
-
)
|
|
702
|
-
|
|
703
|
-
try:
|
|
704
|
-
self._stok = response.json().get('data').get('stok')
|
|
705
|
-
regex_result = search('sysauth=(.*);', response.headers['set-cookie'])
|
|
706
|
-
self._sysauth = regex_result.group(1)
|
|
707
|
-
self._logged = True
|
|
708
|
-
self._smart_network = False
|
|
709
|
-
|
|
710
|
-
except Exception as e:
|
|
711
|
-
error = "TplinkRouter - C5400X - Cannot authorize! Error - {}; Response - {}".format(e, response.text)
|
|
712
|
-
if self._logger:
|
|
713
|
-
self._logger.debug(error)
|
|
714
|
-
raise ClientException(error)
|
|
715
|
-
|
|
716
|
-
def set_led(self, enable: bool) -> None:
|
|
717
|
-
current_state = (self.request('admin/ledgeneral?form=setting&operation=read', 'operation=read')
|
|
718
|
-
.get('enable', 'off') == 'on')
|
|
719
|
-
if current_state != enable:
|
|
720
|
-
self.request('admin/ledgeneral?form=setting&operation=write', 'operation=write')
|
|
721
|
-
|
|
722
|
-
def get_led(self) -> bool:
|
|
723
|
-
|
|
724
|
-
data = self.request('admin/ledgeneral?form=setting&operation=read', 'operation=read')
|
|
725
|
-
led_status = data.get('enable') if 'enable' in data else None
|
|
726
|
-
if led_status == 'on':
|
|
727
|
-
return True
|
|
728
|
-
elif led_status == 'off':
|
|
729
|
-
return False
|
|
730
|
-
else:
|
|
731
|
-
return None
|
|
732
|
-
|
|
733
|
-
def set_wifi(self, wifi: Connection, enable: bool = None, ssid: str = None, hidden: str = None,
|
|
734
|
-
encryption: str = None, psk_version: str = None, psk_cipher: str = None, psk_key: str = None,
|
|
735
|
-
hwmode: str = None, htmode: str = None, channel: int = None, txpower: str = None,
|
|
736
|
-
disabled_all: str = None) -> None:
|
|
737
|
-
values = {
|
|
738
|
-
Connection.HOST_2G: 'wireless_2g',
|
|
739
|
-
Connection.HOST_5G: 'wireless_5g',
|
|
740
|
-
Connection.HOST_6G: 'wireless_6g',
|
|
741
|
-
Connection.GUEST_2G: 'guest_2g',
|
|
742
|
-
Connection.GUEST_5G: 'guest_5g',
|
|
743
|
-
Connection.GUEST_6G: 'guest_6g',
|
|
744
|
-
Connection.IOT_2G: 'iot_2g',
|
|
745
|
-
Connection.IOT_5G: 'iot_5g',
|
|
746
|
-
Connection.IOT_6G: 'iot_6g',
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
value = values.get(wifi)
|
|
750
|
-
if not value:
|
|
751
|
-
raise ValueError(f"Invalid Wi-Fi connection type: {wifi}")
|
|
752
|
-
|
|
753
|
-
if all(v is None for v in [enable, ssid, hidden, encryption, psk_version, psk_cipher, psk_key, hwmode,
|
|
754
|
-
htmode, channel, txpower, disabled_all]):
|
|
755
|
-
raise ValueError("At least one wireless setting must be provided")
|
|
756
|
-
|
|
757
|
-
data = "operation=write"
|
|
758
|
-
|
|
759
|
-
if enable is not None:
|
|
760
|
-
data += f"&enable={'on' if enable else 'off'}"
|
|
761
|
-
if ssid is not None:
|
|
762
|
-
data += f"&ssid={ssid}"
|
|
763
|
-
if hidden is not None:
|
|
764
|
-
data += f"&hidden={hidden}"
|
|
765
|
-
if encryption is not None:
|
|
766
|
-
data += f"&encryption={encryption}"
|
|
767
|
-
if psk_version is not None:
|
|
768
|
-
data += f"&psk_version={psk_version}"
|
|
769
|
-
if psk_cipher is not None:
|
|
770
|
-
data += f"&psk_cipher={psk_cipher}"
|
|
771
|
-
if psk_key is not None:
|
|
772
|
-
data += f"&psk_key={psk_key}"
|
|
773
|
-
if hwmode is not None:
|
|
774
|
-
data += f"&hwmode={hwmode}"
|
|
775
|
-
if htmode is not None:
|
|
776
|
-
data += f"&htmode={htmode}"
|
|
777
|
-
if channel is not None:
|
|
778
|
-
data += f"&channel={channel}"
|
|
779
|
-
if txpower is not None:
|
|
780
|
-
data += f"&txpower={txpower}"
|
|
781
|
-
if disabled_all is not None:
|
|
782
|
-
data += f"&disabled_all={disabled_all}"
|
|
783
|
-
|
|
784
|
-
path = f"admin/wireless?form={value}&{data}"
|
|
785
|
-
|
|
786
|
-
self.request(path, data)
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
class TplinkC1200Router(TplinkC5400XRouter):
|
|
790
|
-
username = ''
|
|
791
|
-
password = ''
|
|
792
|
-
_pwdNN = ''
|
|
793
|
-
_pwdEE = ''
|
|
794
|
-
_encryption = EncryptionWrapper()
|
|
795
|
-
|
|
796
|
-
def supports(self) -> bool:
|
|
797
|
-
if len(self.password) > 125:
|
|
798
|
-
return False
|
|
799
|
-
|
|
800
|
-
try:
|
|
801
|
-
self._request_pwd()
|
|
802
|
-
return True
|
|
803
|
-
except ClientException:
|
|
804
|
-
return False
|
|
805
|
-
|
|
806
|
-
def authorize(self) -> None:
|
|
807
|
-
if self._pwdNN == '':
|
|
808
|
-
self._request_pwd()
|
|
809
|
-
|
|
810
|
-
response = self._try_login()
|
|
811
|
-
|
|
812
|
-
is_valid_json = False
|
|
813
|
-
try:
|
|
814
|
-
response.json()
|
|
815
|
-
is_valid_json = True
|
|
816
|
-
except BaseException:
|
|
817
|
-
"""Ignore"""
|
|
818
|
-
|
|
819
|
-
if is_valid_json is False or response.status_code == 403:
|
|
820
|
-
self._logged = False
|
|
821
|
-
self._request_pwd()
|
|
822
|
-
response = self._try_login()
|
|
823
|
-
|
|
824
|
-
data = response.text
|
|
825
|
-
try:
|
|
826
|
-
data = response.json()
|
|
827
|
-
data = self._decrypt_response(data)
|
|
828
|
-
|
|
829
|
-
self._stok = data[self._data_block]['stok']
|
|
830
|
-
regex_result = search(
|
|
831
|
-
'sysauth=(.*);', response.headers['set-cookie'])
|
|
832
|
-
self._sysauth = regex_result.group(1)
|
|
833
|
-
self._logged = True
|
|
834
|
-
|
|
835
|
-
except Exception as e:
|
|
836
|
-
error = ("TplinkRouter - C1200 - Cannot authorize! Error - {}; Response - {}".format(e, data))
|
|
837
|
-
if self._logger:
|
|
838
|
-
self._logger.debug(error)
|
|
839
|
-
if 'data' in vars() and data.get('errorcode') == 'login failed':
|
|
840
|
-
raise AuthorizeError(error)
|
|
841
|
-
raise ClientException(error)
|
|
842
|
-
|
|
843
|
-
def _request_pwd(self) -> None:
|
|
844
|
-
url = '{}/cgi-bin/luci/;stok=/login?form=login'.format(self.host)
|
|
845
|
-
response = post(
|
|
846
|
-
url, params={'operation': 'read'},
|
|
847
|
-
timeout=self.timeout,
|
|
848
|
-
verify=self._verify_ssl,
|
|
849
|
-
)
|
|
850
|
-
|
|
851
|
-
try:
|
|
852
|
-
data = response.json()
|
|
853
|
-
|
|
854
|
-
args = data[self._data_block]['password']
|
|
855
|
-
|
|
856
|
-
self._pwdNN = args[0]
|
|
857
|
-
self._pwdEE = args[1]
|
|
858
|
-
|
|
859
|
-
except Exception as e:
|
|
860
|
-
error = ('TplinkRouter - C1200 - {} - Unknown error for pwd! Error - {}; Response - {}'
|
|
861
|
-
.format(self.__class__.__name__, e, response.text))
|
|
862
|
-
if self._logger:
|
|
863
|
-
self._logger.debug(error)
|
|
864
|
-
raise ClientException(error)
|
|
865
|
-
|
|
866
|
-
def _try_login(self) -> Response:
|
|
867
|
-
url = '{}/cgi-bin/luci/;stok=/login?form=login'.format(self.host)
|
|
868
|
-
|
|
869
|
-
crypted_pwd = self._encryption.encrypt_password_C1200(self.password, self._pwdNN, self._pwdEE)
|
|
870
|
-
|
|
871
|
-
body = self._get_login_data(crypted_pwd)
|
|
872
|
-
|
|
873
|
-
return post(
|
|
874
|
-
url,
|
|
875
|
-
data=body,
|
|
876
|
-
headers=self._headers_login,
|
|
877
|
-
timeout=self.timeout,
|
|
878
|
-
verify=self._verify_ssl,
|
|
879
|
-
)
|
|
880
|
-
|
|
881
|
-
@staticmethod
|
|
882
|
-
def _get_login_data(crypted_pwd: str) -> str:
|
|
883
|
-
return 'operation=login&password={}'.format(crypted_pwd)
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
class TPLinkMRClient(AbstractRouter):
|
|
887
|
-
REQUEST_RETRIES = 3
|
|
888
|
-
|
|
889
|
-
HEADERS = {
|
|
890
|
-
'Accept': '*/*',
|
|
891
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
|
|
892
|
-
'Referer': 'http://192.168.1.1/' # updated on the fly
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
HTTP_RET_OK = 0
|
|
896
|
-
HTTP_ERR_CGI_INVALID_ANSI = 71017
|
|
897
|
-
HTTP_ERR_USER_PWD_NOT_CORRECT = 71233
|
|
898
|
-
HTTP_ERR_USER_BAD_REQUEST = 71234
|
|
899
|
-
|
|
900
|
-
CLIENT_TYPES = {
|
|
901
|
-
0: Connection.WIRED,
|
|
902
|
-
1: Connection.HOST_2G,
|
|
903
|
-
3: Connection.HOST_5G,
|
|
904
|
-
2: Connection.GUEST_2G,
|
|
905
|
-
4: Connection.GUEST_5G,
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
WIFI_SET = {
|
|
909
|
-
Connection.HOST_2G: '1,1,0,0,0,0',
|
|
910
|
-
Connection.HOST_5G: '1,2,0,0,0,0',
|
|
911
|
-
Connection.GUEST_2G: '1,1,1,0,0,0',
|
|
912
|
-
Connection.GUEST_5G: '1,2,1,0,0,0',
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
class ActItem:
|
|
916
|
-
GET = 1
|
|
917
|
-
SET = 2
|
|
918
|
-
ADD = 3
|
|
919
|
-
DEL = 4
|
|
920
|
-
GL = 5
|
|
921
|
-
GS = 6
|
|
922
|
-
OP = 7
|
|
923
|
-
CGI = 8
|
|
924
|
-
|
|
925
|
-
def __init__(self, type: int, oid: str, stack: str = '0,0,0,0,0,0', pstack: str = '0,0,0,0,0,0',
|
|
926
|
-
attrs: list = []):
|
|
927
|
-
self.type = type
|
|
928
|
-
self.oid = oid
|
|
929
|
-
self.stack = stack
|
|
930
|
-
self.pstack = pstack
|
|
931
|
-
self.attrs = attrs
|
|
932
|
-
|
|
933
|
-
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
934
|
-
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
935
|
-
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
936
|
-
|
|
937
|
-
self.req = Session()
|
|
938
|
-
self._token = None
|
|
939
|
-
self._hash = md5((self.username + self.password).encode()).hexdigest()
|
|
940
|
-
self._nn = None
|
|
941
|
-
self._ee = None
|
|
942
|
-
self._seq = None
|
|
943
|
-
|
|
944
|
-
self._encryption = EncryptionWrapperMR()
|
|
945
|
-
|
|
946
|
-
def supports(self) -> bool:
|
|
947
|
-
try:
|
|
948
|
-
self._req_rsa_key()
|
|
949
|
-
return True
|
|
950
|
-
except AssertionError:
|
|
951
|
-
return False
|
|
952
|
-
|
|
953
|
-
def authorize(self) -> None:
|
|
954
|
-
'''
|
|
955
|
-
Establishes a login session to the host using provided credentials
|
|
956
|
-
'''
|
|
957
|
-
# hash the password
|
|
958
|
-
|
|
959
|
-
# request the RSA public key from the host
|
|
960
|
-
self._nn, self._ee, self._seq = self._req_rsa_key()
|
|
961
|
-
|
|
962
|
-
# authenticate
|
|
963
|
-
self._req_login()
|
|
964
|
-
|
|
965
|
-
# request TokenID
|
|
966
|
-
self._token = self._req_token()
|
|
967
|
-
|
|
968
|
-
def logout(self) -> None:
|
|
969
|
-
'''
|
|
970
|
-
Logs out from the host
|
|
971
|
-
'''
|
|
972
|
-
if self._token is None:
|
|
973
|
-
return
|
|
974
|
-
|
|
975
|
-
acts = [
|
|
976
|
-
# 8\r\n[/cgi/logout#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n
|
|
977
|
-
self.ActItem(self.ActItem.CGI, '/cgi/logout')
|
|
978
|
-
]
|
|
979
|
-
|
|
980
|
-
response, _ = self.req_act(acts)
|
|
981
|
-
ret_code = self._parse_ret_val(response)
|
|
982
|
-
|
|
983
|
-
if ret_code == self.HTTP_RET_OK:
|
|
984
|
-
self._token = None
|
|
985
|
-
|
|
986
|
-
def get_firmware(self) -> Firmware:
|
|
987
|
-
acts = [
|
|
988
|
-
self.ActItem(self.ActItem.GET, 'IGD_DEV_INFO', attrs=[
|
|
989
|
-
'hardwareVersion',
|
|
990
|
-
'modelName',
|
|
991
|
-
'softwareVersion'
|
|
992
|
-
])
|
|
993
|
-
]
|
|
994
|
-
_, values = self.req_act(acts)
|
|
995
|
-
|
|
996
|
-
firmware = Firmware(values.get('hardwareVersion', ''), values.get('modelName', ''),
|
|
997
|
-
values.get('softwareVersion', ''))
|
|
998
|
-
|
|
999
|
-
return firmware
|
|
1000
|
-
|
|
1001
|
-
def get_status(self) -> Status:
|
|
1002
|
-
status = Status()
|
|
1003
|
-
acts = [
|
|
1004
|
-
self.ActItem(self.ActItem.GS, 'LAN_IP_INTF', attrs=['X_TP_MACAddress', 'IPInterfaceIPAddress']),
|
|
1005
|
-
self.ActItem(self.ActItem.GS, 'WAN_IP_CONN',
|
|
1006
|
-
attrs=['enable', 'MACAddress', 'externalIPAddress', 'defaultGateway']),
|
|
1007
|
-
self.ActItem(self.ActItem.GL, 'LAN_WLAN', attrs=['enable', 'X_TP_Band']),
|
|
1008
|
-
self.ActItem(self.ActItem.GL, 'LAN_WLAN_GUESTNET', attrs=['enable', 'name']),
|
|
1009
|
-
self.ActItem(self.ActItem.GL, 'LAN_HOST_ENTRY', attrs=[
|
|
1010
|
-
'IPAddress',
|
|
1011
|
-
'MACAddress',
|
|
1012
|
-
'hostName',
|
|
1013
|
-
'X_TP_ConnType',
|
|
1014
|
-
'active',
|
|
1015
|
-
]),
|
|
1016
|
-
self.ActItem(self.ActItem.GS, 'LAN_WLAN_ASSOC_DEV', attrs=[
|
|
1017
|
-
'associatedDeviceMACAddress',
|
|
1018
|
-
'X_TP_TotalPacketsSent',
|
|
1019
|
-
'X_TP_TotalPacketsReceived',
|
|
1020
|
-
]),
|
|
1021
|
-
]
|
|
1022
|
-
_, values = self.req_act(acts)
|
|
1023
|
-
|
|
1024
|
-
if values['0'].__class__ == list:
|
|
1025
|
-
values['0'] = values['0'][0]
|
|
1026
|
-
|
|
1027
|
-
status._lan_macaddr = EUI48(values['0']['X_TP_MACAddress'])
|
|
1028
|
-
status._lan_ipv4_addr = IPv4Address(values['0']['IPInterfaceIPAddress'])
|
|
1029
|
-
|
|
1030
|
-
for item in self._to_list(values.get('1')):
|
|
1031
|
-
if int(item['enable']) == 0 and values.get('1').__class__ == list:
|
|
1032
|
-
continue
|
|
1033
|
-
status._wan_macaddr = EUI48(item['MACAddress']) if item.get('MACAddress') else None
|
|
1034
|
-
status._wan_ipv4_addr = IPv4Address(item['externalIPAddress'])
|
|
1035
|
-
status._wan_ipv4_gateway = IPv4Address(item['defaultGateway'])
|
|
1036
|
-
|
|
1037
|
-
if values['2'].__class__ != list:
|
|
1038
|
-
status.wifi_2g_enable = bool(int(values['2']['enable']))
|
|
1039
|
-
else:
|
|
1040
|
-
status.wifi_2g_enable = bool(int(values['2'][0]['enable']))
|
|
1041
|
-
status.wifi_5g_enable = bool(int(values['2'][1]['enable']))
|
|
1042
|
-
|
|
1043
|
-
if values['3'].__class__ != list:
|
|
1044
|
-
status.guest_2g_enable = bool(int(values['3']['enable']))
|
|
1045
|
-
else:
|
|
1046
|
-
status.guest_2g_enable = bool(int(values['3'][0]['enable']))
|
|
1047
|
-
status.guest_5g_enable = bool(int(values['3'][1]['enable']))
|
|
1048
|
-
|
|
1049
|
-
devices = {}
|
|
1050
|
-
for val in self._to_list(values.get('4')):
|
|
1051
|
-
if int(val['active']) == 0:
|
|
1052
|
-
continue
|
|
1053
|
-
conn = self.CLIENT_TYPES.get(int(val['X_TP_ConnType']))
|
|
1054
|
-
if conn is None:
|
|
1055
|
-
continue
|
|
1056
|
-
elif conn == Connection.WIRED:
|
|
1057
|
-
status.wired_total += 1
|
|
1058
|
-
elif conn.is_guest_wifi():
|
|
1059
|
-
status.guest_clients_total += 1
|
|
1060
|
-
elif conn.is_host_wifi():
|
|
1061
|
-
status.wifi_clients_total += 1
|
|
1062
|
-
devices[val['MACAddress']] = Device(conn,
|
|
1063
|
-
EUI48(val['MACAddress']),
|
|
1064
|
-
IPv4Address(val['IPAddress']),
|
|
1065
|
-
val['hostName'])
|
|
1066
|
-
|
|
1067
|
-
for val in self._to_list(values.get('5')):
|
|
1068
|
-
if val['associatedDeviceMACAddress'] not in devices:
|
|
1069
|
-
status.wifi_clients_total += 1
|
|
1070
|
-
devices[val['associatedDeviceMACAddress']] = Device(
|
|
1071
|
-
Connection.HOST_2G,
|
|
1072
|
-
EUI48(val['associatedDeviceMACAddress']),
|
|
1073
|
-
IPv4Address('0.0.0.0'),
|
|
1074
|
-
'')
|
|
1075
|
-
devices[val['associatedDeviceMACAddress']].packets_sent = int(val['X_TP_TotalPacketsSent'])
|
|
1076
|
-
devices[val['associatedDeviceMACAddress']].packets_received = int(val['X_TP_TotalPacketsReceived'])
|
|
1077
|
-
|
|
1078
|
-
status.devices = list(devices.values())
|
|
1079
|
-
status.clients_total = status.wired_total + status.wifi_clients_total + status.guest_clients_total
|
|
1080
|
-
|
|
1081
|
-
return status
|
|
1082
|
-
|
|
1083
|
-
def get_ipv4_reservations(self) -> [IPv4Reservation]:
|
|
1084
|
-
acts = [
|
|
1085
|
-
self.ActItem(5, 'LAN_DHCP_STATIC_ADDR', attrs=['enable', 'chaddr', 'yiaddr']),
|
|
1086
|
-
]
|
|
1087
|
-
_, values = self.req_act(acts)
|
|
1088
|
-
|
|
1089
|
-
ipv4_reservations = []
|
|
1090
|
-
for item in self._to_list(values):
|
|
1091
|
-
ipv4_reservations.append(
|
|
1092
|
-
IPv4Reservation(
|
|
1093
|
-
EUI48(item['chaddr']),
|
|
1094
|
-
IPv4Address(item['yiaddr']),
|
|
1095
|
-
'',
|
|
1096
|
-
bool(int(item['enable']))
|
|
1097
|
-
))
|
|
1098
|
-
|
|
1099
|
-
return ipv4_reservations
|
|
1100
|
-
|
|
1101
|
-
def get_ipv4_dhcp_leases(self) -> [IPv4DHCPLease]:
|
|
1102
|
-
acts = [
|
|
1103
|
-
self.ActItem(5, 'LAN_HOST_ENTRY', attrs=['IPAddress', 'MACAddress', 'hostName', 'leaseTimeRemaining']),
|
|
1104
|
-
]
|
|
1105
|
-
_, values = self.req_act(acts)
|
|
1106
|
-
|
|
1107
|
-
dhcp_leases = []
|
|
1108
|
-
for item in self._to_list(values):
|
|
1109
|
-
lease_time = item['leaseTimeRemaining']
|
|
1110
|
-
dhcp_leases.append(
|
|
1111
|
-
IPv4DHCPLease(
|
|
1112
|
-
EUI48(item['MACAddress']),
|
|
1113
|
-
IPv4Address(item['IPAddress']),
|
|
1114
|
-
item['hostName'],
|
|
1115
|
-
str(timedelta(seconds=int(lease_time))) if lease_time.isdigit() else 'Permanent',
|
|
1116
|
-
))
|
|
1117
|
-
|
|
1118
|
-
return dhcp_leases
|
|
1119
|
-
|
|
1120
|
-
def get_ipv4_status(self) -> IPv4Status:
|
|
1121
|
-
acts = [
|
|
1122
|
-
self.ActItem(self.ActItem.GS, 'LAN_IP_INTF',
|
|
1123
|
-
attrs=['X_TP_MACAddress', 'IPInterfaceIPAddress', 'IPInterfaceSubnetMask']),
|
|
1124
|
-
self.ActItem(self.ActItem.GET, 'LAN_HOST_CFG', '1,0,0,0,0,0', attrs=['DHCPServerEnable']),
|
|
1125
|
-
self.ActItem(self.ActItem.GS, 'WAN_IP_CONN',
|
|
1126
|
-
attrs=['enable', 'MACAddress', 'externalIPAddress', 'defaultGateway', 'name', 'subnetMask',
|
|
1127
|
-
'DNSServers']),
|
|
1128
|
-
]
|
|
1129
|
-
_, values = self.req_act(acts)
|
|
1130
|
-
|
|
1131
|
-
ipv4_status = IPv4Status()
|
|
1132
|
-
ipv4_status._lan_macaddr = EUI48(values['0']['X_TP_MACAddress'])
|
|
1133
|
-
ipv4_status._lan_ipv4_ipaddr = IPv4Address(values['0']['IPInterfaceIPAddress'])
|
|
1134
|
-
ipv4_status._lan_ipv4_netmask = IPv4Address(values['0']['IPInterfaceSubnetMask'])
|
|
1135
|
-
ipv4_status.lan_ipv4_dhcp_enable = bool(int(values['1']['DHCPServerEnable']))
|
|
1136
|
-
|
|
1137
|
-
for item in self._to_list(values.get('2')):
|
|
1138
|
-
if int(item['enable']) == 0 and values.get('2').__class__ == list:
|
|
1139
|
-
continue
|
|
1140
|
-
ipv4_status._wan_macaddr = EUI48(item['MACAddress'])
|
|
1141
|
-
ipv4_status._wan_ipv4_ipaddr = IPv4Address(item['externalIPAddress'])
|
|
1142
|
-
ipv4_status._wan_ipv4_gateway = IPv4Address(item['defaultGateway'])
|
|
1143
|
-
ipv4_status.wan_ipv4_conntype = item['name']
|
|
1144
|
-
ipv4_status._wan_ipv4_netmask = IPv4Address(item['subnetMask'])
|
|
1145
|
-
dns = item['DNSServers'].split(',')
|
|
1146
|
-
ipv4_status._wan_ipv4_pridns = IPv4Address(dns[0])
|
|
1147
|
-
ipv4_status._wan_ipv4_snddns = IPv4Address(dns[1])
|
|
1148
|
-
|
|
1149
|
-
return ipv4_status
|
|
1150
|
-
|
|
1151
|
-
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
1152
|
-
acts = [
|
|
1153
|
-
self.ActItem(
|
|
1154
|
-
self.ActItem.SET,
|
|
1155
|
-
'LAN_WLAN' if wifi in [Connection.HOST_2G, Connection.HOST_5G] else 'LAN_WLAN_MSSIDENTRY',
|
|
1156
|
-
self.WIFI_SET[wifi],
|
|
1157
|
-
attrs=['enable={}'.format(int(enable))]),
|
|
1158
|
-
]
|
|
1159
|
-
self.req_act(acts)
|
|
1160
|
-
|
|
1161
|
-
def send_sms(self, phone_number: str, message: str) -> None:
|
|
1162
|
-
acts = [
|
|
1163
|
-
self.ActItem(
|
|
1164
|
-
self.ActItem.SET, 'LTE_SMS_SENDNEWMSG', attrs=[
|
|
1165
|
-
'index=1',
|
|
1166
|
-
'to={}'.format(phone_number),
|
|
1167
|
-
'textContent={}'.format(message),
|
|
1168
|
-
]),
|
|
1169
|
-
]
|
|
1170
|
-
self.req_act(acts)
|
|
1171
|
-
|
|
1172
|
-
def reboot(self) -> None:
|
|
1173
|
-
acts = [
|
|
1174
|
-
self.ActItem(self.ActItem.OP, 'ACT_REBOOT')
|
|
1175
|
-
]
|
|
1176
|
-
self.req_act(acts)
|
|
1177
|
-
|
|
1178
|
-
def req_act(self, acts: list):
|
|
1179
|
-
'''
|
|
1180
|
-
Requests ACTs via the cgi_gdpr proxy
|
|
1181
|
-
'''
|
|
1182
|
-
act_types = []
|
|
1183
|
-
act_data = []
|
|
1184
|
-
|
|
1185
|
-
for act in acts:
|
|
1186
|
-
act_types.append(str(act.type))
|
|
1187
|
-
act_data.append('[{}#{}#{}]{},{}\r\n{}\r\n'.format(
|
|
1188
|
-
act.oid,
|
|
1189
|
-
act.stack,
|
|
1190
|
-
act.pstack,
|
|
1191
|
-
len(act_types) - 1, # index, starts at 0
|
|
1192
|
-
len(act.attrs),
|
|
1193
|
-
'\r\n'.join(act.attrs)
|
|
1194
|
-
))
|
|
1195
|
-
|
|
1196
|
-
data = '&'.join(act_types) + '\r\n' + ''.join(act_data)
|
|
1197
|
-
|
|
1198
|
-
url = self._get_url('cgi_gdpr')
|
|
1199
|
-
(code, response) = self._request(url, data_str=data, encrypt=True)
|
|
1200
|
-
|
|
1201
|
-
if code != 200:
|
|
1202
|
-
error = 'TplinkRouter - MR - Response with error; Request {} - Response {}'.format(data, response)
|
|
1203
|
-
if self._logger:
|
|
1204
|
-
self._logger.debug(error)
|
|
1205
|
-
raise ClientError(error)
|
|
1206
|
-
|
|
1207
|
-
result = self._merge_response(response)
|
|
1208
|
-
|
|
1209
|
-
return response, result.get('0') if len(result) == 1 and result.get('0') else result
|
|
1210
|
-
|
|
1211
|
-
@staticmethod
|
|
1212
|
-
def _to_list(response: dict | list | None) -> list:
|
|
1213
|
-
if response is None:
|
|
1214
|
-
return []
|
|
1215
|
-
|
|
1216
|
-
return [response] if response.__class__ != list else response
|
|
1217
|
-
|
|
1218
|
-
@staticmethod
|
|
1219
|
-
def _merge_response(response: str) -> dict:
|
|
1220
|
-
result = {}
|
|
1221
|
-
obj = {}
|
|
1222
|
-
lines = response.split('\n')
|
|
1223
|
-
for line in lines:
|
|
1224
|
-
if line.startswith('['):
|
|
1225
|
-
regexp = search(r'\[\d,\d,\d,\d,\d,\d\](\d)', line)
|
|
1226
|
-
if regexp is not None:
|
|
1227
|
-
obj = {}
|
|
1228
|
-
index = regexp.group(1)
|
|
1229
|
-
item = result.get(index)
|
|
1230
|
-
if item is not None:
|
|
1231
|
-
if item.__class__ != list:
|
|
1232
|
-
result[index] = [item]
|
|
1233
|
-
result[index].append(obj)
|
|
1234
|
-
else:
|
|
1235
|
-
result[index] = obj
|
|
1236
|
-
continue
|
|
1237
|
-
if '=' in line:
|
|
1238
|
-
keyval = line.split('=')
|
|
1239
|
-
assert len(keyval) == 2
|
|
1240
|
-
|
|
1241
|
-
obj[keyval[0]] = keyval[1]
|
|
1242
|
-
|
|
1243
|
-
return result if result else []
|
|
1244
|
-
|
|
1245
|
-
def _get_url(self, endpoint: str, params: dict = {}, include_ts: bool = True) -> str:
|
|
1246
|
-
# add timestamp param
|
|
1247
|
-
if include_ts:
|
|
1248
|
-
params['_'] = str(round(time() * 1000))
|
|
1249
|
-
|
|
1250
|
-
# format params into a string
|
|
1251
|
-
params_arr = []
|
|
1252
|
-
for attr, value in params.items():
|
|
1253
|
-
params_arr.append('{}={}'.format(attr, value))
|
|
1254
|
-
|
|
1255
|
-
# format url
|
|
1256
|
-
return '{}/{}{}{}'.format(
|
|
1257
|
-
self.host,
|
|
1258
|
-
endpoint,
|
|
1259
|
-
'?' if len(params_arr) > 0 else '',
|
|
1260
|
-
'&'.join(params_arr)
|
|
1261
|
-
)
|
|
1262
|
-
|
|
1263
|
-
def _req_token(self):
|
|
1264
|
-
'''
|
|
1265
|
-
Requests the TokenID, used for CGI authentication (together with cookies)
|
|
1266
|
-
- token is inlined as JS var in the index (/) html page
|
|
1267
|
-
e.g.: <script type="text/javascript">var token="086724f57013f16e042e012becf825";</script>
|
|
1268
|
-
|
|
1269
|
-
Return value:
|
|
1270
|
-
TokenID string
|
|
1271
|
-
'''
|
|
1272
|
-
url = self._get_url('')
|
|
1273
|
-
(code, response) = self._request(url, method='GET')
|
|
1274
|
-
assert code == 200
|
|
1275
|
-
|
|
1276
|
-
result = search('var token="(.*)";', response)
|
|
1277
|
-
|
|
1278
|
-
assert result is not None
|
|
1279
|
-
assert result.group(1) != ''
|
|
1280
|
-
|
|
1281
|
-
return result.group(1)
|
|
1282
|
-
|
|
1283
|
-
def _req_rsa_key(self):
|
|
1284
|
-
'''
|
|
1285
|
-
Requests the RSA public key from the host
|
|
1286
|
-
|
|
1287
|
-
Return value:
|
|
1288
|
-
((n, e), seq) tuple
|
|
1289
|
-
'''
|
|
1290
|
-
url = self._get_url('cgi/getParm')
|
|
1291
|
-
(code, response) = self._request(url)
|
|
1292
|
-
assert code == 200
|
|
1293
|
-
|
|
1294
|
-
# assert return code
|
|
1295
|
-
assert self._parse_ret_val(response) == self.HTTP_RET_OK
|
|
1296
|
-
|
|
1297
|
-
# parse public key
|
|
1298
|
-
ee = search('var ee="(.*)";', response)
|
|
1299
|
-
nn = search('var nn="(.*)";', response)
|
|
1300
|
-
seq = search('var seq="(.*)";', response)
|
|
1301
|
-
|
|
1302
|
-
assert ee and nn and seq
|
|
1303
|
-
ee = ee.group(1)
|
|
1304
|
-
nn = nn.group(1)
|
|
1305
|
-
seq = seq.group(1)
|
|
1306
|
-
assert len(ee) == 6
|
|
1307
|
-
assert len(nn) == 128
|
|
1308
|
-
assert seq.isnumeric()
|
|
1309
|
-
|
|
1310
|
-
return nn, ee, int(seq)
|
|
1311
|
-
|
|
1312
|
-
def _req_login(self) -> None:
|
|
1313
|
-
'''
|
|
1314
|
-
Authenticates to the host
|
|
1315
|
-
- sets the session token after successful login
|
|
1316
|
-
- data/signature is passed as a GET parameter, NOT as a raw request data
|
|
1317
|
-
(unlike for regular encrypted requests to the /cgi_gdpr endpoint)
|
|
1318
|
-
|
|
1319
|
-
Example session token (set as a cookie):
|
|
1320
|
-
{'JSESSIONID': '4d786fede0164d7613411c7b6ec61e'}
|
|
1321
|
-
'''
|
|
1322
|
-
# encrypt username + password
|
|
1323
|
-
|
|
1324
|
-
sign, data = self._prepare_data(self.username + '\n' + self.password, True)
|
|
1325
|
-
assert len(sign) == 256
|
|
1326
|
-
|
|
1327
|
-
data = {
|
|
1328
|
-
'data': quote(data, safe='~()*!.\''),
|
|
1329
|
-
'sign': sign,
|
|
1330
|
-
'Action': 1,
|
|
1331
|
-
'LoginStatus': 0,
|
|
1332
|
-
'isMobile': 0
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
url = self._get_url('cgi/login', data)
|
|
1336
|
-
(code, response) = self._request(url)
|
|
1337
|
-
assert code == 200
|
|
1338
|
-
|
|
1339
|
-
# parse and match return code
|
|
1340
|
-
ret_code = self._parse_ret_val(response)
|
|
1341
|
-
error = ''
|
|
1342
|
-
if ret_code == self.HTTP_ERR_USER_PWD_NOT_CORRECT:
|
|
1343
|
-
info = search('var currAuthTimes=(.*);\nvar currForbidTime=(.*);', response)
|
|
1344
|
-
assert info is not None
|
|
1345
|
-
|
|
1346
|
-
error = 'TplinkRouter - MR - Login failed, wrong password. Auth times: {}/5, Forbid time: {}'.format(
|
|
1347
|
-
info.group(1), info.group(2))
|
|
1348
|
-
elif ret_code == self.HTTP_ERR_USER_BAD_REQUEST:
|
|
1349
|
-
error = 'TplinkRouter - MR - Login failed. Generic error code: {}'.format(ret_code)
|
|
1350
|
-
elif ret_code != self.HTTP_RET_OK:
|
|
1351
|
-
error = 'TplinkRouter - MR - Login failed. Unknown error code: {}'.format(ret_code)
|
|
1352
|
-
|
|
1353
|
-
if error:
|
|
1354
|
-
if self._logger:
|
|
1355
|
-
self._logger.debug(error)
|
|
1356
|
-
raise ClientException(error)
|
|
1357
|
-
|
|
1358
|
-
def _request(self, url, method='POST', data_str=None, encrypt=False):
|
|
1359
|
-
'''
|
|
1360
|
-
Prepares and sends an HTTP request to the host
|
|
1361
|
-
- sets up the headers, handles token auth
|
|
1362
|
-
- encrypts/decrypts the data, if needed
|
|
1363
|
-
|
|
1364
|
-
Return value:
|
|
1365
|
-
(status_code, response_text) tuple
|
|
1366
|
-
'''
|
|
1367
|
-
headers = self.HEADERS
|
|
1368
|
-
|
|
1369
|
-
# add referer to request headers,
|
|
1370
|
-
# otherwise we get 403 Forbidden
|
|
1371
|
-
headers['Referer'] = self.host
|
|
1372
|
-
|
|
1373
|
-
# add token to request headers,
|
|
1374
|
-
# used for CGI auth (together with JSESSIONID cookie)
|
|
1375
|
-
if self._token is not None:
|
|
1376
|
-
headers['TokenID'] = self._token
|
|
1377
|
-
|
|
1378
|
-
# encrypt request data if needed (for the /cgi_gdpr endpoint)
|
|
1379
|
-
if encrypt:
|
|
1380
|
-
sign, data = self._prepare_data(data_str, False)
|
|
1381
|
-
data = 'sign={}\r\ndata={}\r\n'.format(sign, data)
|
|
1382
|
-
else:
|
|
1383
|
-
data = data_str
|
|
1384
|
-
|
|
1385
|
-
retry = 0
|
|
1386
|
-
while retry < self.REQUEST_RETRIES:
|
|
1387
|
-
# send the request
|
|
1388
|
-
if method == 'POST':
|
|
1389
|
-
r = self.req.post(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
|
|
1390
|
-
elif method == 'GET':
|
|
1391
|
-
r = self.req.get(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
|
|
1392
|
-
else:
|
|
1393
|
-
raise Exception('Unsupported method ' + str(method))
|
|
1394
|
-
|
|
1395
|
-
# sometimes we get 500 here, not sure why... just retry the request
|
|
1396
|
-
if r.status_code != 500 and '<title>500 Internal Server Error</title>' not in r.text:
|
|
1397
|
-
break
|
|
1398
|
-
|
|
1399
|
-
sleep(0.05)
|
|
1400
|
-
retry += 1
|
|
1401
|
-
|
|
1402
|
-
# decrypt the response, if needed
|
|
1403
|
-
if encrypt and (r.status_code == 200) and (r.text != ''):
|
|
1404
|
-
return r.status_code, self._encryption.aes_decrypt(r.text)
|
|
1405
|
-
else:
|
|
1406
|
-
return r.status_code, r.text
|
|
1407
|
-
|
|
1408
|
-
def _parse_ret_val(self, response_text):
|
|
1409
|
-
'''
|
|
1410
|
-
Parses $.ret value from the response text
|
|
1411
|
-
|
|
1412
|
-
Return value:
|
|
1413
|
-
return code (int)
|
|
1414
|
-
'''
|
|
1415
|
-
result = search(r'\$\.ret=(.*);', response_text)
|
|
1416
|
-
assert result is not None
|
|
1417
|
-
assert result.group(1).isnumeric()
|
|
1418
|
-
|
|
1419
|
-
return int(result.group(1))
|
|
1420
|
-
|
|
1421
|
-
def _prepare_data(self, data: str, is_login: bool) -> tuple[str, str]:
|
|
1422
|
-
encrypted_data = self._encryption.aes_encrypt(data)
|
|
1423
|
-
data_len = len(encrypted_data)
|
|
1424
|
-
# get encrypted signature
|
|
1425
|
-
signature = self._encryption.get_signature(int(self._seq) + data_len, is_login, self._hash, self._nn, self._ee)
|
|
1426
|
-
|
|
1427
|
-
# format expected raw request data
|
|
1428
|
-
return signature, encrypted_data
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
class TplinkRouterProvider:
|
|
1432
|
-
@staticmethod
|
|
1433
|
-
def get_client(host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
1434
|
-
verify_ssl: bool = True, timeout: int = 30) -> AbstractRouter:
|
|
1435
|
-
for client in [TplinkC5400XRouter, TPLinkMRClient, TplinkC6V4Router, TPLinkDecoClient, TplinkRouter]:
|
|
1436
|
-
router = client(host, password, username, logger, verify_ssl, timeout)
|
|
1437
|
-
if router.supports():
|
|
1438
|
-
return router
|
|
1439
|
-
|
|
1440
|
-
router = TplinkC1200Router(host, password, username, logger, verify_ssl, timeout)
|
|
1441
|
-
try:
|
|
1442
|
-
router.authorize()
|
|
1443
|
-
return router
|
|
1444
|
-
except AuthorizeError as e:
|
|
1445
|
-
if logger:
|
|
1446
|
-
logger.error(e.__str__())
|
|
1447
|
-
raise ClientException(('Login failed! Please check if your router local password is correct or '
|
|
1448
|
-
'try to use web encrypted password instead. Check the documentation!'
|
|
1449
|
-
))
|
|
1450
|
-
|
|
1451
|
-
raise ClientException('You need to use web encrypted password instead. Check the documentation!')
|