tplinkrouterc6u 5.4.2__py3-none-any.whl → 5.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- test/test_client_c6u.py +2 -0
- test/test_client_c80.py +455 -0
- test/test_client_mr.py +1 -0
- tplinkrouterc6u/__init__.py +2 -1
- tplinkrouterc6u/client/c6u.py +1 -0
- tplinkrouterc6u/client/c80.py +406 -0
- tplinkrouterc6u/client/ex.py +2 -2
- tplinkrouterc6u/client/mr.py +209 -208
- tplinkrouterc6u/client/vr.py +129 -0
- tplinkrouterc6u/common/dataclass.py +1 -0
- tplinkrouterc6u/provider.py +21 -15
- {tplinkrouterc6u-5.4.2.dist-info → tplinkrouterc6u-5.6.0.dist-info}/METADATA +8 -1
- {tplinkrouterc6u-5.4.2.dist-info → tplinkrouterc6u-5.6.0.dist-info}/RECORD +16 -13
- {tplinkrouterc6u-5.4.2.dist-info → tplinkrouterc6u-5.6.0.dist-info}/WHEEL +1 -1
- {tplinkrouterc6u-5.4.2.dist-info → tplinkrouterc6u-5.6.0.dist-info}/LICENSE +0 -0
- {tplinkrouterc6u-5.4.2.dist-info → tplinkrouterc6u-5.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from logging import Logger
|
|
3
|
+
from urllib import parse
|
|
4
|
+
from base64 import b64encode, b64decode
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from ipaddress import IPv4Address
|
|
7
|
+
import re
|
|
8
|
+
from Crypto.Cipher import AES
|
|
9
|
+
from Crypto.Util.Padding import pad, unpad
|
|
10
|
+
from macaddress import EUI48
|
|
11
|
+
import requests
|
|
12
|
+
from requests import Session
|
|
13
|
+
from tplinkrouterc6u.common.package_enum import Connection
|
|
14
|
+
from tplinkrouterc6u.common.exception import ClientException
|
|
15
|
+
from tplinkrouterc6u.common.encryption import EncryptionWrapper
|
|
16
|
+
from tplinkrouterc6u.common.dataclass import Firmware, Status, IPv4Status, IPv4Reservation
|
|
17
|
+
from tplinkrouterc6u.common.dataclass import IPv4DHCPLease, Device, VPNStatus
|
|
18
|
+
from tplinkrouterc6u.client_abstract import AbstractRouter
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RouterConstants:
|
|
22
|
+
AUTH_TOKEN_INDEX1 = 3
|
|
23
|
+
AUTH_TOKEN_INDEX2 = 4
|
|
24
|
+
DEFAULT_AES_VALUE = "0000000000000000"
|
|
25
|
+
|
|
26
|
+
HOST_WIFI_2G_REQUEST = '33|1,1,0'
|
|
27
|
+
HOST_WIFI_5G_REQUEST = '33|2,1,0'
|
|
28
|
+
GUEST_WIFI_2G_REQUEST = '33|1,2,0'
|
|
29
|
+
GUEST_WIFI_5G_REQUEST = '33|2,2,0'
|
|
30
|
+
IOT_WIFI_2G_REQUEST = '33|1,9,0'
|
|
31
|
+
IOT_WIFI_5G_REQUEST = '33|2,9,0'
|
|
32
|
+
|
|
33
|
+
CONNECTION_REQUESTS_MAP = {
|
|
34
|
+
Connection.HOST_2G: HOST_WIFI_2G_REQUEST,
|
|
35
|
+
Connection.HOST_5G: HOST_WIFI_5G_REQUEST,
|
|
36
|
+
Connection.GUEST_2G: GUEST_WIFI_2G_REQUEST,
|
|
37
|
+
Connection.GUEST_5G: GUEST_WIFI_5G_REQUEST,
|
|
38
|
+
Connection.IOT_2G: IOT_WIFI_2G_REQUEST,
|
|
39
|
+
Connection.IOT_5G: IOT_WIFI_5G_REQUEST
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
CONNECTION_TYPE_MAP = {
|
|
43
|
+
'0': "Dynamic IP",
|
|
44
|
+
'1': 'Static IP',
|
|
45
|
+
'2': 'PPPoE',
|
|
46
|
+
'3': 'L2TP',
|
|
47
|
+
'4': 'PPTP'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RouterConfig:
|
|
52
|
+
"""Configuration parameters for the router."""
|
|
53
|
+
ENCODING: str = ("yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8"
|
|
54
|
+
"ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70T"
|
|
55
|
+
"OoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW")
|
|
56
|
+
KEY: str = "RDpbLfCPsJZ7fiv"
|
|
57
|
+
PAD_CHAR: str = chr(187)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class EncryptionState:
|
|
62
|
+
"""Holds encryption-related state."""
|
|
63
|
+
def __init__(self):
|
|
64
|
+
self.nn_rsa = ''
|
|
65
|
+
self.ee_rsa = ''
|
|
66
|
+
self.seq = ''
|
|
67
|
+
self.key_aes = ''
|
|
68
|
+
self.iv_aes = ''
|
|
69
|
+
self.aes_string = ''
|
|
70
|
+
self.token = ''
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TplinkC80Router(AbstractRouter):
|
|
74
|
+
DATA_REGEX = re.compile(r'id (\d+\|\d,\d,\d)\r\n(.*?)(?=\r\nid \d+\||$)', re.DOTALL)
|
|
75
|
+
|
|
76
|
+
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
77
|
+
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
78
|
+
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
79
|
+
self._session = Session()
|
|
80
|
+
self._encryption = EncryptionState()
|
|
81
|
+
|
|
82
|
+
def supports(self) -> bool:
|
|
83
|
+
response = self.request(2, 1, data='0|1,0,0')
|
|
84
|
+
return response.status_code == 200 and response.text.startswith('00000')
|
|
85
|
+
|
|
86
|
+
def authorize(self) -> None:
|
|
87
|
+
encoded_password = TplinkC80Router._encrypt_password(self.password)
|
|
88
|
+
|
|
89
|
+
# Get token encryption strings and encrypt the password
|
|
90
|
+
response = self.request(2, 1)
|
|
91
|
+
self._encryption.token = TplinkC80Router._encode_token(encoded_password, response)
|
|
92
|
+
|
|
93
|
+
# Get RSA exponent, modulus and sequence number
|
|
94
|
+
response = self.request(16, 0, data='get')
|
|
95
|
+
|
|
96
|
+
responseText = response.text.splitlines()
|
|
97
|
+
if len(responseText) < 4:
|
|
98
|
+
raise ClientException("Invalid response for RSA keys from router")
|
|
99
|
+
self._encryption.ee_rsa = responseText[1]
|
|
100
|
+
self._encryption.nn_rsa = responseText[2]
|
|
101
|
+
self._encryption.seq = responseText[3]
|
|
102
|
+
|
|
103
|
+
# Generate key and initialization vector
|
|
104
|
+
self._encryption.key_aes = RouterConstants.DEFAULT_AES_VALUE
|
|
105
|
+
self._encryption.iv_aes = RouterConstants.DEFAULT_AES_VALUE
|
|
106
|
+
self._encryption.aes_string = f'k={self._encryption.key_aes}&i={self._encryption.iv_aes}'
|
|
107
|
+
|
|
108
|
+
# Encrypt AES string
|
|
109
|
+
aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.aes_string, self._encryption.nn_rsa,
|
|
110
|
+
self._encryption.ee_rsa)
|
|
111
|
+
# Register AES string for decryption on server side
|
|
112
|
+
self.request(16, 0, True, data=f'set {aes_string_encrypted}')
|
|
113
|
+
# Some auth request, might be redundant
|
|
114
|
+
response = self.request(7, 0, True)
|
|
115
|
+
|
|
116
|
+
def logout(self) -> None:
|
|
117
|
+
self.request(11, 0, True)
|
|
118
|
+
|
|
119
|
+
def get_firmware(self) -> Firmware:
|
|
120
|
+
text = '0|1,0,0'
|
|
121
|
+
|
|
122
|
+
body = self._encrypt_body(text)
|
|
123
|
+
|
|
124
|
+
response = self.request(2, 1, True, data=body)
|
|
125
|
+
response_text = self._decrypt_data(response.text)
|
|
126
|
+
device_datamap = dict(line.split(" ", 1) for line in response_text.split("\r\n")[1:-1])
|
|
127
|
+
|
|
128
|
+
return Firmware(parse.unquote(device_datamap['hardVer']), parse.unquote(device_datamap['modelName']),
|
|
129
|
+
parse.unquote(device_datamap['softVer']))
|
|
130
|
+
|
|
131
|
+
def get_status(self) -> Status:
|
|
132
|
+
mac_info_request = "1|1,0,0"
|
|
133
|
+
lan_ip_request = "4|1,0,0"
|
|
134
|
+
wan_ip_request = "23|1,0,0"
|
|
135
|
+
device_data_request = '13|1,0,0'
|
|
136
|
+
all_requests = [
|
|
137
|
+
mac_info_request, lan_ip_request, wan_ip_request, device_data_request,
|
|
138
|
+
RouterConstants.HOST_WIFI_2G_REQUEST, RouterConstants.HOST_WIFI_5G_REQUEST,
|
|
139
|
+
RouterConstants.GUEST_WIFI_2G_REQUEST, RouterConstants.GUEST_WIFI_5G_REQUEST,
|
|
140
|
+
RouterConstants.IOT_WIFI_2G_REQUEST, RouterConstants.IOT_WIFI_5G_REQUEST
|
|
141
|
+
]
|
|
142
|
+
request_text = '#'.join(all_requests)
|
|
143
|
+
body = self._encrypt_body(request_text)
|
|
144
|
+
|
|
145
|
+
response = self.request(2, 1, True, data=body)
|
|
146
|
+
response_text = self._decrypt_data(response.text)
|
|
147
|
+
|
|
148
|
+
matches = TplinkC80Router.DATA_REGEX.findall(response_text)
|
|
149
|
+
|
|
150
|
+
data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
|
|
151
|
+
|
|
152
|
+
def extract_value(response_list, prefix):
|
|
153
|
+
return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
|
|
154
|
+
|
|
155
|
+
network_info = {
|
|
156
|
+
'lan_mac': extract_value(data_blocks[mac_info_request], "mac 0 "),
|
|
157
|
+
'wan_mac': extract_value(data_blocks[mac_info_request], "mac 1 "),
|
|
158
|
+
'lan_ip': extract_value(data_blocks[lan_ip_request], "ip "),
|
|
159
|
+
'wan_ip': extract_value(data_blocks[wan_ip_request], "ip "),
|
|
160
|
+
'gateway_ip': extract_value(data_blocks[wan_ip_request], "gateway "),
|
|
161
|
+
'uptime': extract_value(data_blocks[wan_ip_request], "upTime ")
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
wifi_status = {key: extract_value(data_blocks[request], "bEnable ") == '1'
|
|
165
|
+
for key, request in RouterConstants.CONNECTION_REQUESTS_MAP.items()}
|
|
166
|
+
|
|
167
|
+
device_data_response = data_blocks[device_data_request]
|
|
168
|
+
|
|
169
|
+
mapped_devices = self._parse_devices(device_data_response)
|
|
170
|
+
|
|
171
|
+
status = Status()
|
|
172
|
+
status._wan_macaddr = EUI48(network_info['wan_mac'])
|
|
173
|
+
status._lan_macaddr = EUI48(network_info['lan_mac'])
|
|
174
|
+
status._lan_ipv4_addr = IPv4Address(network_info['lan_ip'])
|
|
175
|
+
status._wan_ipv4_addr = IPv4Address(network_info['wan_ip'])
|
|
176
|
+
status._wan_ipv4_gateway = IPv4Address(network_info['gateway_ip'])
|
|
177
|
+
status.wan_ipv4_uptime = int(network_info['uptime']) // 100
|
|
178
|
+
|
|
179
|
+
status.wifi_2g_enable = wifi_status[Connection.HOST_2G]
|
|
180
|
+
status.wifi_5g_enable = wifi_status[Connection.HOST_5G]
|
|
181
|
+
status.guest_2g_enable = wifi_status[Connection.GUEST_2G]
|
|
182
|
+
status.guest_5g_enable = wifi_status[Connection.GUEST_5G]
|
|
183
|
+
status.iot_2g_enable = wifi_status[Connection.IOT_2G]
|
|
184
|
+
status.iot_5g_enable = wifi_status[Connection.IOT_5G]
|
|
185
|
+
|
|
186
|
+
status.wired_total = sum(1 for device in mapped_devices if device.type == Connection.WIRED)
|
|
187
|
+
status.wifi_clients_total = sum(1 for device in mapped_devices
|
|
188
|
+
if device.type in (Connection.HOST_2G, Connection.HOST_5G))
|
|
189
|
+
status.guest_clients_total = sum(1 for device in mapped_devices
|
|
190
|
+
if device.type in (Connection.GUEST_2G, Connection.GUEST_5G))
|
|
191
|
+
status.iot_clients_total = sum(1 for device in mapped_devices
|
|
192
|
+
if device.type in (Connection.IOT_2G, Connection.IOT_5G))
|
|
193
|
+
status.clients_total = (status.wired_total + status.wifi_clients_total +
|
|
194
|
+
status.guest_clients_total + status.iot_clients_total)
|
|
195
|
+
|
|
196
|
+
status.devices = mapped_devices
|
|
197
|
+
return status
|
|
198
|
+
|
|
199
|
+
def reboot(self) -> None:
|
|
200
|
+
self.request(6, 1, True)
|
|
201
|
+
|
|
202
|
+
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
203
|
+
enable_string = f'bEnable {int(enable)}'
|
|
204
|
+
text = f'id {RouterConstants.CONNECTION_REQUESTS_MAP[wifi]}\r\n{enable_string}'
|
|
205
|
+
body = self._encrypt_body(text)
|
|
206
|
+
self.request(1, 0, True, data=body)
|
|
207
|
+
|
|
208
|
+
def get_ipv4_status(self) -> IPv4Status:
|
|
209
|
+
mac_info_request = "1|1,0,0"
|
|
210
|
+
lan_ip_request = "4|1,0,0"
|
|
211
|
+
dhcp_request = "8|1,0,0"
|
|
212
|
+
link_type_request = "22|1,0,0"
|
|
213
|
+
wan_ip_request = "23|1,0,0"
|
|
214
|
+
static_ip_request = "24|1,0,0"
|
|
215
|
+
all_requests = [
|
|
216
|
+
mac_info_request, lan_ip_request, dhcp_request, link_type_request, wan_ip_request, static_ip_request]
|
|
217
|
+
request_text = '#'.join(all_requests)
|
|
218
|
+
body = self._encrypt_body(request_text)
|
|
219
|
+
|
|
220
|
+
response = self.request(2, 1, True, data=body)
|
|
221
|
+
response_text = self._decrypt_data(response.text)
|
|
222
|
+
|
|
223
|
+
matches = TplinkC80Router.DATA_REGEX.findall(response_text)
|
|
224
|
+
|
|
225
|
+
data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
|
|
226
|
+
|
|
227
|
+
network_info = {
|
|
228
|
+
'lan_mac': self._extract_value(data_blocks[mac_info_request], "mac 0 "),
|
|
229
|
+
'wan_mac': self._extract_value(data_blocks[mac_info_request], "mac 1 "),
|
|
230
|
+
'lan_ip': self._extract_value(data_blocks[lan_ip_request], "ip "),
|
|
231
|
+
'wan_ip': self._extract_value(data_blocks[wan_ip_request], "ip "),
|
|
232
|
+
'gateway_ip': self._extract_value(data_blocks[wan_ip_request], "gateway "),
|
|
233
|
+
'uptime': self._extract_value(data_blocks[wan_ip_request], "upTime "),
|
|
234
|
+
'wan_mask': self._extract_value(data_blocks[wan_ip_request], "mask "),
|
|
235
|
+
'lan_mask': self._extract_value(data_blocks[lan_ip_request], "mask "),
|
|
236
|
+
'dns_1': self._extract_value(data_blocks[wan_ip_request], "dns 0 "),
|
|
237
|
+
'dns_2': self._extract_value(data_blocks[wan_ip_request], "dns 1 "),
|
|
238
|
+
'dhcp_enabled': self._extract_value(data_blocks[dhcp_request], "enable "),
|
|
239
|
+
'link_type': self._extract_value(data_blocks[link_type_request], "linkType "),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
ipv4status = IPv4Status()
|
|
243
|
+
ipv4status._wan_macaddr = EUI48(network_info['wan_mac'])
|
|
244
|
+
ipv4status._wan_ipv4_ipaddr = IPv4Address(network_info['wan_ip'])
|
|
245
|
+
ipv4status._wan_ipv4_gateway = IPv4Address(network_info['gateway_ip'])
|
|
246
|
+
ipv4status._wan_ipv4_conntype = RouterConstants.CONNECTION_TYPE_MAP[network_info['link_type']]
|
|
247
|
+
ipv4status._wan_ipv4_netmask = IPv4Address(network_info['wan_mask'])
|
|
248
|
+
ipv4status._wan_ipv4_pridns = IPv4Address(network_info['dns_1'])
|
|
249
|
+
ipv4status._wan_ipv4_snddns = IPv4Address(network_info['dns_2'])
|
|
250
|
+
ipv4status._lan_macaddr = EUI48(network_info['lan_mac'])
|
|
251
|
+
ipv4status._lan_ipv4_ipaddr = IPv4Address(network_info['lan_ip'])
|
|
252
|
+
ipv4status.lan_ipv4_dhcp_enable = network_info['dhcp_enabled'] == '1'
|
|
253
|
+
ipv4status._lan_ipv4_netmask = IPv4Address(network_info['lan_mask'])
|
|
254
|
+
return ipv4status
|
|
255
|
+
|
|
256
|
+
def get_ipv4_reservations(self) -> list[IPv4Reservation]:
|
|
257
|
+
body = self._encrypt_body('12|1,0,0')
|
|
258
|
+
|
|
259
|
+
response = self.request(2, 1, True, data=body)
|
|
260
|
+
response_text = self._decrypt_data(response.text)
|
|
261
|
+
matches = TplinkC80Router.DATA_REGEX.findall(response_text)
|
|
262
|
+
|
|
263
|
+
data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
|
|
264
|
+
filtered_reservations = self._parse_response_to_dict(data_blocks['12|1,0,0'])
|
|
265
|
+
|
|
266
|
+
mapped_reservations: list[IPv4Reservation] = []
|
|
267
|
+
for reservation in filtered_reservations:
|
|
268
|
+
reservation_to_add = IPv4Reservation(EUI48(reservation['mac']), IPv4Address(reservation['ip']),
|
|
269
|
+
reservation['name'], reservation['dhcpsEnable'] == '1')
|
|
270
|
+
mapped_reservations.append(reservation_to_add)
|
|
271
|
+
return mapped_reservations
|
|
272
|
+
|
|
273
|
+
def get_dhcp_leases(self) -> list[IPv4DHCPLease]:
|
|
274
|
+
body = self._encrypt_body('9|1,0,0')
|
|
275
|
+
|
|
276
|
+
response = self.request(2, 1, True, data=body)
|
|
277
|
+
response_text = self._decrypt_data(response.text)
|
|
278
|
+
matches = TplinkC80Router.DATA_REGEX.findall(response_text)
|
|
279
|
+
|
|
280
|
+
data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
|
|
281
|
+
|
|
282
|
+
filtered_leases = self._parse_response_to_dict(data_blocks['9|1,0,0'])
|
|
283
|
+
|
|
284
|
+
mapped_leases: list[IPv4DHCPLease] = []
|
|
285
|
+
for lease in filtered_leases:
|
|
286
|
+
lease_to_add = IPv4DHCPLease(EUI48(lease['mac']), IPv4Address(lease['ip']),
|
|
287
|
+
lease['hostName'], f'expires {lease["expires"]}')
|
|
288
|
+
mapped_leases.append(lease_to_add)
|
|
289
|
+
|
|
290
|
+
return mapped_leases
|
|
291
|
+
|
|
292
|
+
def get_vpn_status(self) -> VPNStatus:
|
|
293
|
+
body = self._encrypt_body("22|1,0,0")
|
|
294
|
+
|
|
295
|
+
response = self.request(2, 1, True, data=body)
|
|
296
|
+
response_text = self._decrypt_data(response.text)
|
|
297
|
+
matches = TplinkC80Router.DATA_REGEX.findall(response_text)
|
|
298
|
+
|
|
299
|
+
data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
|
|
300
|
+
|
|
301
|
+
vpn_status = VPNStatus()
|
|
302
|
+
vpn_status.pptpvpn_enable = self._extract_value(data_blocks["22|1,0,0"], "linkType ") == '4'
|
|
303
|
+
|
|
304
|
+
return vpn_status
|
|
305
|
+
|
|
306
|
+
def _parse_devices(self, device_data_response: list[str]) -> list[Device]:
|
|
307
|
+
filtered_devices = self._parse_response_to_dict(device_data_response)
|
|
308
|
+
|
|
309
|
+
device_type_to_connection = {
|
|
310
|
+
0: Connection.WIRED,
|
|
311
|
+
1: Connection.HOST_2G, 2: Connection.GUEST_2G,
|
|
312
|
+
3: Connection.HOST_5G, 4: Connection.GUEST_5G,
|
|
313
|
+
13: Connection.IOT_2G, 14: Connection.IOT_5G
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
mapped_devices = []
|
|
317
|
+
for device in filtered_devices:
|
|
318
|
+
if device['online'] == '1':
|
|
319
|
+
device_type = int(device['type'])
|
|
320
|
+
connection_type = device_type_to_connection.get(device_type, Connection.UNKNOWN)
|
|
321
|
+
else:
|
|
322
|
+
connection_type = Connection.UNKNOWN
|
|
323
|
+
|
|
324
|
+
device_to_add = Device(connection_type, EUI48(device['mac']), IPv4Address(device['ip']), device['name'])
|
|
325
|
+
device_to_add.up_speed = int(device['up'])
|
|
326
|
+
device_to_add.down_speed = int(device['down'])
|
|
327
|
+
mapped_devices.append(device_to_add)
|
|
328
|
+
return mapped_devices
|
|
329
|
+
|
|
330
|
+
def _parse_response_to_dict(self, response_data: list[str]) -> list[dict]:
|
|
331
|
+
result_dict = defaultdict(dict)
|
|
332
|
+
for entry in response_data:
|
|
333
|
+
parts = entry.split(' ', 2)
|
|
334
|
+
key, id_str = parts[0], parts[1]
|
|
335
|
+
value = parts[2] if len(parts) == 3 else ''
|
|
336
|
+
result_dict[int(id_str)][key] = value
|
|
337
|
+
|
|
338
|
+
return [v for _, v in result_dict.items() if v.get("ip") != "0.0.0.0"]
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _encrypt_password(pwd: str, key: str = RouterConfig.KEY, encoding: str = RouterConfig.ENCODING) -> str:
|
|
342
|
+
max_len = max(len(key), len(pwd))
|
|
343
|
+
pwd = pwd.ljust(max_len, RouterConfig.PAD_CHAR)
|
|
344
|
+
key = key.ljust(max_len, RouterConfig.PAD_CHAR)
|
|
345
|
+
|
|
346
|
+
result = []
|
|
347
|
+
for i in range(max_len):
|
|
348
|
+
result.append(encoding[(ord(pwd[i]) ^ ord(key[i])) % len(encoding)])
|
|
349
|
+
|
|
350
|
+
return "".join(result)
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def _encode_token(encoded_password: str, response: str) -> str:
|
|
354
|
+
response_text = response.text.splitlines()
|
|
355
|
+
auth_info1 = response_text[RouterConstants.AUTH_TOKEN_INDEX1]
|
|
356
|
+
auth_info2 = response_text[RouterConstants.AUTH_TOKEN_INDEX2]
|
|
357
|
+
|
|
358
|
+
encoded_token = TplinkC80Router._encrypt_password(encoded_password, auth_info1, auth_info2)
|
|
359
|
+
return parse.quote(encoded_token, safe='!()*')
|
|
360
|
+
|
|
361
|
+
def _get_signature(self, datalen: int) -> str:
|
|
362
|
+
encryption = self._encryption
|
|
363
|
+
r = f'{encryption.aes_string}&s={str(int(encryption.seq) + datalen)}'
|
|
364
|
+
e = ''
|
|
365
|
+
n = 0
|
|
366
|
+
while n < len(r):
|
|
367
|
+
e += EncryptionWrapper.rsa_encrypt(r[n:53], encryption.nn_rsa, encryption.ee_rsa)
|
|
368
|
+
n += 53
|
|
369
|
+
return e
|
|
370
|
+
|
|
371
|
+
def _encrypt_body(self, text: str) -> str:
|
|
372
|
+
encryption = self._encryption
|
|
373
|
+
|
|
374
|
+
key_bytes = encryption.key_aes.encode("utf-8")
|
|
375
|
+
iv_bytes = encryption.iv_aes.encode("utf-8")
|
|
376
|
+
|
|
377
|
+
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
|
378
|
+
data = b64encode(cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))).decode()
|
|
379
|
+
|
|
380
|
+
sign = self._get_signature(len(data))
|
|
381
|
+
return f'sign={sign}\r\ndata={data}'
|
|
382
|
+
|
|
383
|
+
def _decrypt_data(self, encrypted_text: str) -> str:
|
|
384
|
+
key_bytes = self._encryption.key_aes.encode("utf-8")
|
|
385
|
+
iv_bytes = self._encryption.iv_aes.encode("utf-8")
|
|
386
|
+
|
|
387
|
+
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
|
388
|
+
decrypted_padded = cipher.decrypt(b64decode(encrypted_text))
|
|
389
|
+
return unpad(decrypted_padded, AES.block_size).decode("utf-8")
|
|
390
|
+
|
|
391
|
+
def _extract_value(self, response_list, prefix):
|
|
392
|
+
return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
|
|
393
|
+
|
|
394
|
+
def request(self, code: int, asyn: int, use_token: bool = False, data: str = None):
|
|
395
|
+
url = f"{self.host}/?code={code}&asyn={asyn}"
|
|
396
|
+
if use_token:
|
|
397
|
+
url += f"&id={self._encryption.token}"
|
|
398
|
+
try:
|
|
399
|
+
response = self._session.post(url, data=data, timeout=self.timeout)
|
|
400
|
+
# Raises exception for 4XX/5XX status codes for all requests except 1st in authorize
|
|
401
|
+
if not (code == 2 and asyn == 1 and use_token is False and data is None):
|
|
402
|
+
response.raise_for_status()
|
|
403
|
+
return response
|
|
404
|
+
except requests.exceptions.RequestException as e:
|
|
405
|
+
self._logger.error(f"Network error: {e}")
|
|
406
|
+
raise ClientException(f"Network error: {str(e)}") from e
|
tplinkrouterc6u/client/ex.py
CHANGED
|
@@ -49,7 +49,6 @@ class TPLinkEXClient(TPLinkMRClientBase):
|
|
|
49
49
|
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
50
50
|
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
51
51
|
|
|
52
|
-
self.username = 'user'
|
|
53
52
|
self._url_rsa_key = 'cgi/getGDPRParm'
|
|
54
53
|
|
|
55
54
|
def logout(self) -> None:
|
|
@@ -291,7 +290,8 @@ class TPLinkEXClient(TPLinkMRClientBase):
|
|
|
291
290
|
ret_code = self._parse_ret_val(response)
|
|
292
291
|
error = ''
|
|
293
292
|
if ret_code == self.HTTP_ERR_USER_PWD_NOT_CORRECT:
|
|
294
|
-
error = 'TplinkRouter - EX - Login failed, wrong user or password.'
|
|
293
|
+
error = ('TplinkRouter - EX - Login failed, wrong user or password. '
|
|
294
|
+
'Try to pass user instead of admin in username')
|
|
295
295
|
elif ret_code == self.HTTP_ERR_USER_BAD_REQUEST:
|
|
296
296
|
error = 'TplinkRouter - EX - Login failed. Generic error code: {}'.format(ret_code)
|
|
297
297
|
elif ret_code != self.HTTP_RET_OK:
|