tplinkrouterc6u 5.10.2__py3-none-any.whl → 5.11.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.
@@ -0,0 +1,296 @@
1
+ from unittest import main, TestCase
2
+ from ipaddress import IPv4Address
3
+ from macaddress import EUI48
4
+ from tplinkrouterc6u.common.dataclass import Firmware, Status, Device
5
+ from tplinkrouterc6u.common.dataclass import IPv4Status, IPv4Reservation, IPv4DHCPLease
6
+ from tplinkrouterc6u import Connection, ClientException
7
+ from tplinkrouterc6u.client.re330 import TplinkRE330Router
8
+
9
+
10
+ IPV4_STATUS_RESPONSE = ('00000\r\nid 1|1,0,0\r\noldAuthKey \r\nsetWzd 1\r\nmode 3\r\nlogLevel 3\r\nfastpath 1\r\n'
11
+ 'mac 0 00-00-00-00-00-01\r\nmac 1 00-00-00-00-00-02\r\nauthKey keykeykey\r\nid 4|1,0,0\r\n'
12
+ 'ip 2.2.2.2\r\nmask 255.255.255.0\r\ngateway 3.3.3.3\r\ndns 0 1.1.1.1\r\n'
13
+ 'dns 1 8.8.8.8\r\nmode 0\r\nid 8|1,0,0\r\nmode 2\r\npoolStart 192.168.1.100\r\n'
14
+ 'poolEnd 192.168.1.199\r\n'
15
+ 'leaseTime 120\r\ndns 0 0.0.0.0\r\ndns 1 0.0.0.0\r\ngateway 0.0.0.0\r\nhostName \r\n'
16
+ 'id 22|1,0,0\r\nenable 1\r\nwirelessWanNoUsed 0\r\nlinkMode 0\r\nlinkType 0\r\nid 23|1,0,0\r\n'
17
+ 'ip 4.4.4.4\r\nmask 255.255.252.0\r\ngateway 5.5.5.5\r\ndns 0 1.1.1.1\r\ndns 1 8.8.8.8\r\n'
18
+ 'status 1\r\ncode 0\r\nupTime 0\r\ninPkts 0\r\ninOctets 0\r\noutPkts 0\r\noutOctets 0\r\n'
19
+ 'inRates 0\r\noutRates 0\r\ndualMode 0\r\ndualIp 0.0.0.0\r\ndualMask 0.0.0.0\r\n'
20
+ 'dualGateway 0.0.0.0\r\ndualDns 0 0.0.0.0\r\ndualDns 1 0.0.0.0\r\ndualCode 0\r\ndualStatus 0')
21
+
22
+ STATUS_RESPONSE_TEXT = ('00000\r\nid 1|1,0,0\r\noldAuthKey \r\nsetWzd 1\r\nmode 3\r\nlogLevel 3\r\nfastpath 1\r\n'
23
+ 'mac 0 00-00-00-00-00-01\r\nmac 1 00-00-00-00-00-02\r\nauthKey keykeykey\r\nid 4|1,0,0\r\n'
24
+ 'ip 2.2.2.2\r\nmask 255.255.255.0\r\ngateway 3.3.3.3\r\ndns 0 1.1.1.1\r\ndns 1 8.8.8.8\r\n'
25
+ 'mode 0\r\nid 23|1,0,0\r\nip 4.4.4.4\r\nmask 255.255.255.0\r\ngateway 5.5.5.5\r\n'
26
+ 'dns 0 1.1.1.1\r\ndns 1 8.8.8.8\r\nstatus 1\r\ncode 0\r\nupTime 0\r\ninPkts 0\r\ninOctets 0\r\n'
27
+ 'outPkts 0\r\noutOctets 0\r\ninRates 0\r\noutRates 0\r\ndualMode 0\r\ndualIp 0.0.0.0\r\n'
28
+ 'dualMask 0.0.0.0\r\ndualGateway 0.0.0.0\r\ndualDns 0 0.0.0.0\r\ndualDns 1 0.0.0.0\r\n'
29
+ 'dualCode 0\r\ndualStatus 0\r\nid 13|1,0,0\r\nip 0 10.10.10.10\r\nip 1 11.11.11.11\r\n'
30
+ 'ip 2 0.0.0.0\r\nip 3 0.0.0.0\r\nip 4 0.0.0.0\r\nip 5 0.0.0.0\r\nmac 0 00-00-00-00-00-03\r\n'
31
+ 'mac 1 00-00-00-00-00-04\r\nmac 2 00-00-00-00-00-00\r\nmac 3 00-00-00-00-00-00\r\n'
32
+ 'mac 4 00-00-00-00-00-00\r\nmac 5 00-00-00-00-00-00\r\n'
33
+ 'reserved 0 \r\nreserved 1 \r\nreserved 2 \r\nreserved 3 \r\nreserved 4 \r\nreserved 5 \r\n'
34
+ 'bindEntry 0 0\r\nbindEntry 1 0\r\nbindEntry 2 0\r\nbindEntry 3 0\r\nbindEntry 4 0\r\n'
35
+ 'bindEntry 5 0\r\nstaMgtEntry 0 0\r\nstaMgtEntry 1 0\r\nstaMgtEntry 2 0\r\nstaMgtEntry 3 0\r\n'
36
+ 'staMgtEntry 4 0\r\nstaMgtEntry 5 0\r\ntype 0 3\r\ntype 1 1\r\ntype 2 0\r\ntype 3 0\r\n'
37
+ 'type 4 0\r\ntype 5 0\r\nonline 0 1\r\nonline 1 1\r\nonline 2 0\r\nonline 3 0\r\nonline 4 0\r\n'
38
+ 'online 5 0\r\nname 0 ANONYMOUS\r\nname 1 BANANA-12\r\nname 2 \r\nname 3 \r\nname 4 \r\n'
39
+ 'name 5 \r\nDevType 0 OTHER\r\nDevType 1 OTHER\r\nDevType 2 \r\nDevType 3 \r\nDevType 4 \r\n'
40
+ 'DevType 5 \r\nid 33|1,1,0\r\nuUnit 0\r\ncSsidPrefix \r\nuRadiusIp 0.0.0.0\r\n'
41
+ 'uRadiusGKUpdateIntvl 0\r\nuPskGKUpdateIntvl 0\r\nuKeyLength 0 0\r\nuKeyLength 1 0\r\n'
42
+ 'uKeyLength 2 0\r\nuKeyLength 3 0\r\ncKeyVal 0 \r\ncKeyVal 1 \r\ncKeyVal 2 \r\ncKeyVal 3 \r\n'
43
+ 'uRadiusPort 1812\r\nuKeyType 1\r\nuDefaultKey 1\r\nbEnable 1\r\nbBcastSsid 1\r\n'
44
+ 'cSsid SuperWifi\r\nbSecurityEnable 1\r\nuAuthType 3\r\nuWEPSecOpt 3\r\nuRadiusSecOpt 3\r\n'
45
+ 'uPSKSecOpt 2\r\nuRadiusEncryptType 4\r\nuPSKEncryptType 3\r\ncRadiusSecret \r\n'
46
+ 'cPskSecret YouThoughtIdForgetMyKey?\r\nbSecCheck 0\r\nbEnabled 1\r\nbPinEnabled 0\r\n'
47
+ 'cUsrPIN 11100111\r\nbConfigured 0\r\nbIsLocked 0\r\nid 33|2,1,0\r\nuUnit 0\r\ncSsidPrefix \r\n'
48
+ 'uRadiusIp 0.0.0.0\r\nuRadiusGKUpdateIntvl 0\r\nuPskGKUpdateIntvl 0\r\nuKeyLength 0 0\r\n'
49
+ 'uKeyLength 1 0\r\nuKeyLength 2 0\r\nuKeyLength 3 0\r\ncKeyVal 0 \r\ncKeyVal 1 \r\n'
50
+ 'cKeyVal 2 \r\ncKeyVal 3 \r\nuRadiusPort 1812\r\nuKeyType 1\r\nuDefaultKey 1\r\nbEnable 1\r\n'
51
+ 'bBcastSsid 1\r\ncSsid SuperWifi\r\nbSecurityEnable 1\r\nuAuthType 3\r\nuWEPSecOpt 3\r\n'
52
+ 'uRadiusSecOpt 3\r\nuPSKSecOpt 2\r\nuRadiusEncryptType 4\r\nuPSKEncryptType 3\r\n'
53
+ 'cRadiusSecret \r\ncPskSecret YouThoughtIdForgetMyKey?\r\nbSecCheck 0\r\n'
54
+ 'bEnabled 1\r\nbPinEnabled 0\r\ncUsrPIN 11100111\r\nbConfigured 0\r\nbIsLocked 0')
55
+
56
+
57
+ class ResponseMock():
58
+ def __init__(self, text, status_code=0):
59
+ self.text = text
60
+ self.status_code = status_code
61
+
62
+
63
+ class TplinkRE330RouterTest(TplinkRE330Router):
64
+ response = ''
65
+
66
+ def _init_session(self) -> None:
67
+ pass
68
+
69
+ def request(self, code: int, asyn: int, use_token: bool = False, data: str = None) -> dict | None:
70
+
71
+ # Responses
72
+ if code == 2 and (asyn == 0 or asyn == 1):
73
+ if use_token is False:
74
+ if data == '50|1,0,0':
75
+ # Supports
76
+ return ResponseMock(self.response, 200)
77
+ else:
78
+ # Authorization
79
+ return ResponseMock('blabla\r\nblabla\r\nblabla\r\nauthinfo1\r\nauthinfo2')
80
+ elif use_token is True:
81
+ return ResponseMock(self.response)
82
+ if code == 7 and asyn == 1:
83
+ if use_token is False:
84
+ # Authorization
85
+ return ResponseMock('00007\r\n00004\r\n00002\r\n'
86
+ 'BC97577E65233B3E1137C61091D64176C334E52AD78FFBDDABC826B685435E'
87
+ '9D3DE83FE70C2AC62D6B13BD8EADA10B5623F9354DA0E99636A4F5519CA2DC2DC3\r\n'
88
+ '12345656\r\n00000')
89
+ elif use_token is True:
90
+ return ResponseMock('00000')
91
+ elif code == 16 and asyn == 0:
92
+ if use_token is False:
93
+ # Authorization
94
+ return ResponseMock('00000\r\n010001\r\nBC97577E65233B3E1137C61091D64176C334E52AD78FFBDDABC826B685435E'
95
+ '9D3DE83FE70C2AC62D6B13BD8EADA10B5623F9354DA0E99636A4F5519CA2DC2DC3\r\n12345656')
96
+ elif use_token is True:
97
+ # Authorization
98
+ return ResponseMock('00000')
99
+ elif code == 7 and asyn == 0:
100
+ return ResponseMock('00000')
101
+
102
+ raise ClientException()
103
+
104
+ def set_encrypted_response(self, response_text) -> None:
105
+ self.response = self._encrypt_body(response_text).split('data=')[1]
106
+
107
+
108
+ class TestTPLinkClient(TestCase):
109
+
110
+ def test_supports(self) -> None:
111
+ response = ('00000\r\nid 50|1,0,0\r\ncurrentLanguage\r\n'
112
+ 'languageList bg_BG,cs_CZ,de_DE,en_US,es_ES,es_LA,fr_FR,hu_HU,it_IT,ja_JP,ko_KR,nl_NL,pl_PL,pt_BR,'
113
+ 'pt_PT,ro_RO,ru_RU,sk_SK,tr_TR,uk_UA,vi_VN,zh_TW\r\nsetByUser 0')
114
+
115
+ client = TplinkRE330RouterTest('', '')
116
+ client.response = response
117
+ supports = client.supports()
118
+ self.assertTrue(supports)
119
+
120
+ def test_authorize(self) -> None:
121
+ client = TplinkRE330RouterTest('', '')
122
+ client.authorize()
123
+
124
+ encryption = client._encryption
125
+ self.assertEqual(encryption.ee_rsa, '010001')
126
+ self.assertEqual(encryption.nn_rsa, 'BC97577E65233B3E1137C61091D64176C334E52AD78FFBDDABC826B685435E9D3DE83FE70C'
127
+ '2AC62D6B13BD8EADA10B5623F9354DA0E99636A4F5519CA2DC2DC3')
128
+ self.assertEqual(encryption.seq, '12345656')
129
+
130
+ def test_get_firmware(self) -> None:
131
+ response = ('00000\r\nid 0|1,0,0\r\nfullName TP-Link%20Wireless%20Extender%20RE330\r\nfacturer TP-Link\r\n'
132
+ 'modelName RE330\r\nmodelVer 1\r\nsoftVer 1.0.23%20Build%20230418%20Rel.60395n\r\n'
133
+ 'hardVer %20RE330%201.0\r\nprodId 0x3300001\r\ncloudShouldActive 1\r\ncountryId 0x0\r\n'
134
+ 'specialId 0x5545\r\ncountryCode 0x4544\r\nmainVer 0x5a010017\r\nminorVer 0x1\r\noemId 0x1\r\n'
135
+ 'deviceId 8002A1F018FA0C879DB62FA981FB0D1D231D490F\r\n'
136
+ 'hardwareId 5E055ADC85F0800C6C3044E5A3180E2A\r\nfirmwareId FFFFFFFFFFFFFFFFFFFF033001004555\r\n'
137
+ 'oem_id B30DDAF6C31C08B9C50A48B0B9168003\r\nfacturerType 0')
138
+
139
+ client = TplinkRE330RouterTest('', '')
140
+ client.authorize()
141
+
142
+ client.set_encrypted_response(response)
143
+
144
+ firmware = client.get_firmware()
145
+
146
+ self.assertIsInstance(firmware, Firmware)
147
+ self.assertEqual(firmware.hardware_version, ' RE330 1.0')
148
+ self.assertEqual(firmware.model, 'RE330')
149
+ self.assertEqual(firmware.firmware_version, '1.0.23 Build 230418 Rel.60395n')
150
+
151
+ def test_get_ipv4_status(self) -> None:
152
+
153
+ client = TplinkRE330RouterTest('', '')
154
+ client.authorize()
155
+
156
+ client.set_encrypted_response(IPV4_STATUS_RESPONSE)
157
+
158
+ ipv4_status: IPv4Status = client.get_ipv4_status()
159
+
160
+ self.assertIsInstance(ipv4_status, IPv4Status)
161
+ self.assertEqual(ipv4_status.wan_macaddress, EUI48('00-00-00-00-00-02'))
162
+ self.assertEqual(ipv4_status._wan_ipv4_ipaddr, IPv4Address('4.4.4.4'))
163
+ self.assertEqual(ipv4_status._wan_ipv4_gateway, IPv4Address('5.5.5.5'))
164
+ self.assertEqual(ipv4_status._wan_ipv4_conntype, 'Dynamic IP')
165
+ self.assertEqual(ipv4_status._wan_ipv4_netmask, IPv4Address('255.255.252.0'))
166
+ self.assertEqual(ipv4_status._wan_ipv4_pridns, IPv4Address('1.1.1.1'))
167
+ self.assertEqual(ipv4_status._wan_ipv4_snddns, IPv4Address('8.8.8.8'))
168
+ self.assertEqual(ipv4_status._lan_macaddr, EUI48('00-00-00-00-00-01'))
169
+ self.assertEqual(ipv4_status._lan_ipv4_ipaddr, IPv4Address('2.2.2.2'))
170
+ self.assertEqual(ipv4_status.lan_ipv4_dhcp_enable, False)
171
+ self.assertEqual(ipv4_status._lan_ipv4_netmask, IPv4Address('255.255.255.0'))
172
+
173
+ def test_get_ipv4_reservations(self) -> None:
174
+ response = ('00000\r\nid 12|1,0,0\r\nip 0 192.168.0.112\r\nip 1 0.0.0.0\r\nmac 0 00-00-00-00-00-00\r\n'
175
+ 'mac 1 00-00-00-00-00-01\r\nreserved 0\r\nreserved 1\r\nbindEntry 0 0\r\nbindEntry 1 0\r\n'
176
+ 'staMgtEntry 0 0\r\nstaMgtEntry 1 1\r\nname 0 Galaxy-S21\r\nname 1 Camera\r\nreserved_name 0\r\n'
177
+ 'reserved_name 1\r\nblocked 0 0\r\nblocked 1 0\r\nupLimit 0 0\r\nupLimit 1 0\r\ndownLimit 0 0'
178
+ '\r\ndownLimit 1 0\r\nqosEntry 0 0\r\nqosEntry 1 0\r\npriTime 0 0\r\npriTime 1 0\r\n'
179
+ 'dhcpsEntry 0 1\r\ndhcpsEntry 1 0\r\ndhcpsEnable 0 1\r\ndhcpsEnable 1 0\r\nslEnable 0 0\r\n'
180
+ 'slEnable 1 0\r\nstart 0 0\r\nstart 1 0\r\nend 0 0\r\nend 1 0\r\nday 0 0\r\nday 1 0\r\n'
181
+ 'startMin 0 0\r\nstartMin 1 0\r\nendMin 0 0\r\nendMin 1 0\r\ndevType 0 0\r\ndevType 1 0\r\n'
182
+ 'reserved2 0 0\r\nreserved2 1 0\r\ndisable 1')
183
+
184
+ client = TplinkRE330RouterTest('', '')
185
+ client.authorize()
186
+
187
+ client.set_encrypted_response(response)
188
+
189
+ ipv4_reservations: list[IPv4Reservation] = client.get_ipv4_reservations()
190
+ ipv4_reservation: IPv4Reservation = ipv4_reservations[0]
191
+
192
+ self.assertIsInstance(ipv4_reservation, IPv4Reservation)
193
+ self.assertEqual(ipv4_reservation.macaddress, EUI48('00-00-00-00-00-00'))
194
+ self.assertEqual(ipv4_reservation.ipaddress, IPv4Address('192.168.0.112'))
195
+ self.assertEqual(ipv4_reservation.hostname, 'Galaxy-S21')
196
+ self.assertEqual(ipv4_reservation.enabled, True)
197
+
198
+ def test_get_dhcp_leases(self) -> None:
199
+ response = ('00000\r\nid 9|1,0,0\r\nhostName 0 Galaxy-S21\r\nhostName 1 iPhone\r\nhostName 2 PC\r\n'
200
+ 'hostName 3 Laptop\r\nmac 0 00-00-00-00-00-00\r\nmac 1 00-00-00-00-00-01\r\n'
201
+ 'mac 2 00-00-00-00-00-02\r\nmac 3 00-00-00-00-00-03\r\nreserved 0\r\nreserved 1'
202
+ '\r\nreserved 2\r\nreserved 3\r\nstate 0 5\r\nstate 1 5\r\nstate 2 5\r\nstate 3 5'
203
+ '\r\nip 0 192.168.0.112\r\nip 1 192.168.0.101\r\nip 2 192.168.0.245\r\nip 3 192.168.0.186'
204
+ '\r\nexpires 0 4294967295\r\nexpires 1 3669\r\nexpires 2 4025\r\nexpires 3 4202')
205
+
206
+ client = TplinkRE330RouterTest('', '')
207
+ client.authorize()
208
+
209
+ client.set_encrypted_response(response)
210
+
211
+ dhcp_leases: list[IPv4DHCPLease] = client.get_dhcp_leases()
212
+
213
+ self.assertIsInstance(dhcp_leases[0], IPv4DHCPLease)
214
+ self.assertEqual(dhcp_leases[0].macaddress, EUI48('00-00-00-00-00-00'))
215
+ self.assertEqual(dhcp_leases[0].ipaddress, IPv4Address('192.168.0.112'))
216
+ self.assertEqual(dhcp_leases[0].hostname, 'Galaxy-S21')
217
+ self.assertEqual(dhcp_leases[0].lease_time, 'expires 4294967295')
218
+
219
+ self.assertIsInstance(dhcp_leases[1], IPv4DHCPLease)
220
+ self.assertEqual(dhcp_leases[1].macaddress, EUI48('00-00-00-00-00-01'))
221
+ self.assertEqual(dhcp_leases[1].ipaddress, IPv4Address('192.168.0.101'))
222
+ self.assertEqual(dhcp_leases[1].hostname, 'iPhone')
223
+ self.assertEqual(dhcp_leases[1].lease_time, 'expires 3669')
224
+
225
+ def test_get_status(self) -> None:
226
+ client = TplinkRE330RouterTest('', '')
227
+ client.authorize()
228
+
229
+ client.set_encrypted_response(STATUS_RESPONSE_TEXT)
230
+ status = client.get_status()
231
+
232
+ self.assertIsInstance(status, Status)
233
+ self.assertEqual(status.wan_macaddr, '00-00-00-00-00-02')
234
+ self.assertIsInstance(status.wan_macaddress, EUI48)
235
+ self.assertEqual(status.lan_macaddr, '00-00-00-00-00-01')
236
+ self.assertIsInstance(status.lan_macaddress, EUI48)
237
+ self.assertEqual(status.wan_ipv4_addr, '4.4.4.4')
238
+ self.assertIsInstance(status.lan_ipv4_address, IPv4Address)
239
+ self.assertEqual(status.lan_ipv4_addr, '2.2.2.2')
240
+ self.assertEqual(status.wan_ipv4_gateway, '5.5.5.5')
241
+ self.assertIsInstance(status.wan_ipv4_address, IPv4Address)
242
+ self.assertEqual(status.wired_total, 0)
243
+ self.assertEqual(status.wifi_clients_total, 2)
244
+ self.assertEqual(status.guest_clients_total, 0)
245
+ self.assertEqual(status.clients_total, 2)
246
+ self.assertEqual(status.iot_clients_total, 0)
247
+ self.assertFalse(status.guest_2g_enable)
248
+ self.assertFalse(status.guest_5g_enable)
249
+ self.assertFalse(status.iot_2g_enable)
250
+ self.assertFalse(status.iot_5g_enable)
251
+ self.assertTrue(status.wifi_2g_enable)
252
+ self.assertTrue(status.wifi_5g_enable)
253
+ self.assertEqual(status.wan_ipv4_uptime, 0)
254
+ self.assertEqual(status.mem_usage, None)
255
+ self.assertEqual(status.cpu_usage, None)
256
+ self.assertEqual(len(status.devices), 2)
257
+
258
+ device = status.devices[0]
259
+ self.assertIsInstance(device, Device)
260
+ self.assertEqual(device.type, Connection.HOST_5G)
261
+ self.assertEqual(device.macaddr, '00-00-00-00-00-03')
262
+ self.assertIsInstance(device.macaddress, EUI48)
263
+ self.assertEqual(device.ipaddr, '10.10.10.10')
264
+ self.assertIsInstance(device.ipaddress, IPv4Address)
265
+ self.assertEqual(device.hostname, 'ANONYMOUS')
266
+ self.assertEqual(device.up_speed, 0)
267
+ self.assertEqual(device.down_speed, 0)
268
+ self.assertEqual(device.active, True)
269
+
270
+ device = status.devices[1]
271
+ self.assertIsInstance(device, Device)
272
+ self.assertEqual(device.type, Connection.HOST_2G)
273
+ self.assertEqual(device.macaddr, '00-00-00-00-00-04')
274
+ self.assertIsInstance(device.macaddress, EUI48)
275
+ self.assertEqual(device.ipaddr, '11.11.11.11')
276
+ self.assertIsInstance(device.ipaddress, IPv4Address)
277
+ self.assertEqual(device.hostname, 'BANANA-12')
278
+ self.assertEqual(device.up_speed, 0)
279
+ self.assertEqual(device.down_speed, 0)
280
+ self.assertEqual(device.active, True)
281
+
282
+ def test_get_led_status(self) -> None:
283
+ client = TplinkRE330RouterTest('', '')
284
+ client.authorize()
285
+
286
+ client.set_encrypted_response('00000\r\nid 112|1,0,0\r\nenable 1')
287
+ led_status = client.get_led_status()
288
+ self.assertEqual(led_status, True)
289
+
290
+ client.set_encrypted_response('00000\r\nid 112|1,0,0\r\nenable 0')
291
+ led_status = client.get_led_status()
292
+ self.assertEqual(led_status, False)
293
+
294
+
295
+ if __name__ == '__main__':
296
+ main()
@@ -2,6 +2,7 @@ from tplinkrouterc6u.client.c6u import TplinkRouter
2
2
  from tplinkrouterc6u.client.deco import TPLinkDecoClient
3
3
  from tplinkrouterc6u.client_abstract import AbstractRouter
4
4
  from tplinkrouterc6u.client.mr import TPLinkMRClient
5
+ from tplinkrouterc6u.client.mr200 import TPLinkMR200Client
5
6
  from tplinkrouterc6u.client.ex import TPLinkEXClient
6
7
  from tplinkrouterc6u.client.vr import TPLinkVRClient
7
8
  from tplinkrouterc6u.client.c80 import TplinkC80Router
@@ -9,6 +10,7 @@ from tplinkrouterc6u.client.c5400x import TplinkC5400XRouter
9
10
  from tplinkrouterc6u.client.c1200 import TplinkC1200Router
10
11
  from tplinkrouterc6u.client.xdr import TPLinkXDRClient
11
12
  from tplinkrouterc6u.client.wdr import TplinkWDRRouter
13
+ from tplinkrouterc6u.client.re330 import TplinkRE330Router
12
14
  from tplinkrouterc6u.provider import TplinkRouterProvider
13
15
  from tplinkrouterc6u.common.package_enum import Connection, VPN
14
16
  from tplinkrouterc6u.common.dataclass import (
@@ -0,0 +1,412 @@
1
+ import base64
2
+ from tplinkrouterc6u.client.mr import TPLinkMRClient
3
+ from Crypto.PublicKey import RSA
4
+ from binascii import hexlify
5
+ from Crypto.Cipher import PKCS1_v1_5
6
+ from re import search
7
+ from 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
13
+ from tplinkrouterc6u.common.dataclass import (
14
+ Firmware,
15
+ Status,
16
+ Device,
17
+ IPv4Reservation,
18
+ IPv4DHCPLease,
19
+ IPv4Status,
20
+ SMS,
21
+ LTEStatus,
22
+ VPNStatus,
23
+ )
24
+ from tplinkrouterc6u.common.exception import ClientException, ClientError, AuthorizeError
25
+
26
+
27
+ class TPLinkMR200Client(TPLinkMRClient):
28
+
29
+ def authorize(self) -> None:
30
+ self.req.headers = {'referer': f'{self.host}/', 'origin': self.host}
31
+ params = self.__get_params()
32
+
33
+ # Construct the RSA public key manually using modulus (n) and exponent (e)
34
+ n = int(params["nn"])
35
+ e = int(params["ee"])
36
+ pub_key = RSA.construct((n, e))
37
+
38
+ # Create an RSA cipher with PKCS#1 v1.5 padding (same as rsa.encrypt)
39
+ cipher = PKCS1_v1_5.new(pub_key)
40
+
41
+ # Encrypt username
42
+ rsa_username = cipher.encrypt(self.username.encode("utf-8"))
43
+ rsa_username_hex = hexlify(rsa_username).decode("utf-8")
44
+
45
+ # Encrypt password (after base64 encoding, as in your original code)
46
+ rsa_password = cipher.encrypt(base64.b64encode(self.password.encode("utf-8")))
47
+ rsa_password_hex = hexlify(rsa_password).decode("utf-8")
48
+
49
+ # Send login request
50
+ self.req.post(
51
+ f'{self.host}/cgi/login?UserName={rsa_username_hex}&Passwd={rsa_password_hex}&Action=1&LoginStatus=0'
52
+ )
53
+
54
+ # Try to extract token
55
+ r = self.req.get(self.host)
56
+ try:
57
+ self.req.headers["TokenID"] = search(r'var token="(.*)";', r.text).group(1)
58
+ except AttributeError:
59
+ raise AuthorizeError()
60
+
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
+ def get_vpn_status(self) -> VPNStatus:
220
+ status = VPNStatus()
221
+ 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'),
226
+ ]
227
+ _, values = self.req_act(acts)
228
+
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
239
+
240
+ return status
241
+
242
+ def set_vpn(self, vpn: VPN, enable: bool) -> None:
243
+ acts = [
244
+ self.ActItem(self.ActItem.SET, vpn.value, attrs=['enable={}'.format(int(enable))])
245
+ ]
246
+
247
+ self.req_act(acts)
248
+
249
+ def logout(self) -> None:
250
+ '''
251
+ Logs out from the host
252
+ '''
253
+ if self._token is None:
254
+ return
255
+
256
+ acts = [
257
+ # 8\r\n[/cgi/logout#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n
258
+ self.ActItem(self.ActItem.CGI, '/cgi/logout')
259
+ ]
260
+
261
+ response, _ = self.req_act(acts)
262
+ ret_code = self._parse_ret_val(response)
263
+
264
+ if ret_code == self.HTTP_RET_OK:
265
+ del self.req.headers["TokenID"]
266
+
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
+ def get_lte_status(self) -> LTEStatus:
341
+ status = LTEStatus()
342
+ 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'),
347
+ ]
348
+ _, values = self.req_act(acts)
349
+
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'])
354
+
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'])
358
+
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'])
364
+
365
+ status.isp_name = values['3']['ispName']
366
+
367
+ return status
368
+
369
+ def __get_params(self, retry=False):
370
+ try:
371
+ r = self.req.get(f"{self.host}/cgi/getParm", timeout=5)
372
+ result = {}
373
+ for line in r.text.splitlines()[0:2]:
374
+ match = search(r"var (.*)=\"(.*)\"", line)
375
+ result[match.group(1)] = int(match.group(2), 16)
376
+ return result
377
+ except Exception:
378
+ if not retry:
379
+ return self.__get_params(True)
380
+ raise ClientException()
381
+
382
+ def req_act(self, acts: list):
383
+ '''
384
+ Requests ACTs via the cgi_gdpr proxy
385
+ '''
386
+ act_types = []
387
+ act_data = []
388
+
389
+ for act in acts:
390
+ act_types.append(str(act.type))
391
+ act_data.append('[{}#{}#{}]{},{}\r\n{}\r\n'.format(
392
+ act.oid,
393
+ act.stack,
394
+ act.pstack,
395
+ len(act_types) - 1, # index, starts at 0
396
+ len(act.attrs),
397
+ '\r\n'.join(act.attrs)
398
+ ))
399
+
400
+ data = ''.join(act_data)
401
+ url = f"{self.host}/cgi?" + '&'.join(act_types)
402
+ (code, response) = self.req.post(url, data=data)
403
+
404
+ if code != 200:
405
+ error = 'TplinkRouter - MR200 - Response with error; Request {} - Response {}'.format(data, response)
406
+ if self._logger:
407
+ self._logger.debug(error)
408
+ raise ClientError(error)
409
+
410
+ result = self._merge_response(response)
411
+
412
+ return response, result.get('0') if len(result) == 1 and result.get('0') else result
@@ -0,0 +1,423 @@
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 datetime import datetime
14
+ import random
15
+ from tplinkrouterc6u.common.package_enum import Connection
16
+ from tplinkrouterc6u.common.exception import ClientException
17
+ from tplinkrouterc6u.common.encryption import EncryptionWrapper
18
+ from tplinkrouterc6u.common.dataclass import Firmware, Status, IPv4Status, IPv4Reservation
19
+ from tplinkrouterc6u.common.dataclass import IPv4DHCPLease, Device
20
+ from tplinkrouterc6u.client_abstract import AbstractRouter
21
+
22
+
23
+ class RouterConstants:
24
+ AUTH_TOKEN_INDEX1 = 3
25
+ AUTH_TOKEN_INDEX2 = 4
26
+
27
+ HOST_WIFI_2G_REQUEST = '33|1,1,0'
28
+ HOST_WIFI_5G_REQUEST = '33|2,1,0'
29
+
30
+ CONNECTION_REQUESTS_MAP = {
31
+ Connection.HOST_2G: HOST_WIFI_2G_REQUEST,
32
+ Connection.HOST_5G: HOST_WIFI_5G_REQUEST,
33
+ }
34
+
35
+ CONNECTION_TYPE_MAP = {
36
+ '0': 'Dynamic IP',
37
+ '1': 'Static IP',
38
+ '2': 'PPPoE',
39
+ '3': 'L2TP',
40
+ '4': 'PPTP'
41
+ }
42
+
43
+
44
+ class RouterConfig:
45
+ """Configuration parameters for the router."""
46
+ ENCODING: str = ("yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8"
47
+ "ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70T"
48
+ "OoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW")
49
+ KEY: str = "RDpbLfCPsJZ7fiv"
50
+ PAD_CHAR: str = chr(187)
51
+
52
+
53
+ @dataclass
54
+ class EncryptionState:
55
+ """Holds encryption-related state."""
56
+
57
+ def __init__(self):
58
+ self.nn_rsa = ''
59
+ self.ee_rsa = ''
60
+ self.seq = ''
61
+ self.key_aes = ''
62
+ self.iv_aes = ''
63
+ self.aes_string = ''
64
+ self.token = ''
65
+
66
+
67
+ # Note: This router doesn't support VPN and up/down speeds per device
68
+ class TplinkRE330Router(AbstractRouter):
69
+ DATA_REGEX = re.compile(r'id (\d+\|\d,\d,\d)\r\n(.*?)(?=\r\nid \d+\||$)', re.DOTALL)
70
+
71
+ def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
72
+ verify_ssl: bool = True, timeout: int = 30) -> None:
73
+ super().__init__(host, password, username, logger, verify_ssl, timeout)
74
+ self._session = Session()
75
+ if self._verify_ssl is False:
76
+ self._session.verify = False
77
+ self._encryption = EncryptionState()
78
+ self.host = self.host.rstrip('/')
79
+ # Mandatory Referer header
80
+ self._headers = {
81
+ 'Referer': self.host + '/'
82
+ }
83
+
84
+ def _init_session(self) -> None:
85
+ self._session.get(self.host + "/", headers=self._headers)
86
+
87
+ def supports(self) -> bool:
88
+ try:
89
+ response = self.request(2, 0, data='50|1,0,0')
90
+ return response.status_code == 200 and response.text.startswith('00000')
91
+ except Exception:
92
+ return False
93
+
94
+ def authorize(self) -> None:
95
+ # Init session and connexion
96
+ self._init_session()
97
+ encoded_password = TplinkRE330Router._encrypt_password(self.password)
98
+
99
+ # Get token encryption strings and encrypt the password
100
+ response = self.request(7, 1)
101
+ self._encryption.token = TplinkRE330Router._encode_token(encoded_password, response)
102
+
103
+ # Get RSA exponent, modulus and sequence number
104
+ response = self.request(16, 0, data='get')
105
+
106
+ responseText = response.text.splitlines()
107
+ if len(responseText) < 4:
108
+ raise ClientException("Invalid response for RSA keys from router")
109
+ self._encryption.ee_rsa = responseText[1]
110
+ self._encryption.nn_rsa = responseText[2]
111
+ self._encryption.seq = responseText[3]
112
+
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
+ # Encrypt AES string
118
+ aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.aes_string, self._encryption.nn_rsa,
119
+ self._encryption.ee_rsa)
120
+ # Mandatory intermediate request
121
+ response = self.request(7, 0, True)
122
+ # Register AES string for decryption on server side
123
+ self.request(16, 0, True, data=f'set {aes_string_encrypted}')
124
+
125
+ def logout(self) -> None:
126
+ self.request(11, 0, True)
127
+
128
+ def get_firmware(self) -> Firmware:
129
+ text = '0|1,0,0'
130
+
131
+ body = self._encrypt_body(text)
132
+ response = self.request(2, 1, True, data=body)
133
+ response_text = self._decrypt_data(response.text)
134
+ device_datamap = dict(line.split(" ", 1) for line in response_text.split("\r\n")[1:-1])
135
+
136
+ return Firmware(parse.unquote(device_datamap['hardVer']), parse.unquote(device_datamap['modelName']),
137
+ parse.unquote(device_datamap['softVer']))
138
+
139
+ def get_status(self) -> Status:
140
+ mac_info_request = "1|1,0,0"
141
+ lan_ip_request = "4|1,0,0"
142
+ wan_ip_request = "23|1,0,0"
143
+ device_data_request = '13|1,0,0'
144
+ all_requests = [
145
+ mac_info_request, lan_ip_request, wan_ip_request, device_data_request,
146
+ RouterConstants.HOST_WIFI_2G_REQUEST, RouterConstants.HOST_WIFI_5G_REQUEST
147
+ ]
148
+ request_text = '#'.join(all_requests)
149
+ body = self._encrypt_body(request_text)
150
+
151
+ response = self.request(2, 1, True, data=body)
152
+ response_text = self._decrypt_data(response.text)
153
+
154
+ matches = TplinkRE330Router.DATA_REGEX.findall(response_text)
155
+
156
+ data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
157
+
158
+ def extract_value(response_list, prefix):
159
+ return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
160
+
161
+ network_info = {
162
+ 'lan_mac': extract_value(data_blocks[mac_info_request], "mac 0 "),
163
+ 'wan_mac': extract_value(data_blocks[mac_info_request], "mac 1 "),
164
+ 'lan_ip': extract_value(data_blocks[lan_ip_request], "ip "),
165
+ 'wan_ip': extract_value(data_blocks[wan_ip_request], "ip "),
166
+ 'gateway_ip': extract_value(data_blocks[wan_ip_request], "gateway "),
167
+ 'uptime': extract_value(data_blocks[wan_ip_request], "upTime ")
168
+ }
169
+
170
+ wifi_status = {}
171
+ for key, request in RouterConstants.CONNECTION_REQUESTS_MAP.items():
172
+ value = data_blocks.get(request)
173
+ wifi_status[key] = extract_value(data_blocks.get(request), "bEnable ") == '1' if value else None
174
+
175
+ device_data_response = data_blocks[device_data_request]
176
+
177
+ mapped_devices = self._parse_devices(device_data_response)
178
+
179
+ status = Status()
180
+ status._wan_macaddr = EUI48(network_info['wan_mac'])
181
+ status._lan_macaddr = EUI48(network_info['lan_mac'])
182
+ status._lan_ipv4_addr = IPv4Address(network_info['lan_ip'])
183
+ status._wan_ipv4_addr = IPv4Address(network_info['wan_ip'])
184
+ status._wan_ipv4_gateway = IPv4Address(network_info['gateway_ip'])
185
+ status.wan_ipv4_uptime = int(network_info['uptime']) // 100
186
+
187
+ status.wifi_2g_enable = wifi_status[Connection.HOST_2G]
188
+ status.wifi_5g_enable = wifi_status[Connection.HOST_5G]
189
+
190
+ status.wired_total = sum(1 for device in mapped_devices if device.type == Connection.WIRED)
191
+ status.wifi_clients_total = sum(1 for device in mapped_devices
192
+ if device.type in (Connection.HOST_2G, Connection.HOST_5G))
193
+ status.guest_clients_total = sum(1 for device in mapped_devices
194
+ if device.type in (Connection.GUEST_2G, Connection.GUEST_5G))
195
+ status.iot_clients_total = sum(1 for device in mapped_devices
196
+ if device.type in (Connection.IOT_2G, Connection.IOT_5G))
197
+ status.clients_total = (status.wired_total + status.wifi_clients_total +
198
+ status.guest_clients_total + status.iot_clients_total)
199
+
200
+ status.devices = mapped_devices
201
+ return status
202
+
203
+ def set_led_status(self, status: bool) -> None:
204
+ text = f'id 112|1,0,0\r\nenable {1 if status else 0}\r\n'
205
+ body = self._encrypt_body(text)
206
+ self.request(1, 0, True, data=body)
207
+
208
+ def get_led_status(self) -> bool:
209
+ text = '112|1,0,0'
210
+ body = self._encrypt_body(text)
211
+ response = self.request(2, 0, True, data=body)
212
+
213
+ response_text = self._decrypt_data(response.text)
214
+ response_text = response_text.splitlines()
215
+ if len(response_text) < 3:
216
+ raise ClientException("Invalid response for LED status from router")
217
+
218
+ return response_text[2][-1:] == '1'
219
+
220
+ def reboot(self) -> None:
221
+ self.request(6, 1, True)
222
+
223
+ def set_wifi(self, wifi: Connection, enable: bool) -> None:
224
+ enable_string = f'bEnable {int(enable)}'
225
+ text = f'id {RouterConstants.CONNECTION_REQUESTS_MAP[wifi]}\r\n{enable_string}'
226
+ body = self._encrypt_body(text)
227
+ self.request(1, 0, True, data=body)
228
+
229
+ def get_ipv4_status(self) -> IPv4Status:
230
+ mac_info_request = "1|1,0,0"
231
+ lan_ip_request = "4|1,0,0"
232
+ dhcp_request = "8|1,0,0"
233
+ link_type_request = "22|1,0,0"
234
+ wan_ip_request = "23|1,0,0"
235
+ static_ip_request = "24|1,0,0"
236
+ all_requests = [
237
+ mac_info_request, lan_ip_request, dhcp_request, link_type_request, wan_ip_request, static_ip_request]
238
+ request_text = '#'.join(all_requests)
239
+ body = self._encrypt_body(request_text)
240
+
241
+ response = self.request(2, 1, True, data=body)
242
+ response_text = self._decrypt_data(response.text)
243
+
244
+ matches = TplinkRE330Router.DATA_REGEX.findall(response_text)
245
+
246
+ data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
247
+
248
+ network_info = {
249
+ 'lan_mac': self._extract_value(data_blocks[mac_info_request], "mac 0 "),
250
+ 'wan_mac': self._extract_value(data_blocks[mac_info_request], "mac 1 "),
251
+ 'lan_ip': self._extract_value(data_blocks[lan_ip_request], "ip "),
252
+ 'wan_ip': self._extract_value(data_blocks[wan_ip_request], "ip "),
253
+ 'gateway_ip': self._extract_value(data_blocks[wan_ip_request], "gateway "),
254
+ 'uptime': self._extract_value(data_blocks[wan_ip_request], "upTime "),
255
+ 'wan_mask': self._extract_value(data_blocks[wan_ip_request], "mask "),
256
+ 'lan_mask': self._extract_value(data_blocks[lan_ip_request], "mask "),
257
+ 'dns_1': self._extract_value(data_blocks[wan_ip_request], "dns 0 "),
258
+ 'dns_2': self._extract_value(data_blocks[wan_ip_request], "dns 1 "),
259
+ 'dhcp_enabled': self._extract_value(data_blocks[dhcp_request], "enable "),
260
+ 'link_type': self._extract_value(data_blocks[link_type_request], "linkType "),
261
+ }
262
+
263
+ ipv4status = IPv4Status()
264
+ ipv4status._wan_macaddr = EUI48(network_info['wan_mac'])
265
+ ipv4status._wan_ipv4_ipaddr = IPv4Address(network_info['wan_ip'])
266
+ ipv4status._wan_ipv4_gateway = IPv4Address(network_info['gateway_ip'])
267
+ ipv4status._wan_ipv4_conntype = RouterConstants.CONNECTION_TYPE_MAP[network_info['link_type']]
268
+ ipv4status._wan_ipv4_netmask = IPv4Address(network_info['wan_mask'])
269
+ ipv4status._wan_ipv4_pridns = IPv4Address(network_info['dns_1'])
270
+ ipv4status._wan_ipv4_snddns = IPv4Address(network_info['dns_2'])
271
+ ipv4status._lan_macaddr = EUI48(network_info['lan_mac'])
272
+ ipv4status._lan_ipv4_ipaddr = IPv4Address(network_info['lan_ip'])
273
+ ipv4status.lan_ipv4_dhcp_enable = network_info['dhcp_enabled'] == '1'
274
+ ipv4status._lan_ipv4_netmask = IPv4Address(network_info['lan_mask'])
275
+ return ipv4status
276
+
277
+ def get_ipv4_reservations(self) -> list[IPv4Reservation]:
278
+ body = self._encrypt_body('12|1,0,0')
279
+
280
+ response = self.request(2, 1, True, data=body)
281
+ response_text = self._decrypt_data(response.text)
282
+ matches = TplinkRE330Router.DATA_REGEX.findall(response_text)
283
+
284
+ data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
285
+ filtered_reservations = self._parse_response_to_dict(data_blocks['12|1,0,0'])
286
+
287
+ mapped_reservations: list[IPv4Reservation] = []
288
+ for reservation in filtered_reservations:
289
+ reservation_to_add = IPv4Reservation(EUI48(reservation['mac']), IPv4Address(reservation['ip']),
290
+ reservation['name'], reservation['dhcpsEnable'] == '1')
291
+ mapped_reservations.append(reservation_to_add)
292
+ return mapped_reservations
293
+
294
+ def get_dhcp_leases(self) -> list[IPv4DHCPLease]:
295
+ body = self._encrypt_body('9|1,0,0')
296
+
297
+ response = self.request(2, 1, True, data=body)
298
+ response_text = self._decrypt_data(response.text)
299
+ matches = TplinkRE330Router.DATA_REGEX.findall(response_text)
300
+
301
+ data_blocks = {match[0]: match[1].strip().split("\r\n") for match in matches}
302
+
303
+ filtered_leases = self._parse_response_to_dict(data_blocks['9|1,0,0'])
304
+
305
+ mapped_leases: list[IPv4DHCPLease] = []
306
+ for lease in filtered_leases:
307
+ lease_to_add = IPv4DHCPLease(EUI48(lease['mac']), IPv4Address(lease['ip']),
308
+ lease['hostName'], f'expires {lease["expires"]}')
309
+ mapped_leases.append(lease_to_add)
310
+
311
+ return mapped_leases
312
+
313
+ def _parse_devices(self, device_data_response: list[str]) -> list[Device]:
314
+ filtered_devices = self._parse_response_to_dict(device_data_response)
315
+
316
+ device_type_to_connection = {
317
+ 0: Connection.WIRED,
318
+ 1: Connection.HOST_2G, 2: Connection.GUEST_2G,
319
+ 3: Connection.HOST_5G, 4: Connection.GUEST_5G,
320
+ 13: Connection.IOT_2G, 14: Connection.IOT_5G
321
+ }
322
+
323
+ mapped_devices = []
324
+ for device in filtered_devices:
325
+ if device['online'] == '1':
326
+ device_type = int(device['type'])
327
+ connection_type = device_type_to_connection.get(device_type, Connection.UNKNOWN)
328
+ else:
329
+ connection_type = Connection.UNKNOWN
330
+
331
+ device_to_add = Device(connection_type, EUI48(device['mac']), IPv4Address(device['ip']), device['name'])
332
+ device_to_add.up_speed = 0 # Not supported by the router
333
+ device_to_add.down_speed = 0 # Not supported by the router
334
+ device_to_add.active = device['online'] == '1'
335
+ mapped_devices.append(device_to_add)
336
+ return mapped_devices
337
+
338
+ def _parse_response_to_dict(self, response_data: list[str]) -> list[dict]:
339
+ result_dict = defaultdict(dict)
340
+ for entry in response_data:
341
+ parts = entry.split(' ', 2)
342
+ key, id_str = parts[0], parts[1]
343
+ value = parts[2] if len(parts) == 3 else ''
344
+ result_dict[int(id_str)][key] = value
345
+
346
+ return [v for _, v in result_dict.items() if v.get("ip") != "0.0.0.0"]
347
+
348
+ @staticmethod
349
+ def _encrypt_password(pwd: str, key: str = RouterConfig.KEY, encoding: str = RouterConfig.ENCODING) -> str:
350
+ max_len = max(len(key), len(pwd))
351
+ pwd = pwd.ljust(max_len, RouterConfig.PAD_CHAR)
352
+ key = key.ljust(max_len, RouterConfig.PAD_CHAR)
353
+
354
+ result = []
355
+ for i in range(max_len):
356
+ result.append(encoding[(ord(pwd[i]) ^ ord(key[i])) % len(encoding)])
357
+
358
+ return "".join(result)
359
+
360
+ @staticmethod
361
+ def _encode_token(encoded_password: str, response: requests.Response) -> str:
362
+ response_text = response.text.splitlines()
363
+ auth_info1 = response_text[RouterConstants.AUTH_TOKEN_INDEX1]
364
+ auth_info2 = response_text[RouterConstants.AUTH_TOKEN_INDEX2]
365
+
366
+ encoded_token = TplinkRE330Router._encrypt_password(encoded_password, auth_info1, auth_info2)
367
+ return parse.quote(encoded_token, safe='!()*')
368
+
369
+ def _get_signature(self, datalen: int) -> str:
370
+ encryption = self._encryption
371
+ r = f'{encryption.aes_string}&s={str(int(encryption.seq) + datalen)}'
372
+ e = ''
373
+ n = 0
374
+ while n < len(r):
375
+ e += EncryptionWrapper.rsa_encrypt(r[n:53], encryption.nn_rsa, encryption.ee_rsa)
376
+ n += 53
377
+ return e
378
+
379
+ 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
+
388
+ sign = self._get_signature(len(data))
389
+ return f'sign={sign}\r\ndata={data}'
390
+
391
+ 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")
398
+
399
+ def _extract_value(self, response_list, prefix):
400
+ return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
401
+
402
+ def request(self, code: int, asyn: int, use_token: bool = False, data: str = None):
403
+ url = f"{self.host}/?code={code}&asyn={asyn}"
404
+ if use_token:
405
+ url += f"&id={self._encryption.token}"
406
+ try:
407
+ response = self._session.post(url, data=data, timeout=self.timeout,
408
+ verify=self._verify_ssl, headers=self._headers)
409
+ # Raises exception for 4XX/5XX status codes for all requests except 1st in authorize
410
+ if not (code == 7 and asyn == 1 and use_token is False and data is None):
411
+ response.raise_for_status()
412
+ return response
413
+ except requests.exceptions.RequestException as e:
414
+ self._logger.error(f"Network error: {e}")
415
+ 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
@@ -12,6 +12,7 @@ from tplinkrouterc6u.client.c1200 import TplinkC1200Router
12
12
  from tplinkrouterc6u.client.c80 import TplinkC80Router
13
13
  from tplinkrouterc6u.client.vr import TPLinkVRClient
14
14
  from tplinkrouterc6u.client.wdr import TplinkWDRRouter
15
+ from tplinkrouterc6u.client.re330 import TplinkRE330Router
15
16
 
16
17
 
17
18
  class TplinkRouterProvider:
@@ -19,7 +20,7 @@ class TplinkRouterProvider:
19
20
  def get_client(host: str, password: str, username: str = 'admin', logger: Logger = None,
20
21
  verify_ssl: bool = True, timeout: int = 30) -> AbstractRouter:
21
22
  for client in [TplinkC5400XRouter, TPLinkVRClient, TPLinkEXClient, TPLinkMRClient, TPLinkDecoClient,
22
- TPLinkXDRClient, TplinkRouter, TplinkC80Router, TplinkWDRRouter]:
23
+ TPLinkXDRClient, TplinkRouter, TplinkC80Router, TplinkWDRRouter, TplinkRE330Router]:
23
24
  router = client(host, password, username, logger, verify_ssl, timeout)
24
25
  if router.supports():
25
26
  return router
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tplinkrouterc6u
3
- Version: 5.10.2
3
+ Version: 5.11.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
@@ -343,7 +343,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
343
343
  - Archer MR200 (v5, v5.3, v6.0)
344
344
  - Archer MR550 v1
345
345
  - Archer MR600 (v1, v2, v3)
346
- - Archer NX200
346
+ - Archer NX200 v2.0
347
347
  - Archer VR400 v3
348
348
  - Archer VR600 v3
349
349
  - Archer VR900v
@@ -382,6 +382,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
382
382
  - VX420-G2h v1.1
383
383
  - VX800v v1
384
384
  - XC220-G3v v2.30
385
+ - RE330 v1
385
386
  ### <a id="mercusys">MERCUSYS routers</a>
386
387
  - MR47BE v1.0
387
388
  - MR50G 1.0
@@ -5,11 +5,12 @@ test/test_client_c80.py,sha256=RY_1SgRVcQQdN9h0_IXA0YW4_0flEB_uel05QvDDfws,42359
5
5
  test/test_client_deco.py,sha256=YPLKRD8GoyDYHfRgdXvCk8iVNw8zdMJW-AHVnNbpdTM,31719
6
6
  test/test_client_ex.py,sha256=Kg6svEKtyGAfyF9yrLh2qZa2tK1mlEBJJwXRsq1MAjo,26591
7
7
  test/test_client_mr.py,sha256=lePxkmjcPzcrSFcaT8bT67L154cVJIOWrFlXMDOa8oY,33423
8
+ test/test_client_re330.py,sha256=MgefuvOzfZtZOujrcOsjiTDiGEAujfeFXshcq7gn32Q,17044
8
9
  test/test_client_wdr.py,sha256=0ZnRNP57MbuMv2cxFS8iIoVyv8Q6gtY0Q03gtHp9AWY,13492
9
10
  test/test_client_xdr.py,sha256=mgn-xL5mD5sHD8DjTz9vpY7jeh4Ob6Um6Y8v5Qgx2jA,23374
10
- tplinkrouterc6u/__init__.py,sha256=DDy6XVFH8Ne5C9JToRWNzRqevUfNutLUTlDhvIMsebo,1006
11
+ tplinkrouterc6u/__init__.py,sha256=WHMzO1mOGJqx7n89jHfNtieehlE3JEF17gtFmyZrqRM,1124
11
12
  tplinkrouterc6u/client_abstract.py,sha256=3UYzmll774S_Gb5E0FTVO_rI3-XFM7PSklg1-V-2jls,1419
12
- tplinkrouterc6u/provider.py,sha256=bzH0WW2peC66f8NqfiI3t_niX6MFO8SAdKP9WZL-IV4,2152
13
+ tplinkrouterc6u/provider.py,sha256=uv7AjDVOnud-hiXJdjoAbzy9UWPtiwHRIHRenjkVO_o,2230
13
14
  tplinkrouterc6u/client/__init__.py,sha256=KBy3fmtA9wgyFrb0Urh2x4CkKtWVnESdp-vxmuOvq0k,27
14
15
  tplinkrouterc6u/client/c1200.py,sha256=4XEYidEGmVIJk0YQLvmTnd0Gqa7glH2gUWvjreHpWrk,3178
15
16
  tplinkrouterc6u/client/c5400x.py,sha256=9E0omBSbWY_ljrs5MTCMu5brmrLtzsDB5O62Db8lP8Q,4329
@@ -18,6 +19,8 @@ tplinkrouterc6u/client/c80.py,sha256=ArVhza_fnXcEO-_fsQOd1l2QvmSfsswtohKxrZxEnoU
18
19
  tplinkrouterc6u/client/deco.py,sha256=cpKRggKD2RvSmMZuD6tzsZmehAUCU9oLiTTHcZBW81Y,8898
19
20
  tplinkrouterc6u/client/ex.py,sha256=gXWsVKAMo4CsX_Qeihb2iCARcsA3E9m2vgIWiJB3sjs,13197
20
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
21
24
  tplinkrouterc6u/client/vr.py,sha256=7Tbu0IrWtr4HHtyrnLFXEJi1QctzhilciL7agtwQ0R8,5025
22
25
  tplinkrouterc6u/client/wdr.py,sha256=i54PEifjhfOScDpgNBXygw9U4bfsVtle846_YjnDoBs,21679
23
26
  tplinkrouterc6u/client/xdr.py,sha256=QaZ_5vCaf8BV_JEs3S2Nz-QDREBYHGh3OUWIVS-fefY,10406
@@ -27,8 +30,8 @@ tplinkrouterc6u/common/encryption.py,sha256=4HelTxzN6esMfDZRBt3m8bwB9Nj_biKijnCn
27
30
  tplinkrouterc6u/common/exception.py,sha256=_0G8ZvW5__CsGifHrsZeULdl8c6EUD071sDCQsQgrHY,140
28
31
  tplinkrouterc6u/common/helper.py,sha256=23b04fk9HuVinrZXMCS5R1rmF8uZ7eM-Cdnp7Br9NR0,572
29
32
  tplinkrouterc6u/common/package_enum.py,sha256=4ykL_2Pw0nDEIH_qR9UJlFF6stTgSfhPz32r8KT-sh8,1624
30
- tplinkrouterc6u-5.10.2.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
31
- tplinkrouterc6u-5.10.2.dist-info/METADATA,sha256=H6GhpsW1U050iXJUTCs52FYTOTnh0tRM8DP3WUQ6Noo,16647
32
- tplinkrouterc6u-5.10.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- tplinkrouterc6u-5.10.2.dist-info/top_level.txt,sha256=1iSCCIueqgEkrTxtQ-jiHe99jAB10zqrVdBcwvNfe_M,21
34
- tplinkrouterc6u-5.10.2.dist-info/RECORD,,
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,,