tplinkrouterc6u 5.0.3__py3-none-any.whl → 5.1.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_ex.py +339 -0
- test/test_client_mr.py +185 -0
- tplinkrouterc6u/__init__.py +13 -12
- tplinkrouterc6u/client/__init__.py +1 -0
- tplinkrouterc6u/client/api_cgi_bin.py +435 -0
- tplinkrouterc6u/client/c1200.py +102 -0
- tplinkrouterc6u/client/c5400x.py +109 -0
- tplinkrouterc6u/client/c6v4.py +38 -0
- tplinkrouterc6u/client/deco.py +177 -0
- tplinkrouterc6u/client/ex.py +295 -0
- tplinkrouterc6u/client/mr.py +645 -0
- tplinkrouterc6u/client_abstract.py +48 -0
- tplinkrouterc6u/common/__init__.py +1 -0
- tplinkrouterc6u/{dataclass.py → common/dataclass.py} +12 -1
- tplinkrouterc6u/provider.py +37 -0
- {tplinkrouterc6u-5.0.3.dist-info → tplinkrouterc6u-5.1.0.dist-info}/METADATA +16 -1
- tplinkrouterc6u-5.1.0.dist-info/RECORD +28 -0
- {tplinkrouterc6u-5.0.3.dist-info → tplinkrouterc6u-5.1.0.dist-info}/WHEEL +1 -1
- tplinkrouterc6u/client.py +0 -1451
- tplinkrouterc6u-5.0.3.dist-info/RECORD +0 -17
- /tplinkrouterc6u/{encryption.py → common/encryption.py} +0 -0
- /tplinkrouterc6u/{exception.py → common/exception.py} +0 -0
- /tplinkrouterc6u/{helper.py → common/helper.py} +0 -0
- /tplinkrouterc6u/{package_enum.py → common/package_enum.py} +0 -0
- {tplinkrouterc6u-5.0.3.dist-info → tplinkrouterc6u-5.1.0.dist-info}/LICENSE +0 -0
- {tplinkrouterc6u-5.0.3.dist-info → tplinkrouterc6u-5.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
from hashlib import md5
|
|
2
|
+
from re import search
|
|
3
|
+
from time import time, sleep
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
from requests import Session
|
|
6
|
+
from datetime import timedelta, datetime
|
|
7
|
+
from macaddress import EUI48
|
|
8
|
+
from ipaddress import IPv4Address
|
|
9
|
+
from logging import Logger
|
|
10
|
+
from tplinkrouterc6u.common.encryption import EncryptionWrapperMR
|
|
11
|
+
from tplinkrouterc6u.common.package_enum import Connection
|
|
12
|
+
from tplinkrouterc6u.common.dataclass import (
|
|
13
|
+
Firmware,
|
|
14
|
+
Status,
|
|
15
|
+
Device,
|
|
16
|
+
IPv4Reservation,
|
|
17
|
+
IPv4DHCPLease,
|
|
18
|
+
IPv4Status,
|
|
19
|
+
SMS,
|
|
20
|
+
)
|
|
21
|
+
from tplinkrouterc6u.common.exception import ClientException, ClientError
|
|
22
|
+
from tplinkrouterc6u.client_abstract import AbstractRouter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TPLinkMRClientBase(AbstractRouter):
|
|
26
|
+
REQUEST_RETRIES = 3
|
|
27
|
+
|
|
28
|
+
HEADERS = {
|
|
29
|
+
'Accept': '*/*',
|
|
30
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
|
|
31
|
+
'Referer': 'http://192.168.1.1/' # updated on the fly
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
HTTP_RET_OK = 0
|
|
35
|
+
HTTP_ERR_CGI_INVALID_ANSI = 71017
|
|
36
|
+
HTTP_ERR_USER_PWD_NOT_CORRECT = 71233
|
|
37
|
+
HTTP_ERR_USER_BAD_REQUEST = 71234
|
|
38
|
+
|
|
39
|
+
CLIENT_TYPES = {
|
|
40
|
+
0: Connection.WIRED,
|
|
41
|
+
1: Connection.HOST_2G,
|
|
42
|
+
3: Connection.HOST_5G,
|
|
43
|
+
2: Connection.GUEST_2G,
|
|
44
|
+
4: Connection.GUEST_5G,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
WIFI_SET = {
|
|
48
|
+
Connection.HOST_2G: '1,1,0,0,0,0',
|
|
49
|
+
Connection.HOST_5G: '1,2,0,0,0,0',
|
|
50
|
+
Connection.GUEST_2G: '1,1,1,0,0,0',
|
|
51
|
+
Connection.GUEST_5G: '1,2,1,0,0,0',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class ActItem:
|
|
55
|
+
GET = 1
|
|
56
|
+
SET = 2
|
|
57
|
+
ADD = 3
|
|
58
|
+
DEL = 4
|
|
59
|
+
GL = 5
|
|
60
|
+
GS = 6
|
|
61
|
+
OP = 7
|
|
62
|
+
CGI = 8
|
|
63
|
+
|
|
64
|
+
def __init__(self, type: int, oid: str, stack: str = '0,0,0,0,0,0', pstack: str = '0,0,0,0,0,0',
|
|
65
|
+
attrs: list = []):
|
|
66
|
+
self.type = type
|
|
67
|
+
self.oid = oid
|
|
68
|
+
self.stack = stack
|
|
69
|
+
self.pstack = pstack
|
|
70
|
+
self.attrs = attrs
|
|
71
|
+
|
|
72
|
+
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
73
|
+
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
74
|
+
super().__init__(host, password, username, logger, verify_ssl, timeout)
|
|
75
|
+
|
|
76
|
+
self.req = Session()
|
|
77
|
+
self._token = None
|
|
78
|
+
self._hash = md5(f"{self.username}{self.password}".encode()).hexdigest()
|
|
79
|
+
self._nn = None
|
|
80
|
+
self._ee = None
|
|
81
|
+
self._seq = None
|
|
82
|
+
self._url_rsa_key = 'cgi/getParm'
|
|
83
|
+
|
|
84
|
+
self._encryption = EncryptionWrapperMR()
|
|
85
|
+
|
|
86
|
+
def supports(self) -> bool:
|
|
87
|
+
try:
|
|
88
|
+
self._req_rsa_key()
|
|
89
|
+
return True
|
|
90
|
+
except ClientException:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def authorize(self) -> None:
|
|
94
|
+
'''
|
|
95
|
+
Establishes a login session to the host using provided credentials
|
|
96
|
+
'''
|
|
97
|
+
# hash the password
|
|
98
|
+
|
|
99
|
+
# request the RSA public key from the host
|
|
100
|
+
self._nn, self._ee, self._seq = self._req_rsa_key()
|
|
101
|
+
|
|
102
|
+
# authenticate
|
|
103
|
+
self._req_login()
|
|
104
|
+
|
|
105
|
+
# request TokenID
|
|
106
|
+
self._token = self._req_token()
|
|
107
|
+
|
|
108
|
+
def reboot(self) -> None:
|
|
109
|
+
acts = [
|
|
110
|
+
self.ActItem(self.ActItem.OP, 'ACT_REBOOT')
|
|
111
|
+
]
|
|
112
|
+
self.req_act(acts)
|
|
113
|
+
|
|
114
|
+
def req_act(self, acts: list):
|
|
115
|
+
'''
|
|
116
|
+
Requests ACTs via the cgi_gdpr proxy
|
|
117
|
+
'''
|
|
118
|
+
act_types = []
|
|
119
|
+
act_data = []
|
|
120
|
+
|
|
121
|
+
for act in acts:
|
|
122
|
+
act_types.append(str(act.type))
|
|
123
|
+
act_data.append('[{}#{}#{}]{},{}\r\n{}\r\n'.format(
|
|
124
|
+
act.oid,
|
|
125
|
+
act.stack,
|
|
126
|
+
act.pstack,
|
|
127
|
+
len(act_types) - 1, # index, starts at 0
|
|
128
|
+
len(act.attrs),
|
|
129
|
+
'\r\n'.join(act.attrs)
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
data = '&'.join(act_types) + '\r\n' + ''.join(act_data)
|
|
133
|
+
|
|
134
|
+
url = self._get_url('cgi_gdpr')
|
|
135
|
+
(code, response) = self._request(url, data_str=data, encrypt=True)
|
|
136
|
+
|
|
137
|
+
if code != 200:
|
|
138
|
+
error = 'TplinkRouter - MR - Response with error; Request {} - Response {}'.format(data, response)
|
|
139
|
+
if self._logger:
|
|
140
|
+
self._logger.debug(error)
|
|
141
|
+
raise ClientError(error)
|
|
142
|
+
|
|
143
|
+
result = self._merge_response(response)
|
|
144
|
+
|
|
145
|
+
return response, result.get('0') if len(result) == 1 and result.get('0') else result
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _to_list(response: dict | list | None) -> list:
|
|
149
|
+
if response is None:
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
return [response] if response.__class__ != list else response
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _merge_response(response: str) -> dict:
|
|
156
|
+
result = {}
|
|
157
|
+
obj = {}
|
|
158
|
+
lines = response.split('\n')
|
|
159
|
+
for line in lines:
|
|
160
|
+
if line.startswith('['):
|
|
161
|
+
regexp = search(r'\[\d,\d,\d,\d,\d,\d\](\d)', line)
|
|
162
|
+
if regexp is not None:
|
|
163
|
+
obj = {}
|
|
164
|
+
index = regexp.group(1)
|
|
165
|
+
item = result.get(index)
|
|
166
|
+
if item is not None:
|
|
167
|
+
if item.__class__ != list:
|
|
168
|
+
result[index] = [item]
|
|
169
|
+
result[index].append(obj)
|
|
170
|
+
else:
|
|
171
|
+
result[index] = obj
|
|
172
|
+
continue
|
|
173
|
+
if '=' in line:
|
|
174
|
+
keyval = line.split('=')
|
|
175
|
+
assert len(keyval) == 2
|
|
176
|
+
|
|
177
|
+
obj[keyval[0]] = keyval[1]
|
|
178
|
+
|
|
179
|
+
return result if result else []
|
|
180
|
+
|
|
181
|
+
def _get_url(self, endpoint: str, params: dict = {}, include_ts: bool = True) -> str:
|
|
182
|
+
# add timestamp param
|
|
183
|
+
if include_ts:
|
|
184
|
+
params['_'] = str(round(time() * 1000))
|
|
185
|
+
|
|
186
|
+
# format params into a string
|
|
187
|
+
params_arr = []
|
|
188
|
+
for attr, value in params.items():
|
|
189
|
+
params_arr.append('{}={}'.format(attr, value))
|
|
190
|
+
|
|
191
|
+
# format url
|
|
192
|
+
return '{}/{}{}{}'.format(
|
|
193
|
+
self.host,
|
|
194
|
+
endpoint,
|
|
195
|
+
'?' if len(params_arr) > 0 else '',
|
|
196
|
+
'&'.join(params_arr)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _req_token(self):
|
|
200
|
+
'''
|
|
201
|
+
Requests the TokenID, used for CGI authentication (together with cookies)
|
|
202
|
+
- token is inlined as JS var in the index (/) html page
|
|
203
|
+
e.g.: <script type="text/javascript">var token="086724f57013f16e042e012becf825";</script>
|
|
204
|
+
|
|
205
|
+
Return value:
|
|
206
|
+
TokenID string
|
|
207
|
+
'''
|
|
208
|
+
url = self._get_url('')
|
|
209
|
+
(code, response) = self._request(url, method='GET')
|
|
210
|
+
assert code == 200
|
|
211
|
+
|
|
212
|
+
result = search('var token="(.*)";', response)
|
|
213
|
+
|
|
214
|
+
assert result is not None
|
|
215
|
+
assert result.group(1) != ''
|
|
216
|
+
|
|
217
|
+
return result.group(1)
|
|
218
|
+
|
|
219
|
+
def _req_rsa_key(self):
|
|
220
|
+
'''
|
|
221
|
+
Requests the RSA public key from the host
|
|
222
|
+
|
|
223
|
+
Return value:
|
|
224
|
+
((n, e), seq) tuple
|
|
225
|
+
'''
|
|
226
|
+
response = ''
|
|
227
|
+
try:
|
|
228
|
+
url = self._get_url(self._url_rsa_key)
|
|
229
|
+
(code, response) = self._request(url)
|
|
230
|
+
assert code == 200
|
|
231
|
+
|
|
232
|
+
# assert return code
|
|
233
|
+
assert self._parse_ret_val(response) == self.HTTP_RET_OK
|
|
234
|
+
|
|
235
|
+
# parse public key
|
|
236
|
+
ee = search('var ee="(.*)";', response)
|
|
237
|
+
nn = search('var nn="(.*)";', response)
|
|
238
|
+
seq = search('var seq="(.*)";', response)
|
|
239
|
+
|
|
240
|
+
assert ee and nn and seq
|
|
241
|
+
ee = ee.group(1)
|
|
242
|
+
nn = nn.group(1)
|
|
243
|
+
seq = seq.group(1)
|
|
244
|
+
assert len(ee) == 6
|
|
245
|
+
assert len(nn) == 128
|
|
246
|
+
assert seq.isnumeric()
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
error = ('TplinkRouter - {} - Unknown error rsa_key! Error - {}; Response - {}'
|
|
250
|
+
.format(self.__class__.__name__, e, response))
|
|
251
|
+
if self._logger:
|
|
252
|
+
self._logger.debug(error)
|
|
253
|
+
raise ClientException(error)
|
|
254
|
+
|
|
255
|
+
return nn, ee, int(seq)
|
|
256
|
+
|
|
257
|
+
def _req_login(self) -> None:
|
|
258
|
+
'''
|
|
259
|
+
Authenticates to the host
|
|
260
|
+
- sets the session token after successful login
|
|
261
|
+
- data/signature is passed as a GET parameter, NOT as a raw request data
|
|
262
|
+
(unlike for regular encrypted requests to the /cgi_gdpr endpoint)
|
|
263
|
+
|
|
264
|
+
Example session token (set as a cookie):
|
|
265
|
+
{'JSESSIONID': '4d786fede0164d7613411c7b6ec61e'}
|
|
266
|
+
'''
|
|
267
|
+
# encrypt username + password
|
|
268
|
+
|
|
269
|
+
sign, data = self._prepare_data(self.username + '\n' + self.password, True)
|
|
270
|
+
assert len(sign) == 256
|
|
271
|
+
|
|
272
|
+
data = {
|
|
273
|
+
'data': quote(data, safe='~()*!.\''),
|
|
274
|
+
'sign': sign,
|
|
275
|
+
'Action': 1,
|
|
276
|
+
'LoginStatus': 0,
|
|
277
|
+
'isMobile': 0
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
url = self._get_url('cgi/login', data)
|
|
281
|
+
(code, response) = self._request(url)
|
|
282
|
+
assert code == 200
|
|
283
|
+
|
|
284
|
+
# parse and match return code
|
|
285
|
+
ret_code = self._parse_ret_val(response)
|
|
286
|
+
error = ''
|
|
287
|
+
if ret_code == self.HTTP_ERR_USER_PWD_NOT_CORRECT:
|
|
288
|
+
info = search('var currAuthTimes=(.*);\nvar currForbidTime=(.*);', response)
|
|
289
|
+
assert info is not None
|
|
290
|
+
|
|
291
|
+
error = 'TplinkRouter - MR - Login failed, wrong password. Auth times: {}/5, Forbid time: {}'.format(
|
|
292
|
+
info.group(1), info.group(2))
|
|
293
|
+
elif ret_code == self.HTTP_ERR_USER_BAD_REQUEST:
|
|
294
|
+
error = 'TplinkRouter - MR - Login failed. Generic error code: {}'.format(ret_code)
|
|
295
|
+
elif ret_code != self.HTTP_RET_OK:
|
|
296
|
+
error = 'TplinkRouter - MR - Login failed. Unknown error code: {}'.format(ret_code)
|
|
297
|
+
|
|
298
|
+
if error:
|
|
299
|
+
if self._logger:
|
|
300
|
+
self._logger.debug(error)
|
|
301
|
+
raise ClientException(error)
|
|
302
|
+
|
|
303
|
+
def _request(self, url, method='POST', data_str=None, encrypt=False):
|
|
304
|
+
'''
|
|
305
|
+
Prepares and sends an HTTP request to the host
|
|
306
|
+
- sets up the headers, handles token auth
|
|
307
|
+
- encrypts/decrypts the data, if needed
|
|
308
|
+
|
|
309
|
+
Return value:
|
|
310
|
+
(status_code, response_text) tuple
|
|
311
|
+
'''
|
|
312
|
+
headers = self.HEADERS
|
|
313
|
+
|
|
314
|
+
# add referer to request headers,
|
|
315
|
+
# otherwise we get 403 Forbidden
|
|
316
|
+
headers['Referer'] = self.host
|
|
317
|
+
|
|
318
|
+
# add token to request headers,
|
|
319
|
+
# used for CGI auth (together with JSESSIONID cookie)
|
|
320
|
+
if self._token is not None:
|
|
321
|
+
headers['TokenID'] = self._token
|
|
322
|
+
|
|
323
|
+
# encrypt request data if needed (for the /cgi_gdpr endpoint)
|
|
324
|
+
if encrypt:
|
|
325
|
+
sign, data = self._prepare_data(data_str, False)
|
|
326
|
+
data = 'sign={}\r\ndata={}\r\n'.format(sign, data)
|
|
327
|
+
else:
|
|
328
|
+
data = data_str
|
|
329
|
+
|
|
330
|
+
retry = 0
|
|
331
|
+
while retry < self.REQUEST_RETRIES:
|
|
332
|
+
# send the request
|
|
333
|
+
if method == 'POST':
|
|
334
|
+
r = self.req.post(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
|
|
335
|
+
elif method == 'GET':
|
|
336
|
+
r = self.req.get(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
|
|
337
|
+
else:
|
|
338
|
+
raise Exception('Unsupported method ' + str(method))
|
|
339
|
+
|
|
340
|
+
# sometimes we get 500 here, not sure why... just retry the request
|
|
341
|
+
if r.status_code != 500 and '<title>500 Internal Server Error</title>' not in r.text:
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
sleep(0.05)
|
|
345
|
+
retry += 1
|
|
346
|
+
|
|
347
|
+
# decrypt the response, if needed
|
|
348
|
+
if encrypt and (r.status_code == 200) and (r.text != ''):
|
|
349
|
+
return r.status_code, self._encryption.aes_decrypt(r.text)
|
|
350
|
+
else:
|
|
351
|
+
return r.status_code, r.text
|
|
352
|
+
|
|
353
|
+
def _parse_ret_val(self, response_text):
|
|
354
|
+
'''
|
|
355
|
+
Parses $.ret value from the response text
|
|
356
|
+
|
|
357
|
+
Return value:
|
|
358
|
+
return code (int)
|
|
359
|
+
'''
|
|
360
|
+
result = search(r'\$\.ret=(.*);', response_text)
|
|
361
|
+
assert result is not None
|
|
362
|
+
assert result.group(1).isnumeric()
|
|
363
|
+
|
|
364
|
+
return int(result.group(1))
|
|
365
|
+
|
|
366
|
+
def _prepare_data(self, data: str, is_login: bool) -> tuple[str, str]:
|
|
367
|
+
encrypted_data = self._encryption.aes_encrypt(data)
|
|
368
|
+
data_len = len(encrypted_data)
|
|
369
|
+
# get encrypted signature
|
|
370
|
+
signature = self._encryption.get_signature(int(self._seq) + data_len, is_login, self._hash, self._nn, self._ee)
|
|
371
|
+
|
|
372
|
+
# format expected raw request data
|
|
373
|
+
return signature, encrypted_data
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class TPLinkMRClient(TPLinkMRClientBase):
|
|
377
|
+
|
|
378
|
+
def logout(self) -> None:
|
|
379
|
+
'''
|
|
380
|
+
Logs out from the host
|
|
381
|
+
'''
|
|
382
|
+
if self._token is None:
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
acts = [
|
|
386
|
+
# 8\r\n[/cgi/logout#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n
|
|
387
|
+
self.ActItem(self.ActItem.CGI, '/cgi/logout')
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
response, _ = self.req_act(acts)
|
|
391
|
+
ret_code = self._parse_ret_val(response)
|
|
392
|
+
|
|
393
|
+
if ret_code == self.HTTP_RET_OK:
|
|
394
|
+
self._token = None
|
|
395
|
+
|
|
396
|
+
def get_firmware(self) -> Firmware:
|
|
397
|
+
acts = [
|
|
398
|
+
self.ActItem(self.ActItem.GET, 'IGD_DEV_INFO', attrs=[
|
|
399
|
+
'hardwareVersion',
|
|
400
|
+
'modelName',
|
|
401
|
+
'softwareVersion'
|
|
402
|
+
])
|
|
403
|
+
]
|
|
404
|
+
_, values = self.req_act(acts)
|
|
405
|
+
|
|
406
|
+
firmware = Firmware(values.get('hardwareVersion', ''), values.get('modelName', ''),
|
|
407
|
+
values.get('softwareVersion', ''))
|
|
408
|
+
|
|
409
|
+
return firmware
|
|
410
|
+
|
|
411
|
+
def get_status(self) -> Status:
|
|
412
|
+
status = Status()
|
|
413
|
+
acts = [
|
|
414
|
+
self.ActItem(self.ActItem.GS, 'LAN_IP_INTF', attrs=['X_TP_MACAddress', 'IPInterfaceIPAddress']),
|
|
415
|
+
self.ActItem(self.ActItem.GS, 'WAN_IP_CONN',
|
|
416
|
+
attrs=['enable', 'MACAddress', 'externalIPAddress', 'defaultGateway']),
|
|
417
|
+
self.ActItem(self.ActItem.GL, 'LAN_WLAN', attrs=['enable', 'X_TP_Band']),
|
|
418
|
+
self.ActItem(self.ActItem.GL, 'LAN_WLAN_GUESTNET', attrs=['enable', 'name']),
|
|
419
|
+
self.ActItem(self.ActItem.GL, 'LAN_HOST_ENTRY', attrs=[
|
|
420
|
+
'IPAddress',
|
|
421
|
+
'MACAddress',
|
|
422
|
+
'hostName',
|
|
423
|
+
'X_TP_ConnType',
|
|
424
|
+
'active',
|
|
425
|
+
]),
|
|
426
|
+
self.ActItem(self.ActItem.GS, 'LAN_WLAN_ASSOC_DEV', attrs=[
|
|
427
|
+
'associatedDeviceMACAddress',
|
|
428
|
+
'X_TP_TotalPacketsSent',
|
|
429
|
+
'X_TP_TotalPacketsReceived',
|
|
430
|
+
]),
|
|
431
|
+
]
|
|
432
|
+
_, values = self.req_act(acts)
|
|
433
|
+
|
|
434
|
+
if values['0'].__class__ == list:
|
|
435
|
+
values['0'] = values['0'][0]
|
|
436
|
+
|
|
437
|
+
status._lan_macaddr = EUI48(values['0']['X_TP_MACAddress'])
|
|
438
|
+
status._lan_ipv4_addr = IPv4Address(values['0']['IPInterfaceIPAddress'])
|
|
439
|
+
|
|
440
|
+
for item in self._to_list(values.get('1')):
|
|
441
|
+
if int(item['enable']) == 0 and values.get('1').__class__ == list:
|
|
442
|
+
continue
|
|
443
|
+
status._wan_macaddr = EUI48(item['MACAddress']) if item.get('MACAddress') else None
|
|
444
|
+
status._wan_ipv4_addr = IPv4Address(item['externalIPAddress'])
|
|
445
|
+
status._wan_ipv4_gateway = IPv4Address(item['defaultGateway'])
|
|
446
|
+
|
|
447
|
+
if values['2'].__class__ != list:
|
|
448
|
+
status.wifi_2g_enable = bool(int(values['2']['enable']))
|
|
449
|
+
else:
|
|
450
|
+
status.wifi_2g_enable = bool(int(values['2'][0]['enable']))
|
|
451
|
+
status.wifi_5g_enable = bool(int(values['2'][1]['enable']))
|
|
452
|
+
|
|
453
|
+
if values['3'].__class__ != list:
|
|
454
|
+
status.guest_2g_enable = bool(int(values['3']['enable']))
|
|
455
|
+
else:
|
|
456
|
+
status.guest_2g_enable = bool(int(values['3'][0]['enable']))
|
|
457
|
+
status.guest_5g_enable = bool(int(values['3'][1]['enable']))
|
|
458
|
+
|
|
459
|
+
devices = {}
|
|
460
|
+
for val in self._to_list(values.get('4')):
|
|
461
|
+
if int(val['active']) == 0:
|
|
462
|
+
continue
|
|
463
|
+
conn = self.CLIENT_TYPES.get(int(val['X_TP_ConnType']))
|
|
464
|
+
if conn is None:
|
|
465
|
+
continue
|
|
466
|
+
elif conn == Connection.WIRED:
|
|
467
|
+
status.wired_total += 1
|
|
468
|
+
elif conn.is_guest_wifi():
|
|
469
|
+
status.guest_clients_total += 1
|
|
470
|
+
elif conn.is_host_wifi():
|
|
471
|
+
status.wifi_clients_total += 1
|
|
472
|
+
devices[val['MACAddress']] = Device(conn,
|
|
473
|
+
EUI48(val['MACAddress']),
|
|
474
|
+
IPv4Address(val['IPAddress']),
|
|
475
|
+
val['hostName'])
|
|
476
|
+
|
|
477
|
+
for val in self._to_list(values.get('5')):
|
|
478
|
+
if val['associatedDeviceMACAddress'] not in devices:
|
|
479
|
+
status.wifi_clients_total += 1
|
|
480
|
+
devices[val['associatedDeviceMACAddress']] = Device(
|
|
481
|
+
Connection.HOST_2G,
|
|
482
|
+
EUI48(val['associatedDeviceMACAddress']),
|
|
483
|
+
IPv4Address('0.0.0.0'),
|
|
484
|
+
'')
|
|
485
|
+
devices[val['associatedDeviceMACAddress']].packets_sent = int(val['X_TP_TotalPacketsSent'])
|
|
486
|
+
devices[val['associatedDeviceMACAddress']].packets_received = int(val['X_TP_TotalPacketsReceived'])
|
|
487
|
+
|
|
488
|
+
status.devices = list(devices.values())
|
|
489
|
+
status.clients_total = status.wired_total + status.wifi_clients_total + status.guest_clients_total
|
|
490
|
+
|
|
491
|
+
return status
|
|
492
|
+
|
|
493
|
+
def get_ipv4_reservations(self) -> [IPv4Reservation]:
|
|
494
|
+
acts = [
|
|
495
|
+
self.ActItem(self.ActItem.GL, 'LAN_DHCP_STATIC_ADDR', attrs=['enable', 'chaddr', 'yiaddr']),
|
|
496
|
+
]
|
|
497
|
+
_, values = self.req_act(acts)
|
|
498
|
+
|
|
499
|
+
ipv4_reservations = []
|
|
500
|
+
for item in self._to_list(values):
|
|
501
|
+
ipv4_reservations.append(
|
|
502
|
+
IPv4Reservation(
|
|
503
|
+
EUI48(item['chaddr']),
|
|
504
|
+
IPv4Address(item['yiaddr']),
|
|
505
|
+
'',
|
|
506
|
+
bool(int(item['enable']))
|
|
507
|
+
))
|
|
508
|
+
|
|
509
|
+
return ipv4_reservations
|
|
510
|
+
|
|
511
|
+
def get_ipv4_dhcp_leases(self) -> [IPv4DHCPLease]:
|
|
512
|
+
acts = [
|
|
513
|
+
self.ActItem(self.ActItem.GL, 'LAN_HOST_ENTRY', attrs=['IPAddress', 'MACAddress', 'hostName',
|
|
514
|
+
'leaseTimeRemaining']),
|
|
515
|
+
]
|
|
516
|
+
_, values = self.req_act(acts)
|
|
517
|
+
|
|
518
|
+
dhcp_leases = []
|
|
519
|
+
for item in self._to_list(values):
|
|
520
|
+
lease_time = item['leaseTimeRemaining']
|
|
521
|
+
dhcp_leases.append(
|
|
522
|
+
IPv4DHCPLease(
|
|
523
|
+
EUI48(item['MACAddress']),
|
|
524
|
+
IPv4Address(item['IPAddress']),
|
|
525
|
+
item['hostName'],
|
|
526
|
+
str(timedelta(seconds=int(lease_time))) if lease_time.isdigit() else 'Permanent',
|
|
527
|
+
))
|
|
528
|
+
|
|
529
|
+
return dhcp_leases
|
|
530
|
+
|
|
531
|
+
def get_ipv4_status(self) -> IPv4Status:
|
|
532
|
+
acts = [
|
|
533
|
+
self.ActItem(self.ActItem.GS, 'LAN_IP_INTF',
|
|
534
|
+
attrs=['X_TP_MACAddress', 'IPInterfaceIPAddress', 'IPInterfaceSubnetMask']),
|
|
535
|
+
self.ActItem(self.ActItem.GET, 'LAN_HOST_CFG', '1,0,0,0,0,0', attrs=['DHCPServerEnable']),
|
|
536
|
+
self.ActItem(self.ActItem.GS, 'WAN_IP_CONN',
|
|
537
|
+
attrs=['enable', 'MACAddress', 'externalIPAddress', 'defaultGateway', 'name', 'subnetMask',
|
|
538
|
+
'DNSServers']),
|
|
539
|
+
]
|
|
540
|
+
_, values = self.req_act(acts)
|
|
541
|
+
|
|
542
|
+
ipv4_status = IPv4Status()
|
|
543
|
+
ipv4_status._lan_macaddr = EUI48(values['0']['X_TP_MACAddress'])
|
|
544
|
+
ipv4_status._lan_ipv4_ipaddr = IPv4Address(values['0']['IPInterfaceIPAddress'])
|
|
545
|
+
ipv4_status._lan_ipv4_netmask = IPv4Address(values['0']['IPInterfaceSubnetMask'])
|
|
546
|
+
ipv4_status.lan_ipv4_dhcp_enable = bool(int(values['1']['DHCPServerEnable']))
|
|
547
|
+
|
|
548
|
+
for item in self._to_list(values.get('2')):
|
|
549
|
+
if int(item['enable']) == 0 and values.get('2').__class__ == list:
|
|
550
|
+
continue
|
|
551
|
+
ipv4_status._wan_macaddr = EUI48(item['MACAddress'])
|
|
552
|
+
ipv4_status._wan_ipv4_ipaddr = IPv4Address(item['externalIPAddress'])
|
|
553
|
+
ipv4_status._wan_ipv4_gateway = IPv4Address(item['defaultGateway'])
|
|
554
|
+
ipv4_status.wan_ipv4_conntype = item['name']
|
|
555
|
+
ipv4_status._wan_ipv4_netmask = IPv4Address(item['subnetMask'])
|
|
556
|
+
dns = item['DNSServers'].split(',')
|
|
557
|
+
ipv4_status._wan_ipv4_pridns = IPv4Address(dns[0])
|
|
558
|
+
ipv4_status._wan_ipv4_snddns = IPv4Address(dns[1])
|
|
559
|
+
|
|
560
|
+
return ipv4_status
|
|
561
|
+
|
|
562
|
+
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
563
|
+
acts = [
|
|
564
|
+
self.ActItem(
|
|
565
|
+
self.ActItem.SET,
|
|
566
|
+
'LAN_WLAN' if wifi in [Connection.HOST_2G, Connection.HOST_5G] else 'LAN_WLAN_MSSIDENTRY',
|
|
567
|
+
self.WIFI_SET[wifi],
|
|
568
|
+
attrs=['enable={}'.format(int(enable))]),
|
|
569
|
+
]
|
|
570
|
+
self.req_act(acts)
|
|
571
|
+
|
|
572
|
+
def send_sms(self, phone_number: str, message: str) -> None:
|
|
573
|
+
acts = [
|
|
574
|
+
self.ActItem(
|
|
575
|
+
self.ActItem.SET, 'LTE_SMS_SENDNEWMSG', attrs=[
|
|
576
|
+
'index=1',
|
|
577
|
+
'to={}'.format(phone_number),
|
|
578
|
+
'textContent={}'.format(message),
|
|
579
|
+
]),
|
|
580
|
+
]
|
|
581
|
+
self.req_act(acts)
|
|
582
|
+
|
|
583
|
+
def get_sms(self) -> [SMS]:
|
|
584
|
+
acts = [
|
|
585
|
+
self.ActItem(
|
|
586
|
+
self.ActItem.SET, 'LTE_SMS_RECVMSGBOX', attrs=['PageNumber=1']),
|
|
587
|
+
self.ActItem(
|
|
588
|
+
self.ActItem.GL, 'LTE_SMS_RECVMSGENTRY', attrs=['index', 'from', 'content', 'receivedTime',
|
|
589
|
+
'unread']),
|
|
590
|
+
]
|
|
591
|
+
_, values = self.req_act(acts)
|
|
592
|
+
|
|
593
|
+
messages = []
|
|
594
|
+
if values:
|
|
595
|
+
i = 1
|
|
596
|
+
for item in self._to_list(values.get('1')):
|
|
597
|
+
messages.append(
|
|
598
|
+
SMS(
|
|
599
|
+
i, item['from'], item['content'], datetime.fromisoformat(item['receivedTime']),
|
|
600
|
+
True if item['unread'] == '1' else False
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
i += 1
|
|
604
|
+
|
|
605
|
+
return messages
|
|
606
|
+
|
|
607
|
+
def set_sms_read(self, sms: SMS) -> None:
|
|
608
|
+
acts = [
|
|
609
|
+
self.ActItem(
|
|
610
|
+
self.ActItem.SET, 'LTE_SMS_RECVMSGENTRY', f'{sms.id},0,0,0,0,0', attrs=['unread=0']),
|
|
611
|
+
]
|
|
612
|
+
self.req_act(acts)
|
|
613
|
+
|
|
614
|
+
def delete_sms(self, sms: SMS) -> None:
|
|
615
|
+
acts = [
|
|
616
|
+
self.ActItem(
|
|
617
|
+
self.ActItem.DEL, 'LTE_SMS_RECVMSGENTRY', f'{sms.id},0,0,0,0,0'),
|
|
618
|
+
]
|
|
619
|
+
self.req_act(acts)
|
|
620
|
+
|
|
621
|
+
def send_ussd(self, command: str) -> str:
|
|
622
|
+
acts = [
|
|
623
|
+
self.ActItem(
|
|
624
|
+
self.ActItem.SET, 'LTE_USSD', attrs=[
|
|
625
|
+
'action=1',
|
|
626
|
+
f"reqContent={command}",
|
|
627
|
+
]),
|
|
628
|
+
]
|
|
629
|
+
self.req_act(acts)
|
|
630
|
+
|
|
631
|
+
status = '0'
|
|
632
|
+
while status == '0':
|
|
633
|
+
sleep(1)
|
|
634
|
+
acts = [
|
|
635
|
+
self.ActItem(
|
|
636
|
+
self.ActItem.GET, 'LTE_USSD', attrs=['sessionStatus', 'sendResult', 'response', 'ussdStatus']),
|
|
637
|
+
]
|
|
638
|
+
_, values = self.req_act(acts)
|
|
639
|
+
|
|
640
|
+
status = values.get('ussdStatus', '2')
|
|
641
|
+
|
|
642
|
+
if status == '1':
|
|
643
|
+
return values.get('response')
|
|
644
|
+
elif status == '2':
|
|
645
|
+
raise ClientError('Cannot send USSD!')
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from requests.packages import urllib3
|
|
2
|
+
from logging import Logger
|
|
3
|
+
from tplinkrouterc6u.common.package_enum import Connection
|
|
4
|
+
from tplinkrouterc6u.common.dataclass import Firmware, Status
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AbstractRouter(ABC):
|
|
9
|
+
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
|
|
10
|
+
verify_ssl: bool = True, timeout: int = 30) -> None:
|
|
11
|
+
self.username = username
|
|
12
|
+
self.password = password
|
|
13
|
+
self.timeout = timeout
|
|
14
|
+
self._logger = logger
|
|
15
|
+
self.host = host
|
|
16
|
+
if not (self.host.startswith('http://') or self.host.startswith('https://')):
|
|
17
|
+
self.host = "http://{}".format(self.host)
|
|
18
|
+
self._verify_ssl = verify_ssl
|
|
19
|
+
if self._verify_ssl is False:
|
|
20
|
+
urllib3.disable_warnings()
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def supports(self) -> bool:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def authorize(self) -> None:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def logout(self) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def get_firmware(self) -> Firmware:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def get_status(self) -> Status:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def reboot(self) -> None:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def set_wifi(self, wifi: Connection, enable: bool) -> None:
|
|
48
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""All extra classes are here"""
|