tplinkrouterc6u 5.11.0__py3-none-any.whl → 5.12.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.
@@ -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.key_aes = ''
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.aes_string, self._encryption.nn_rsa,
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.aes_string}&s={str(int(encryption.seq) + datalen)}'
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
- encryption = self._encryption
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
- key_bytes = self._encryption.key_aes.encode("utf-8")
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
- Establishes a login session to the host using provided credentials
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
@@ -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 time import sleep
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,8 +14,14 @@ 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.req.headers = {'referer': f'{self.host}/', 'origin': self.host}
31
25
  params = self.__get_params()
32
26
 
33
27
  # Construct the RSA public key manually using modulus (n) and exponent (e)
@@ -58,203 +52,31 @@ class TPLinkMR200Client(TPLinkMRClient):
58
52
  except AttributeError:
59
53
  raise AuthorizeError()
60
54
 
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
55
  def get_vpn_status(self) -> VPNStatus:
220
56
  status = VPNStatus()
221
57
  acts = [
222
- self.ActItem(self.ActItem.GET, 'OPENVPN'),
223
- self.ActItem(self.ActItem.GET, 'PPTPVPN'),
224
- self.ActItem(self.ActItem.GL, 'OVPN_CLIENT'),
225
- self.ActItem(self.ActItem.GL, 'PVPN_CLIENT'),
58
+ self.ActItem(self.ActItem.GL, 'IPSEC_CFG'),
226
59
  ]
227
60
  _, values = self.req_act(acts)
228
61
 
229
- status.openvpn_enable = values['0']['enable'] == '1'
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
62
+ status.ipsecvpn_enable = values.get('enable') == '1'
239
63
 
240
64
  return status
241
65
 
242
66
  def set_vpn(self, vpn: VPN, enable: bool) -> None:
243
67
  acts = [
244
- self.ActItem(self.ActItem.SET, vpn.value, attrs=['enable={}'.format(int(enable))])
68
+ self.ActItem(
69
+ self.ActItem.SET,
70
+ 'IPSEC_CFG',
71
+ '1,0,0,0,0,0',
72
+ attrs=['enable={}'.format(int(enable))]
73
+ )
245
74
  ]
246
75
 
247
76
  self.req_act(acts)
248
77
 
249
78
  def logout(self) -> None:
250
- '''
251
- Logs out from the host
252
- '''
253
- if self._token is None:
254
- return
255
-
256
79
  acts = [
257
- # 8\r\n[/cgi/logout#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n
258
80
  self.ActItem(self.ActItem.CGI, '/cgi/logout')
259
81
  ]
260
82
 
@@ -264,109 +86,38 @@ class TPLinkMR200Client(TPLinkMRClient):
264
86
  if ret_code == self.HTTP_RET_OK:
265
87
  del self.req.headers["TokenID"]
266
88
 
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
89
  def get_lte_status(self) -> LTEStatus:
341
90
  status = LTEStatus()
342
91
  acts = [
343
- self.ActItem(self.ActItem.GET, 'WAN_LTE_LINK_CFG', '2,1,0,0,0,0'),
344
- self.ActItem(self.ActItem.GET, 'WAN_LTE_INTF_CFG', '2,0,0,0,0,0'),
345
- self.ActItem(self.ActItem.GET, 'LTE_NET_STATUS', '2,1,0,0,0,0'),
346
- self.ActItem(self.ActItem.GET, 'LTE_PROF_STAT', '2,1,0,0,0,0'),
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'),
347
99
  ]
348
100
  _, values = self.req_act(acts)
349
101
 
350
- status.enable = int(values['0']['enable'])
351
- status.connect_status = int(values['0']['connectStatus'])
352
- status.network_type = int(values['0']['networkType'])
353
- status.sim_status = int(values['0']['simStatus'])
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)
354
107
 
355
- status.total_statistics = int(float(values['1']['totalStatistics']))
356
- status.cur_rx_speed = int(values['1']['curRxSpeed'])
357
- status.cur_tx_speed = int(values['1']['curTxSpeed'])
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)
358
111
 
359
- status.sms_unread_count = int(values['2']['smsUnreadCount'])
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'])
112
+ status.isp_name = values['2'].get('profileName', '')
364
113
 
365
- status.isp_name = values['3']['ispName']
114
+ sms_list = self.get_sms()
115
+ status.sms_unread_count = sum(1 for m in sms_list if getattr(m, 'unread', False))
366
116
 
367
117
  return status
368
118
 
369
119
  def __get_params(self, retry=False):
120
+ self.req.headers = {'referer': f'{self.host}/', 'origin': self.host}
370
121
  try:
371
122
  r = self.req.get(f"{self.host}/cgi/getParm", timeout=5)
372
123
  result = {}
@@ -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
- (code, response) = self.req.post(url, data=data)
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
- result = self._merge_response(response)
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
@@ -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.key_aes = ''
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.aes_string, self._encryption.nn_rsa,
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.aes_string}&s={str(int(encryption.seq) + datalen)}'
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
- encryption = self._encryption
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
- key_bytes = self._encryption.key_aes.encode("utf-8")
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
@@ -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
 
@@ -310,5 +310,6 @@ class LTEStatus:
310
310
  class VPNStatus:
311
311
  openvpn_enable: bool | None = None
312
312
  pptpvpn_enable: bool | None = None
313
+ ipsecvpn_enable: bool | None = None
313
314
  openvpn_clients_total: int = 0
314
315
  pptpvpn_clients_total: int = 0
@@ -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))
@@ -49,6 +49,7 @@ class Connection(Enum):
49
49
  class VPN(Enum):
50
50
  OPEN_VPN = 'OPENVPN'
51
51
  PPTP_VPN = 'PPTPVPN'
52
+ IPSEC = 'IPSEC'
52
53
 
53
54
  @property
54
55
  def lowercase(self) -> str:
@@ -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.ex import TPLinkEXClient
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, TPLinkEXClient, TPLinkMRClient, TPLinkDecoClient,
23
- TPLinkXDRClient, TplinkRouter, TplinkC80Router, TplinkWDRRouter, TplinkRE330Router]:
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 or '
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.11.0
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
@@ -8,30 +8,30 @@ test/test_client_mr.py,sha256=lePxkmjcPzcrSFcaT8bT67L154cVJIOWrFlXMDOa8oY,33423
8
8
  test/test_client_re330.py,sha256=MgefuvOzfZtZOujrcOsjiTDiGEAujfeFXshcq7gn32Q,17044
9
9
  test/test_client_wdr.py,sha256=0ZnRNP57MbuMv2cxFS8iIoVyv8Q6gtY0Q03gtHp9AWY,13492
10
10
  test/test_client_xdr.py,sha256=mgn-xL5mD5sHD8DjTz9vpY7jeh4Ob6Um6Y8v5Qgx2jA,23374
11
- tplinkrouterc6u/__init__.py,sha256=WHMzO1mOGJqx7n89jHfNtieehlE3JEF17gtFmyZrqRM,1124
11
+ tplinkrouterc6u/__init__.py,sha256=33L1bT4g8pVn6XpM3nebdYRtz0DXTapRtFROd3uFjmg,1162
12
12
  tplinkrouterc6u/client_abstract.py,sha256=3UYzmll774S_Gb5E0FTVO_rI3-XFM7PSklg1-V-2jls,1419
13
- tplinkrouterc6u/provider.py,sha256=uv7AjDVOnud-hiXJdjoAbzy9UWPtiwHRIHRenjkVO_o,2230
13
+ tplinkrouterc6u/provider.py,sha256=lqxw_pQ4VBYKS3jKrG1zVd4zVnlA6T8MaIRcqq3JAtM,2477
14
14
  tplinkrouterc6u/client/__init__.py,sha256=KBy3fmtA9wgyFrb0Urh2x4CkKtWVnESdp-vxmuOvq0k,27
15
15
  tplinkrouterc6u/client/c1200.py,sha256=4XEYidEGmVIJk0YQLvmTnd0Gqa7glH2gUWvjreHpWrk,3178
16
16
  tplinkrouterc6u/client/c5400x.py,sha256=9E0omBSbWY_ljrs5MTCMu5brmrLtzsDB5O62Db8lP8Q,4329
17
17
  tplinkrouterc6u/client/c6u.py,sha256=n4OMAxg0NXChYaVpWCvx3ZFUxVfynTMy-pyd1CTj9s4,19694
18
- tplinkrouterc6u/client/c80.py,sha256=ArVhza_fnXcEO-_fsQOd1l2QvmSfsswtohKxrZxEnoU,18568
18
+ tplinkrouterc6u/client/c80.py,sha256=efE0DEjEfzRFr35fjKA_hsv9YaWy_2dgLAaurDM-WQk,17665
19
19
  tplinkrouterc6u/client/deco.py,sha256=cpKRggKD2RvSmMZuD6tzsZmehAUCU9oLiTTHcZBW81Y,8898
20
- tplinkrouterc6u/client/ex.py,sha256=gXWsVKAMo4CsX_Qeihb2iCARcsA3E9m2vgIWiJB3sjs,13197
21
- tplinkrouterc6u/client/mr.py,sha256=keMii7cetOvY1iq9_gWbWMWjuPShHhLXyrbGwX4Og44,26136
22
- tplinkrouterc6u/client/mr200.py,sha256=W6MhhRxi7cjw0C8BWvPQdMFmE-eYoIGsStH88OXKiKo,15288
23
- tplinkrouterc6u/client/re330.py,sha256=ELGc-_SUE4zSnzLuI74z-E4quP8i-Jt6I4905HzE0A0,18661
20
+ tplinkrouterc6u/client/ex.py,sha256=tOcMugCViAcISULg8otp3NjdkPyuUXihcoe_0lql3AQ,14886
21
+ tplinkrouterc6u/client/mr.py,sha256=7MtnKqmtbggWBx6RIiJzlGuSyVDbC8MFynPrc34bSd0,28948
22
+ tplinkrouterc6u/client/mr200.py,sha256=-5CjqK8rbCAyiP7XPLlSJO3xqSPnQ8DeQ6S1jo4XKnc,5786
23
+ tplinkrouterc6u/client/re330.py,sha256=9Wj4VpYJbVwZJUh9s3magdeL3Jl-B7qyrWfrVBxRk4A,17465
24
24
  tplinkrouterc6u/client/vr.py,sha256=7Tbu0IrWtr4HHtyrnLFXEJi1QctzhilciL7agtwQ0R8,5025
25
25
  tplinkrouterc6u/client/wdr.py,sha256=i54PEifjhfOScDpgNBXygw9U4bfsVtle846_YjnDoBs,21679
26
- tplinkrouterc6u/client/xdr.py,sha256=QaZ_5vCaf8BV_JEs3S2Nz-QDREBYHGh3OUWIVS-fefY,10406
26
+ tplinkrouterc6u/client/xdr.py,sha256=jdDhQZgJjReN3wMWKch4VHEun2OzUFyzmOWyY5-1IP8,10490
27
27
  tplinkrouterc6u/common/__init__.py,sha256=pCTvVZ9CAwgb7MxRnLx0y1rI0sTKSwT24FfxWfQXeTM,33
28
- tplinkrouterc6u/common/dataclass.py,sha256=mIQFJoDmKZccV5M3NjguIGm7N9ROEt71Weg0ExYWrEQ,7709
29
- tplinkrouterc6u/common/encryption.py,sha256=4HelTxzN6esMfDZRBt3m8bwB9Nj_biKijnCnrGWPWKg,6228
28
+ tplinkrouterc6u/common/dataclass.py,sha256=NmwN6Iqpd9Ne7Zr-R0J1OZQz28NRp5Qzh6NjVFZV_DA,7749
29
+ tplinkrouterc6u/common/encryption.py,sha256=EWfgGafOz0YgPilBndVaupnjw6JrzhVBdZkBy3oWhj0,10229
30
30
  tplinkrouterc6u/common/exception.py,sha256=_0G8ZvW5__CsGifHrsZeULdl8c6EUD071sDCQsQgrHY,140
31
31
  tplinkrouterc6u/common/helper.py,sha256=23b04fk9HuVinrZXMCS5R1rmF8uZ7eM-Cdnp7Br9NR0,572
32
- tplinkrouterc6u/common/package_enum.py,sha256=4ykL_2Pw0nDEIH_qR9UJlFF6stTgSfhPz32r8KT-sh8,1624
33
- tplinkrouterc6u-5.11.0.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
34
- tplinkrouterc6u-5.11.0.dist-info/METADATA,sha256=KfWtkeZaFzuCLfpzGduvJfqVbKW_qI3EDnNjEnMLXJM,16663
35
- tplinkrouterc6u-5.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
- tplinkrouterc6u-5.11.0.dist-info/top_level.txt,sha256=1iSCCIueqgEkrTxtQ-jiHe99jAB10zqrVdBcwvNfe_M,21
37
- tplinkrouterc6u-5.11.0.dist-info/RECORD,,
32
+ tplinkrouterc6u/common/package_enum.py,sha256=CMHVSgk4RSZyFoPi3499-sJDYg-nfnyJbz1iArFU9Hw,1644
33
+ tplinkrouterc6u-5.12.0.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
34
+ tplinkrouterc6u-5.12.0.dist-info/METADATA,sha256=ZyKhYdvdTxEjUCuFKwlbj8E5nssESZZFRmtGs65pKsk,17267
35
+ tplinkrouterc6u-5.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
+ tplinkrouterc6u-5.12.0.dist-info/top_level.txt,sha256=1iSCCIueqgEkrTxtQ-jiHe99jAB10zqrVdBcwvNfe_M,21
37
+ tplinkrouterc6u-5.12.0.dist-info/RECORD,,