tplinkrouterc6u 5.10.3__py3-none-any.whl → 5.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- test/test_client_re330.py +296 -0
- tplinkrouterc6u/__init__.py +3 -2
- tplinkrouterc6u/client/c80.py +7 -28
- tplinkrouterc6u/client/ex.py +38 -1
- tplinkrouterc6u/client/mr.py +76 -5
- tplinkrouterc6u/client/mr200.py +113 -5
- tplinkrouterc6u/client/re330.py +393 -0
- tplinkrouterc6u/client/xdr.py +2 -0
- tplinkrouterc6u/common/dataclass.py +1 -0
- tplinkrouterc6u/common/encryption.py +113 -0
- tplinkrouterc6u/common/package_enum.py +1 -0
- tplinkrouterc6u/provider.py +9 -5
- {tplinkrouterc6u-5.10.3.dist-info → tplinkrouterc6u-5.12.0.dist-info}/METADATA +16 -5
- {tplinkrouterc6u-5.10.3.dist-info → tplinkrouterc6u-5.12.0.dist-info}/RECORD +17 -15
- {tplinkrouterc6u-5.10.3.dist-info → tplinkrouterc6u-5.12.0.dist-info}/WHEEL +0 -0
- {tplinkrouterc6u-5.10.3.dist-info → tplinkrouterc6u-5.12.0.dist-info}/licenses/LICENSE +0 -0
- {tplinkrouterc6u-5.10.3.dist-info → tplinkrouterc6u-5.12.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
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
from tplinkrouterc6u.client.c6u import TplinkRouter
|
|
2
2
|
from tplinkrouterc6u.client.deco import TPLinkDecoClient
|
|
3
3
|
from tplinkrouterc6u.client_abstract import AbstractRouter
|
|
4
|
-
from tplinkrouterc6u.client.mr import TPLinkMRClient
|
|
4
|
+
from tplinkrouterc6u.client.mr import TPLinkMRClient, TPLinkMRClientGCM
|
|
5
5
|
from tplinkrouterc6u.client.mr200 import TPLinkMR200Client
|
|
6
|
-
from tplinkrouterc6u.client.ex import TPLinkEXClient
|
|
6
|
+
from tplinkrouterc6u.client.ex import TPLinkEXClient, TPLinkEXClientGCM
|
|
7
7
|
from tplinkrouterc6u.client.vr import TPLinkVRClient
|
|
8
8
|
from tplinkrouterc6u.client.c80 import TplinkC80Router
|
|
9
9
|
from tplinkrouterc6u.client.c5400x import TplinkC5400XRouter
|
|
10
10
|
from tplinkrouterc6u.client.c1200 import TplinkC1200Router
|
|
11
11
|
from tplinkrouterc6u.client.xdr import TPLinkXDRClient
|
|
12
12
|
from tplinkrouterc6u.client.wdr import TplinkWDRRouter
|
|
13
|
+
from tplinkrouterc6u.client.re330 import TplinkRE330Router
|
|
13
14
|
from tplinkrouterc6u.provider import TplinkRouterProvider
|
|
14
15
|
from tplinkrouterc6u.common.package_enum import Connection, VPN
|
|
15
16
|
from tplinkrouterc6u.common.dataclass import (
|
tplinkrouterc6u/client/c80.py
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from logging import Logger
|
|
3
3
|
from urllib import parse
|
|
4
|
-
from base64 import b64encode, b64decode
|
|
5
4
|
from collections import defaultdict
|
|
6
5
|
from ipaddress import IPv4Address
|
|
7
6
|
import re
|
|
8
|
-
from Crypto.Cipher import AES
|
|
9
|
-
from Crypto.Util.Padding import pad, unpad
|
|
10
7
|
from macaddress import EUI48
|
|
11
8
|
import requests
|
|
12
9
|
from requests import Session
|
|
@@ -21,7 +18,6 @@ from tplinkrouterc6u.client_abstract import AbstractRouter
|
|
|
21
18
|
class RouterConstants:
|
|
22
19
|
AUTH_TOKEN_INDEX1 = 3
|
|
23
20
|
AUTH_TOKEN_INDEX2 = 4
|
|
24
|
-
DEFAULT_AES_VALUE = "0000000000000000"
|
|
25
21
|
|
|
26
22
|
HOST_WIFI_2G_REQUEST = '33|1,1,0'
|
|
27
23
|
HOST_WIFI_5G_REQUEST = '33|2,1,0'
|
|
@@ -64,9 +60,7 @@ class EncryptionState:
|
|
|
64
60
|
self.nn_rsa = ''
|
|
65
61
|
self.ee_rsa = ''
|
|
66
62
|
self.seq = ''
|
|
67
|
-
self.
|
|
68
|
-
self.iv_aes = ''
|
|
69
|
-
self.aes_string = ''
|
|
63
|
+
self.aes = EncryptionWrapper()
|
|
70
64
|
self.token = ''
|
|
71
65
|
|
|
72
66
|
|
|
@@ -105,14 +99,11 @@ class TplinkC80Router(AbstractRouter):
|
|
|
105
99
|
self._encryption.nn_rsa = responseText[2]
|
|
106
100
|
self._encryption.seq = responseText[3]
|
|
107
101
|
|
|
108
|
-
# Generate key and initialization vector
|
|
109
|
-
self._encryption.key_aes = RouterConstants.DEFAULT_AES_VALUE
|
|
110
|
-
self._encryption.iv_aes = RouterConstants.DEFAULT_AES_VALUE
|
|
111
|
-
self._encryption.aes_string = f'k={self._encryption.key_aes}&i={self._encryption.iv_aes}'
|
|
112
|
-
|
|
113
102
|
# Encrypt AES string
|
|
114
|
-
aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.
|
|
103
|
+
aes_string_encrypted = EncryptionWrapper.rsa_encrypt(self._encryption.aes._get_aes_string(),
|
|
104
|
+
self._encryption.nn_rsa,
|
|
115
105
|
self._encryption.ee_rsa)
|
|
106
|
+
|
|
116
107
|
# Register AES string for decryption on server side
|
|
117
108
|
self.request(16, 0, True, data=f'set {aes_string_encrypted}')
|
|
118
109
|
# Some auth request, might be redundant
|
|
@@ -368,7 +359,7 @@ class TplinkC80Router(AbstractRouter):
|
|
|
368
359
|
|
|
369
360
|
def _get_signature(self, datalen: int) -> str:
|
|
370
361
|
encryption = self._encryption
|
|
371
|
-
r = f'{encryption.
|
|
362
|
+
r = f'{encryption.aes._get_aes_string()}&s={str(int(encryption.seq) + datalen)}'
|
|
372
363
|
e = ''
|
|
373
364
|
n = 0
|
|
374
365
|
while n < len(r):
|
|
@@ -377,24 +368,12 @@ class TplinkC80Router(AbstractRouter):
|
|
|
377
368
|
return e
|
|
378
369
|
|
|
379
370
|
def _encrypt_body(self, text: str) -> str:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
key_bytes = encryption.key_aes.encode("utf-8")
|
|
383
|
-
iv_bytes = encryption.iv_aes.encode("utf-8")
|
|
384
|
-
|
|
385
|
-
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
|
386
|
-
data = b64encode(cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))).decode()
|
|
387
|
-
|
|
371
|
+
data = self._encryption.aes.aes_encrypt(text)
|
|
388
372
|
sign = self._get_signature(len(data))
|
|
389
373
|
return f'sign={sign}\r\ndata={data}'
|
|
390
374
|
|
|
391
375
|
def _decrypt_data(self, encrypted_text: str) -> str:
|
|
392
|
-
|
|
393
|
-
iv_bytes = self._encryption.iv_aes.encode("utf-8")
|
|
394
|
-
|
|
395
|
-
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
|
396
|
-
decrypted_padded = cipher.decrypt(b64decode(encrypted_text))
|
|
397
|
-
return unpad(decrypted_padded, AES.block_size).decode("utf-8")
|
|
376
|
+
return self._encryption.aes.aes_decrypt(encrypted_text)
|
|
398
377
|
|
|
399
378
|
def _extract_value(self, response_list, prefix):
|
|
400
379
|
return next((s.split(prefix, 1)[1] for s in response_list if s.startswith(prefix)), None)
|
tplinkrouterc6u/client/ex.py
CHANGED
|
@@ -15,9 +15,10 @@ from tplinkrouterc6u.common.dataclass import (
|
|
|
15
15
|
IPv4Status,
|
|
16
16
|
VPNStatus)
|
|
17
17
|
from tplinkrouterc6u.common.exception import ClientException, ClientError
|
|
18
|
-
from tplinkrouterc6u.client.mr import TPLinkMRClientBase
|
|
18
|
+
from tplinkrouterc6u.client.mr import TPLinkMRClientBase, TPLinkMRClientBaseGCM
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
# Class for EX series routers which supports old firmwares with AES cipher CBC mode
|
|
21
22
|
class TPLinkEXClient(TPLinkMRClientBase):
|
|
22
23
|
WIFI_SET = {
|
|
23
24
|
Connection.HOST_2G: '1,0,0,0,0,0',
|
|
@@ -331,3 +332,39 @@ class TPLinkEXClient(TPLinkMRClientBase):
|
|
|
331
332
|
]
|
|
332
333
|
|
|
333
334
|
self.req_act(acts)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# Class for EX series routers which supports AES cipher GCM mode
|
|
338
|
+
class TPLinkEXClientGCM(TPLinkMRClientBaseGCM, TPLinkEXClient):
|
|
339
|
+
|
|
340
|
+
def _req_login(self) -> None:
|
|
341
|
+
login_data = ('{"data":{"UserName":"%s","Passwd":"%s","Action": "1","stack":"0,0,0,0,0,0",'
|
|
342
|
+
'"pstack":"0,0,0,0,0,0"},"operation":"cgi","oid":"/cgi/login"}') % (
|
|
343
|
+
b64encode(bytes(self.username, "utf-8")).decode("utf-8"),
|
|
344
|
+
b64encode(bytes(self.password, "utf-8")).decode("utf-8")
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
sign, data, tag = self._prepare_data(login_data, True)
|
|
348
|
+
assert len(sign) == 256
|
|
349
|
+
|
|
350
|
+
request_data = f"sign={sign}\r\ndata={data}\r\ntag={tag}\r\n"
|
|
351
|
+
|
|
352
|
+
url = f"{self.host}/cgi_gdpr?9"
|
|
353
|
+
(code, response) = self._request(url, data_str=request_data)
|
|
354
|
+
response = self._encryption.aes_decrypt(response)
|
|
355
|
+
|
|
356
|
+
# parse and match return code
|
|
357
|
+
ret_code = self._parse_ret_val(response)
|
|
358
|
+
error = ''
|
|
359
|
+
if ret_code == self.HTTP_ERR_USER_PWD_NOT_CORRECT:
|
|
360
|
+
error = ('TplinkRouter - EX - Login failed, wrong user or password. '
|
|
361
|
+
'Try to pass user instead of admin in username')
|
|
362
|
+
elif ret_code == self.HTTP_ERR_USER_BAD_REQUEST:
|
|
363
|
+
error = 'TplinkRouter - EX - Login failed. Generic error code: {}'.format(ret_code)
|
|
364
|
+
elif ret_code != self.HTTP_RET_OK:
|
|
365
|
+
error = 'TplinkRouter - EX - Login failed. Unknown error code: {}'.format(ret_code)
|
|
366
|
+
|
|
367
|
+
if error:
|
|
368
|
+
if self._logger:
|
|
369
|
+
self._logger.debug(error)
|
|
370
|
+
raise ClientException(error)
|
tplinkrouterc6u/client/mr.py
CHANGED
|
@@ -8,7 +8,7 @@ from macaddress import EUI48
|
|
|
8
8
|
from ipaddress import IPv4Address
|
|
9
9
|
from logging import Logger
|
|
10
10
|
from tplinkrouterc6u.common.helper import get_ip, get_mac, get_value
|
|
11
|
-
from tplinkrouterc6u.common.encryption import EncryptionWrapperMR
|
|
11
|
+
from tplinkrouterc6u.common.encryption import EncryptionWrapperMR, EncryptionWrapperMRGCM
|
|
12
12
|
from tplinkrouterc6u.common.package_enum import Connection, VPN
|
|
13
13
|
from tplinkrouterc6u.common.dataclass import (
|
|
14
14
|
Firmware,
|
|
@@ -80,6 +80,7 @@ class TPLinkMRClientBase(AbstractRouter):
|
|
|
80
80
|
if self._verify_ssl is False:
|
|
81
81
|
self.req.verify = False
|
|
82
82
|
self._token = None
|
|
83
|
+
self._authorized_at = None
|
|
83
84
|
self._hash = md5(f"{self.username}{self.password}".encode()).hexdigest()
|
|
84
85
|
self._nn = None
|
|
85
86
|
self._ee = None
|
|
@@ -96,10 +97,9 @@ class TPLinkMRClientBase(AbstractRouter):
|
|
|
96
97
|
return False
|
|
97
98
|
|
|
98
99
|
def authorize(self) -> None:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# hash the password
|
|
100
|
+
if self._token is not None and self._authorized_at >= (datetime.now() - timedelta(seconds=3)):
|
|
101
|
+
return
|
|
102
|
+
self._token = None
|
|
103
103
|
|
|
104
104
|
# request the RSA public key from the host
|
|
105
105
|
self._nn, self._ee, self._seq = self._req_rsa_key()
|
|
@@ -109,6 +109,7 @@ class TPLinkMRClientBase(AbstractRouter):
|
|
|
109
109
|
|
|
110
110
|
# request TokenID
|
|
111
111
|
self._token = self._req_token()
|
|
112
|
+
self._authorized_at = datetime.now()
|
|
112
113
|
|
|
113
114
|
def reboot(self) -> None:
|
|
114
115
|
acts = [
|
|
@@ -587,6 +588,71 @@ class TPLinkMRClientBase(AbstractRouter):
|
|
|
587
588
|
return signature, encrypted_data
|
|
588
589
|
|
|
589
590
|
|
|
591
|
+
class TPLinkMRClientBaseGCM(TPLinkMRClientBase):
|
|
592
|
+
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
593
|
+
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
594
|
+
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
595
|
+
|
|
596
|
+
self._encryption = EncryptionWrapperMRGCM()
|
|
597
|
+
|
|
598
|
+
def supports(self) -> bool:
|
|
599
|
+
try:
|
|
600
|
+
self.authorize()
|
|
601
|
+
return True
|
|
602
|
+
except Exception:
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
def _request(self, url, method='POST', data_str=None, encrypt=False, is_login=False):
|
|
608
|
+
headers = self.HEADERS
|
|
609
|
+
headers['Referer'] = self.host
|
|
610
|
+
|
|
611
|
+
if self._token is not None:
|
|
612
|
+
headers['TokenID'] = self._token
|
|
613
|
+
|
|
614
|
+
if encrypt:
|
|
615
|
+
sign, data, tag = self._prepare_data(data_str, is_login)
|
|
616
|
+
data = 'sign={}\r\ndata={}\r\ntag={}\r\n'.format(sign, data, tag)
|
|
617
|
+
else:
|
|
618
|
+
data = data_str
|
|
619
|
+
|
|
620
|
+
retry = 0
|
|
621
|
+
while retry < self.REQUEST_RETRIES:
|
|
622
|
+
# send the request
|
|
623
|
+
if method == 'POST':
|
|
624
|
+
r = self.req.post(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
|
|
625
|
+
elif method == 'GET':
|
|
626
|
+
r = self.req.get(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
|
|
627
|
+
else:
|
|
628
|
+
raise Exception('Unsupported method ' + str(method))
|
|
629
|
+
|
|
630
|
+
# sometimes we get 500 here, not sure why... just retry the request
|
|
631
|
+
if (r.status_code not in [500, 406]
|
|
632
|
+
and '<title>500 Internal Server Error</title>' not in r.text
|
|
633
|
+
and '<title>406 Not Acceptable</title>' not in r.text):
|
|
634
|
+
break
|
|
635
|
+
|
|
636
|
+
sleep(0.1)
|
|
637
|
+
retry += 1
|
|
638
|
+
|
|
639
|
+
# decrypt the response, if needed
|
|
640
|
+
if encrypt and (r.status_code == 200) and (r.text != ''):
|
|
641
|
+
return r.status_code, self._encryption.aes_decrypt(r.text)
|
|
642
|
+
else:
|
|
643
|
+
return r.status_code, r.text
|
|
644
|
+
|
|
645
|
+
def _prepare_data(self, data: str, is_login: bool) -> tuple[str, str, str]:
|
|
646
|
+
encrypted_data, tag = self._encryption.aes_encrypt(data)
|
|
647
|
+
data_len = len(encrypted_data)
|
|
648
|
+
# get encrypted signature
|
|
649
|
+
signature = self._encryption.get_signature(int(self._seq) + data_len, is_login, self._hash, self._nn, self._ee)
|
|
650
|
+
|
|
651
|
+
# format expected raw request data
|
|
652
|
+
return signature, encrypted_data, tag
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# Class for MR series routers which supports old firmwares with AES cipher CBC mode
|
|
590
656
|
class TPLinkMRClient(TPLinkMRClientBase):
|
|
591
657
|
|
|
592
658
|
def logout(self) -> None:
|
|
@@ -716,3 +782,8 @@ class TPLinkMRClient(TPLinkMRClientBase):
|
|
|
716
782
|
status.isp_name = values['3']['ispName']
|
|
717
783
|
|
|
718
784
|
return status
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
# Class for MR series routers which supports AES cipher GCM mode
|
|
788
|
+
class TPLinkMRClientGCM(TPLinkMRClientBaseGCM, TPLinkMRClient):
|
|
789
|
+
pass
|