tplinkrouterc6u 5.10.3__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,14 +1,26 @@
1
1
  import base64
2
- from tplinkrouterc6u.common.exception import ClientException, AuthorizeError
3
2
  from tplinkrouterc6u.client.mr import TPLinkMRClient
4
3
  from Crypto.PublicKey import RSA
5
- from re import search
6
4
  from binascii import hexlify
7
5
  from Crypto.Cipher import PKCS1_v1_5
6
+ from re import search
7
+ from tplinkrouterc6u.common.package_enum import VPN
8
+ from tplinkrouterc6u.common.dataclass import (
9
+ LTEStatus,
10
+ VPNStatus,
11
+ )
12
+ from tplinkrouterc6u.common.exception import ClientException, ClientError, AuthorizeError
8
13
 
9
14
 
10
15
  class TPLinkMR200Client(TPLinkMRClient):
11
16
 
17
+ def supports(self) -> bool:
18
+ try:
19
+ self.__get_params()
20
+ return True
21
+ except ClientException:
22
+ return False
23
+
12
24
  def authorize(self) -> None:
13
25
  params = self.__get_params()
14
26
 
@@ -36,11 +48,76 @@ class TPLinkMR200Client(TPLinkMRClient):
36
48
  # Try to extract token
37
49
  r = self.req.get(self.host)
38
50
  try:
39
- self._token = search(r'var token="(.*)";', r.text).group(1)
51
+ self.req.headers["TokenID"] = search(r'var token="(.*)";', r.text).group(1)
40
52
  except AttributeError:
41
53
  raise AuthorizeError()
42
54
 
55
+ def get_vpn_status(self) -> VPNStatus:
56
+ status = VPNStatus()
57
+ acts = [
58
+ self.ActItem(self.ActItem.GL, 'IPSEC_CFG'),
59
+ ]
60
+ _, values = self.req_act(acts)
61
+
62
+ status.ipsecvpn_enable = values.get('enable') == '1'
63
+
64
+ return status
65
+
66
+ def set_vpn(self, vpn: VPN, enable: bool) -> None:
67
+ acts = [
68
+ self.ActItem(
69
+ self.ActItem.SET,
70
+ 'IPSEC_CFG',
71
+ '1,0,0,0,0,0',
72
+ attrs=['enable={}'.format(int(enable))]
73
+ )
74
+ ]
75
+
76
+ self.req_act(acts)
77
+
78
+ def logout(self) -> None:
79
+ acts = [
80
+ self.ActItem(self.ActItem.CGI, '/cgi/logout')
81
+ ]
82
+
83
+ response, _ = self.req_act(acts)
84
+ ret_code = self._parse_ret_val(response)
85
+
86
+ if ret_code == self.HTTP_RET_OK:
87
+ del self.req.headers["TokenID"]
88
+
89
+ def get_lte_status(self) -> LTEStatus:
90
+ status = LTEStatus()
91
+ acts = [
92
+ self.ActItem(self.ActItem.GET, 'WAN_LTE_LINK_CFG', '2,1,0,0,0,0',
93
+ attrs=['enable', 'connectStatus', 'networkType', 'roamingStatus', 'simStatus']),
94
+ self.ActItem(self.ActItem.GET, 'WAN_LTE_INTF_CFG', '2,0,0,0,0,0',
95
+ attrs=['dataLimit', 'enablePaymentDay', 'curStatistics', 'totalStatistics', 'enableDataLimit',
96
+ 'limitation',
97
+ 'curRxSpeed', 'curTxSpeed']),
98
+ self.ActItem(self.ActItem.GET, 'LTE_WAN_CFG', '2,1,0,0,0,0'),
99
+ ]
100
+ _, values = self.req_act(acts)
101
+
102
+ status.enable = values['0'].get('enable', 0)
103
+ status.connect_status = values['0'].get('connectStatus', 0)
104
+ status.network_type = values['0'].get('networkType', 0)
105
+ status.sim_status = values['0'].get('simStatus', 0)
106
+ status.sig_level = values['0'].get('signalStrength', 0)
107
+
108
+ status.total_statistics = values['1'].get('totalStatistics', 0)
109
+ status.cur_rx_speed = values['1'].get('curRxSpeed', 0)
110
+ status.cur_tx_speed = values['1'].get('curTxSpeed', 0)
111
+
112
+ status.isp_name = values['2'].get('profileName', '')
113
+
114
+ sms_list = self.get_sms()
115
+ status.sms_unread_count = sum(1 for m in sms_list if getattr(m, 'unread', False))
116
+
117
+ return status
118
+
43
119
  def __get_params(self, retry=False):
120
+ self.req.headers = {'referer': f'{self.host}/', 'origin': self.host}
44
121
  try:
45
122
  r = self.req.get(f"{self.host}/cgi/getParm", timeout=5)
46
123
  result = {}
@@ -53,5 +130,36 @@ class TPLinkMR200Client(TPLinkMRClient):
53
130
  return self.__get_params(True)
54
131
  raise ClientException()
55
132
 
56
- def _request(self, url, method='POST', data_str=None, encrypt=False, is_login=False):
57
- return super()._request(url, method, data_str, False, is_login)
133
+ def req_act(self, acts: list):
134
+ '''
135
+ Requests ACTs via the cgi_gdpr proxy
136
+ '''
137
+ act_types = []
138
+ act_data = []
139
+
140
+ for act in acts:
141
+ act_types.append(str(act.type))
142
+ act_data.append('[{}#{}#{}]{},{}\r\n{}\r\n'.format(
143
+ act.oid,
144
+ act.stack,
145
+ act.pstack,
146
+ len(act_types) - 1, # index, starts at 0
147
+ len(act.attrs),
148
+ '\r\n'.join(act.attrs)
149
+ ))
150
+
151
+ data = ''.join(act_data)
152
+ url = f"{self.host}/cgi?" + '&'.join(act_types)
153
+ response = self.req.post(url, data=data)
154
+ code = response.status_code
155
+
156
+ if code != 200:
157
+ error = 'TplinkRouter - MR200 - Response with error; Request {} - Response {}'.format(data, response.text)
158
+ if self._logger:
159
+ self._logger.debug(error)
160
+ raise ClientError(error)
161
+
162
+ # Convert Response to string for _merge_response
163
+ result = self._merge_response(response.text)
164
+
165
+ return response, result.get('0') if len(result) == 1 and result.get('0') else result
@@ -0,0 +1,393 @@
1
+ from dataclasses import dataclass
2
+ from logging import Logger
3
+ from urllib import parse
4
+ from collections import defaultdict
5
+ from ipaddress import IPv4Address
6
+ import re
7
+ from macaddress import EUI48
8
+ import requests
9
+ from requests import Session
10
+ from tplinkrouterc6u.common.package_enum import Connection
11
+ from tplinkrouterc6u.common.exception import ClientException
12
+ from tplinkrouterc6u.common.encryption import EncryptionWrapper
13
+ from tplinkrouterc6u.common.dataclass import Firmware, Status, IPv4Status, IPv4Reservation
14
+ from tplinkrouterc6u.common.dataclass import IPv4DHCPLease, Device
15
+ from tplinkrouterc6u.client_abstract import AbstractRouter
16
+
17
+
18
+ class RouterConstants:
19
+ AUTH_TOKEN_INDEX1 = 3
20
+ AUTH_TOKEN_INDEX2 = 4
21
+
22
+ HOST_WIFI_2G_REQUEST = '33|1,1,0'
23
+ HOST_WIFI_5G_REQUEST = '33|2,1,0'
24
+
25
+ CONNECTION_REQUESTS_MAP = {
26
+ Connection.HOST_2G: HOST_WIFI_2G_REQUEST,
27
+ Connection.HOST_5G: HOST_WIFI_5G_REQUEST,
28
+ }
29
+
30
+ CONNECTION_TYPE_MAP = {
31
+ '0': 'Dynamic IP',
32
+ '1': 'Static IP',
33
+ '2': 'PPPoE',
34
+ '3': 'L2TP',
35
+ '4': 'PPTP'
36
+ }
37
+
38
+
39
+ class RouterConfig:
40
+ """Configuration parameters for the router."""
41
+ ENCODING: str = ("yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8"
42
+ "ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70T"
43
+ "OoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW")
44
+ KEY: str = "RDpbLfCPsJZ7fiv"
45
+ PAD_CHAR: str = chr(187)
46
+
47
+
48
+ @dataclass
49
+ class EncryptionState:
50
+ """Holds encryption-related state."""
51
+
52
+ def __init__(self):
53
+ self.nn_rsa = ''
54
+ self.ee_rsa = ''
55
+ self.seq = ''
56
+ self.aes = EncryptionWrapper()
57
+ self.token = ''
58
+
59
+
60
+ # Note: This router doesn't support VPN and up/down speeds per device
61
+ class TplinkRE330Router(AbstractRouter):
62
+ DATA_REGEX = re.compile(r'id (\d+\|\d,\d,\d)\r\n(.*?)(?=\r\nid \d+\||$)', re.DOTALL)
63
+
64
+ def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
65
+ verify_ssl: bool = True, timeout: int = 30) -> None:
66
+ super().__init__(host, password, username, logger, verify_ssl, timeout)
67
+ self._session = Session()
68
+ if self._verify_ssl is False:
69
+ self._session.verify = False
70
+ self._encryption = EncryptionState()
71
+ self.host = self.host.rstrip('/')
72
+ # Mandatory Referer header
73
+ self._headers = {
74
+ 'Referer': self.host + '/'
75
+ }
76
+
77
+ def _init_session(self) -> None:
78
+ self._session.get(self.host + "/", headers=self._headers)
79
+
80
+ def supports(self) -> bool:
81
+ try:
82
+ response = self.request(2, 0, data='50|1,0,0')
83
+ return response.status_code == 200 and response.text.startswith('00000')
84
+ except Exception:
85
+ return False
86
+
87
+ def authorize(self) -> None:
88
+ # Init session and connexion
89
+ self._init_session()
90
+ encoded_password = TplinkRE330Router._encrypt_password(self.password)
91
+
92
+ # Get token encryption strings and encrypt the password
93
+ response = self.request(7, 1)
94
+ self._encryption.token = TplinkRE330Router._encode_token(encoded_password, response)
95
+
96
+ # Get RSA exponent, modulus and sequence number
97
+ response = self.request(16, 0, data='get')
98
+
99
+ responseText = response.text.splitlines()
100
+ if len(responseText) < 4:
101
+ raise ClientException("Invalid response for RSA keys from router")
102
+ self._encryption.ee_rsa = responseText[1]
103
+ self._encryption.nn_rsa = responseText[2]
104
+ self._encryption.seq = responseText[3]
105
+
106
+ # Encrypt AES string
107
+ aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.aes._get_aes_string(),
108
+ self._encryption.nn_rsa,
109
+ self._encryption.ee_rsa)
110
+ # Mandatory intermediate request
111
+ response = self.request(7, 0, True)
112
+ # Register AES string for decryption on server side
113
+ self.request(16, 0, True, data=f'set {aes_string_encrypted}')
114
+
115
+ def logout(self) -> None:
116
+ self.request(11, 0, True)
117
+
118
+ def get_firmware(self) -> Firmware:
119
+ text = '0|1,0,0'
120
+
121
+ body = self._encrypt_body(text)
122
+ response = self.request(2, 1, True, data=body)
123
+ response_text = self._decrypt_data(response.text)
124
+ device_datamap = dict(line.split(" ", 1) for line in response_text.split("\r\n")[1:-1])
125
+
126
+ return Firmware(parse.unquote(device_datamap['hardVer']), parse.unquote(device_datamap['modelName']),
127
+ parse.unquote(device_datamap['softVer']))
128
+
129
+ def get_status(self) -> Status:
130
+ mac_info_request = "1|1,0,0"
131
+ lan_ip_request = "4|1,0,0"
132
+ wan_ip_request = "23|1,0,0"
133
+ device_data_request = '13|1,0,0'
134
+ all_requests = [
135
+ mac_info_request, lan_ip_request, wan_ip_request, device_data_request,
136
+ RouterConstants.HOST_WIFI_2G_REQUEST, RouterConstants.HOST_WIFI_5G_REQUEST
137
+ ]
138
+ request_text = '#'.join(all_requests)
139
+ body = self._encrypt_body(request_text)
140
+
141
+ response = self.request(2, 1, True, data=body)
142
+ response_text = self._decrypt_data(response.text)
143
+
144
+ matches = TplinkRE330Router.DATA_REGEX.findall(response_text)
145
+
146
+ data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
147
+
148
+ def extract_value(response_list, prefix):
149
+ return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
150
+
151
+ network_info = {
152
+ 'lan_mac': extract_value(data_blocks[mac_info_request], "mac 0 "),
153
+ 'wan_mac': extract_value(data_blocks[mac_info_request], "mac 1 "),
154
+ 'lan_ip': extract_value(data_blocks[lan_ip_request], "ip "),
155
+ 'wan_ip': extract_value(data_blocks[wan_ip_request], "ip "),
156
+ 'gateway_ip': extract_value(data_blocks[wan_ip_request], "gateway "),
157
+ 'uptime': extract_value(data_blocks[wan_ip_request], "upTime ")
158
+ }
159
+
160
+ wifi_status = {}
161
+ for key, request in RouterConstants.CONNECTION_REQUESTS_MAP.items():
162
+ value = data_blocks.get(request)
163
+ wifi_status[key] = extract_value(data_blocks.get(request), "bEnable ") == '1' if value else None
164
+
165
+ device_data_response = data_blocks[device_data_request]
166
+
167
+ mapped_devices = self._parse_devices(device_data_response)
168
+
169
+ status = Status()
170
+ status._wan_macaddr = EUI48(network_info['wan_mac'])
171
+ status._lan_macaddr = EUI48(network_info['lan_mac'])
172
+ status._lan_ipv4_addr = IPv4Address(network_info['lan_ip'])
173
+ status._wan_ipv4_addr = IPv4Address(network_info['wan_ip'])
174
+ status._wan_ipv4_gateway = IPv4Address(network_info['gateway_ip'])
175
+ status.wan_ipv4_uptime = int(network_info['uptime']) // 100
176
+
177
+ status.wifi_2g_enable = wifi_status[Connection.HOST_2G]
178
+ status.wifi_5g_enable = wifi_status[Connection.HOST_5G]
179
+
180
+ status.wired_total = sum(1 for device in mapped_devices if device.type == Connection.WIRED)
181
+ status.wifi_clients_total = sum(1 for device in mapped_devices
182
+ if device.type in (Connection.HOST_2G, Connection.HOST_5G))
183
+ status.guest_clients_total = sum(1 for device in mapped_devices
184
+ if device.type in (Connection.GUEST_2G, Connection.GUEST_5G))
185
+ status.iot_clients_total = sum(1 for device in mapped_devices
186
+ if device.type in (Connection.IOT_2G, Connection.IOT_5G))
187
+ status.clients_total = (status.wired_total + status.wifi_clients_total +
188
+ status.guest_clients_total + status.iot_clients_total)
189
+
190
+ status.devices = mapped_devices
191
+ return status
192
+
193
+ def set_led_status(self, status: bool) -> None:
194
+ text = f'id 112|1,0,0\r\nenable {1 if status else 0}\r\n'
195
+ body = self._encrypt_body(text)
196
+ self.request(1, 0, True, data=body)
197
+
198
+ def get_led_status(self) -> bool:
199
+ text = '112|1,0,0'
200
+ body = self._encrypt_body(text)
201
+ response = self.request(2, 0, True, data=body)
202
+
203
+ response_text = self._decrypt_data(response.text)
204
+ response_text = response_text.splitlines()
205
+ if len(response_text) < 3:
206
+ raise ClientException("Invalid response for LED status from router")
207
+
208
+ return response_text[2][-1:] == '1'
209
+
210
+ def reboot(self) -> None:
211
+ self.request(6, 1, True)
212
+
213
+ def set_wifi(self, wifi: Connection, enable: bool) -> None:
214
+ enable_string = f'bEnable {int(enable)}'
215
+ text = f'id {RouterConstants.CONNECTION_REQUESTS_MAP[wifi]}\r\n{enable_string}'
216
+ body = self._encrypt_body(text)
217
+ self.request(1, 0, True, data=body)
218
+
219
+ def get_ipv4_status(self) -> IPv4Status:
220
+ mac_info_request = "1|1,0,0"
221
+ lan_ip_request = "4|1,0,0"
222
+ dhcp_request = "8|1,0,0"
223
+ link_type_request = "22|1,0,0"
224
+ wan_ip_request = "23|1,0,0"
225
+ static_ip_request = "24|1,0,0"
226
+ all_requests = [
227
+ mac_info_request, lan_ip_request, dhcp_request, link_type_request, wan_ip_request, static_ip_request]
228
+ request_text = '#'.join(all_requests)
229
+ body = self._encrypt_body(request_text)
230
+
231
+ response = self.request(2, 1, True, data=body)
232
+ response_text = self._decrypt_data(response.text)
233
+
234
+ matches = TplinkRE330Router.DATA_REGEX.findall(response_text)
235
+
236
+ data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
237
+
238
+ network_info = {
239
+ 'lan_mac': self._extract_value(data_blocks[mac_info_request], "mac 0 "),
240
+ 'wan_mac': self._extract_value(data_blocks[mac_info_request], "mac 1 "),
241
+ 'lan_ip': self._extract_value(data_blocks[lan_ip_request], "ip "),
242
+ 'wan_ip': self._extract_value(data_blocks[wan_ip_request], "ip "),
243
+ 'gateway_ip': self._extract_value(data_blocks[wan_ip_request], "gateway "),
244
+ 'uptime': self._extract_value(data_blocks[wan_ip_request], "upTime "),
245
+ 'wan_mask': self._extract_value(data_blocks[wan_ip_request], "mask "),
246
+ 'lan_mask': self._extract_value(data_blocks[lan_ip_request], "mask "),
247
+ 'dns_1': self._extract_value(data_blocks[wan_ip_request], "dns 0 "),
248
+ 'dns_2': self._extract_value(data_blocks[wan_ip_request], "dns 1 "),
249
+ 'dhcp_enabled': self._extract_value(data_blocks[dhcp_request], "enable "),
250
+ 'link_type': self._extract_value(data_blocks[link_type_request], "linkType "),
251
+ }
252
+
253
+ ipv4status = IPv4Status()
254
+ ipv4status._wan_macaddr = EUI48(network_info['wan_mac'])
255
+ ipv4status._wan_ipv4_ipaddr = IPv4Address(network_info['wan_ip'])
256
+ ipv4status._wan_ipv4_gateway = IPv4Address(network_info['gateway_ip'])
257
+ ipv4status._wan_ipv4_conntype = RouterConstants.CONNECTION_TYPE_MAP[network_info['link_type']]
258
+ ipv4status._wan_ipv4_netmask = IPv4Address(network_info['wan_mask'])
259
+ ipv4status._wan_ipv4_pridns = IPv4Address(network_info['dns_1'])
260
+ ipv4status._wan_ipv4_snddns = IPv4Address(network_info['dns_2'])
261
+ ipv4status._lan_macaddr = EUI48(network_info['lan_mac'])
262
+ ipv4status._lan_ipv4_ipaddr = IPv4Address(network_info['lan_ip'])
263
+ ipv4status.lan_ipv4_dhcp_enable = network_info['dhcp_enabled'] == '1'
264
+ ipv4status._lan_ipv4_netmask = IPv4Address(network_info['lan_mask'])
265
+ return ipv4status
266
+
267
+ def get_ipv4_reservations(self) -> list[IPv4Reservation]:
268
+ body = self._encrypt_body('12|1,0,0')
269
+
270
+ response = self.request(2, 1, True, data=body)
271
+ response_text = self._decrypt_data(response.text)
272
+ matches = TplinkRE330Router.DATA_REGEX.findall(response_text)
273
+
274
+ data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
275
+ filtered_reservations = self._parse_response_to_dict(data_blocks['12|1,0,0'])
276
+
277
+ mapped_reservations: list[IPv4Reservation] = []
278
+ for reservation in filtered_reservations:
279
+ reservation_to_add = IPv4Reservation(EUI48(reservation['mac']), IPv4Address(reservation['ip']),
280
+ reservation['name'], reservation['dhcpsEnable'] == '1')
281
+ mapped_reservations.append(reservation_to_add)
282
+ return mapped_reservations
283
+
284
+ def get_dhcp_leases(self) -> list[IPv4DHCPLease]:
285
+ body = self._encrypt_body('9|1,0,0')
286
+
287
+ response = self.request(2, 1, True, data=body)
288
+ response_text = self._decrypt_data(response.text)
289
+ matches = TplinkRE330Router.DATA_REGEX.findall(response_text)
290
+
291
+ data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
292
+
293
+ filtered_leases = self._parse_response_to_dict(data_blocks['9|1,0,0'])
294
+
295
+ mapped_leases: list[IPv4DHCPLease] = []
296
+ for lease in filtered_leases:
297
+ lease_to_add = IPv4DHCPLease(EUI48(lease['mac']), IPv4Address(lease['ip']),
298
+ lease['hostName'], f'expires {lease["expires"]}')
299
+ mapped_leases.append(lease_to_add)
300
+
301
+ return mapped_leases
302
+
303
+ def _parse_devices(self, device_data_response: list[str]) -> list[Device]:
304
+ filtered_devices = self._parse_response_to_dict(device_data_response)
305
+
306
+ device_type_to_connection = {
307
+ 0: Connection.WIRED,
308
+ 1: Connection.HOST_2G, 2: Connection.GUEST_2G,
309
+ 3: Connection.HOST_5G, 4: Connection.GUEST_5G,
310
+ 13: Connection.IOT_2G, 14: Connection.IOT_5G
311
+ }
312
+
313
+ mapped_devices = []
314
+ for device in filtered_devices:
315
+ if device['online'] == '1':
316
+ device_type = int(device['type'])
317
+ connection_type = device_type_to_connection.get(device_type, Connection.UNKNOWN)
318
+ else:
319
+ connection_type = Connection.UNKNOWN
320
+
321
+ device_to_add = Device(connection_type, EUI48(device['mac']), IPv4Address(device['ip']), device['name'])
322
+ device_to_add.up_speed = 0 # Not supported by the router
323
+ device_to_add.down_speed = 0 # Not supported by the router
324
+ device_to_add.active = device['online'] == '1'
325
+ mapped_devices.append(device_to_add)
326
+ return mapped_devices
327
+
328
+ def _parse_response_to_dict(self, response_data: list[str]) -> list[dict]:
329
+ result_dict = defaultdict(dict)
330
+ for entry in response_data:
331
+ parts = entry.split(' ', 2)
332
+ key, id_str = parts[0], parts[1]
333
+ value = parts[2] if len(parts) == 3 else ''
334
+ result_dict[int(id_str)][key] = value
335
+
336
+ return [v for _, v in result_dict.items() if v.get("ip") != "0.0.0.0"]
337
+
338
+ @staticmethod
339
+ def _encrypt_password(pwd: str, key: str = RouterConfig.KEY, encoding: str = RouterConfig.ENCODING) -> str:
340
+ max_len = max(len(key), len(pwd))
341
+ pwd = pwd.ljust(max_len, RouterConfig.PAD_CHAR)
342
+ key = key.ljust(max_len, RouterConfig.PAD_CHAR)
343
+
344
+ result = []
345
+ for i in range(max_len):
346
+ result.append(encoding[(ord(pwd[i]) ^ ord(key[i])) % len(encoding)])
347
+
348
+ return "".join(result)
349
+
350
+ @staticmethod
351
+ def _encode_token(encoded_password: str, response: requests.Response) -> str:
352
+ response_text = response.text.splitlines()
353
+ auth_info1 = response_text[RouterConstants.AUTH_TOKEN_INDEX1]
354
+ auth_info2 = response_text[RouterConstants.AUTH_TOKEN_INDEX2]
355
+
356
+ encoded_token = TplinkRE330Router._encrypt_password(encoded_password, auth_info1, auth_info2)
357
+ return parse.quote(encoded_token, safe='!()*')
358
+
359
+ def _get_signature(self, datalen: int) -> str:
360
+ encryption = self._encryption
361
+ r = f'{encryption.aes._get_aes_string()}&s={str(int(encryption.seq) + datalen)}'
362
+ e = ''
363
+ n = 0
364
+ while n < len(r):
365
+ e += EncryptionWrapper.rsa_encrypt(r[n:53], encryption.nn_rsa, encryption.ee_rsa)
366
+ n += 53
367
+ return e
368
+
369
+ def _encrypt_body(self, text: str) -> str:
370
+ data = self._encryption.aes.aes_encrypt(text)
371
+ sign = self._get_signature(len(data))
372
+ return f'sign={sign}\r\ndata={data}'
373
+
374
+ def _decrypt_data(self, encrypted_text: str) -> str:
375
+ return self._encryption.aes.aes_decrypt(encrypted_text)
376
+
377
+ def _extract_value(self, response_list, prefix):
378
+ return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
379
+
380
+ def request(self, code: int, asyn: int, use_token: bool = False, data: str = None):
381
+ url = f"{self.host}/?code={code}&asyn={asyn}"
382
+ if use_token:
383
+ url += f"&id={self._encryption.token}"
384
+ try:
385
+ response = self._session.post(url, data=data, timeout=self.timeout,
386
+ verify=self._verify_ssl, headers=self._headers)
387
+ # Raises exception for 4XX/5XX status codes for all requests except 1st in authorize
388
+ if not (code == 7 and asyn == 1 and use_token is False and data is None):
389
+ response.raise_for_status()
390
+ return response
391
+ except requests.exceptions.RequestException as e:
392
+ self._logger.error(f"Network error: {e}")
393
+ raise ClientException(f"Network error: {str(e)}") from e
@@ -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: