tplinkrouterc6u 5.0.3__py3-none-any.whl → 5.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.
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!')