zte-wrapper 1.0.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.
File without changes
@@ -0,0 +1,167 @@
1
+ import logging
2
+ import aiohttp
3
+ from hashlib import sha256
4
+ import urllib.parse
5
+ import time
6
+ import json
7
+ from typing import Literal, Dict, Any, Tuple
8
+ from copy import deepcopy
9
+
10
+ logger = logging.getLogger("zte")
11
+
12
+ GOFORM_COMMANDS = Literal["goform_get_cmd_process", "goform_set_cmd_process"]
13
+ HEADERS = {
14
+ "Accept": "application/json, text/javascript, */*; q=0.01",
15
+ "Accept-Encoding": "gzip, deflate",
16
+ "Accept-Language": "en-US,en;q=0.5",
17
+ "Connection": "keep-alive",
18
+ "Host": "192.168.32.1",
19
+ "Referer": "http://192.168.32.1/",
20
+ "Sec-Gpc": "1",
21
+ "User-Agent": "Mozilla",
22
+ "X-Requested-With": "XMLHttpRequest",
23
+ }
24
+
25
+
26
+ def zte_sha256_string(input: str) -> str:
27
+ return sha256(input.encode("UTF-8")).hexdigest().upper()
28
+
29
+
30
+ class ZTEAuthWrapper:
31
+ def __init__(self, webui_address: str, password: str) -> None:
32
+ self.address: str = webui_address
33
+ self.__password = zte_sha256_string(password)
34
+ self.__auth_code: str | None = None
35
+
36
+ self.headers = HEADERS
37
+ self.headers["Referer"] = f"http://{self.address}/"
38
+ self.headers["Host"] = self.address
39
+
40
+ self.session: aiohttp.ClientSession | None = None
41
+
42
+ def construct_url(self, command: GOFORM_COMMANDS, args: Dict[str, Any]) -> str:
43
+ base_url = f"http://{self.address}/goform/{command}/?"
44
+
45
+ url = base_url + urllib.parse.urlencode(
46
+ {k: urllib.parse.quote(str(v)) for k, v in args.items()}
47
+ )
48
+
49
+ logger.debug(f"Constructed url {url}")
50
+ return url
51
+
52
+ def get_timestamp(self) -> int:
53
+ return round(time.time() * 1000)
54
+
55
+ async def __get_ld(self) -> str:
56
+ async with aiohttp.ClientSession() as session:
57
+ res = await session.get(
58
+ self.construct_url(
59
+ "goform_get_cmd_process",
60
+ {"isTest": "false", "cmd": "LD", "_": self.get_timestamp()},
61
+ ),
62
+ headers=self.headers,
63
+ )
64
+
65
+ logger.info("Got LD token")
66
+ return json.loads((await res.text())).get(
67
+ "LD"
68
+ ) # json.loads instead of res.json() because the stupid API returns the stuff as text/html
69
+
70
+ async def __get_rd0_rd1(self) -> Tuple[str, str]:
71
+ res = await self.request(
72
+ "GET",
73
+ self.construct_url(
74
+ "goform_get_cmd_process",
75
+ {
76
+ "isTest": "false",
77
+ "cmd": "cr_version,wa_inner_version",
78
+ "multi_data": "1",
79
+ },
80
+ ),
81
+ )
82
+ res_text = await res.text()
83
+ logger.debug(f"Retrieved rd0 and rd1: {res_text}")
84
+
85
+ data = json.loads(res_text)
86
+ return (data["wa_inner_version"], data["cr_version"])
87
+
88
+ async def __get_rd_token(self) -> str:
89
+ res = await self.request(
90
+ "GET",
91
+ self.construct_url(
92
+ "goform_get_cmd_process",
93
+ {"isTest": "false", "cmd": "RD", "_": self.get_timestamp()},
94
+ ),
95
+ )
96
+ logger.debug("")
97
+ return json.loads(await res.text())["RD"]
98
+
99
+ # please check notes/tokens.md if it breaks, it might help you a little
100
+ async def construct_ad_token(self) -> str:
101
+ rd0, rd1 = await self.__get_rd0_rd1()
102
+ first_step = zte_sha256_string(rd0 + rd1)
103
+ rd_token = await self.__get_rd_token()
104
+ return zte_sha256_string(first_step + rd_token)
105
+
106
+ async def refresh_auth(self) -> str:
107
+ ld_token: str = await self.__get_ld()
108
+ hashed_password: str = zte_sha256_string(self.__password + ld_token)
109
+
110
+ async with aiohttp.ClientSession() as session:
111
+ logger.debug("Refreshing authentication")
112
+ res = await session.post(
113
+ self.construct_url("goform_set_cmd_process", {}),
114
+ data={
115
+ "isTest": "false",
116
+ "goformId": "LOGIN",
117
+ "password": hashed_password,
118
+ },
119
+ headers=self.headers,
120
+ )
121
+ self.__auth_code = str(res.cookies.get("stok"))
122
+
123
+ return self.__auth_code
124
+
125
+ async def confirm_auth(self) -> bool:
126
+ res = await self.request(
127
+ "GET",
128
+ self.construct_url(
129
+ "goform_get_cmd_process",
130
+ {"isTest": "false", "cmd": "date_month", "_": self.get_timestamp()},
131
+ ),
132
+ skip_auth_check=True,
133
+ )
134
+
135
+ result = bool(json.loads(await res.text())["date_month"])
136
+ logger.debug(f"Logged in? {result}")
137
+
138
+ return result
139
+
140
+ async def request(
141
+ self, method: Literal["GET", "POST"], *args, **kwargs
142
+ ) -> aiohttp.ClientResponse:
143
+ if not self.session:
144
+ self.session = aiohttp.ClientSession()
145
+
146
+ headers = deepcopy(self.headers)
147
+ headers["Cookie"] = f'stok="{self.__auth_code}"'
148
+
149
+ if kwargs.get("skip_auth_check"):
150
+ logger.debug("Skipping authentication checks...")
151
+ del kwargs["skip_auth_check"]
152
+ else:
153
+ if not await self.confirm_auth():
154
+ await self.refresh_auth()
155
+ headers["Cookie"] = f'stok="{self.__auth_code}"'
156
+
157
+ if method == "GET":
158
+ res = await self.session.get(*args, **kwargs, headers=headers)
159
+ elif method == "POST":
160
+ res = await self.session.post(*args, **kwargs, headers=headers)
161
+
162
+ return res
163
+
164
+ async def close(self) -> None:
165
+ logger.info("Closign aiohttp client")
166
+ if self.session:
167
+ await self.session.close()
zte_wrapper/helpers.py ADDED
@@ -0,0 +1,17 @@
1
+ from datetime import datetime
2
+
3
+
4
+ def utf_16_decode(inp: str) -> str:
5
+ return bytes.fromhex(inp).decode("utf-16-be")
6
+
7
+
8
+ def utf_16_encode(inp: str) -> str:
9
+ return inp.encode("utf-16-be").hex().upper()
10
+
11
+
12
+ def get_zte_timestring(timezone_offset: int) -> str:
13
+ now = datetime.now()
14
+
15
+ output = f"{str(now.year)[2:]};{now.month};{now.day};{now.hour+1};{now.minute};{now.second};+{timezone_offset}"
16
+
17
+ return output
zte_wrapper/types.py ADDED
@@ -0,0 +1,233 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Literal, TypedDict
3
+ from datetime import datetime
4
+
5
+ PhoneNumber = str
6
+ RuleType = Literal["TCP"] | Literal["UDP"] | Literal["TCP&UDP"]
7
+ InterfaceType = (
8
+ Literal["WIFI6"]
9
+ | Literal["WIFI1"]
10
+ | Literal["Ethernet"]
11
+ | Literal["WIFI"]
12
+ | Literal[""]
13
+ )
14
+ FirewallProtocolTarget = (
15
+ Literal["TCP"] | Literal["UDP"] | Literal["ICMP"] | Literal["ALL"]
16
+ )
17
+ FirewallAction = Literal["DROP"] | Literal["ACCEPT"]
18
+ Hostname = TypedDict("Hostname", {"hostname": str, "mac": str})
19
+ AutoOrManual = Literal["auto"] | Literal["manual"]
20
+
21
+
22
+ class AuthError(Exception):
23
+ pass
24
+
25
+
26
+ class PortRange:
27
+ start: int
28
+ end: int
29
+ iter: int
30
+
31
+ def __init__(self, start: int, end: int):
32
+ self.start = start
33
+ self.end = end
34
+
35
+ def __iter__(self):
36
+ self.iter = self.start
37
+ return self
38
+
39
+ def __next__(self):
40
+ if self.iter > self.end:
41
+ raise StopIteration
42
+
43
+ val = self.iter
44
+ self.iter += 1
45
+ return val
46
+
47
+
48
+ @dataclass
49
+ class SMSMessage:
50
+ content: str
51
+ tag: int
52
+ id: int
53
+
54
+
55
+ @dataclass
56
+ class SignalStrength:
57
+ sinr_5g: float
58
+ rsrp_5g: float
59
+ rsrq_5g: float
60
+ rssi_5g: float
61
+
62
+ band_5g: str
63
+ band_lte: str
64
+
65
+ pci_lte: float
66
+ rsrq_lte: float
67
+ rsrp_lte: float
68
+ rssi_lte: float
69
+ snr_lte: float
70
+
71
+
72
+ @dataclass
73
+ class NetworkDetails:
74
+ isp_name: str
75
+ download_mbps: float
76
+ upload_mbps: float
77
+ monthly_download_megabytes: float
78
+ monthly_upload_megabytes: float
79
+
80
+
81
+ @dataclass
82
+ class PortforwardingRule:
83
+ ip_addr: str
84
+ comment: str
85
+ port_start: int
86
+ port_end: int
87
+ protocol: RuleType
88
+
89
+
90
+ @dataclass
91
+ class PortforwardingTable:
92
+ gateway_addr: str
93
+ enabled: bool
94
+ rules_amount: int
95
+ rules: List[PortforwardingRule]
96
+
97
+
98
+ @dataclass
99
+ class PortmappingRule:
100
+ ip_addr: str
101
+ comment: str
102
+ port_external: int
103
+ port_internal: int
104
+ protocol: RuleType
105
+
106
+
107
+ @dataclass
108
+ class PortmappingTable:
109
+ gateway_addr: str
110
+ enabled: bool
111
+ rules_amount: int
112
+ rules: List[PortmappingRule]
113
+
114
+
115
+ @dataclass
116
+ class WirelessStation: # basically a wireless device
117
+ addr_type: Literal["DHCP"] | str
118
+ connect_time: int
119
+ hostname: str
120
+ interface_type: InterfaceType
121
+ ip_address: str
122
+ mac_address: str
123
+ mac_bound: bool
124
+ ssid_index: int
125
+ wifi_rssi: int
126
+
127
+
128
+ @dataclass
129
+ class LanStation:
130
+ addr_type: Literal["Static"] | str
131
+ agreed_rate_mbps: int
132
+ connect_time: int
133
+ hostname: str
134
+ ip_address: str
135
+ mac_address: str
136
+ mac_bound: bool
137
+
138
+
139
+ @dataclass
140
+ class OfflineStation:
141
+ interface_type: InterfaceType
142
+ offline_time: datetime
143
+ start_time: datetime
144
+ start_time_t: datetime
145
+ hostname: str
146
+ ip_address: str
147
+ mac_address: str
148
+
149
+
150
+ @dataclass
151
+ class DDNSSettings:
152
+ provider: (
153
+ Literal["freedns.afraid.org"]
154
+ | Literal["dyndns.org"]
155
+ | Literal["zoneedit.org"]
156
+ | Literal["no-ip.com"]
157
+ | str
158
+ )
159
+ account_username: str
160
+ account_password: str
161
+ hash_value: str
162
+ mode: AutoOrManual
163
+ enabled: bool
164
+ domain: str
165
+
166
+
167
+ @dataclass
168
+ class MacBinding:
169
+ domain: str | Literal["(null)"]
170
+ hostname: str | Literal["(null)"]
171
+ ip: str
172
+ mac: str
173
+
174
+
175
+ @dataclass
176
+ class APNProfile:
177
+ data: str
178
+
179
+
180
+ @dataclass
181
+ class APNSettings:
182
+ apn_mode: AutoOrManual
183
+ profile_name: str
184
+ apn_wan_dial: str
185
+ apn_select: AutoOrManual
186
+ pdp_type: Literal["IP"] | str
187
+ pdp_select: AutoOrManual
188
+ pdp_address: str
189
+ index: int
190
+ wan_apn: str
191
+ ppp_auth_mode: str | Literal["none"]
192
+ ppp_username: str
193
+ ppp_password: str
194
+ dns_mode: AutoOrManual
195
+ prefer_dns_manual: str # find later
196
+ standby_dns_manual: str # find later x2
197
+ profiles: List[APNProfile]
198
+
199
+
200
+ @dataclass
201
+ class DHCPSettings:
202
+ enabled: bool
203
+ end_ip: str
204
+ start_ip: str
205
+ lease_time_hours: int
206
+ dhcp_type: Literal["SERVER"] | str
207
+ lan_ip_addr: str
208
+ lan_netmask: str
209
+ mac_addr: str
210
+ mtu: int
211
+ tcp_mss: int
212
+
213
+
214
+ @dataclass
215
+ class FirewallRule:
216
+ mac_addr: str
217
+ source_ip: str
218
+ dest_ip: str
219
+ protocol: FirewallProtocolTarget
220
+
221
+ source_port: PortRange
222
+ dest_port: PortRange
223
+
224
+ action: FirewallAction
225
+ comment: str
226
+
227
+
228
+ @dataclass
229
+ class FirewallConfig:
230
+ default_policy: FirewallAction
231
+ enabled: bool
232
+ rules_ipv4: List[FirewallRule]
233
+ rules_ipv6: List[FirewallRule]
zte_wrapper/wrapper.py ADDED
@@ -0,0 +1,83 @@
1
+ import json
2
+ from typing import Dict, List
3
+ from datetime import datetime
4
+ from .authwrapper import ZTEAuthWrapper
5
+ from .wrappers import (
6
+ portforwarding,
7
+ sms,
8
+ signal,
9
+ devices,
10
+ portmapping,
11
+ ddns,
12
+ networktools,
13
+ bindings,
14
+ apn,
15
+ dhcp,
16
+ firewall,
17
+ )
18
+ from dataclasses import dataclass, asdict
19
+
20
+ import logging
21
+
22
+ logger = logging.getLogger("zte")
23
+
24
+
25
+ class ZTEWrapper(ZTEAuthWrapper):
26
+ def __init__(self, webui_address: str, password: str) -> None:
27
+ super().__init__(webui_address, password)
28
+
29
+ self.portforwarding = portforwarding.PortforwardingWrapper(self)
30
+ self.portmapping = portmapping.PortmappingWrapper(self)
31
+ self.sms = sms.SmsWrapper(self)
32
+ self.signal = signal.SignalWrapper(self)
33
+ self.devices = devices.DeviceWrapper(self)
34
+ self.ddns = ddns.DDNSWrapper(self)
35
+ self.network_tools = networktools.NetworkToolWrapper(self)
36
+ self.bindings = bindings.BindingWrapper(self)
37
+ self.apn = apn.APNWrapper(self)
38
+ self.dhcp = dhcp.DHCPWrapper(self)
39
+ self.firewall = firewall.FirewallWrapper(self)
40
+
41
+ async def perform_dbg_query(self, items: List[str]) -> dict:
42
+ res = await self.request(
43
+ "GET",
44
+ self.construct_url(
45
+ "goform_get_cmd_process",
46
+ {"cmd": ",".join(items), "isTest": "false", "multi_data": "true"},
47
+ ),
48
+ )
49
+
50
+ txt = await res.text()
51
+ try:
52
+ data = json.loads(txt)
53
+ return data
54
+ except Exception as e:
55
+ print(txt)
56
+ print(e)
57
+ return {}
58
+
59
+ async def backup_settings(self) -> dict:
60
+ data = {}
61
+
62
+ logger.info("DDNS...")
63
+ data["ddns"] = await self.ddns.get_ddns_settings()
64
+
65
+ logger.info("Port forwarding...")
66
+ data["portforw"] = await self.portforwarding.get_port_forwarding_rules()
67
+
68
+ logger.info("Port Mapping...")
69
+ data["portmap"] = await self.portmapping.get_portmap_rules()
70
+
71
+ logger.info("Bindings...")
72
+ data["bindings"] = await self.bindings.get_mac_bindings()
73
+
74
+ logger.info("APN...")
75
+ data["apn"] = await self.apn.get_apn_settings()
76
+
77
+ logger.info("DHCP...")
78
+ data["dhcp"] = await self.dhcp.get_settings()
79
+
80
+ logger.info("Firewall...")
81
+ data["firewall"] = await self.firewall.get_config()
82
+
83
+ return data
@@ -0,0 +1,83 @@
1
+ from ..authwrapper import ZTEAuthWrapper
2
+ import json
3
+ from ..types import APNSettings, APNProfile
4
+ from typing import List
5
+
6
+
7
+ class APNWrapper:
8
+ def __init__(self, auth: ZTEAuthWrapper):
9
+ self.auth = auth
10
+
11
+ async def get_apn_settings(self) -> APNSettings:
12
+ res = await self.auth.request(
13
+ "GET",
14
+ self.auth.construct_url(
15
+ "goform_get_cmd_process",
16
+ {
17
+ "cmd": "apn_interface_version,APN_config0,APN_config1,APN_config2,APN_config3,APN_config4,APN_config5,APN_config6,APN_config7,APN_config8,APN_config9,APN_config10,APN_config11,APN_config12,APN_config13,APN_config14,APN_config15,APN_config16,APN_config17,APN_config18,APN_config19,ipv6_APN_config0,ipv6_APN_config1,ipv6_APN_config2,ipv6_APN_config3,ipv6_APN_config4,ipv6_APN_config5,ipv6_APN_config6,ipv6_APN_config7,ipv6_APN_config8,ipv6_APN_config9,ipv6_APN_config10,ipv6_APN_config11,ipv6_APN_config12,ipv6_APN_config13,ipv6_APN_config14,ipv6_APN_config15,ipv6_APN_config16,ipv6_APN_config17,ipv6_APN_config18,ipv6_APN_config19,apn_m_profile_name,profile_name,apn_wan_dial,apn_select,apn_pdp_type,apn_pdp_select,apn_pdp_addr,index,apn_Current_index,apn_auto_config,apn_ipv6_apn_auto_config,apn_mode,apn_wan_apn,apn_ppp_auth_mode,apn_ppp_username,apn_ppp_passwd,dns_mode,prefer_dns_manual,standby_dns_manual,apn_ipv6_wan_apn,apn_ipv6_pdp_type,apn_ipv6_ppp_auth_mode,apn_ipv6_ppp_username,apn_ipv6_ppp_passwd,ipv6_dns_mode,ipv6_prefer_dns_manual,ipv6_standby_dns_manual,apn_num_preset,wan_apn_ui,profile_name_ui,pdp_type_ui,ppp_auth_mode_ui,ppp_username_ui,ppp_passwd_ui,dns_mode_ui,prefer_dns_manual_ui,standby_dns_manual_ui,ipv6_wan_apn_ui,ipv6_ppp_auth_mode_ui,ipv6_ppp_username_ui,ipv6_ppp_passwd_ui,ipv6_dns_mode_ui,ipv6_prefer_dns_manual_ui,ipv6_standby_dns_manual_ui",
18
+ "isTest": "false",
19
+ "multi_data": "1",
20
+ },
21
+ ),
22
+ )
23
+
24
+ data: dict = json.loads(await res.text())
25
+ profiles: List[APNProfile] = []
26
+
27
+ for k, v in data.items():
28
+ k: str = k
29
+ v: str = v
30
+
31
+ if k.startswith("ipv6_APN_config") or k.startswith("APN_config"):
32
+ profiles.append(APNProfile(data=v))
33
+
34
+ settings = APNSettings(
35
+ apn_mode=data["apn_mode"],
36
+ apn_select=data["apn_select"],
37
+ apn_wan_dial="*99#",
38
+ dns_mode=data["dns_mode"],
39
+ pdp_address=data["apn_pdp_addr"],
40
+ pdp_select="auto",
41
+ pdp_type=data["apn_pdp_type"],
42
+ ppp_auth_mode=data["apn_ppp_auth_mode"],
43
+ ppp_password=data["apn_ppp_passwd"],
44
+ ppp_username=data["apn_ppp_username"],
45
+ prefer_dns_manual=data["prefer_dns_manual"],
46
+ profile_name=data["profile_name_ui"],
47
+ standby_dns_manual=data["standby_dns_manual"],
48
+ wan_apn=data["apn_wan_apn"],
49
+ index=int(data["apn_Current_index"]),
50
+ profiles=profiles,
51
+ )
52
+ return settings
53
+
54
+ async def set_apn_settings(self, settings: APNSettings) -> bool:
55
+ d = {
56
+ "isTest": "false",
57
+ "goformId": "APN_PROC_EX",
58
+ "apn_action": "save",
59
+ "apn_mode": settings.apn_mode,
60
+ "profile_name": settings.profile_name,
61
+ "apn_wan_dial": settings.apn_wan_dial,
62
+ "apn_select": settings.apn_select,
63
+ "apn_pdp_type": settings.pdp_type,
64
+ "apn_pdp_select": settings.pdp_select,
65
+ "apn_pdp_addr": settings.pdp_address,
66
+ "index": str(settings.index),
67
+ "apn_wan_apn": settings.wan_apn,
68
+ "apn_ppp_auth_mode": settings.ppp_auth_mode,
69
+ "apn_ppp_username": settings.ppp_username,
70
+ "apn_ppp_passwd": settings.ppp_password,
71
+ "dns_mode": settings.dns_mode,
72
+ "prefer_dns_manual": settings.prefer_dns_manual,
73
+ "standby_dns_manual": settings.standby_dns_manual,
74
+ "AD": await self.auth.construct_ad_token(),
75
+ }
76
+ res = await self.auth.request(
77
+ "POST",
78
+ self.auth.construct_url("goform_set_cmd_process", {}),
79
+ data=d,
80
+ )
81
+
82
+ data = json.loads(await res.text())
83
+ return data["result"] == "success"
@@ -0,0 +1,67 @@
1
+ from ..authwrapper import ZTEAuthWrapper
2
+ import json
3
+ from ..types import MacBinding
4
+ from typing import List
5
+
6
+
7
+ class BindingWrapper:
8
+ def __init__(self, auth: ZTEAuthWrapper):
9
+ self.auth = auth
10
+
11
+ async def get_mac_bindings(self) -> List[MacBinding]:
12
+ res = await self.auth.request(
13
+ "GET",
14
+ self.auth.construct_url(
15
+ "goform_get_cmd_process",
16
+ {
17
+ "isTest": "false",
18
+ "cmd": "current_static_addr_list",
19
+ },
20
+ ),
21
+ )
22
+
23
+ data: dict = json.loads(await res.text())
24
+ bindings: List[MacBinding] = []
25
+
26
+ for binding in data["current_static_addr_list"]:
27
+ bindings.append(
28
+ MacBinding(
29
+ domain=binding["domain"],
30
+ hostname=binding["hostname"],
31
+ ip=binding["ip"],
32
+ mac=binding["mac"],
33
+ )
34
+ )
35
+
36
+ return bindings
37
+
38
+ async def create_mac_binding(self, mac_address: str, ip: str) -> bool:
39
+ res = await self.auth.request(
40
+ "POST",
41
+ self.auth.construct_url("goform_set_cmd_process", {}),
42
+ data={
43
+ "isTest": "false",
44
+ "goformId": "BIND_STATIC_ADDRESS_ADD",
45
+ "mac_address": mac_address,
46
+ "ip_address": ip,
47
+ "AD": await self.auth.construct_ad_token(),
48
+ },
49
+ )
50
+
51
+ data = json.loads(await res.text())
52
+ return data["result"] == "success"
53
+
54
+ async def delete_mac_binding(self, mac_address: str) -> bool:
55
+ res = await self.auth.request(
56
+ "POST",
57
+ self.auth.construct_url("goform_set_cmd_process", {}),
58
+ data={
59
+ "isTest": "false",
60
+ "goformId": "BIND_STATIC_ADDRESS_DEL",
61
+ "mac_address": mac_address,
62
+ "AD": await self.auth.construct_ad_token(),
63
+ },
64
+ )
65
+
66
+ data = json.loads(await res.text())
67
+ return data["result"] == "success"