pyezvizapi 1.0.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.
Potentially problematic release.
This version of pyezvizapi might be problematic. Click here for more details.
- pyezvizapi/__init__.py +54 -0
- pyezvizapi/__main__.py +459 -0
- pyezvizapi/api_endpoints.py +52 -0
- pyezvizapi/camera.py +258 -0
- pyezvizapi/cas.py +169 -0
- pyezvizapi/client.py +2089 -0
- pyezvizapi/constants.py +381 -0
- pyezvizapi/exceptions.py +29 -0
- pyezvizapi/light_bulb.py +126 -0
- pyezvizapi/mqtt.py +259 -0
- pyezvizapi/test_cam_rtsp.py +149 -0
- pyezvizapi/utils.py +160 -0
- pyezvizapi-1.0.0.0.dist-info/LICENSE +201 -0
- pyezvizapi-1.0.0.0.dist-info/LICENSE.md +201 -0
- pyezvizapi-1.0.0.0.dist-info/METADATA +18 -0
- pyezvizapi-1.0.0.0.dist-info/RECORD +19 -0
- pyezvizapi-1.0.0.0.dist-info/WHEEL +5 -0
- pyezvizapi-1.0.0.0.dist-info/entry_points.txt +2 -0
- pyezvizapi-1.0.0.0.dist-info/top_level.txt +1 -0
pyezvizapi/mqtt.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Ezviz cloud MQTT client for push messages."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import paho.mqtt.client as mqtt
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from .api_endpoints import (
|
|
15
|
+
API_ENDPOINT_REGISTER_MQTT,
|
|
16
|
+
API_ENDPOINT_START_MQTT,
|
|
17
|
+
API_ENDPOINT_STOP_MQTT,
|
|
18
|
+
)
|
|
19
|
+
from .constants import (
|
|
20
|
+
APP_SECRET,
|
|
21
|
+
DEFAULT_TIMEOUT,
|
|
22
|
+
FEATURE_CODE,
|
|
23
|
+
MQTT_APP_KEY,
|
|
24
|
+
REQUEST_HEADER,
|
|
25
|
+
)
|
|
26
|
+
from .exceptions import HTTPError, InvalidURL, PyEzvizError
|
|
27
|
+
|
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MQTTClient(threading.Thread):
|
|
32
|
+
"""Open MQTT connection to ezviz cloud."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
token: dict,
|
|
37
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the client object."""
|
|
40
|
+
threading.Thread.__init__(self)
|
|
41
|
+
self._session = requests.session()
|
|
42
|
+
self._session.headers.update(REQUEST_HEADER)
|
|
43
|
+
self._token = token or {
|
|
44
|
+
"session_id": None,
|
|
45
|
+
"rf_session_id": None,
|
|
46
|
+
"username": None,
|
|
47
|
+
"api_url": "apiieu.ezvizlife.com",
|
|
48
|
+
}
|
|
49
|
+
self._timeout = timeout
|
|
50
|
+
self._stop_event = threading.Event()
|
|
51
|
+
self._mqtt_data = {
|
|
52
|
+
"mqtt_clientid": None,
|
|
53
|
+
"ticket": None,
|
|
54
|
+
"push_url": token["service_urls"]["pushAddr"],
|
|
55
|
+
}
|
|
56
|
+
self.mqtt_client = None
|
|
57
|
+
self.rcv_message: dict[Any, Any] = {}
|
|
58
|
+
|
|
59
|
+
def on_subscribe(
|
|
60
|
+
self, client: Any, userdata: Any, mid: Any, granted_qos: Any
|
|
61
|
+
) -> None:
|
|
62
|
+
"""On MQTT message subscribe."""
|
|
63
|
+
# pylint: disable=unused-argument
|
|
64
|
+
_LOGGER.info("Subscribed: %s %s", mid, granted_qos)
|
|
65
|
+
|
|
66
|
+
def on_connect(
|
|
67
|
+
self, client: Any, userdata: Any, flags: Any, return_code: Any
|
|
68
|
+
) -> None:
|
|
69
|
+
"""On MQTT connect."""
|
|
70
|
+
# pylint: disable=unused-argument
|
|
71
|
+
if return_code == 0:
|
|
72
|
+
_LOGGER.info("Connected OK with return code %s", return_code)
|
|
73
|
+
else:
|
|
74
|
+
_LOGGER.info("Connection Error with Return code %s", return_code)
|
|
75
|
+
client.reconnect()
|
|
76
|
+
|
|
77
|
+
def on_message(self, client: Any, userdata: Any, msg: Any) -> None:
|
|
78
|
+
"""On MQTT message receive."""
|
|
79
|
+
# pylint: disable=unused-argument
|
|
80
|
+
try:
|
|
81
|
+
mqtt_message = json.loads(msg.payload)
|
|
82
|
+
|
|
83
|
+
except ValueError as err:
|
|
84
|
+
self.stop()
|
|
85
|
+
raise PyEzvizError(
|
|
86
|
+
"Impossible to decode mqtt message: " + str(err)
|
|
87
|
+
) from err
|
|
88
|
+
|
|
89
|
+
mqtt_message["ext"] = mqtt_message["ext"].split(",")
|
|
90
|
+
|
|
91
|
+
# Format payload message and keep latest device message.
|
|
92
|
+
self.rcv_message[mqtt_message["ext"][2]] = {
|
|
93
|
+
"id": mqtt_message["id"],
|
|
94
|
+
"alert": mqtt_message["alert"],
|
|
95
|
+
"time": mqtt_message["ext"][1],
|
|
96
|
+
"alert type": mqtt_message["ext"][4],
|
|
97
|
+
"image": mqtt_message["ext"][16] if len(mqtt_message["ext"]) > 16 else None,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_LOGGER.debug(self.rcv_message, exc_info=True)
|
|
101
|
+
|
|
102
|
+
def _mqtt(self) -> mqtt.Client:
|
|
103
|
+
"""Receive MQTT messages from ezviz server."""
|
|
104
|
+
|
|
105
|
+
ezviz_mqtt_client = mqtt.Client(
|
|
106
|
+
client_id=self._mqtt_data["mqtt_clientid"], protocol=4, transport="tcp"
|
|
107
|
+
)
|
|
108
|
+
ezviz_mqtt_client.on_connect = self.on_connect
|
|
109
|
+
ezviz_mqtt_client.on_subscribe = self.on_subscribe
|
|
110
|
+
ezviz_mqtt_client.on_message = self.on_message
|
|
111
|
+
ezviz_mqtt_client.username_pw_set(MQTT_APP_KEY, APP_SECRET)
|
|
112
|
+
|
|
113
|
+
ezviz_mqtt_client.connect(self._mqtt_data["push_url"], 1882, 60)
|
|
114
|
+
ezviz_mqtt_client.subscribe(
|
|
115
|
+
f"{MQTT_APP_KEY}/ticket/{self._mqtt_data['ticket']}", qos=2
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
ezviz_mqtt_client.loop_start()
|
|
119
|
+
return ezviz_mqtt_client
|
|
120
|
+
|
|
121
|
+
def _register_ezviz_push(self) -> None:
|
|
122
|
+
"""Register for push messages."""
|
|
123
|
+
|
|
124
|
+
auth_seq = (
|
|
125
|
+
"Basic "
|
|
126
|
+
+ base64.b64encode(f"{MQTT_APP_KEY}:{APP_SECRET}".encode("ascii")).decode()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
payload = {
|
|
130
|
+
"appKey": MQTT_APP_KEY,
|
|
131
|
+
"clientType": "5",
|
|
132
|
+
"mac": FEATURE_CODE,
|
|
133
|
+
"token": "123456",
|
|
134
|
+
"version": "v1.3.0",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
req = self._session.post(
|
|
139
|
+
f"https://{self._mqtt_data['push_url']}{API_ENDPOINT_REGISTER_MQTT}",
|
|
140
|
+
allow_redirects=False,
|
|
141
|
+
headers={"Authorization": auth_seq},
|
|
142
|
+
data=payload,
|
|
143
|
+
timeout=self._timeout,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
req.raise_for_status()
|
|
147
|
+
|
|
148
|
+
except requests.ConnectionError as err:
|
|
149
|
+
raise InvalidURL("A Invalid URL or Proxy error occured") from err
|
|
150
|
+
|
|
151
|
+
except requests.HTTPError as err:
|
|
152
|
+
raise HTTPError from err
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
json_result = req.json()
|
|
156
|
+
|
|
157
|
+
except ValueError as err:
|
|
158
|
+
raise PyEzvizError(
|
|
159
|
+
"Impossible to decode response: "
|
|
160
|
+
+ str(err)
|
|
161
|
+
+ "\nResponse was: "
|
|
162
|
+
+ str(req.text)
|
|
163
|
+
) from err
|
|
164
|
+
|
|
165
|
+
self._mqtt_data["mqtt_clientid"] = json_result["data"]["clientId"]
|
|
166
|
+
|
|
167
|
+
def run(self) -> None:
|
|
168
|
+
"""Start mqtt thread."""
|
|
169
|
+
|
|
170
|
+
if self._token.get("username") is None:
|
|
171
|
+
self._stop_event.set()
|
|
172
|
+
raise PyEzvizError(
|
|
173
|
+
"Ezviz internal username is required. Call EzvizClient login without token."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self._register_ezviz_push()
|
|
177
|
+
self._start_ezviz_push()
|
|
178
|
+
self.mqtt_client = self._mqtt()
|
|
179
|
+
|
|
180
|
+
def start(self) -> None:
|
|
181
|
+
"""Start mqtt thread as application. Set logging first to see messages."""
|
|
182
|
+
self.run()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
while not self._stop_event.is_set():
|
|
186
|
+
time.sleep(1)
|
|
187
|
+
except KeyboardInterrupt:
|
|
188
|
+
self.stop()
|
|
189
|
+
|
|
190
|
+
def stop(self) -> None:
|
|
191
|
+
"""Stop push notifications."""
|
|
192
|
+
|
|
193
|
+
payload = {
|
|
194
|
+
"appKey": MQTT_APP_KEY,
|
|
195
|
+
"clientId": self._mqtt_data["mqtt_clientid"],
|
|
196
|
+
"clientType": 5,
|
|
197
|
+
"sessionId": self._token["session_id"],
|
|
198
|
+
"username": self._token["username"],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
req = self._session.post(
|
|
203
|
+
f"https://{self._mqtt_data['push_url']}{API_ENDPOINT_STOP_MQTT}",
|
|
204
|
+
data=payload,
|
|
205
|
+
timeout=self._timeout,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
req.raise_for_status()
|
|
209
|
+
|
|
210
|
+
except requests.ConnectionError as err:
|
|
211
|
+
raise InvalidURL("A Invalid URL or Proxy error occured") from err
|
|
212
|
+
|
|
213
|
+
except requests.HTTPError as err:
|
|
214
|
+
raise HTTPError from err
|
|
215
|
+
|
|
216
|
+
finally:
|
|
217
|
+
self._stop_event.set()
|
|
218
|
+
self.mqtt_client.loop_stop()
|
|
219
|
+
|
|
220
|
+
def _start_ezviz_push(self) -> None:
|
|
221
|
+
"""Send start for push messages to ezviz api."""
|
|
222
|
+
|
|
223
|
+
payload = {
|
|
224
|
+
"appKey": MQTT_APP_KEY,
|
|
225
|
+
"clientId": self._mqtt_data["mqtt_clientid"],
|
|
226
|
+
"clientType": 5,
|
|
227
|
+
"sessionId": self._token["session_id"],
|
|
228
|
+
"username": self._token["username"],
|
|
229
|
+
"token": "123456",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
req = self._session.post(
|
|
234
|
+
f"https://{self._mqtt_data['push_url']}{API_ENDPOINT_START_MQTT}",
|
|
235
|
+
allow_redirects=False,
|
|
236
|
+
data=payload,
|
|
237
|
+
timeout=self._timeout,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
req.raise_for_status()
|
|
241
|
+
|
|
242
|
+
except requests.ConnectionError as err:
|
|
243
|
+
raise InvalidURL("A Invalid URL or Proxy error occured") from err
|
|
244
|
+
|
|
245
|
+
except requests.HTTPError as err:
|
|
246
|
+
raise HTTPError from err
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
json_result = req.json()
|
|
250
|
+
|
|
251
|
+
except ValueError as err:
|
|
252
|
+
raise PyEzvizError(
|
|
253
|
+
"Impossible to decode response: "
|
|
254
|
+
+ str(err)
|
|
255
|
+
+ "\nResponse was: "
|
|
256
|
+
+ str(req.text)
|
|
257
|
+
) from err
|
|
258
|
+
|
|
259
|
+
self._mqtt_data["ticket"] = json_result["ticket"]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Test camera RTSP authentication."""
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
from .exceptions import AuthTestResultFailed, InvalidHost
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def genmsg_describe(url, seq, user_agent, auth_seq):
|
|
10
|
+
"""Generate RTSP describe message."""
|
|
11
|
+
msg_ret = "DESCRIBE " + url + " RTSP/1.0\r\n"
|
|
12
|
+
msg_ret += "CSeq: " + str(seq) + "\r\n"
|
|
13
|
+
msg_ret += "Authorization: " + auth_seq + "\r\n"
|
|
14
|
+
msg_ret += "User-Agent: " + user_agent + "\r\n"
|
|
15
|
+
msg_ret += "Accept: application/sdp\r\n"
|
|
16
|
+
msg_ret += "\r\n"
|
|
17
|
+
return msg_ret
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestRTSPAuth:
|
|
21
|
+
"""Test RTSP credentials."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
ip_addr,
|
|
26
|
+
username=None,
|
|
27
|
+
password=None,
|
|
28
|
+
test_uri="",
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initialize RTSP credential test."""
|
|
31
|
+
self._rtsp_details = {
|
|
32
|
+
"bufLen": 1024,
|
|
33
|
+
"defaultServerIp": ip_addr,
|
|
34
|
+
"defaultServerPort": 554,
|
|
35
|
+
"defaultTestUri": test_uri,
|
|
36
|
+
"defaultUserAgent": "RTSP Client",
|
|
37
|
+
"defaultUsername": username,
|
|
38
|
+
"defaultPassword": password,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def generate_auth_string(self, realm, method, uri, nonce):
|
|
42
|
+
"""Generate digest auth string."""
|
|
43
|
+
map_return_info = {}
|
|
44
|
+
m_1 = hashlib.md5(
|
|
45
|
+
f"{self._rtsp_details['defaultUsername']}:"
|
|
46
|
+
f"{realm.decode()}:"
|
|
47
|
+
f"{self._rtsp_details['defaultPassword']}".encode()
|
|
48
|
+
).hexdigest()
|
|
49
|
+
m_2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest()
|
|
50
|
+
response = hashlib.md5(f"{m_1}:{nonce}:{m_2}".encode()).hexdigest()
|
|
51
|
+
|
|
52
|
+
map_return_info = (
|
|
53
|
+
f"Digest "
|
|
54
|
+
f"username=\"{self._rtsp_details['defaultUsername']}\", "
|
|
55
|
+
f'realm="{realm.decode()}", '
|
|
56
|
+
f'algorithm="MD5", '
|
|
57
|
+
f'nonce="{nonce.decode()}", '
|
|
58
|
+
f'uri="{uri}", '
|
|
59
|
+
f'response="{response}"'
|
|
60
|
+
)
|
|
61
|
+
return map_return_info
|
|
62
|
+
|
|
63
|
+
def main(self):
|
|
64
|
+
"""Start main method."""
|
|
65
|
+
session = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
session.connect(
|
|
69
|
+
(
|
|
70
|
+
self._rtsp_details["defaultServerIp"],
|
|
71
|
+
self._rtsp_details["defaultServerPort"],
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
except TimeoutError as err:
|
|
76
|
+
raise AuthTestResultFailed("Invalid ip or camera hibernating") from err
|
|
77
|
+
|
|
78
|
+
except (socket.gaierror, ConnectionRefusedError) as err:
|
|
79
|
+
raise InvalidHost("Invalid IP or Hostname") from err
|
|
80
|
+
|
|
81
|
+
seq = 1
|
|
82
|
+
|
|
83
|
+
url = (
|
|
84
|
+
"rtsp://"
|
|
85
|
+
+ self._rtsp_details["defaultServerIp"]
|
|
86
|
+
+ self._rtsp_details["defaultTestUri"]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
auth_seq = base64.b64encode(
|
|
90
|
+
f"{self._rtsp_details['defaultUsername']}:"
|
|
91
|
+
f"{self._rtsp_details['defaultPassword']}".encode("ascii")
|
|
92
|
+
)
|
|
93
|
+
auth_seq = "Basic " + auth_seq.decode()
|
|
94
|
+
|
|
95
|
+
print(
|
|
96
|
+
genmsg_describe(url, seq, self._rtsp_details["defaultUserAgent"], auth_seq)
|
|
97
|
+
)
|
|
98
|
+
session.send(
|
|
99
|
+
genmsg_describe(
|
|
100
|
+
url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
|
|
101
|
+
).encode()
|
|
102
|
+
)
|
|
103
|
+
msg1 = session.recv(self._rtsp_details["bufLen"])
|
|
104
|
+
seq = seq + 1
|
|
105
|
+
|
|
106
|
+
if msg1.decode().find("200 OK") > 1:
|
|
107
|
+
print(f"Basic auth result: {msg1.decode()}")
|
|
108
|
+
return print("Basic Auth test passed. Credentials Valid!")
|
|
109
|
+
|
|
110
|
+
if msg1.decode().find("Unauthorized") > 1:
|
|
111
|
+
# Basic failed, doing new DESCRIBE with digest authentication.
|
|
112
|
+
start = msg1.decode().find("realm")
|
|
113
|
+
begin = msg1.decode().find('"', start)
|
|
114
|
+
end = msg1.decode().find('"', begin + 1)
|
|
115
|
+
realm = msg1[begin + 1 : end]
|
|
116
|
+
|
|
117
|
+
start = msg1.decode().find("nonce")
|
|
118
|
+
begin = msg1.decode().find('"', start)
|
|
119
|
+
end = msg1.decode().find('"', begin + 1)
|
|
120
|
+
nonce = msg1[begin + 1 : end]
|
|
121
|
+
|
|
122
|
+
auth_seq = self.generate_auth_string(
|
|
123
|
+
realm,
|
|
124
|
+
"DESCRIBE",
|
|
125
|
+
self._rtsp_details["defaultTestUri"],
|
|
126
|
+
nonce,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
print(
|
|
130
|
+
genmsg_describe(
|
|
131
|
+
url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
session.send(
|
|
136
|
+
genmsg_describe(
|
|
137
|
+
url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
|
|
138
|
+
).encode()
|
|
139
|
+
)
|
|
140
|
+
msg1 = session.recv(self._rtsp_details["bufLen"])
|
|
141
|
+
print(f"Digest auth result: {msg1.decode()}")
|
|
142
|
+
|
|
143
|
+
if msg1.decode().find("200 OK") > 1:
|
|
144
|
+
return print("Digest Auth test Passed. Credentials Valid!")
|
|
145
|
+
|
|
146
|
+
if msg1.decode().find("401 Unauthorized") > 1:
|
|
147
|
+
raise AuthTestResultFailed("Credentials not valid!!")
|
|
148
|
+
|
|
149
|
+
return print("Basic Auth test passed. Credentials Valid!")
|
pyezvizapi/utils.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Decrypt camera images."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from hashlib import md5
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from Crypto.Cipher import AES
|
|
10
|
+
|
|
11
|
+
from .exceptions import PyEzvizError
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def convert_to_dict(data: Any) -> Any:
|
|
17
|
+
"""Recursively convert a string representation of a dictionary to a dictionary."""
|
|
18
|
+
if isinstance(data, dict):
|
|
19
|
+
for key, value in data.items():
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
try:
|
|
22
|
+
# Attempt to convert the string back into a dictionary
|
|
23
|
+
data[key] = json.loads(value)
|
|
24
|
+
|
|
25
|
+
except ValueError:
|
|
26
|
+
continue
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
return data
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def string_to_list(data: Any, separator: str = ",") -> Any:
|
|
33
|
+
"""Convert a string representation of a list to a list."""
|
|
34
|
+
if isinstance(data, str):
|
|
35
|
+
if separator in data:
|
|
36
|
+
try:
|
|
37
|
+
# Attempt to convert the string into a list
|
|
38
|
+
return data.split(separator)
|
|
39
|
+
|
|
40
|
+
except AttributeError:
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def fetch_nested_value(data: Any, keys: list, default_value: Any = None) -> Any:
|
|
47
|
+
"""Fetch the value corresponding to the given nested keys in a dictionary.
|
|
48
|
+
|
|
49
|
+
If any of the keys in the path doesn't exist, the default value is returned.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
data (dict): The nested dictionary to search for keys.
|
|
53
|
+
keys (list): A list of keys representing the path to the desired value.
|
|
54
|
+
default_value (optional): The value to return if any of the keys doesn't exist.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The value corresponding to the nested keys or the default value.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
for key in keys:
|
|
62
|
+
data = data[key]
|
|
63
|
+
return data
|
|
64
|
+
except (KeyError, TypeError):
|
|
65
|
+
return default_value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def decrypt_image(input_data: bytes, password: str) -> bytes:
|
|
69
|
+
"""Decrypts image data with provided password.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
input_data (bytes): Encrypted image data
|
|
73
|
+
password (string): Verification code
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
PyEzvizError
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
bytes: Decrypted image data
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if len(input_data) < 48:
|
|
84
|
+
raise PyEzvizError("Invalid image data")
|
|
85
|
+
|
|
86
|
+
# check header
|
|
87
|
+
if input_data[:16] != b"hikencodepicture":
|
|
88
|
+
_LOGGER.warning("Image header doesn't contain 'hikencodepicture'")
|
|
89
|
+
return input_data
|
|
90
|
+
|
|
91
|
+
file_hash = input_data[16:48]
|
|
92
|
+
passwd_hash = (
|
|
93
|
+
md5(str.encode(md5(str.encode(password)).digest().hex())).digest().hex()
|
|
94
|
+
)
|
|
95
|
+
if file_hash != str.encode(passwd_hash):
|
|
96
|
+
raise PyEzvizError("Invalid password")
|
|
97
|
+
|
|
98
|
+
key = str.encode(password.ljust(16, "\u0000")[:16])
|
|
99
|
+
iv_code = bytes([48, 49, 50, 51, 52, 53, 54, 55, 0, 0, 0, 0, 0, 0, 0, 0])
|
|
100
|
+
cipher = AES.new(key, AES.MODE_CBC, iv_code)
|
|
101
|
+
|
|
102
|
+
next_chunk = b""
|
|
103
|
+
output_data = b""
|
|
104
|
+
finished = False
|
|
105
|
+
i = 48 # offset hikencodepicture + hash
|
|
106
|
+
chunk_size = 1024 * AES.block_size
|
|
107
|
+
while not finished:
|
|
108
|
+
chunk, next_chunk = next_chunk, cipher.decrypt(input_data[i : i + chunk_size])
|
|
109
|
+
if len(next_chunk) == 0:
|
|
110
|
+
padding_length = chunk[-1]
|
|
111
|
+
chunk = chunk[:-padding_length]
|
|
112
|
+
finished = True
|
|
113
|
+
output_data += chunk
|
|
114
|
+
i += chunk_size
|
|
115
|
+
return output_data
|
|
116
|
+
|
|
117
|
+
def deep_merge(dict1, dict2):
|
|
118
|
+
"""Recursively merges two dictionaries, handling lists as well.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
dict1 (dict): The first dictionary.
|
|
122
|
+
dict2 (dict): The second dictionary.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
dict: The merged dictionary.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
# If one of the dictionaries is None, return the other one
|
|
129
|
+
if dict1 is None:
|
|
130
|
+
return dict2
|
|
131
|
+
if dict2 is None:
|
|
132
|
+
return dict1
|
|
133
|
+
|
|
134
|
+
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
|
|
135
|
+
if isinstance(dict1, list) and isinstance(dict2, list):
|
|
136
|
+
return dict1 + dict2
|
|
137
|
+
return dict2
|
|
138
|
+
|
|
139
|
+
# Create a new dictionary to store the merged result
|
|
140
|
+
merged = {}
|
|
141
|
+
|
|
142
|
+
# Merge keys from both dictionaries
|
|
143
|
+
for key in set(dict1.keys()) | set(dict2.keys()):
|
|
144
|
+
if key in dict1 and key in dict2:
|
|
145
|
+
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
|
146
|
+
merged[key] = deep_merge(dict1[key], dict2[key])
|
|
147
|
+
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
|
|
148
|
+
merged[key] = dict1[key] + dict2[key]
|
|
149
|
+
else:
|
|
150
|
+
# If both values are not dictionaries or lists, keep the value from dict2
|
|
151
|
+
merged[key] = dict2[key]
|
|
152
|
+
elif key in dict1:
|
|
153
|
+
# If the key is only in dict1, keep its value
|
|
154
|
+
merged[key] = dict1[key]
|
|
155
|
+
else:
|
|
156
|
+
# If the key is only in dict2, keep its value
|
|
157
|
+
merged[key] = dict2[key]
|
|
158
|
+
|
|
159
|
+
return merged
|
|
160
|
+
|