Skip to content

Commit 8c19b36

Browse files
Add TL-WR844N router client
1 parent 7e1e6ef commit 8c19b36

5 files changed

Lines changed: 226 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ from tplinkrouterc6u import (
4343
TPLinkCPE210Client,
4444
TPLinkSG108EClient,
4545
TplinkC80Router,
46+
TplinkWR844NRouter,
4647
TplinkWDRRouter,
4748
TPLinkC50Client,
4849
TPLinkWR841NClient,
@@ -431,6 +432,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
431432
- TL-WA1201 3.0
432433
- TL-WA3001 v1.0
433434
- TL-WR841N v14
435+
- TL-WR844N v1.0
434436
- TL-WR3002X v1.0
435437
- TL-WDR3600 V1
436438
- TL-XDR3010 V2

test/test_client_wr844n.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from ipaddress import IPv4Address
2+
from unittest import TestCase, main
3+
4+
from macaddress import EUI48
5+
6+
from tplinkrouterc6u import Connection
7+
from tplinkrouterc6u.client.wr844n import TplinkWR844NRouter
8+
from tplinkrouterc6u.common.dataclass import Device, Firmware, Status
9+
from tplinkrouterc6u.common.exception import ClientException
10+
11+
12+
FIRMWARE_RESPONSE = ('00000\r\nid 0|1,0,0\r\nfullName 300Mbps%20Wi-Fi%20Router\r\nfacturer TP-Link\r\n'
13+
'modelName TL-WR844N\r\nmodelVer 1.0\r\n'
14+
'softVer 1.10.0%20Build%20211011%20Rel.66152n(4555)\r\n'
15+
'hardVer TL-WR844N%201.0\r\nprodId 0x8440001')
16+
17+
STATUS_RESPONSE = ('00000\r\nid 1|1,0,0\r\nauthKey token\r\nreserved\r\nsetWzd 1\r\nmode 4\r\n'
18+
'mac 0 40-ae-30-af-c8-72\r\nmac 1 40-ae-30-af-c8-73\r\nwanMacType 0\r\n'
19+
'id 4|1,0,0\r\nip 192.168.2.1\r\nmask 255.255.255.0\r\nmode 0\r\n'
20+
'smartIp 0\r\ngateway 0.0.0.0\r\n'
21+
'id 9|1,0,0\r\nhostName 0 LGwebOSTV\r\nhostName 1 Kaspars\r\n'
22+
'hostName 2 Redmi-Note-13-Pro-5G\r\nhostName 3\r\n'
23+
'mac 0 60-75-6c-a1-4a-82\r\nmac 1 70-08-10-0b-c6-24\r\n'
24+
'mac 2 d6-02-54-6b-67-c4\r\nmac 3 00-00-00-00-00-00\r\n'
25+
'ip 0 192.168.2.100\r\nip 1 192.168.2.101\r\nip 2 192.168.2.102\r\nip 3 0.0.0.0\r\n'
26+
'state 0 5\r\nstate 1 5\r\nstate 2 5\r\nstate 3 0\r\n'
27+
'expires 0 4979\r\nexpires 1 5514\r\nexpires 2 6296\r\nexpires 3 0\r\n'
28+
'id 0|1,0,0\r\nmodelName TL-WR844N\r\nhardVer TL-WR844N%201.0')
29+
30+
31+
class ResponseMock:
32+
def __init__(self, text, status_code=0):
33+
self.text = text
34+
self.status_code = status_code
35+
36+
37+
class TplinkWR844NRouterTest(TplinkWR844NRouter):
38+
response = ''
39+
40+
def request(self, code: int, asyn: int, use_token: bool = False, data: str = None) -> ResponseMock | None:
41+
if code == 2 and asyn == 1:
42+
if use_token is False:
43+
if data == '0|1,0,0':
44+
return ResponseMock(FIRMWARE_RESPONSE, 200)
45+
return ResponseMock('blabla\r\nblabla\r\nblabla\r\nauthinfo1\r\nauthinfo2')
46+
return ResponseMock(self.response)
47+
if (code == 16 or code == 7) and asyn == 0:
48+
if use_token is False:
49+
return ResponseMock('00000\r\n010001\r\nBC97577E65233B3E1137C61091D64176C334E52AD78FFBDDABC826B685435E'
50+
'9D3DE83FE70C2AC62D6B13BD8EADA10B5623F9354DA0E99636A4F5519CA2DC2DC3\r\n12345656')
51+
return ResponseMock('00000')
52+
53+
raise ClientException()
54+
55+
56+
class TestTPLinkWR844NClient(TestCase):
57+
def test_supports(self) -> None:
58+
client = TplinkWR844NRouterTest('', '')
59+
60+
self.assertTrue(client.supports())
61+
62+
def test_get_firmware_handles_plaintext_response(self) -> None:
63+
client = TplinkWR844NRouterTest('', '')
64+
client.authorize()
65+
client.response = FIRMWARE_RESPONSE
66+
67+
firmware = client.get_firmware()
68+
69+
self.assertIsInstance(firmware, Firmware)
70+
self.assertEqual(firmware.hardware_version, 'TL-WR844N 1.0')
71+
self.assertEqual(firmware.model, 'TL-WR844N')
72+
self.assertEqual(firmware.firmware_version, '1.10.0 Build 211011 Rel.66152n(4555)')
73+
74+
def test_get_status_uses_dhcp_client_block(self) -> None:
75+
client = TplinkWR844NRouterTest('http://192.168.0.68', '')
76+
client.authorize()
77+
client.response = STATUS_RESPONSE
78+
79+
status = client.get_status()
80+
81+
self.assertIsInstance(status, Status)
82+
self.assertEqual(status.lan_macaddr, '40-AE-30-AF-C8-72')
83+
self.assertIsInstance(status.lan_macaddress, EUI48)
84+
self.assertEqual(status.wan_macaddr, '40-AE-30-AF-C8-73')
85+
self.assertEqual(status.lan_ipv4_addr, '192.168.2.1')
86+
self.assertIsInstance(status.lan_ipv4_address, IPv4Address)
87+
self.assertEqual(status.wan_ipv4_addr, '192.168.0.68')
88+
self.assertEqual(status.conn_type, 'Router/AP')
89+
self.assertTrue(status.wifi_2g_enable)
90+
self.assertEqual(status.clients_total, 3)
91+
self.assertEqual(status.wifi_clients_total, 3)
92+
self.assertEqual(status.wired_total, 0)
93+
self.assertEqual(len(status.devices), 3)
94+
95+
device = status.devices[0]
96+
self.assertIsInstance(device, Device)
97+
self.assertEqual(device.type, Connection.HOST_2G)
98+
self.assertEqual(device.hostname, 'LGwebOSTV')
99+
self.assertEqual(device.ipaddr, '192.168.2.100')
100+
self.assertEqual(device.macaddr, '60-75-6C-A1-4A-82')
101+
102+
device = status.devices[2]
103+
self.assertEqual(device.hostname, 'Redmi-Note-13-Pro-5G')
104+
self.assertEqual(device.ipaddr, '192.168.2.102')
105+
self.assertEqual(device.macaddr, 'D6-02-54-6B-67-C4')
106+
107+
108+
if __name__ == '__main__':
109+
main()

tplinkrouterc6u/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from tplinkrouterc6u.client.vr import TPLinkVRClient
1212
from tplinkrouterc6u.client.vr400v2 import TPLinkVR400v2Client
1313
from tplinkrouterc6u.client.c80 import TplinkC80Router
14+
from tplinkrouterc6u.client.wr844n import TplinkWR844NRouter
1415
from tplinkrouterc6u.client.c5400x import TplinkC5400XRouter
1516
from tplinkrouterc6u.client.c3200 import TplinkC3200Router
1617
from tplinkrouterc6u.client.c1200 import TplinkC1200Router

tplinkrouterc6u/client/wr844n.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import re
2+
from logging import Logger
3+
from urllib import parse
4+
from urllib.parse import urlparse
5+
6+
from tplinkrouterc6u.client.c80 import TplinkC80Router
7+
from tplinkrouterc6u.common.dataclass import Device, Status
8+
from tplinkrouterc6u.common.helper import get_ip, get_mac
9+
from tplinkrouterc6u.common.package_enum import Connection
10+
11+
12+
class TplinkWR844NRouter(TplinkC80Router):
13+
"""Client for TL-WR844N firmware that returns plaintext C80-style data."""
14+
15+
BLOCK_REGEX = re.compile(r'id (\d+\|\d,\d,\d)\r\n(.*?)(?=\r\nid \d+\||$)', re.DOTALL)
16+
17+
def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
18+
verify_ssl: bool = True, timeout: int = 30) -> None:
19+
super().__init__(host, password, username, logger, verify_ssl, timeout)
20+
21+
def supports(self) -> bool:
22+
try:
23+
response = self.request(2, 1, data='0|1,0,0')
24+
return response.status_code == 200 and self._is_wr844n_response(response.text)
25+
except Exception:
26+
return False
27+
28+
def get_status(self) -> Status:
29+
request_text = '#'.join([
30+
'1|1,0,0',
31+
'4|1,0,0',
32+
'9|1,0,0',
33+
'23|1,0,0',
34+
'0|1,0,0',
35+
])
36+
response_text = self._request_plaintext(request_text)
37+
data_blocks = self._parse_data_blocks(response_text)
38+
39+
mac_info = self._parse_last_values_from_block(data_blocks.get('1|1,0,0', []))
40+
lan_info = self._parse_last_values_from_block(data_blocks.get('4|1,0,0', []))
41+
wan_info = self._parse_last_values_from_block(data_blocks.get('23|1,0,0', []))
42+
43+
devices = self._parse_dhcp_devices(data_blocks.get('9|1,0,0', []))
44+
45+
status = Status()
46+
status._lan_macaddr = get_mac(mac_info.get('mac 0', '00-00-00-00-00-00'))
47+
status._wan_macaddr = get_mac(mac_info.get('mac 1', '00-00-00-00-00-00'))
48+
status._lan_ipv4_addr = get_ip(lan_info.get('ip') or self._host_ip())
49+
status._wan_ipv4_addr = get_ip(wan_info.get('ip') or self._host_ip())
50+
51+
gateway = wan_info.get('gateway') or lan_info.get('gateway')
52+
if gateway and gateway != '0.0.0.0':
53+
status._wan_ipv4_gateway = get_ip(gateway)
54+
55+
uptime = wan_info.get('upTime')
56+
status.wan_ipv4_uptime = int(uptime) // 100 if uptime and uptime.isdigit() else None
57+
status.devices = devices
58+
status.wired_total = 0
59+
status.wifi_clients_total = len(devices)
60+
status.clients_total = len(devices)
61+
status.wifi_2g_enable = True
62+
status.conn_type = 'Router/AP'
63+
return status
64+
65+
def _request_plaintext(self, text: str) -> str:
66+
body = self._encrypt_body(text)
67+
response = self.request(2, 1, True, data=body)
68+
return self._decrypt_data(response.text)
69+
70+
def _decrypt_data(self, encrypted_text: str) -> str:
71+
if self._is_plain_response(encrypted_text):
72+
return encrypted_text
73+
return super()._decrypt_data(encrypted_text)
74+
75+
def _parse_data_blocks(self, response_text: str) -> dict[str, list[str]]:
76+
matches = TplinkWR844NRouter.BLOCK_REGEX.findall(response_text)
77+
return {match[0]: [line for line in match[1].strip().split('\r\n') if line] for match in matches}
78+
79+
def _parse_dhcp_devices(self, response_data: list[str]) -> list[Device]:
80+
devices: list[Device] = []
81+
for item in self._parse_response_to_dict(response_data):
82+
ip = item.get('ip')
83+
mac = item.get('mac')
84+
if not ip or not mac or mac == '00-00-00-00-00-00':
85+
continue
86+
devices.append(Device(Connection.HOST_2G, get_mac(mac), get_ip(ip),
87+
parse.unquote(item.get('hostName', ''))))
88+
return devices
89+
90+
def _parse_last_values(self, text: str) -> dict[str, str]:
91+
return self._parse_last_values_from_block([line for line in text.splitlines() if line])
92+
93+
def _parse_last_values_from_block(self, lines: list[str]) -> dict[str, str]:
94+
values: dict[str, str] = {}
95+
for line in lines:
96+
if line == '00000' or line.startswith('id '):
97+
continue
98+
key, _, value = line.rpartition(' ')
99+
if key:
100+
values[key] = value.strip()
101+
return values
102+
103+
def _host_ip(self) -> str:
104+
return urlparse(self.host).hostname or '0.0.0.0'
105+
106+
@staticmethod
107+
def _is_wr844n_response(text: str) -> bool:
108+
return TplinkWR844NRouter._is_plain_response(text) and 'modelName TL-WR844N' in text
109+
110+
@staticmethod
111+
def _is_plain_response(text: str) -> bool:
112+
return isinstance(text, str) and text.startswith('00000\r\n')

tplinkrouterc6u/provider.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from tplinkrouterc6u.client.c3200 import TplinkC3200Router
1717
from tplinkrouterc6u.client.c1200 import TplinkC1200Router
1818
from tplinkrouterc6u.client.c80 import TplinkC80Router
19+
from tplinkrouterc6u.client.wr844n import TplinkWR844NRouter
1920
from tplinkrouterc6u.client.vr import TPLinkVRClient
2021
from tplinkrouterc6u.client.vr400v2 import TPLinkVR400v2Client
2122
from tplinkrouterc6u.client.r import TPLinkRClient
@@ -81,6 +82,7 @@ def get_clients() -> dict[str, type[AbstractRouter]]:
8182
TplinkRouterSG.__name__: TplinkRouterSG,
8283
TplinkRouterV1_11.__name__: TplinkRouterV1_11,
8384
TplinkRouter.__name__: TplinkRouter,
85+
TplinkWR844NRouter.__name__: TplinkWR844NRouter,
8486
TplinkC80Router.__name__: TplinkC80Router,
8587
TplinkWDRRouter.__name__: TplinkWDRRouter,
8688
TplinkRE330Router.__name__: TplinkRE330Router,

0 commit comments

Comments
 (0)