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.
- test/test_client_re330.py +296 -0
- tplinkrouterc6u/__init__.py +2 -0
- tplinkrouterc6u/client/mr200.py +412 -0
- tplinkrouterc6u/client/re330.py +423 -0
- tplinkrouterc6u/provider.py +2 -1
- {tplinkrouterc6u-5.10.2.dist-info → tplinkrouterc6u-5.11.0.dist-info}/METADATA +3 -2
- {tplinkrouterc6u-5.10.2.dist-info → tplinkrouterc6u-5.11.0.dist-info}/RECORD +10 -7
- {tplinkrouterc6u-5.10.2.dist-info → tplinkrouterc6u-5.11.0.dist-info}/WHEEL +0 -0
- {tplinkrouterc6u-5.10.2.dist-info → tplinkrouterc6u-5.11.0.dist-info}/licenses/LICENSE +0 -0
- {tplinkrouterc6u-5.10.2.dist-info → tplinkrouterc6u-5.11.0.dist-info}/top_level.txt +0 -0
|
@@ -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()
|
tplinkrouterc6u/__init__.py
CHANGED
|
@@ -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
|
tplinkrouterc6u/provider.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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=
|
|
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.
|
|
31
|
-
tplinkrouterc6u-5.
|
|
32
|
-
tplinkrouterc6u-5.
|
|
33
|
-
tplinkrouterc6u-5.
|
|
34
|
-
tplinkrouterc6u-5.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|