tplinkrouterc6u 5.11.0__py3-none-any.whl → 5.12.1__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_mr_200.py +43 -0
- tplinkrouterc6u/__init__.py +2 -2
- tplinkrouterc6u/client/c80.py +7 -28
- tplinkrouterc6u/client/ex.py +38 -1
- tplinkrouterc6u/client/mr.py +76 -5
- tplinkrouterc6u/client/mr200.py +49 -296
- tplinkrouterc6u/client/re330.py +6 -36
- tplinkrouterc6u/client/xdr.py +2 -0
- tplinkrouterc6u/common/dataclass.py +1 -0
- tplinkrouterc6u/common/encryption.py +113 -0
- tplinkrouterc6u/common/package_enum.py +1 -0
- tplinkrouterc6u/provider.py +8 -5
- {tplinkrouterc6u-5.11.0.dist-info → tplinkrouterc6u-5.12.1.dist-info}/METADATA +19 -5
- {tplinkrouterc6u-5.11.0.dist-info → tplinkrouterc6u-5.12.1.dist-info}/RECORD +17 -16
- {tplinkrouterc6u-5.11.0.dist-info → tplinkrouterc6u-5.12.1.dist-info}/WHEEL +0 -0
- {tplinkrouterc6u-5.11.0.dist-info → tplinkrouterc6u-5.12.1.dist-info}/licenses/LICENSE +0 -0
- {tplinkrouterc6u-5.11.0.dist-info → tplinkrouterc6u-5.12.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from unittest import main, TestCase
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
from requests import Session
|
|
4
|
+
from tplinkrouterc6u import TPLinkMR200Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestTPLinkMR200Client(TestCase):
|
|
8
|
+
def setUp(self):
|
|
9
|
+
self.obj = TPLinkMR200Client('', '')
|
|
10
|
+
|
|
11
|
+
def test_supports_false(self):
|
|
12
|
+
responses = [
|
|
13
|
+
'var param1="0x1A"\nvar param2="0x2B"\nignored line\n',
|
|
14
|
+
'404',
|
|
15
|
+
'var nn="dfgdfg"\nvar ee="0x2B"\n'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
fake_responses = []
|
|
19
|
+
for text in responses:
|
|
20
|
+
r = MagicMock()
|
|
21
|
+
r.text = text
|
|
22
|
+
fake_responses.append(r)
|
|
23
|
+
|
|
24
|
+
with patch.object(Session, "get", side_effect=fake_responses):
|
|
25
|
+
for _ in range(len(fake_responses)):
|
|
26
|
+
result = self.obj.supports()
|
|
27
|
+
self.assertFalse(result)
|
|
28
|
+
|
|
29
|
+
def test_supports_true(self):
|
|
30
|
+
fake_response = MagicMock()
|
|
31
|
+
fake_response.text = (
|
|
32
|
+
'var nn="0x1A"\n'
|
|
33
|
+
'var ee="0x2B"\n'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
with patch.object(Session, "get", return_value=fake_response):
|
|
37
|
+
result = self.obj.supports()
|
|
38
|
+
|
|
39
|
+
self.assertEqual(result, True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == '__main__':
|
|
43
|
+
main()
|
tplinkrouterc6u/__init__.py
CHANGED
|
@@ -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
|
tplinkrouterc6u/client/c80.py
CHANGED
|
@@ -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)
|
tplinkrouterc6u/client/ex.py
CHANGED
|
@@ -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)
|
tplinkrouterc6u/client/mr.py
CHANGED
|
@@ -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
|
tplinkrouterc6u/client/mr200.py
CHANGED
|
@@ -4,20 +4,8 @@ from Crypto.PublicKey import RSA
|
|
|
4
4
|
from binascii import hexlify
|
|
5
5
|
from Crypto.Cipher import PKCS1_v1_5
|
|
6
6
|
from re import search
|
|
7
|
-
from
|
|
8
|
-
from datetime import timedelta, datetime
|
|
9
|
-
from macaddress import EUI48
|
|
10
|
-
from ipaddress import IPv4Address
|
|
11
|
-
from tplinkrouterc6u.common.helper import get_ip, get_mac, get_value
|
|
12
|
-
from tplinkrouterc6u.common.package_enum import Connection, VPN
|
|
7
|
+
from tplinkrouterc6u.common.package_enum import VPN
|
|
13
8
|
from tplinkrouterc6u.common.dataclass import (
|
|
14
|
-
Firmware,
|
|
15
|
-
Status,
|
|
16
|
-
Device,
|
|
17
|
-
IPv4Reservation,
|
|
18
|
-
IPv4DHCPLease,
|
|
19
|
-
IPv4Status,
|
|
20
|
-
SMS,
|
|
21
9
|
LTEStatus,
|
|
22
10
|
VPNStatus,
|
|
23
11
|
)
|
|
@@ -26,14 +14,18 @@ from tplinkrouterc6u.common.exception import ClientException, ClientError, Autho
|
|
|
26
14
|
|
|
27
15
|
class TPLinkMR200Client(TPLinkMRClient):
|
|
28
16
|
|
|
17
|
+
def supports(self) -> bool:
|
|
18
|
+
try:
|
|
19
|
+
self.__get_params()
|
|
20
|
+
return True
|
|
21
|
+
except ClientException:
|
|
22
|
+
return False
|
|
23
|
+
|
|
29
24
|
def authorize(self) -> None:
|
|
30
|
-
self.
|
|
31
|
-
params = self.__get_params()
|
|
25
|
+
self.__get_params()
|
|
32
26
|
|
|
33
27
|
# Construct the RSA public key manually using modulus (n) and exponent (e)
|
|
34
|
-
|
|
35
|
-
e = int(params["ee"])
|
|
36
|
-
pub_key = RSA.construct((n, e))
|
|
28
|
+
pub_key = RSA.construct((self._nn, self._ee))
|
|
37
29
|
|
|
38
30
|
# Create an RSA cipher with PKCS#1 v1.5 padding (same as rsa.encrypt)
|
|
39
31
|
cipher = PKCS1_v1_5.new(pub_key)
|
|
@@ -58,203 +50,31 @@ class TPLinkMR200Client(TPLinkMRClient):
|
|
|
58
50
|
except AttributeError:
|
|
59
51
|
raise AuthorizeError()
|
|
60
52
|
|
|
61
|
-
def get_firmware(self) -> Firmware:
|
|
62
|
-
acts = [
|
|
63
|
-
self.ActItem(self.ActItem.GET, 'IGD_DEV_INFO')
|
|
64
|
-
]
|
|
65
|
-
_, values = self.req_act(acts)
|
|
66
|
-
|
|
67
|
-
firmware = Firmware(values.get('hardwareVersion', ''), values.get('modelName', ''),
|
|
68
|
-
values.get('softwareVersion', ''))
|
|
69
|
-
|
|
70
|
-
return firmware
|
|
71
|
-
|
|
72
|
-
def get_status(self) -> Status:
|
|
73
|
-
status = Status()
|
|
74
|
-
acts = [
|
|
75
|
-
self.ActItem(self.ActItem.GS, 'LAN_IP_INTF'),
|
|
76
|
-
self.ActItem(self.ActItem.GS, 'WAN_IP_CONN'),
|
|
77
|
-
self.ActItem(self.ActItem.GL, 'LAN_WLAN'),
|
|
78
|
-
self.ActItem(self.ActItem.GL, 'LAN_WLAN_GUESTNET'),
|
|
79
|
-
self.ActItem(self.ActItem.GL, 'LAN_HOST_ENTRY'),
|
|
80
|
-
self.ActItem(self.ActItem.GS, 'LAN_WLAN_ASSOC_DEV'),
|
|
81
|
-
]
|
|
82
|
-
_, values = self.req_act(acts)
|
|
83
|
-
|
|
84
|
-
if values['0'].__class__ == list:
|
|
85
|
-
values['0'] = values['0'][0]
|
|
86
|
-
|
|
87
|
-
status._lan_macaddr = EUI48(values['0']['X_TP_MACAddress'])
|
|
88
|
-
status._lan_ipv4_addr = IPv4Address(values['0']['IPInterfaceIPAddress'])
|
|
89
|
-
|
|
90
|
-
for item in self._to_list(values.get('1')):
|
|
91
|
-
if int(item['enable']) == 0 and values.get('1').__class__ == list:
|
|
92
|
-
continue
|
|
93
|
-
status._wan_macaddr = EUI48(item['MACAddress']) if item.get('MACAddress') else None
|
|
94
|
-
status._wan_ipv4_addr = IPv4Address(item['externalIPAddress'])
|
|
95
|
-
status._wan_ipv4_gateway = IPv4Address(item['defaultGateway'])
|
|
96
|
-
status.conn_type = item.get('name', '')
|
|
97
|
-
|
|
98
|
-
if values['2'].__class__ != list:
|
|
99
|
-
status.wifi_2g_enable = bool(int(values['2']['enable']))
|
|
100
|
-
else:
|
|
101
|
-
status.wifi_2g_enable = bool(int(values['2'][0]['enable']))
|
|
102
|
-
status.wifi_5g_enable = bool(int(values['2'][1]['enable']))
|
|
103
|
-
|
|
104
|
-
if values['3'].__class__ != list:
|
|
105
|
-
status.guest_2g_enable = bool(int(values['3']['enable']))
|
|
106
|
-
else:
|
|
107
|
-
status.guest_2g_enable = bool(int(values['3'][0]['enable']))
|
|
108
|
-
status.guest_5g_enable = bool(int(values['3'][1]['enable']))
|
|
109
|
-
|
|
110
|
-
devices = {}
|
|
111
|
-
for val in self._to_list(values.get('4')):
|
|
112
|
-
if int(val['active']) == 0:
|
|
113
|
-
continue
|
|
114
|
-
conn = self.CLIENT_TYPES.get(int(val['X_TP_ConnType']))
|
|
115
|
-
if conn is None:
|
|
116
|
-
continue
|
|
117
|
-
elif conn == Connection.WIRED:
|
|
118
|
-
status.wired_total += 1
|
|
119
|
-
elif conn.is_guest_wifi():
|
|
120
|
-
status.guest_clients_total += 1
|
|
121
|
-
elif conn.is_host_wifi():
|
|
122
|
-
status.wifi_clients_total += 1
|
|
123
|
-
devices[val['MACAddress']] = Device(conn,
|
|
124
|
-
EUI48(val['MACAddress']),
|
|
125
|
-
IPv4Address(val['IPAddress']),
|
|
126
|
-
val['hostName'])
|
|
127
|
-
|
|
128
|
-
for val in self._to_list(values.get('5')):
|
|
129
|
-
if val['associatedDeviceMACAddress'] not in devices:
|
|
130
|
-
status.wifi_clients_total += 1
|
|
131
|
-
devices[val['associatedDeviceMACAddress']] = Device(
|
|
132
|
-
Connection.HOST_2G,
|
|
133
|
-
EUI48(val['associatedDeviceMACAddress']),
|
|
134
|
-
IPv4Address('0.0.0.0'),
|
|
135
|
-
'')
|
|
136
|
-
devices[val['associatedDeviceMACAddress']].packets_sent = int(val['X_TP_TotalPacketsSent'])
|
|
137
|
-
devices[val['associatedDeviceMACAddress']].packets_received = int(val['X_TP_TotalPacketsReceived'])
|
|
138
|
-
|
|
139
|
-
status.devices = list(devices.values())
|
|
140
|
-
status.clients_total = status.wired_total + status.wifi_clients_total + status.guest_clients_total
|
|
141
|
-
|
|
142
|
-
return status
|
|
143
|
-
|
|
144
|
-
def get_ipv4_reservations(self) -> [IPv4Reservation]:
|
|
145
|
-
acts = [
|
|
146
|
-
self.ActItem(self.ActItem.GL, 'LAN_DHCP_STATIC_ADDR'),
|
|
147
|
-
]
|
|
148
|
-
_, values = self.req_act(acts)
|
|
149
|
-
|
|
150
|
-
ipv4_reservations = []
|
|
151
|
-
for item in self._to_list(values):
|
|
152
|
-
ipv4_reservations.append(
|
|
153
|
-
IPv4Reservation(
|
|
154
|
-
EUI48(item['chaddr']),
|
|
155
|
-
IPv4Address(item['yiaddr']),
|
|
156
|
-
'',
|
|
157
|
-
bool(int(item['enable']))
|
|
158
|
-
))
|
|
159
|
-
|
|
160
|
-
return ipv4_reservations
|
|
161
|
-
|
|
162
|
-
def get_ipv4_dhcp_leases(self) -> [IPv4DHCPLease]:
|
|
163
|
-
acts = [
|
|
164
|
-
self.ActItem(self.ActItem.GL, 'LAN_HOST_ENTRY'),
|
|
165
|
-
]
|
|
166
|
-
_, values = self.req_act(acts)
|
|
167
|
-
|
|
168
|
-
dhcp_leases = []
|
|
169
|
-
for item in self._to_list(values):
|
|
170
|
-
lease_time = item['leaseTimeRemaining']
|
|
171
|
-
dhcp_leases.append(
|
|
172
|
-
IPv4DHCPLease(
|
|
173
|
-
EUI48(item['MACAddress']),
|
|
174
|
-
IPv4Address(item['IPAddress']),
|
|
175
|
-
item['hostName'],
|
|
176
|
-
str(timedelta(seconds=int(lease_time))) if lease_time.isdigit() else 'Permanent',
|
|
177
|
-
))
|
|
178
|
-
|
|
179
|
-
return dhcp_leases
|
|
180
|
-
|
|
181
|
-
def get_ipv4_status(self) -> IPv4Status:
|
|
182
|
-
acts = [
|
|
183
|
-
self.ActItem(self.ActItem.GS, 'LAN_IP_INTF'),
|
|
184
|
-
self.ActItem(self.ActItem.GET, 'LAN_HOST_CFG', '1,0,0,0,0,0'),
|
|
185
|
-
self.ActItem(self.ActItem.GS, 'WAN_IP_CONN'),
|
|
186
|
-
]
|
|
187
|
-
_, values = self.req_act(acts)
|
|
188
|
-
|
|
189
|
-
ipv4_status = IPv4Status()
|
|
190
|
-
ipv4_status._lan_macaddr = get_mac(get_value(values, ['0', 'X_TP_MACAddress'], '00:00:00:00:00:00'))
|
|
191
|
-
ipv4_status._lan_ipv4_ipaddr = get_ip(get_value(values, ['0', 'IPInterfaceIPAddress'], '0.0.0.0'))
|
|
192
|
-
ipv4_status._lan_ipv4_netmask = get_ip(get_value(values, ['0', 'IPInterfaceSubnetMask'], '0.0.0.0'))
|
|
193
|
-
ipv4_status.lan_ipv4_dhcp_enable = bool(int(get_value(values, ['1', 'DHCPServerEnable'], '0')))
|
|
194
|
-
|
|
195
|
-
for item in self._to_list(values.get('2')):
|
|
196
|
-
if int(item.get('enable', '0')) == 0 and values.get('2').__class__ == list:
|
|
197
|
-
continue
|
|
198
|
-
ipv4_status._wan_macaddr = get_mac(item.get('MACAddress', '00:00:00:00:00:00'))
|
|
199
|
-
ipv4_status._wan_ipv4_ipaddr = get_ip(item.get('externalIPAddress', '0.0.0.0'))
|
|
200
|
-
ipv4_status._wan_ipv4_gateway = get_ip(item.get('defaultGateway', '0.0.0.0'))
|
|
201
|
-
ipv4_status._wan_ipv4_conntype = item.get('name', '')
|
|
202
|
-
ipv4_status._wan_ipv4_netmask = get_ip(item.get('subnetMask', '0.0.0.0'))
|
|
203
|
-
dns = item.get('DNSServers', '').split(',')
|
|
204
|
-
ipv4_status._wan_ipv4_pridns = get_ip(dns[0] if len(dns) > 0 else '0.0.0.0')
|
|
205
|
-
ipv4_status._wan_ipv4_snddns = get_ip(dns[1] if len(dns) > 1 else '0.0.0.0')
|
|
206
|
-
|
|
207
|
-
return ipv4_status
|
|
208
|
-
|
|
209
|
-
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
210
|
-
acts = [
|
|
211
|
-
self.ActItem(
|
|
212
|
-
self.ActItem.SET,
|
|
213
|
-
'LAN_WLAN' if wifi in [Connection.HOST_2G, Connection.HOST_5G] else 'LAN_WLAN_MSSIDENTRY',
|
|
214
|
-
self.WIFI_SET[wifi],
|
|
215
|
-
attrs=['enable={}'.format(int(enable))]),
|
|
216
|
-
]
|
|
217
|
-
self.req_act(acts)
|
|
218
|
-
|
|
219
53
|
def get_vpn_status(self) -> VPNStatus:
|
|
220
54
|
status = VPNStatus()
|
|
221
55
|
acts = [
|
|
222
|
-
self.ActItem(self.ActItem.
|
|
223
|
-
self.ActItem(self.ActItem.GET, 'PPTPVPN'),
|
|
224
|
-
self.ActItem(self.ActItem.GL, 'OVPN_CLIENT'),
|
|
225
|
-
self.ActItem(self.ActItem.GL, 'PVPN_CLIENT'),
|
|
56
|
+
self.ActItem(self.ActItem.GL, 'IPSEC_CFG'),
|
|
226
57
|
]
|
|
227
58
|
_, values = self.req_act(acts)
|
|
228
59
|
|
|
229
|
-
status.
|
|
230
|
-
status.pptpvpn_enable = values['1']['enable'] == '1'
|
|
231
|
-
|
|
232
|
-
for item in values['2']:
|
|
233
|
-
if item['connAct'] == '1':
|
|
234
|
-
status.openvpn_clients_total += 1
|
|
235
|
-
|
|
236
|
-
for item in values['3']:
|
|
237
|
-
if item['connAct'] == '1':
|
|
238
|
-
status.pptpvpn_clients_total += 1
|
|
60
|
+
status.ipsecvpn_enable = values.get('enable') == '1'
|
|
239
61
|
|
|
240
62
|
return status
|
|
241
63
|
|
|
242
64
|
def set_vpn(self, vpn: VPN, enable: bool) -> None:
|
|
243
65
|
acts = [
|
|
244
|
-
self.ActItem(
|
|
66
|
+
self.ActItem(
|
|
67
|
+
self.ActItem.SET,
|
|
68
|
+
'IPSEC_CFG',
|
|
69
|
+
'1,0,0,0,0,0',
|
|
70
|
+
attrs=['enable={}'.format(int(enable))]
|
|
71
|
+
)
|
|
245
72
|
]
|
|
246
73
|
|
|
247
74
|
self.req_act(acts)
|
|
248
75
|
|
|
249
76
|
def logout(self) -> None:
|
|
250
|
-
'''
|
|
251
|
-
Logs out from the host
|
|
252
|
-
'''
|
|
253
|
-
if self._token is None:
|
|
254
|
-
return
|
|
255
|
-
|
|
256
77
|
acts = [
|
|
257
|
-
# 8\r\n[/cgi/logout#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n
|
|
258
78
|
self.ActItem(self.ActItem.CGI, '/cgi/logout')
|
|
259
79
|
]
|
|
260
80
|
|
|
@@ -264,120 +84,51 @@ class TPLinkMR200Client(TPLinkMRClient):
|
|
|
264
84
|
if ret_code == self.HTTP_RET_OK:
|
|
265
85
|
del self.req.headers["TokenID"]
|
|
266
86
|
|
|
267
|
-
def send_sms(self, phone_number: str, message: str) -> None:
|
|
268
|
-
acts = [
|
|
269
|
-
self.ActItem(
|
|
270
|
-
self.ActItem.SET, 'LTE_SMS_SENDNEWMSG', attrs=[
|
|
271
|
-
'index=1',
|
|
272
|
-
'to={}'.format(phone_number),
|
|
273
|
-
'textContent={}'.format(message),
|
|
274
|
-
]),
|
|
275
|
-
]
|
|
276
|
-
self.req_act(acts)
|
|
277
|
-
|
|
278
|
-
def get_sms(self) -> [SMS]:
|
|
279
|
-
acts = [
|
|
280
|
-
self.ActItem(
|
|
281
|
-
self.ActItem.SET, 'LTE_SMS_RECVMSGBOX', attrs=['PageNumber=1']),
|
|
282
|
-
self.ActItem(
|
|
283
|
-
self.ActItem.GL, 'LTE_SMS_RECVMSGENTRY'),
|
|
284
|
-
]
|
|
285
|
-
_, values = self.req_act(acts)
|
|
286
|
-
|
|
287
|
-
messages = []
|
|
288
|
-
if values:
|
|
289
|
-
i = 1
|
|
290
|
-
for item in self._to_list(values.get('1')):
|
|
291
|
-
messages.append(
|
|
292
|
-
SMS(
|
|
293
|
-
i, item['from'], item['content'], datetime.fromisoformat(item['receivedTime']),
|
|
294
|
-
item['unread'] == '1'
|
|
295
|
-
)
|
|
296
|
-
)
|
|
297
|
-
i += 1
|
|
298
|
-
|
|
299
|
-
return messages
|
|
300
|
-
|
|
301
|
-
def set_sms_read(self, sms: SMS) -> None:
|
|
302
|
-
acts = [
|
|
303
|
-
self.ActItem(
|
|
304
|
-
self.ActItem.SET, 'LTE_SMS_RECVMSGENTRY', f'{sms.id},0,0,0,0,0', attrs=['unread=0']),
|
|
305
|
-
]
|
|
306
|
-
self.req_act(acts)
|
|
307
|
-
|
|
308
|
-
def delete_sms(self, sms: SMS) -> None:
|
|
309
|
-
acts = [
|
|
310
|
-
self.ActItem(
|
|
311
|
-
self.ActItem.DEL, 'LTE_SMS_RECVMSGENTRY', f'{sms.id},0,0,0,0,0'),
|
|
312
|
-
]
|
|
313
|
-
self.req_act(acts)
|
|
314
|
-
|
|
315
|
-
def send_ussd(self, command: str) -> str:
|
|
316
|
-
acts = [
|
|
317
|
-
self.ActItem(
|
|
318
|
-
self.ActItem.SET, 'LTE_USSD', attrs=[
|
|
319
|
-
'action=1',
|
|
320
|
-
f"reqContent={command}",
|
|
321
|
-
]),
|
|
322
|
-
]
|
|
323
|
-
self.req_act(acts)
|
|
324
|
-
|
|
325
|
-
status = '0'
|
|
326
|
-
while status == '0':
|
|
327
|
-
sleep(1)
|
|
328
|
-
acts = [
|
|
329
|
-
self.ActItem(self.ActItem.GET, 'LTE_USSD'),
|
|
330
|
-
]
|
|
331
|
-
_, values = self.req_act(acts)
|
|
332
|
-
|
|
333
|
-
status = values.get('ussdStatus', '2')
|
|
334
|
-
|
|
335
|
-
if status == '1':
|
|
336
|
-
return values.get('response')
|
|
337
|
-
elif status == '2':
|
|
338
|
-
raise ClientError('Cannot send USSD!')
|
|
339
|
-
|
|
340
87
|
def get_lte_status(self) -> LTEStatus:
|
|
341
88
|
status = LTEStatus()
|
|
342
89
|
acts = [
|
|
343
|
-
self.ActItem(self.ActItem.GET, 'WAN_LTE_LINK_CFG', '2,1,0,0,0,0'
|
|
344
|
-
|
|
345
|
-
self.ActItem(self.ActItem.GET, '
|
|
346
|
-
|
|
90
|
+
self.ActItem(self.ActItem.GET, 'WAN_LTE_LINK_CFG', '2,1,0,0,0,0',
|
|
91
|
+
attrs=['enable', 'connectStatus', 'networkType', 'roamingStatus', 'simStatus']),
|
|
92
|
+
self.ActItem(self.ActItem.GET, 'WAN_LTE_INTF_CFG', '2,0,0,0,0,0',
|
|
93
|
+
attrs=['dataLimit', 'enablePaymentDay', 'curStatistics', 'totalStatistics', 'enableDataLimit',
|
|
94
|
+
'limitation',
|
|
95
|
+
'curRxSpeed', 'curTxSpeed']),
|
|
96
|
+
self.ActItem(self.ActItem.GET, 'LTE_WAN_CFG', '2,1,0,0,0,0'),
|
|
347
97
|
]
|
|
348
98
|
_, values = self.req_act(acts)
|
|
349
99
|
|
|
350
|
-
status.enable =
|
|
351
|
-
status.connect_status =
|
|
352
|
-
status.network_type =
|
|
353
|
-
status.sim_status =
|
|
100
|
+
status.enable = values['0'].get('enable', 0)
|
|
101
|
+
status.connect_status = values['0'].get('connectStatus', 0)
|
|
102
|
+
status.network_type = values['0'].get('networkType', 0)
|
|
103
|
+
status.sim_status = values['0'].get('simStatus', 0)
|
|
104
|
+
status.sig_level = values['0'].get('signalStrength', 0)
|
|
354
105
|
|
|
355
|
-
status.total_statistics =
|
|
356
|
-
status.cur_rx_speed =
|
|
357
|
-
status.cur_tx_speed =
|
|
106
|
+
status.total_statistics = values['1'].get('totalStatistics', 0)
|
|
107
|
+
status.cur_rx_speed = values['1'].get('curRxSpeed', 0)
|
|
108
|
+
status.cur_tx_speed = values['1'].get('curTxSpeed', 0)
|
|
358
109
|
|
|
359
|
-
status.
|
|
360
|
-
status.sig_level = int(values['2']['sigLevel'])
|
|
361
|
-
status.rsrp = int(values['2']['rfInfoRsrp'])
|
|
362
|
-
status.rsrq = int(values['2']['rfInfoRsrq'])
|
|
363
|
-
status.snr = int(values['2']['rfInfoSnr'])
|
|
110
|
+
status.isp_name = values['2'].get('profileName', '')
|
|
364
111
|
|
|
365
|
-
|
|
112
|
+
sms_list = self.get_sms()
|
|
113
|
+
status.sms_unread_count = sum(1 for m in sms_list if getattr(m, 'unread', False))
|
|
366
114
|
|
|
367
115
|
return status
|
|
368
116
|
|
|
369
|
-
def __get_params(self, retry=False):
|
|
117
|
+
def __get_params(self, retry=False) -> None:
|
|
118
|
+
self.req.headers = {'referer': f'{self.host}/', 'origin': self.host}
|
|
370
119
|
try:
|
|
371
120
|
r = self.req.get(f"{self.host}/cgi/getParm", timeout=5)
|
|
372
121
|
result = {}
|
|
373
122
|
for line in r.text.splitlines()[0:2]:
|
|
374
123
|
match = search(r"var (.*)=\"(.*)\"", line)
|
|
375
124
|
result[match.group(1)] = int(match.group(2), 16)
|
|
376
|
-
|
|
377
|
-
|
|
125
|
+
|
|
126
|
+
self._nn = int(result["nn"])
|
|
127
|
+
self._ee = int(result["ee"])
|
|
128
|
+
except Exception as e:
|
|
378
129
|
if not retry:
|
|
379
|
-
|
|
380
|
-
raise ClientException()
|
|
130
|
+
self.__get_params(True)
|
|
131
|
+
raise ClientException(str(e))
|
|
381
132
|
|
|
382
133
|
def req_act(self, acts: list):
|
|
383
134
|
'''
|
|
@@ -399,14 +150,16 @@ class TPLinkMR200Client(TPLinkMRClient):
|
|
|
399
150
|
|
|
400
151
|
data = ''.join(act_data)
|
|
401
152
|
url = f"{self.host}/cgi?" + '&'.join(act_types)
|
|
402
|
-
|
|
153
|
+
response = self.req.post(url, data=data)
|
|
154
|
+
code = response.status_code
|
|
403
155
|
|
|
404
156
|
if code != 200:
|
|
405
|
-
error = 'TplinkRouter - MR200 - Response with error; Request {} - Response {}'.format(data, response)
|
|
157
|
+
error = 'TplinkRouter - MR200 - Response with error; Request {} - Response {}'.format(data, response.text)
|
|
406
158
|
if self._logger:
|
|
407
159
|
self._logger.debug(error)
|
|
408
160
|
raise ClientError(error)
|
|
409
161
|
|
|
410
|
-
|
|
162
|
+
# Convert Response to string for _merge_response
|
|
163
|
+
result = self._merge_response(response.text)
|
|
411
164
|
|
|
412
165
|
return response, result.get('0') if len(result) == 1 and result.get('0') else result
|
tplinkrouterc6u/client/re330.py
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
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
|
|
13
|
-
from datetime import datetime
|
|
14
|
-
import random
|
|
15
10
|
from tplinkrouterc6u.common.package_enum import Connection
|
|
16
11
|
from tplinkrouterc6u.common.exception import ClientException
|
|
17
12
|
from tplinkrouterc6u.common.encryption import EncryptionWrapper
|
|
@@ -58,9 +53,7 @@ class EncryptionState:
|
|
|
58
53
|
self.nn_rsa = ''
|
|
59
54
|
self.ee_rsa = ''
|
|
60
55
|
self.seq = ''
|
|
61
|
-
self.
|
|
62
|
-
self.iv_aes = ''
|
|
63
|
-
self.aes_string = ''
|
|
56
|
+
self.aes = EncryptionWrapper()
|
|
64
57
|
self.token = ''
|
|
65
58
|
|
|
66
59
|
|
|
@@ -110,12 +103,9 @@ class TplinkRE330Router(AbstractRouter):
|
|
|
110
103
|
self._encryption.nn_rsa = responseText[2]
|
|
111
104
|
self._encryption.seq = responseText[3]
|
|
112
105
|
|
|
113
|
-
# Generate key and initialization vector
|
|
114
|
-
self._encryption.key_aes, self._encryption.iv_aes = self._generate_AES_key()
|
|
115
|
-
self._encryption.aes_string = f'k={self._encryption.key_aes}&i={self._encryption.iv_aes}'
|
|
116
|
-
|
|
117
106
|
# Encrypt AES string
|
|
118
|
-
aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.
|
|
107
|
+
aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.aes._get_aes_string(),
|
|
108
|
+
self._encryption.nn_rsa,
|
|
119
109
|
self._encryption.ee_rsa)
|
|
120
110
|
# Mandatory intermediate request
|
|
121
111
|
response = self.request(7, 0, True)
|
|
@@ -368,7 +358,7 @@ class TplinkRE330Router(AbstractRouter):
|
|
|
368
358
|
|
|
369
359
|
def _get_signature(self, datalen: int) -> str:
|
|
370
360
|
encryption = self._encryption
|
|
371
|
-
r = f'{encryption.
|
|
361
|
+
r = f'{encryption.aes._get_aes_string()}&s={str(int(encryption.seq) + datalen)}'
|
|
372
362
|
e = ''
|
|
373
363
|
n = 0
|
|
374
364
|
while n < len(r):
|
|
@@ -377,24 +367,12 @@ class TplinkRE330Router(AbstractRouter):
|
|
|
377
367
|
return e
|
|
378
368
|
|
|
379
369
|
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
|
-
|
|
370
|
+
data = self._encryption.aes.aes_encrypt(text)
|
|
388
371
|
sign = self._get_signature(len(data))
|
|
389
372
|
return f'sign={sign}\r\ndata={data}'
|
|
390
373
|
|
|
391
374
|
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")
|
|
375
|
+
return self._encryption.aes.aes_decrypt(encrypted_text)
|
|
398
376
|
|
|
399
377
|
def _extract_value(self, response_list, prefix):
|
|
400
378
|
return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
|
|
@@ -413,11 +391,3 @@ class TplinkRE330Router(AbstractRouter):
|
|
|
413
391
|
except requests.exceptions.RequestException as e:
|
|
414
392
|
self._logger.error(f"Network error: {e}")
|
|
415
393
|
raise ClientException(f"Network error: {str(e)}") from e
|
|
416
|
-
|
|
417
|
-
@staticmethod
|
|
418
|
-
def _generate_AES_key() -> tuple[str, str]:
|
|
419
|
-
KEY_LEN = int(128 / 8)
|
|
420
|
-
IV_LEN = 16
|
|
421
|
-
key = (str(int(datetime.now().timestamp())) + str(int(random.random()*1000000000)))[:KEY_LEN]
|
|
422
|
-
iv = (str(int(datetime.now().timestamp())) + str(int(random.random()*1000000000)))[:IV_LEN]
|
|
423
|
-
return key, iv
|
tplinkrouterc6u/client/xdr.py
CHANGED
|
@@ -116,6 +116,8 @@ class TPLinkXDRClient(AbstractRouter):
|
|
|
116
116
|
dev = Device(conn_type, get_mac(item['mac']), get_ip(item['ip']), unquote(item['hostname']))
|
|
117
117
|
dev.up_speed = item['up_speed']
|
|
118
118
|
dev.down_speed = item['down_speed']
|
|
119
|
+
if 'online' in item:
|
|
120
|
+
dev.active = item['online'] == '1'
|
|
119
121
|
status.devices.append(dev)
|
|
120
122
|
return status
|
|
121
123
|
|
|
@@ -189,3 +189,116 @@ class EncryptionWrapperMR:
|
|
|
189
189
|
n = int('0x' + nn, 16)
|
|
190
190
|
e = int('0x' + ee, 16)
|
|
191
191
|
return RSA.construct((n, e))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class EncryptionWrapperMRGCM:
|
|
195
|
+
RSA_USE_PKCS_V1_5 = False
|
|
196
|
+
AES_KEY_LEN = 128 // 8
|
|
197
|
+
AES_IV_LEN = 12 # previously 16
|
|
198
|
+
|
|
199
|
+
def __init__(self) -> None:
|
|
200
|
+
ts = str(round(time() * 1000))
|
|
201
|
+
|
|
202
|
+
key = (ts + str(randint(100000000, 1000000000 - 1)))[:self.AES_KEY_LEN]
|
|
203
|
+
iv = (ts + str(randint(100000000, 1000000000 - 1)))[:self.AES_IV_LEN]
|
|
204
|
+
|
|
205
|
+
assert len(key) == self.AES_KEY_LEN
|
|
206
|
+
assert len(iv) == self.AES_IV_LEN
|
|
207
|
+
|
|
208
|
+
self._key = key
|
|
209
|
+
self._iv = iv
|
|
210
|
+
|
|
211
|
+
def aes_encrypt(self, raw: str) -> tuple[str, str]:
|
|
212
|
+
# not sure what the padding was for here:
|
|
213
|
+
# data_padded = pad(raw.encode('utf8'), 16, 'pkcs7')
|
|
214
|
+
|
|
215
|
+
# encrypt the body
|
|
216
|
+
aes_encryptor = self._make_aes_cipher()
|
|
217
|
+
encrypted_data_bytes, tag = aes_encryptor.encrypt_and_digest(raw.encode('utf-8'))
|
|
218
|
+
return (
|
|
219
|
+
b64encode(encrypted_data_bytes).decode(), # router expects b64 ciphertext
|
|
220
|
+
b64encode(tag).decode() # router expects b64 tag (this is new)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def aes_decrypt(self, data: str):
|
|
224
|
+
# decode base64 string
|
|
225
|
+
tag = b64decode(data[-24:]) # last 24 characters are the tag
|
|
226
|
+
encrypted_response_data = b64decode(data[:-24])
|
|
227
|
+
# decrypt the response using our AES key
|
|
228
|
+
aes_decryptor = self._make_aes_cipher()
|
|
229
|
+
response = aes_decryptor.decrypt_and_verify(encrypted_response_data, tag)
|
|
230
|
+
|
|
231
|
+
# not sure what unpad did here before:
|
|
232
|
+
# return unpad(response, 16, 'pkcs7').decode('utf8')
|
|
233
|
+
return response.decode('utf8')
|
|
234
|
+
|
|
235
|
+
def get_signature(self, seq: int, is_login: bool, hash: str, nn: str, ee: str) -> str:
|
|
236
|
+
if is_login:
|
|
237
|
+
# on login we also send our AES key, which is subsequently
|
|
238
|
+
# used for E2E encrypted communication
|
|
239
|
+
# key and iv now base64 not like before
|
|
240
|
+
key = b64encode(self._key.encode('utf-8')).decode()
|
|
241
|
+
iv = b64encode(self._iv.encode('utf-8')).decode()
|
|
242
|
+
sign_data = 'key={}&iv={}&h={}&s={}'.format(key, iv, hash, seq)
|
|
243
|
+
else:
|
|
244
|
+
sign_data = 'h={}&s={}'.format(hash, seq)
|
|
245
|
+
|
|
246
|
+
# set step based on whether PKCS padding is used
|
|
247
|
+
rsa_byte_len = len(nn) // 2 # hexlen / 2 * 8 / 8
|
|
248
|
+
step = (rsa_byte_len - 11) if self.RSA_USE_PKCS_V1_5 else rsa_byte_len
|
|
249
|
+
|
|
250
|
+
# encrypt the signature using the RSA public key
|
|
251
|
+
rsa_key = self._make_rsa_pub_key(nn, ee)
|
|
252
|
+
|
|
253
|
+
# make the PKCS#1 v1.5 cipher
|
|
254
|
+
if self.RSA_USE_PKCS_V1_5:
|
|
255
|
+
rsa = PKCS1_v1_5.new(rsa_key)
|
|
256
|
+
|
|
257
|
+
signature = ''
|
|
258
|
+
pos = 0
|
|
259
|
+
|
|
260
|
+
while pos < len(sign_data):
|
|
261
|
+
sign_data_bin = sign_data[pos: pos + step].encode('utf8')
|
|
262
|
+
|
|
263
|
+
if self.RSA_USE_PKCS_V1_5:
|
|
264
|
+
# encrypt using the PKCS#1 v1.5 padding
|
|
265
|
+
enc = rsa.encrypt(sign_data_bin)
|
|
266
|
+
else:
|
|
267
|
+
# encrypt using NOPADDING
|
|
268
|
+
# ... pad the end with zero bytes
|
|
269
|
+
while len(sign_data_bin) < step:
|
|
270
|
+
sign_data_bin = sign_data_bin + b'\0'
|
|
271
|
+
|
|
272
|
+
# step 3a (OS2IP)
|
|
273
|
+
em_int = bytes_to_long(sign_data_bin)
|
|
274
|
+
|
|
275
|
+
# step 3b (RSAEP)
|
|
276
|
+
m_int = rsa_key._encrypt(em_int)
|
|
277
|
+
|
|
278
|
+
# step 3c (I2OSP)
|
|
279
|
+
enc = long_to_bytes(m_int, 1)
|
|
280
|
+
|
|
281
|
+
# hexlify to string
|
|
282
|
+
enc_str = hexlify(enc).decode('utf8')
|
|
283
|
+
|
|
284
|
+
# pad the start with '0' hex char
|
|
285
|
+
while len(enc_str) < rsa_byte_len * 2:
|
|
286
|
+
enc_str = '0' + enc_str
|
|
287
|
+
|
|
288
|
+
signature += enc_str
|
|
289
|
+
pos = pos + step
|
|
290
|
+
|
|
291
|
+
return signature
|
|
292
|
+
|
|
293
|
+
def _make_aes_cipher(self) -> AES:
|
|
294
|
+
# consider renaming _iv to _nonce
|
|
295
|
+
return AES.new(self._key.encode('utf-8'), AES.MODE_GCM, nonce=self._iv.encode('utf-8'))
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def _make_rsa_pub_key(nn: str, ee: str):
|
|
299
|
+
'''
|
|
300
|
+
Makes a new RSA pub key from tuple (n, e)
|
|
301
|
+
'''
|
|
302
|
+
n = int('0x' + nn, 16)
|
|
303
|
+
e = int('0x' + ee, 16)
|
|
304
|
+
return RSA.construct((n, e))
|
tplinkrouterc6u/provider.py
CHANGED
|
@@ -5,8 +5,9 @@ from tplinkrouterc6u.common.exception import ClientException
|
|
|
5
5
|
from tplinkrouterc6u.client.c6u import TplinkRouter
|
|
6
6
|
from tplinkrouterc6u.client.deco import TPLinkDecoClient
|
|
7
7
|
from tplinkrouterc6u.client_abstract import AbstractRouter
|
|
8
|
-
from tplinkrouterc6u.client.mr import TPLinkMRClient
|
|
9
|
-
from tplinkrouterc6u.client.
|
|
8
|
+
from tplinkrouterc6u.client.mr import TPLinkMRClient, TPLinkMRClientGCM
|
|
9
|
+
from tplinkrouterc6u.client.mr200 import TPLinkMR200Client
|
|
10
|
+
from tplinkrouterc6u.client.ex import TPLinkEXClient, TPLinkEXClientGCM
|
|
10
11
|
from tplinkrouterc6u.client.c5400x import TplinkC5400XRouter
|
|
11
12
|
from tplinkrouterc6u.client.c1200 import TplinkC1200Router
|
|
12
13
|
from tplinkrouterc6u.client.c80 import TplinkC80Router
|
|
@@ -19,13 +20,15 @@ class TplinkRouterProvider:
|
|
|
19
20
|
@staticmethod
|
|
20
21
|
def get_client(host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
21
22
|
verify_ssl: bool = True, timeout: int = 30) -> AbstractRouter:
|
|
22
|
-
for client in [TplinkC5400XRouter, TPLinkVRClient,
|
|
23
|
-
|
|
23
|
+
for client in [TplinkC5400XRouter, TPLinkVRClient, TPLinkEXClientGCM, TPLinkEXClient, TPLinkMRClientGCM,
|
|
24
|
+
TPLinkMRClient, TPLinkMR200Client, TPLinkDecoClient, TPLinkXDRClient, TplinkRouter,
|
|
25
|
+
TplinkC80Router, TplinkWDRRouter, TplinkRE330Router]:
|
|
24
26
|
router = client(host, password, username, logger, verify_ssl, timeout)
|
|
25
27
|
if router.supports():
|
|
26
28
|
return router
|
|
27
29
|
|
|
28
|
-
message = ('Login failed! Please check if your router local password is correct
|
|
30
|
+
message = ('Login failed! Please check if your router local password is correct,'
|
|
31
|
+
'check if the default router username is correct or '
|
|
29
32
|
'try to use web encrypted password instead. Check the documentation!')
|
|
30
33
|
router = TplinkC1200Router(host, password, username, logger, verify_ssl, timeout)
|
|
31
34
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tplinkrouterc6u
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.12.1
|
|
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)
|
|
@@ -316,6 +323,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
316
323
|
- Archer AX73 (V1, V2.0)
|
|
317
324
|
- Archer AX75 V1
|
|
318
325
|
- Archer AX90 V1.20
|
|
326
|
+
- Archer AX95 v1.0
|
|
319
327
|
- Archer AXE75 V1
|
|
320
328
|
- Archer AXE5400 v1.0
|
|
321
329
|
- Archer AXE16000
|
|
@@ -323,6 +331,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
323
331
|
- Archer AX3000 V1
|
|
324
332
|
- Archer AX6000 V1
|
|
325
333
|
- Archer AX11000 V1
|
|
334
|
+
- Archer BE220 v1.0
|
|
326
335
|
- Archer BE230 v1.0
|
|
327
336
|
- Archer BE400 v1.0
|
|
328
337
|
- Archer BE550 v1.0
|
|
@@ -340,7 +349,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
340
349
|
- Archer C80 (1.0, 2.20)
|
|
341
350
|
- Archer C5400X V1
|
|
342
351
|
- Archer GX90 v1.0
|
|
343
|
-
- Archer MR200 (v5, v5.3, v6.0)
|
|
352
|
+
- Archer MR200 (v2, v5, v5.3, v6.0)
|
|
344
353
|
- Archer MR550 v1
|
|
345
354
|
- Archer MR600 (v1, v2, v3)
|
|
346
355
|
- Archer NX200 v2.0
|
|
@@ -349,6 +358,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
349
358
|
- Archer VR900v
|
|
350
359
|
- Archer VR1200v v1
|
|
351
360
|
- Archer VR2100v v1
|
|
361
|
+
- Archer VX231v v1.0
|
|
352
362
|
- Archer VX1800v v1.0
|
|
353
363
|
- BE11000 2.0
|
|
354
364
|
- Deco M4 2.0
|
|
@@ -379,11 +389,15 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
|
|
|
379
389
|
- TL-WA3001 v1.0
|
|
380
390
|
- TL-XDR3010 V2
|
|
381
391
|
- TL-WDR3600 V1
|
|
392
|
+
- TL-XDR6088 v1.0.30
|
|
382
393
|
- VX420-G2h v1.1
|
|
383
394
|
- VX800v v1
|
|
384
395
|
- XC220-G3v v2.30
|
|
396
|
+
- RE305 4.0
|
|
397
|
+
- RE315 1.0
|
|
385
398
|
- RE330 v1
|
|
386
399
|
### <a id="mercusys">MERCUSYS routers</a>
|
|
400
|
+
- AC10 1.20
|
|
387
401
|
- MR47BE v1.0
|
|
388
402
|
- MR50G 1.0
|
|
389
403
|
- H60XR 1.0
|
|
@@ -5,33 +5,34 @@ test/test_client_c80.py,sha256=RY_1SgRVcQQdN9h0_IXA0YW4_0flEB_uel05QvDDfws,42359
|
|
|
5
5
|
test/test_client_deco.py,sha256=YPLKRD8GoyDYHfRgdXvCk8iVNw8zdMJW-AHVnNbpdTM,31719
|
|
6
6
|
test/test_client_ex.py,sha256=Kg6svEKtyGAfyF9yrLh2qZa2tK1mlEBJJwXRsq1MAjo,26591
|
|
7
7
|
test/test_client_mr.py,sha256=lePxkmjcPzcrSFcaT8bT67L154cVJIOWrFlXMDOa8oY,33423
|
|
8
|
+
test/test_client_mr_200.py,sha256=86yANn5SUhVW6Uc5q5s_aTNL7tDnREeXk378G61v_TM,1186
|
|
8
9
|
test/test_client_re330.py,sha256=MgefuvOzfZtZOujrcOsjiTDiGEAujfeFXshcq7gn32Q,17044
|
|
9
10
|
test/test_client_wdr.py,sha256=0ZnRNP57MbuMv2cxFS8iIoVyv8Q6gtY0Q03gtHp9AWY,13492
|
|
10
11
|
test/test_client_xdr.py,sha256=mgn-xL5mD5sHD8DjTz9vpY7jeh4Ob6Um6Y8v5Qgx2jA,23374
|
|
11
|
-
tplinkrouterc6u/__init__.py,sha256=
|
|
12
|
+
tplinkrouterc6u/__init__.py,sha256=33L1bT4g8pVn6XpM3nebdYRtz0DXTapRtFROd3uFjmg,1162
|
|
12
13
|
tplinkrouterc6u/client_abstract.py,sha256=3UYzmll774S_Gb5E0FTVO_rI3-XFM7PSklg1-V-2jls,1419
|
|
13
|
-
tplinkrouterc6u/provider.py,sha256=
|
|
14
|
+
tplinkrouterc6u/provider.py,sha256=lqxw_pQ4VBYKS3jKrG1zVd4zVnlA6T8MaIRcqq3JAtM,2477
|
|
14
15
|
tplinkrouterc6u/client/__init__.py,sha256=KBy3fmtA9wgyFrb0Urh2x4CkKtWVnESdp-vxmuOvq0k,27
|
|
15
16
|
tplinkrouterc6u/client/c1200.py,sha256=4XEYidEGmVIJk0YQLvmTnd0Gqa7glH2gUWvjreHpWrk,3178
|
|
16
17
|
tplinkrouterc6u/client/c5400x.py,sha256=9E0omBSbWY_ljrs5MTCMu5brmrLtzsDB5O62Db8lP8Q,4329
|
|
17
18
|
tplinkrouterc6u/client/c6u.py,sha256=n4OMAxg0NXChYaVpWCvx3ZFUxVfynTMy-pyd1CTj9s4,19694
|
|
18
|
-
tplinkrouterc6u/client/c80.py,sha256=
|
|
19
|
+
tplinkrouterc6u/client/c80.py,sha256=efE0DEjEfzRFr35fjKA_hsv9YaWy_2dgLAaurDM-WQk,17665
|
|
19
20
|
tplinkrouterc6u/client/deco.py,sha256=cpKRggKD2RvSmMZuD6tzsZmehAUCU9oLiTTHcZBW81Y,8898
|
|
20
|
-
tplinkrouterc6u/client/ex.py,sha256=
|
|
21
|
-
tplinkrouterc6u/client/mr.py,sha256=
|
|
22
|
-
tplinkrouterc6u/client/mr200.py,sha256
|
|
23
|
-
tplinkrouterc6u/client/re330.py,sha256=
|
|
21
|
+
tplinkrouterc6u/client/ex.py,sha256=tOcMugCViAcISULg8otp3NjdkPyuUXihcoe_0lql3AQ,14886
|
|
22
|
+
tplinkrouterc6u/client/mr.py,sha256=7MtnKqmtbggWBx6RIiJzlGuSyVDbC8MFynPrc34bSd0,28948
|
|
23
|
+
tplinkrouterc6u/client/mr200.py,sha256=-EkaSpbhyX8QolwIwRdvl5KGTL0fmzA7xCOo_YQGb3c,5800
|
|
24
|
+
tplinkrouterc6u/client/re330.py,sha256=9Wj4VpYJbVwZJUh9s3magdeL3Jl-B7qyrWfrVBxRk4A,17465
|
|
24
25
|
tplinkrouterc6u/client/vr.py,sha256=7Tbu0IrWtr4HHtyrnLFXEJi1QctzhilciL7agtwQ0R8,5025
|
|
25
26
|
tplinkrouterc6u/client/wdr.py,sha256=i54PEifjhfOScDpgNBXygw9U4bfsVtle846_YjnDoBs,21679
|
|
26
|
-
tplinkrouterc6u/client/xdr.py,sha256=
|
|
27
|
+
tplinkrouterc6u/client/xdr.py,sha256=jdDhQZgJjReN3wMWKch4VHEun2OzUFyzmOWyY5-1IP8,10490
|
|
27
28
|
tplinkrouterc6u/common/__init__.py,sha256=pCTvVZ9CAwgb7MxRnLx0y1rI0sTKSwT24FfxWfQXeTM,33
|
|
28
|
-
tplinkrouterc6u/common/dataclass.py,sha256=
|
|
29
|
-
tplinkrouterc6u/common/encryption.py,sha256=
|
|
29
|
+
tplinkrouterc6u/common/dataclass.py,sha256=NmwN6Iqpd9Ne7Zr-R0J1OZQz28NRp5Qzh6NjVFZV_DA,7749
|
|
30
|
+
tplinkrouterc6u/common/encryption.py,sha256=EWfgGafOz0YgPilBndVaupnjw6JrzhVBdZkBy3oWhj0,10229
|
|
30
31
|
tplinkrouterc6u/common/exception.py,sha256=_0G8ZvW5__CsGifHrsZeULdl8c6EUD071sDCQsQgrHY,140
|
|
31
32
|
tplinkrouterc6u/common/helper.py,sha256=23b04fk9HuVinrZXMCS5R1rmF8uZ7eM-Cdnp7Br9NR0,572
|
|
32
|
-
tplinkrouterc6u/common/package_enum.py,sha256=
|
|
33
|
-
tplinkrouterc6u-5.
|
|
34
|
-
tplinkrouterc6u-5.
|
|
35
|
-
tplinkrouterc6u-5.
|
|
36
|
-
tplinkrouterc6u-5.
|
|
37
|
-
tplinkrouterc6u-5.
|
|
33
|
+
tplinkrouterc6u/common/package_enum.py,sha256=CMHVSgk4RSZyFoPi3499-sJDYg-nfnyJbz1iArFU9Hw,1644
|
|
34
|
+
tplinkrouterc6u-5.12.1.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
|
|
35
|
+
tplinkrouterc6u-5.12.1.dist-info/METADATA,sha256=NouSjagT1VJU9WCS5HXt9ltRavcTduMKjMDsAFDcw5A,17330
|
|
36
|
+
tplinkrouterc6u-5.12.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
37
|
+
tplinkrouterc6u-5.12.1.dist-info/top_level.txt,sha256=1iSCCIueqgEkrTxtQ-jiHe99jAB10zqrVdBcwvNfe_M,21
|
|
38
|
+
tplinkrouterc6u-5.12.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|