tplinkrouterc6u 5.11.0__tar.gz → 5.12.0__tar.gz
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-5.11.0 → tplinkrouterc6u-5.12.0}/PKG-INFO +15 -5
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/README.md +13 -4
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/setup.py +2 -1
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/__init__.py +2 -2
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/c80.py +7 -28
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/ex.py +38 -1
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/mr.py +76 -5
- tplinkrouterc6u-5.12.0/tplinkrouterc6u/client/mr200.py +165 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/re330.py +6 -36
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/xdr.py +2 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/common/dataclass.py +1 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/common/encryption.py +113 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/common/package_enum.py +1 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/provider.py +8 -5
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u.egg-info/PKG-INFO +15 -5
- tplinkrouterc6u-5.11.0/tplinkrouterc6u/client/mr200.py +0 -412
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/LICENSE +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/setup.cfg +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/__init__.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_c1200.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_c6u.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_c80.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_deco.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_ex.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_mr.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_re330.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_wdr.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/test/test_client_xdr.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/__init__.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/c1200.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/c5400x.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/c6u.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/deco.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/vr.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client/wdr.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/client_abstract.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/common/__init__.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/common/exception.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u/common/helper.py +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u.egg-info/SOURCES.txt +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u.egg-info/dependency_links.txt +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u.egg-info/requires.txt +0 -0
- {tplinkrouterc6u-5.11.0 → tplinkrouterc6u-5.12.0}/tplinkrouterc6u.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tplinkrouterc6u
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.12.0
|
|
4
4
|
Summary: TP-Link Router API (supports also Mercusys Router)
|
|
5
5
|
Home-page: https://github.com/AlexandrErohin/TP-Link-Archer-C6U
|
|
6
6
|
Author: Alex Erohin
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
16
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
16
17
|
Requires-Python: >=3.10
|
|
17
18
|
Description-Content-Type: text/markdown
|
|
@@ -48,7 +49,7 @@ Python package for API access and management for TP-Link and Mercusys Routers. S
|
|
|
48
49
|
- [pycryptodome](https://pypi.org/project/pycryptodome/)
|
|
49
50
|
|
|
50
51
|
## Usage
|
|
51
|
-
Enter the host & credentials used to log in to your router management page. Username is admin by default. But you may pass username as third parameter
|
|
52
|
+
Enter the host & credentials used to log in to your router management page. Username is `admin` by default. But you may pass username as third parameter. Some routers have default username - `user`
|
|
52
53
|
|
|
53
54
|
```python
|
|
54
55
|
from tplinkrouterc6u import (
|
|
@@ -56,13 +57,17 @@ from tplinkrouterc6u import (
|
|
|
56
57
|
TplinkRouter,
|
|
57
58
|
TplinkC1200Router,
|
|
58
59
|
TplinkC5400XRouter,
|
|
59
|
-
TPLinkMRClient,
|
|
60
|
+
TPLinkMRClient, # Class for MR series routers which supports old firmwares with AES cipher CBC mode
|
|
61
|
+
TPLinkMRClientGCM, # Class for MR series routers which supports AES cipher GCM mode
|
|
62
|
+
TPLinkMR200Client,
|
|
60
63
|
TPLinkVRClient,
|
|
61
|
-
TPLinkEXClient,
|
|
64
|
+
TPLinkEXClient, # Class for EX series routers which supports old firmwares with AES cipher CBC mode
|
|
65
|
+
TPLinkEXClientGCM, # Class for EX series routers which supports AES cipher GCM mode
|
|
62
66
|
TPLinkXDRClient,
|
|
63
67
|
TPLinkDecoClient,
|
|
64
68
|
TplinkC80Router,
|
|
65
69
|
TplinkWDRRouter,
|
|
70
|
+
TplinkRE330Router,
|
|
66
71
|
Connection
|
|
67
72
|
)
|
|
68
73
|
from logging import Logger
|
|
@@ -243,6 +248,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
243
248
|
| --- |---|---|
|
|
244
249
|
| openvpn_enable | OpenVPN is enabled | bool |
|
|
245
250
|
| pptpvpn_enable | PPTPVPN is enabled | bool |
|
|
251
|
+
| ipsecvpn_enable | IPSEC is enabled | bool |
|
|
246
252
|
| openvpn_clients_total | OpenVPN clients connected | int |
|
|
247
253
|
| pptpvpn_clients_total | PPTPVPN clients connected | int |
|
|
248
254
|
|
|
@@ -292,6 +298,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
292
298
|
### <a id="vpn">VPN</a>
|
|
293
299
|
- VPN.OPEN_VPN
|
|
294
300
|
- VPN.PPTP_VPN
|
|
301
|
+
- VPN.IPSEC
|
|
295
302
|
|
|
296
303
|
## <a id="supports">Supported routers</a>
|
|
297
304
|
- [TP-LINK routers](#tplink)
|
|
@@ -340,7 +347,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
340
347
|
- Archer C80 (1.0, 2.20)
|
|
341
348
|
- Archer C5400X V1
|
|
342
349
|
- Archer GX90 v1.0
|
|
343
|
-
- Archer MR200 (v5, v5.3, v6.0)
|
|
350
|
+
- Archer MR200 (v2, v5, v5.3, v6.0)
|
|
344
351
|
- Archer MR550 v1
|
|
345
352
|
- Archer MR600 (v1, v2, v3)
|
|
346
353
|
- Archer NX200 v2.0
|
|
@@ -349,6 +356,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
349
356
|
- Archer VR900v
|
|
350
357
|
- Archer VR1200v v1
|
|
351
358
|
- Archer VR2100v v1
|
|
359
|
+
- Archer VX231v v1.0
|
|
352
360
|
- Archer VX1800v v1.0
|
|
353
361
|
- BE11000 2.0
|
|
354
362
|
- Deco M4 2.0
|
|
@@ -379,11 +387,13 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
379
387
|
- TL-WA3001 v1.0
|
|
380
388
|
- TL-XDR3010 V2
|
|
381
389
|
- TL-WDR3600 V1
|
|
390
|
+
- TL-XDR6088 v1.0.30
|
|
382
391
|
- VX420-G2h v1.1
|
|
383
392
|
- VX800v v1
|
|
384
393
|
- XC220-G3v v2.30
|
|
385
394
|
- RE330 v1
|
|
386
395
|
### <a id="mercusys">MERCUSYS routers</a>
|
|
396
|
+
- AC10 1.20
|
|
387
397
|
- MR47BE v1.0
|
|
388
398
|
- MR50G 1.0
|
|
389
399
|
- H60XR 1.0
|
|
@@ -16,7 +16,7 @@ Python package for API access and management for TP-Link and Mercusys Routers. S
|
|
|
16
16
|
- [pycryptodome](https://pypi.org/project/pycryptodome/)
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
19
|
-
Enter the host & credentials used to log in to your router management page. Username is admin by default. But you may pass username as third parameter
|
|
19
|
+
Enter the host & credentials used to log in to your router management page. Username is `admin` by default. But you may pass username as third parameter. Some routers have default username - `user`
|
|
20
20
|
|
|
21
21
|
```python
|
|
22
22
|
from tplinkrouterc6u import (
|
|
@@ -24,13 +24,17 @@ from tplinkrouterc6u import (
|
|
|
24
24
|
TplinkRouter,
|
|
25
25
|
TplinkC1200Router,
|
|
26
26
|
TplinkC5400XRouter,
|
|
27
|
-
TPLinkMRClient,
|
|
27
|
+
TPLinkMRClient, # Class for MR series routers which supports old firmwares with AES cipher CBC mode
|
|
28
|
+
TPLinkMRClientGCM, # Class for MR series routers which supports AES cipher GCM mode
|
|
29
|
+
TPLinkMR200Client,
|
|
28
30
|
TPLinkVRClient,
|
|
29
|
-
TPLinkEXClient,
|
|
31
|
+
TPLinkEXClient, # Class for EX series routers which supports old firmwares with AES cipher CBC mode
|
|
32
|
+
TPLinkEXClientGCM, # Class for EX series routers which supports AES cipher GCM mode
|
|
30
33
|
TPLinkXDRClient,
|
|
31
34
|
TPLinkDecoClient,
|
|
32
35
|
TplinkC80Router,
|
|
33
36
|
TplinkWDRRouter,
|
|
37
|
+
TplinkRE330Router,
|
|
34
38
|
Connection
|
|
35
39
|
)
|
|
36
40
|
from logging import Logger
|
|
@@ -211,6 +215,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
211
215
|
| --- |---|---|
|
|
212
216
|
| openvpn_enable | OpenVPN is enabled | bool |
|
|
213
217
|
| pptpvpn_enable | PPTPVPN is enabled | bool |
|
|
218
|
+
| ipsecvpn_enable | IPSEC is enabled | bool |
|
|
214
219
|
| openvpn_clients_total | OpenVPN clients connected | int |
|
|
215
220
|
| pptpvpn_clients_total | PPTPVPN clients connected | int |
|
|
216
221
|
|
|
@@ -260,6 +265,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
260
265
|
### <a id="vpn">VPN</a>
|
|
261
266
|
- VPN.OPEN_VPN
|
|
262
267
|
- VPN.PPTP_VPN
|
|
268
|
+
- VPN.IPSEC
|
|
263
269
|
|
|
264
270
|
## <a id="supports">Supported routers</a>
|
|
265
271
|
- [TP-LINK routers](#tplink)
|
|
@@ -308,7 +314,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
308
314
|
- Archer C80 (1.0, 2.20)
|
|
309
315
|
- Archer C5400X V1
|
|
310
316
|
- Archer GX90 v1.0
|
|
311
|
-
- Archer MR200 (v5, v5.3, v6.0)
|
|
317
|
+
- Archer MR200 (v2, v5, v5.3, v6.0)
|
|
312
318
|
- Archer MR550 v1
|
|
313
319
|
- Archer MR600 (v1, v2, v3)
|
|
314
320
|
- Archer NX200 v2.0
|
|
@@ -317,6 +323,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
317
323
|
- Archer VR900v
|
|
318
324
|
- Archer VR1200v v1
|
|
319
325
|
- Archer VR2100v v1
|
|
326
|
+
- Archer VX231v v1.0
|
|
320
327
|
- Archer VX1800v v1.0
|
|
321
328
|
- BE11000 2.0
|
|
322
329
|
- Deco M4 2.0
|
|
@@ -347,11 +354,13 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
347
354
|
- TL-WA3001 v1.0
|
|
348
355
|
- TL-XDR3010 V2
|
|
349
356
|
- TL-WDR3600 V1
|
|
357
|
+
- TL-XDR6088 v1.0.30
|
|
350
358
|
- VX420-G2h v1.1
|
|
351
359
|
- VX800v v1
|
|
352
360
|
- XC220-G3v v2.30
|
|
353
361
|
- RE330 v1
|
|
354
362
|
### <a id="mercusys">MERCUSYS routers</a>
|
|
363
|
+
- AC10 1.20
|
|
355
364
|
- MR47BE v1.0
|
|
356
365
|
- MR50G 1.0
|
|
357
366
|
- H60XR 1.0
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="tplinkrouterc6u",
|
|
8
|
-
version="5.
|
|
8
|
+
version="5.12.0",
|
|
9
9
|
author="Alex Erohin",
|
|
10
10
|
author_email="alexanderErohin@yandex.ru",
|
|
11
11
|
description="TP-Link Router API (supports also Mercusys Router)",
|
|
@@ -21,6 +21,7 @@ setuptools.setup(
|
|
|
21
21
|
"Programming Language :: Python :: 3.11",
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
23
|
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
24
25
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
25
26
|
],
|
|
26
27
|
install_requires=['requests', 'pycryptodome', 'macaddress'],
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from tplinkrouterc6u.client.c6u import TplinkRouter
|
|
2
2
|
from tplinkrouterc6u.client.deco import TPLinkDecoClient
|
|
3
3
|
from tplinkrouterc6u.client_abstract import AbstractRouter
|
|
4
|
-
from tplinkrouterc6u.client.mr import TPLinkMRClient
|
|
4
|
+
from tplinkrouterc6u.client.mr import TPLinkMRClient, TPLinkMRClientGCM
|
|
5
5
|
from tplinkrouterc6u.client.mr200 import TPLinkMR200Client
|
|
6
|
-
from tplinkrouterc6u.client.ex import TPLinkEXClient
|
|
6
|
+
from tplinkrouterc6u.client.ex import TPLinkEXClient, TPLinkEXClientGCM
|
|
7
7
|
from tplinkrouterc6u.client.vr import TPLinkVRClient
|
|
8
8
|
from tplinkrouterc6u.client.c80 import TplinkC80Router
|
|
9
9
|
from tplinkrouterc6u.client.c5400x import TplinkC5400XRouter
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from logging import Logger
|
|
3
3
|
from urllib import parse
|
|
4
|
-
from base64 import b64encode, b64decode
|
|
5
4
|
from collections import defaultdict
|
|
6
5
|
from ipaddress import IPv4Address
|
|
7
6
|
import re
|
|
8
|
-
from Crypto.Cipher import AES
|
|
9
|
-
from Crypto.Util.Padding import pad, unpad
|
|
10
7
|
from macaddress import EUI48
|
|
11
8
|
import requests
|
|
12
9
|
from requests import Session
|
|
@@ -21,7 +18,6 @@ from tplinkrouterc6u.client_abstract import AbstractRouter
|
|
|
21
18
|
class RouterConstants:
|
|
22
19
|
AUTH_TOKEN_INDEX1 = 3
|
|
23
20
|
AUTH_TOKEN_INDEX2 = 4
|
|
24
|
-
DEFAULT_AES_VALUE = "0000000000000000"
|
|
25
21
|
|
|
26
22
|
HOST_WIFI_2G_REQUEST = '33|1,1,0'
|
|
27
23
|
HOST_WIFI_5G_REQUEST = '33|2,1,0'
|
|
@@ -64,9 +60,7 @@ class EncryptionState:
|
|
|
64
60
|
self.nn_rsa = ''
|
|
65
61
|
self.ee_rsa = ''
|
|
66
62
|
self.seq = ''
|
|
67
|
-
self.
|
|
68
|
-
self.iv_aes = ''
|
|
69
|
-
self.aes_string = ''
|
|
63
|
+
self.aes = EncryptionWrapper()
|
|
70
64
|
self.token = ''
|
|
71
65
|
|
|
72
66
|
|
|
@@ -105,14 +99,11 @@ class TplinkC80Router(AbstractRouter):
|
|
|
105
99
|
self._encryption.nn_rsa = responseText[2]
|
|
106
100
|
self._encryption.seq = responseText[3]
|
|
107
101
|
|
|
108
|
-
# Generate key and initialization vector
|
|
109
|
-
self._encryption.key_aes = RouterConstants.DEFAULT_AES_VALUE
|
|
110
|
-
self._encryption.iv_aes = RouterConstants.DEFAULT_AES_VALUE
|
|
111
|
-
self._encryption.aes_string = f'k={self._encryption.key_aes}&i={self._encryption.iv_aes}'
|
|
112
|
-
|
|
113
102
|
# Encrypt AES string
|
|
114
|
-
aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.
|
|
103
|
+
aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.aes._get_aes_string(),
|
|
104
|
+
self._encryption.nn_rsa,
|
|
115
105
|
self._encryption.ee_rsa)
|
|
106
|
+
|
|
116
107
|
# Register AES string for decryption on server side
|
|
117
108
|
self.request(16, 0, True, data=f'set {aes_string_encrypted}')
|
|
118
109
|
# Some auth request, might be redundant
|
|
@@ -368,7 +359,7 @@ class TplinkC80Router(AbstractRouter):
|
|
|
368
359
|
|
|
369
360
|
def _get_signature(self, datalen: int) -> str:
|
|
370
361
|
encryption = self._encryption
|
|
371
|
-
r = f'{encryption.
|
|
362
|
+
r = f'{encryption.aes._get_aes_string()}&s={str(int(encryption.seq) + datalen)}'
|
|
372
363
|
e = ''
|
|
373
364
|
n = 0
|
|
374
365
|
while n < len(r):
|
|
@@ -377,24 +368,12 @@ class TplinkC80Router(AbstractRouter):
|
|
|
377
368
|
return e
|
|
378
369
|
|
|
379
370
|
def _encrypt_body(self, text: str) -> str:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
key_bytes = encryption.key_aes.encode("utf-8")
|
|
383
|
-
iv_bytes = encryption.iv_aes.encode("utf-8")
|
|
384
|
-
|
|
385
|
-
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
|
386
|
-
data = b64encode(cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))).decode()
|
|
387
|
-
|
|
371
|
+
data = self._encryption.aes.aes_encrypt(text)
|
|
388
372
|
sign = self._get_signature(len(data))
|
|
389
373
|
return f'sign={sign}\r\ndata={data}'
|
|
390
374
|
|
|
391
375
|
def _decrypt_data(self, encrypted_text: str) -> str:
|
|
392
|
-
|
|
393
|
-
iv_bytes = self._encryption.iv_aes.encode("utf-8")
|
|
394
|
-
|
|
395
|
-
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
|
396
|
-
decrypted_padded = cipher.decrypt(b64decode(encrypted_text))
|
|
397
|
-
return unpad(decrypted_padded, AES.block_size).decode("utf-8")
|
|
376
|
+
return self._encryption.aes.aes_decrypt(encrypted_text)
|
|
398
377
|
|
|
399
378
|
def _extract_value(self, response_list, prefix):
|
|
400
379
|
return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
|
|
@@ -15,9 +15,10 @@ from tplinkrouterc6u.common.dataclass import (
|
|
|
15
15
|
IPv4Status,
|
|
16
16
|
VPNStatus)
|
|
17
17
|
from tplinkrouterc6u.common.exception import ClientException, ClientError
|
|
18
|
-
from tplinkrouterc6u.client.mr import TPLinkMRClientBase
|
|
18
|
+
from tplinkrouterc6u.client.mr import TPLinkMRClientBase, TPLinkMRClientBaseGCM
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
# Class for EX series routers which supports old firmwares with AES cipher CBC mode
|
|
21
22
|
class TPLinkEXClient(TPLinkMRClientBase):
|
|
22
23
|
WIFI_SET = {
|
|
23
24
|
Connection.HOST_2G: '1,0,0,0,0,0',
|
|
@@ -331,3 +332,39 @@ class TPLinkEXClient(TPLinkMRClientBase):
|
|
|
331
332
|
]
|
|
332
333
|
|
|
333
334
|
self.req_act(acts)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# Class for EX series routers which supports AES cipher GCM mode
|
|
338
|
+
class TPLinkEXClientGCM(TPLinkMRClientBaseGCM, TPLinkEXClient):
|
|
339
|
+
|
|
340
|
+
def _req_login(self) -> None:
|
|
341
|
+
login_data = ('{"data":{"UserName":"%s","Passwd":"%s","Action": "1","stack":"0,0,0,0,0,0",'
|
|
342
|
+
'"pstack":"0,0,0,0,0,0"},"operation":"cgi","oid":"/cgi/login"}') % (
|
|
343
|
+
b64encode(bytes(self.username, "utf-8")).decode("utf-8"),
|
|
344
|
+
b64encode(bytes(self.password, "utf-8")).decode("utf-8")
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
sign, data, tag = self._prepare_data(login_data, True)
|
|
348
|
+
assert len(sign) == 256
|
|
349
|
+
|
|
350
|
+
request_data = f"sign={sign}\r\ndata={data}\r\ntag={tag}\r\n"
|
|
351
|
+
|
|
352
|
+
url = f"{self.host}/cgi_gdpr?9"
|
|
353
|
+
(code, response) = self._request(url, data_str=request_data)
|
|
354
|
+
response = self._encryption.aes_decrypt(response)
|
|
355
|
+
|
|
356
|
+
# parse and match return code
|
|
357
|
+
ret_code = self._parse_ret_val(response)
|
|
358
|
+
error = ''
|
|
359
|
+
if ret_code == self.HTTP_ERR_USER_PWD_NOT_CORRECT:
|
|
360
|
+
error = ('TplinkRouter - EX - Login failed, wrong user or password. '
|
|
361
|
+
'Try to pass user instead of admin in username')
|
|
362
|
+
elif ret_code == self.HTTP_ERR_USER_BAD_REQUEST:
|
|
363
|
+
error = 'TplinkRouter - EX - Login failed. Generic error code: {}'.format(ret_code)
|
|
364
|
+
elif ret_code != self.HTTP_RET_OK:
|
|
365
|
+
error = 'TplinkRouter - EX - Login failed. Unknown error code: {}'.format(ret_code)
|
|
366
|
+
|
|
367
|
+
if error:
|
|
368
|
+
if self._logger:
|
|
369
|
+
self._logger.debug(error)
|
|
370
|
+
raise ClientException(error)
|
|
@@ -8,7 +8,7 @@ from macaddress import EUI48
|
|
|
8
8
|
from ipaddress import IPv4Address
|
|
9
9
|
from logging import Logger
|
|
10
10
|
from tplinkrouterc6u.common.helper import get_ip, get_mac, get_value
|
|
11
|
-
from tplinkrouterc6u.common.encryption import EncryptionWrapperMR
|
|
11
|
+
from tplinkrouterc6u.common.encryption import EncryptionWrapperMR, EncryptionWrapperMRGCM
|
|
12
12
|
from tplinkrouterc6u.common.package_enum import Connection, VPN
|
|
13
13
|
from tplinkrouterc6u.common.dataclass import (
|
|
14
14
|
Firmware,
|
|
@@ -80,6 +80,7 @@ class TPLinkMRClientBase(AbstractRouter):
|
|
|
80
80
|
if self._verify_ssl is False:
|
|
81
81
|
self.req.verify = False
|
|
82
82
|
self._token = None
|
|
83
|
+
self._authorized_at = None
|
|
83
84
|
self._hash = md5(f"{self.username}{self.password}".encode()).hexdigest()
|
|
84
85
|
self._nn = None
|
|
85
86
|
self._ee = None
|
|
@@ -96,10 +97,9 @@ class TPLinkMRClientBase(AbstractRouter):
|
|
|
96
97
|
return False
|
|
97
98
|
|
|
98
99
|
def authorize(self) -> None:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# hash the password
|
|
100
|
+
if self._token is not None and self._authorized_at >= (datetime.now() - timedelta(seconds=3)):
|
|
101
|
+
return
|
|
102
|
+
self._token = None
|
|
103
103
|
|
|
104
104
|
# request the RSA public key from the host
|
|
105
105
|
self._nn, self._ee, self._seq = self._req_rsa_key()
|
|
@@ -109,6 +109,7 @@ class TPLinkMRClientBase(AbstractRouter):
|
|
|
109
109
|
|
|
110
110
|
# request TokenID
|
|
111
111
|
self._token = self._req_token()
|
|
112
|
+
self._authorized_at = datetime.now()
|
|
112
113
|
|
|
113
114
|
def reboot(self) -> None:
|
|
114
115
|
acts = [
|
|
@@ -587,6 +588,71 @@ class TPLinkMRClientBase(AbstractRouter):
|
|
|
587
588
|
return signature, encrypted_data
|
|
588
589
|
|
|
589
590
|
|
|
591
|
+
class TPLinkMRClientBaseGCM(TPLinkMRClientBase):
|
|
592
|
+
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
593
|
+
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
594
|
+
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
595
|
+
|
|
596
|
+
self._encryption = EncryptionWrapperMRGCM()
|
|
597
|
+
|
|
598
|
+
def supports(self) -> bool:
|
|
599
|
+
try:
|
|
600
|
+
self.authorize()
|
|
601
|
+
return True
|
|
602
|
+
except Exception:
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
def _request(self, url, method='POST', data_str=None, encrypt=False, is_login=False):
|
|
608
|
+
headers = self.HEADERS
|
|
609
|
+
headers['Referer'] = self.host
|
|
610
|
+
|
|
611
|
+
if self._token is not None:
|
|
612
|
+
headers['TokenID'] = self._token
|
|
613
|
+
|
|
614
|
+
if encrypt:
|
|
615
|
+
sign, data, tag = self._prepare_data(data_str, is_login)
|
|
616
|
+
data = 'sign={}\r\ndata={}\r\ntag={}\r\n'.format(sign, data, tag)
|
|
617
|
+
else:
|
|
618
|
+
data = data_str
|
|
619
|
+
|
|
620
|
+
retry = 0
|
|
621
|
+
while retry < self.REQUEST_RETRIES:
|
|
622
|
+
# send the request
|
|
623
|
+
if method == 'POST':
|
|
624
|
+
r = self.req.post(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
|
|
625
|
+
elif method == 'GET':
|
|
626
|
+
r = self.req.get(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
|
|
627
|
+
else:
|
|
628
|
+
raise Exception('Unsupported method ' + str(method))
|
|
629
|
+
|
|
630
|
+
# sometimes we get 500 here, not sure why... just retry the request
|
|
631
|
+
if (r.status_code not in [500, 406]
|
|
632
|
+
and '<title>500 Internal Server Error</title>' not in r.text
|
|
633
|
+
and '<title>406 Not Acceptable</title>' not in r.text):
|
|
634
|
+
break
|
|
635
|
+
|
|
636
|
+
sleep(0.1)
|
|
637
|
+
retry += 1
|
|
638
|
+
|
|
639
|
+
# decrypt the response, if needed
|
|
640
|
+
if encrypt and (r.status_code == 200) and (r.text != ''):
|
|
641
|
+
return r.status_code, self._encryption.aes_decrypt(r.text)
|
|
642
|
+
else:
|
|
643
|
+
return r.status_code, r.text
|
|
644
|
+
|
|
645
|
+
def _prepare_data(self, data: str, is_login: bool) -> tuple[str, str, str]:
|
|
646
|
+
encrypted_data, tag = self._encryption.aes_encrypt(data)
|
|
647
|
+
data_len = len(encrypted_data)
|
|
648
|
+
# get encrypted signature
|
|
649
|
+
signature = self._encryption.get_signature(int(self._seq) + data_len, is_login, self._hash, self._nn, self._ee)
|
|
650
|
+
|
|
651
|
+
# format expected raw request data
|
|
652
|
+
return signature, encrypted_data, tag
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# Class for MR series routers which supports old firmwares with AES cipher CBC mode
|
|
590
656
|
class TPLinkMRClient(TPLinkMRClientBase):
|
|
591
657
|
|
|
592
658
|
def logout(self) -> None:
|
|
@@ -716,3 +782,8 @@ class TPLinkMRClient(TPLinkMRClientBase):
|
|
|
716
782
|
status.isp_name = values['3']['ispName']
|
|
717
783
|
|
|
718
784
|
return status
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
# Class for MR series routers which supports AES cipher GCM mode
|
|
788
|
+
class TPLinkMRClientGCM(TPLinkMRClientBaseGCM, TPLinkMRClient):
|
|
789
|
+
pass
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from tplinkrouterc6u.client.mr import TPLinkMRClient
|
|
3
|
+
from Crypto.PublicKey import RSA
|
|
4
|
+
from binascii import hexlify
|
|
5
|
+
from Crypto.Cipher import PKCS1_v1_5
|
|
6
|
+
from re import search
|
|
7
|
+
from tplinkrouterc6u.common.package_enum import VPN
|
|
8
|
+
from tplinkrouterc6u.common.dataclass import (
|
|
9
|
+
LTEStatus,
|
|
10
|
+
VPNStatus,
|
|
11
|
+
)
|
|
12
|
+
from tplinkrouterc6u.common.exception import ClientException, ClientError, AuthorizeError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TPLinkMR200Client(TPLinkMRClient):
|
|
16
|
+
|
|
17
|
+
def supports(self) -> bool:
|
|
18
|
+
try:
|
|
19
|
+
self.__get_params()
|
|
20
|
+
return True
|
|
21
|
+
except ClientException:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
def authorize(self) -> None:
|
|
25
|
+
params = self.__get_params()
|
|
26
|
+
|
|
27
|
+
# Construct the RSA public key manually using modulus (n) and exponent (e)
|
|
28
|
+
n = int(params["nn"])
|
|
29
|
+
e = int(params["ee"])
|
|
30
|
+
pub_key = RSA.construct((n, e))
|
|
31
|
+
|
|
32
|
+
# Create an RSA cipher with PKCS#1 v1.5 padding (same as rsa.encrypt)
|
|
33
|
+
cipher = PKCS1_v1_5.new(pub_key)
|
|
34
|
+
|
|
35
|
+
# Encrypt username
|
|
36
|
+
rsa_username = cipher.encrypt(self.username.encode("utf-8"))
|
|
37
|
+
rsa_username_hex = hexlify(rsa_username).decode("utf-8")
|
|
38
|
+
|
|
39
|
+
# Encrypt password (after base64 encoding, as in your original code)
|
|
40
|
+
rsa_password = cipher.encrypt(base64.b64encode(self.password.encode("utf-8")))
|
|
41
|
+
rsa_password_hex = hexlify(rsa_password).decode("utf-8")
|
|
42
|
+
|
|
43
|
+
# Send login request
|
|
44
|
+
self.req.post(
|
|
45
|
+
f'{self.host}/cgi/login?UserName={rsa_username_hex}&Passwd={rsa_password_hex}&Action=1&LoginStatus=0'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Try to extract token
|
|
49
|
+
r = self.req.get(self.host)
|
|
50
|
+
try:
|
|
51
|
+
self.req.headers["TokenID"] = search(r'var token="(.*)";', r.text).group(1)
|
|
52
|
+
except AttributeError:
|
|
53
|
+
raise AuthorizeError()
|
|
54
|
+
|
|
55
|
+
def get_vpn_status(self) -> VPNStatus:
|
|
56
|
+
status = VPNStatus()
|
|
57
|
+
acts = [
|
|
58
|
+
self.ActItem(self.ActItem.GL, 'IPSEC_CFG'),
|
|
59
|
+
]
|
|
60
|
+
_, values = self.req_act(acts)
|
|
61
|
+
|
|
62
|
+
status.ipsecvpn_enable = values.get('enable') == '1'
|
|
63
|
+
|
|
64
|
+
return status
|
|
65
|
+
|
|
66
|
+
def set_vpn(self, vpn: VPN, enable: bool) -> None:
|
|
67
|
+
acts = [
|
|
68
|
+
self.ActItem(
|
|
69
|
+
self.ActItem.SET,
|
|
70
|
+
'IPSEC_CFG',
|
|
71
|
+
'1,0,0,0,0,0',
|
|
72
|
+
attrs=['enable={}'.format(int(enable))]
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
self.req_act(acts)
|
|
77
|
+
|
|
78
|
+
def logout(self) -> None:
|
|
79
|
+
acts = [
|
|
80
|
+
self.ActItem(self.ActItem.CGI, '/cgi/logout')
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
response, _ = self.req_act(acts)
|
|
84
|
+
ret_code = self._parse_ret_val(response)
|
|
85
|
+
|
|
86
|
+
if ret_code == self.HTTP_RET_OK:
|
|
87
|
+
del self.req.headers["TokenID"]
|
|
88
|
+
|
|
89
|
+
def get_lte_status(self) -> LTEStatus:
|
|
90
|
+
status = LTEStatus()
|
|
91
|
+
acts = [
|
|
92
|
+
self.ActItem(self.ActItem.GET, 'WAN_LTE_LINK_CFG', '2,1,0,0,0,0',
|
|
93
|
+
attrs=['enable', 'connectStatus', 'networkType', 'roamingStatus', 'simStatus']),
|
|
94
|
+
self.ActItem(self.ActItem.GET, 'WAN_LTE_INTF_CFG', '2,0,0,0,0,0',
|
|
95
|
+
attrs=['dataLimit', 'enablePaymentDay', 'curStatistics', 'totalStatistics', 'enableDataLimit',
|
|
96
|
+
'limitation',
|
|
97
|
+
'curRxSpeed', 'curTxSpeed']),
|
|
98
|
+
self.ActItem(self.ActItem.GET, 'LTE_WAN_CFG', '2,1,0,0,0,0'),
|
|
99
|
+
]
|
|
100
|
+
_, values = self.req_act(acts)
|
|
101
|
+
|
|
102
|
+
status.enable = values['0'].get('enable', 0)
|
|
103
|
+
status.connect_status = values['0'].get('connectStatus', 0)
|
|
104
|
+
status.network_type = values['0'].get('networkType', 0)
|
|
105
|
+
status.sim_status = values['0'].get('simStatus', 0)
|
|
106
|
+
status.sig_level = values['0'].get('signalStrength', 0)
|
|
107
|
+
|
|
108
|
+
status.total_statistics = values['1'].get('totalStatistics', 0)
|
|
109
|
+
status.cur_rx_speed = values['1'].get('curRxSpeed', 0)
|
|
110
|
+
status.cur_tx_speed = values['1'].get('curTxSpeed', 0)
|
|
111
|
+
|
|
112
|
+
status.isp_name = values['2'].get('profileName', '')
|
|
113
|
+
|
|
114
|
+
sms_list = self.get_sms()
|
|
115
|
+
status.sms_unread_count = sum(1 for m in sms_list if getattr(m, 'unread', False))
|
|
116
|
+
|
|
117
|
+
return status
|
|
118
|
+
|
|
119
|
+
def __get_params(self, retry=False):
|
|
120
|
+
self.req.headers = {'referer': f'{self.host}/', 'origin': self.host}
|
|
121
|
+
try:
|
|
122
|
+
r = self.req.get(f"{self.host}/cgi/getParm", timeout=5)
|
|
123
|
+
result = {}
|
|
124
|
+
for line in r.text.splitlines()[0:2]:
|
|
125
|
+
match = search(r"var (.*)=\"(.*)\"", line)
|
|
126
|
+
result[match.group(1)] = int(match.group(2), 16)
|
|
127
|
+
return result
|
|
128
|
+
except Exception:
|
|
129
|
+
if not retry:
|
|
130
|
+
return self.__get_params(True)
|
|
131
|
+
raise ClientException()
|
|
132
|
+
|
|
133
|
+
def req_act(self, acts: list):
|
|
134
|
+
'''
|
|
135
|
+
Requests ACTs via the cgi_gdpr proxy
|
|
136
|
+
'''
|
|
137
|
+
act_types = []
|
|
138
|
+
act_data = []
|
|
139
|
+
|
|
140
|
+
for act in acts:
|
|
141
|
+
act_types.append(str(act.type))
|
|
142
|
+
act_data.append('[{}#{}#{}]{},{}\r\n{}\r\n'.format(
|
|
143
|
+
act.oid,
|
|
144
|
+
act.stack,
|
|
145
|
+
act.pstack,
|
|
146
|
+
len(act_types) - 1, # index, starts at 0
|
|
147
|
+
len(act.attrs),
|
|
148
|
+
'\r\n'.join(act.attrs)
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
data = ''.join(act_data)
|
|
152
|
+
url = f"{self.host}/cgi?" + '&'.join(act_types)
|
|
153
|
+
response = self.req.post(url, data=data)
|
|
154
|
+
code = response.status_code
|
|
155
|
+
|
|
156
|
+
if code != 200:
|
|
157
|
+
error = 'TplinkRouter - MR200 - Response with error; Request {} - Response {}'.format(data, response.text)
|
|
158
|
+
if self._logger:
|
|
159
|
+
self._logger.debug(error)
|
|
160
|
+
raise ClientError(error)
|
|
161
|
+
|
|
162
|
+
# Convert Response to string for _merge_response
|
|
163
|
+
result = self._merge_response(response.text)
|
|
164
|
+
|
|
165
|
+
return response, result.get('0') if len(result) == 1 and result.get('0') else result
|