OpenDahua 1.0.0__tar.gz
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.
- opendahua-1.0.0/PKG-INFO +8 -0
- opendahua-1.0.0/pyproject.toml +22 -0
- opendahua-1.0.0/src/opendahua/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api/api_client.py +5 -0
- opendahua-1.0.0/src/opendahua/api/api_request.py +44 -0
- opendahua-1.0.0/src/opendahua/api/api_response.py +9 -0
- opendahua-1.0.0/src/opendahua/api_dahua/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api_dahua/api_client_dahua.py +108 -0
- opendahua-1.0.0/src/opendahua/api_dahua/api_dahua_authentication_util.py +129 -0
- opendahua-1.0.0/src/opendahua/api_dahua/api_dahua_body_parser.py +150 -0
- opendahua-1.0.0/src/opendahua/api_dahua/api_dahua_util.py +9 -0
- opendahua-1.0.0/src/opendahua/api_dahua/object/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api_dahua/object/api_dahua_media_file_find_result_item.py +37 -0
- opendahua-1.0.0/src/opendahua/api_dahua/object/api_dahua_media_file_finder_identifier.py +13 -0
- opendahua-1.0.0/src/opendahua/api_dahua/object/api_dahua_video_stream.py +8 -0
- opendahua-1.0.0/src/opendahua/api_dahua/object/datetime_dahua.py +8 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua.py +9 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_machine_name_read.py +28 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_download.py +36 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_finder_close.py +38 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_finder_create.py +29 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_finder_find.py +50 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_finder_read.py +39 -0
- opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_time_current_read.py +32 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua.py +7 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_machine_name_read.py +22 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_download.py +22 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_finder_close.py +12 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_finder_create.py +23 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_finder_find.py +11 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_finder_read.py +45 -0
- opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_time_current_read.py +23 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/api_client_peer_to_peer.py +72 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/api_peer_to_peer_authentication_util.py +123 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/api_peer_to_peer_body_parser.py +18 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/object/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/object/api_peer_to_peer_random_salt.py +8 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_peer_to_peer_encryption_util.py +74 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer.py +9 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_channel_create.py +75 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_device_probe.py +31 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_device_read.py +33 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_server_info_read.py +32 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_server_probe.py +25 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer.py +7 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_channel_create.py +38 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_device_probe.py +8 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_device_read.py +27 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_server_info_read.py +22 -0
- opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_server_probe.py +8 -0
- opendahua-1.0.0/src/opendahua/common_object/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/common_object/dahua_error.py +4 -0
- opendahua-1.0.0/src/opendahua/common_object/dahua_error_bad_request.py +5 -0
- opendahua-1.0.0/src/opendahua/common_object/key.py +8 -0
- opendahua-1.0.0/src/opendahua/common_object/nonce.py +16 -0
- opendahua-1.0.0/src/opendahua/common_util/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/common_util/error_util.py +17 -0
- opendahua-1.0.0/src/opendahua/dahua/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/dahua/dahua_device.py +15 -0
- opendahua-1.0.0/src/opendahua/dahua/dahua_nvr.py +141 -0
- opendahua-1.0.0/src/opendahua/dahua/dahua_peer_to_peer_connection_error.py +4 -0
- opendahua-1.0.0/src/opendahua/dahua/object/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/dahua/object/dahua_video.py +44 -0
- opendahua-1.0.0/src/opendahua/dahua/object/dahua_video_file_type.py +9 -0
- opendahua-1.0.0/src/opendahua/http/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/http/http_header.py +31 -0
- opendahua-1.0.0/src/opendahua/http/http_header_parser.py +28 -0
- opendahua-1.0.0/src/opendahua/http/http_request.py +122 -0
- opendahua-1.0.0/src/opendahua/http/http_request_body.py +25 -0
- opendahua-1.0.0/src/opendahua/http/http_request_method.py +7 -0
- opendahua-1.0.0/src/opendahua/http/http_response.py +51 -0
- opendahua-1.0.0/src/opendahua/http/http_response_body.py +38 -0
- opendahua-1.0.0/src/opendahua/http/http_response_parser.py +121 -0
- opendahua-1.0.0/src/opendahua/http/http_status_code.py +12 -0
- opendahua-1.0.0/src/opendahua/logger.py +74 -0
- opendahua-1.0.0/src/opendahua/main.py +25 -0
- opendahua-1.0.0/src/opendahua/object/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/object/address.py +42 -0
- opendahua-1.0.0/src/opendahua/object/authentication_identifier.py +26 -0
- opendahua-1.0.0/src/opendahua/object/cookie.py +17 -0
- opendahua-1.0.0/src/opendahua/object/log_level.py +8 -0
- opendahua-1.0.0/src/opendahua/object/transaction_identifier.py +18 -0
- opendahua-1.0.0/src/opendahua/object/url.py +48 -0
- opendahua-1.0.0/src/opendahua/ptcp/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_http_client.py +56 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet.py +77 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body.py +19 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_bind.py +42 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_connection_status.py +5 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_data.py +38 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_empty.py +9 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_heartbeat.py +9 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_parser.py +43 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_syn.py +9 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_identifier.py +39 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_parser.py +60 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_type.py +8 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_realm_identifier.py +20 -0
- opendahua-1.0.0/src/opendahua/ptcp/ptcp_socket.py +272 -0
- opendahua-1.0.0/src/opendahua/signaling_client.py +205 -0
- opendahua-1.0.0/src/opendahua/udp/__init__.py +0 -0
- opendahua-1.0.0/src/opendahua/udp/udp_http_client.py +45 -0
- opendahua-1.0.0/src/opendahua/udp/udp_protocol.py +33 -0
- opendahua-1.0.0/src/opendahua/udp/udp_socket.py +80 -0
- opendahua-1.0.0/src/opendahua/udp/udp_socket_closed_error.py +3 -0
opendahua-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: OpenDahua
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: SDK for interacting with Dahua NVRs over their peer-to-peer protocol.
|
|
5
|
+
Requires-Dist: aiofiles>=25.1.0
|
|
6
|
+
Requires-Dist: cryptography>=48.0.0
|
|
7
|
+
Requires-Dist: xmltodict>=1.0.4
|
|
8
|
+
Requires-Python: >=3.14
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "OpenDahua"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "SDK for interacting with Dahua NVRs over their peer-to-peer protocol."
|
|
5
|
+
requires-python = ">=3.14"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"aiofiles>=25.1.0",
|
|
8
|
+
"cryptography>=48.0.0",
|
|
9
|
+
"xmltodict>=1.0.4",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[dependency-groups]
|
|
13
|
+
dev = [
|
|
14
|
+
"pytest==9.0.3"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.uv]
|
|
18
|
+
package = true
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.11.17,<0.12"]
|
|
22
|
+
build-backend = "uv_build"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Type, TypeVar, Generic
|
|
3
|
+
|
|
4
|
+
from opendahua.api.api_response import ApiResponse
|
|
5
|
+
from opendahua.http.http_header import HttpHeader
|
|
6
|
+
from opendahua.http.http_request import HttpRequest
|
|
7
|
+
from opendahua.http.http_request_body import HttpRequestBody
|
|
8
|
+
from opendahua.http.http_request_method import HttpRequestMethod
|
|
9
|
+
from opendahua.object.url import Url
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound=ApiResponse)
|
|
12
|
+
|
|
13
|
+
class ApiRequest(ABC, Generic[T]):
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def determine_endpoint(self) -> Url:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def get_request_method(self) -> HttpRequestMethod:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def determine_body_or_none(self) -> HttpRequestBody|None:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def determine_all_header(self) -> list[HttpHeader]:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_http_request(self) -> HttpRequest:
|
|
34
|
+
return HttpRequest(
|
|
35
|
+
self.get_request_method(),
|
|
36
|
+
self.determine_endpoint(),
|
|
37
|
+
self.determine_all_header(),
|
|
38
|
+
self.determine_body_or_none(),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get_response_class(self) -> Type[T]:
|
|
44
|
+
...
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from opendahua.api.api_request import T
|
|
2
|
+
from opendahua.api_dahua.api_dahua_authentication_util import ApiDahuaAuthenticationUtil
|
|
3
|
+
from opendahua.api_dahua.api_dahua_body_parser import ApiDahuaBodyParser
|
|
4
|
+
from opendahua.api_dahua.request.api_request_dahua import ApiRequestDahua
|
|
5
|
+
from opendahua.common_object.dahua_error import DahuaError
|
|
6
|
+
from opendahua.common_object.dahua_error_bad_request import DahuaErrorBadRequest
|
|
7
|
+
from opendahua.common_object.nonce import Nonce
|
|
8
|
+
from opendahua.dahua.dahua_device import DahuaDevice
|
|
9
|
+
from opendahua.http.http_request import HttpRequest
|
|
10
|
+
from opendahua.http.http_response import HttpResponse
|
|
11
|
+
from opendahua.http.http_status_code import HttpStatusCode
|
|
12
|
+
from opendahua.ptcp.ptcp_http_client import PtcpHttpClient
|
|
13
|
+
from opendahua.signaling_client import SignalingClient
|
|
14
|
+
|
|
15
|
+
class ApiClientDahua:
|
|
16
|
+
# Error constants.
|
|
17
|
+
ERROR_DAHUA_API = "Received error response from Dahua API: \"{http_response}\"."
|
|
18
|
+
ERROR_ALREADY_CONNECTED = "Already connected."
|
|
19
|
+
ERROR_ALREADY_DISCONNECTED = "Already disconnected."
|
|
20
|
+
ERROR_EXPECTED_DIGEST_CHALLENGE = "Received response with status code \"{status_code}\" instead of digest authentication challenge."
|
|
21
|
+
ERROR_NOT_CONNECTED = "You have to use connect() to connect to the device before making API requests."
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __init__(self, device: DahuaDevice):
|
|
25
|
+
self._device: DahuaDevice = device
|
|
26
|
+
self._signaling_client: SignalingClient = SignalingClient(device)
|
|
27
|
+
self._http_client: PtcpHttpClient|None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def connect(self) -> None:
|
|
31
|
+
if self._http_client is None:
|
|
32
|
+
ptcp_socket = await self._signaling_client.connect()
|
|
33
|
+
|
|
34
|
+
http_client = PtcpHttpClient(ptcp_socket)
|
|
35
|
+
|
|
36
|
+
await http_client.connect()
|
|
37
|
+
self._http_client = http_client
|
|
38
|
+
else:
|
|
39
|
+
raise DahuaError(self.ERROR_ALREADY_CONNECTED)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def disconnect(self) -> None:
|
|
43
|
+
if self._http_client is None:
|
|
44
|
+
raise DahuaError(self.ERROR_ALREADY_DISCONNECTED)
|
|
45
|
+
else:
|
|
46
|
+
await self._http_client.disconnect()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def send_request(self, api_request: ApiRequestDahua[T]) -> T:
|
|
50
|
+
client = await self._get_http_client()
|
|
51
|
+
|
|
52
|
+
http_request = api_request.generate_http_request()
|
|
53
|
+
http_response = await client.send_request(http_request)
|
|
54
|
+
|
|
55
|
+
if http_response.get_status_code() == HttpStatusCode.UNAUTHORIZED:
|
|
56
|
+
nonce = ApiDahuaAuthenticationUtil.determine_nonce_from_http_response_unauthorized(http_response)
|
|
57
|
+
realm = ApiDahuaAuthenticationUtil.determine_realm_from_http_response_unauthorized(http_response)
|
|
58
|
+
|
|
59
|
+
http_request = self._add_header_authentication(http_request, nonce, realm)
|
|
60
|
+
|
|
61
|
+
http_response = await client.send_request(http_request)
|
|
62
|
+
|
|
63
|
+
if self._is_http_response_success(http_response):
|
|
64
|
+
return self._parse_api_response(api_request, http_response)
|
|
65
|
+
elif http_response.get_status_code() == HttpStatusCode.BAD_REQUEST:
|
|
66
|
+
raise DahuaErrorBadRequest(self.ERROR_DAHUA_API.format(http_response=http_response))
|
|
67
|
+
else:
|
|
68
|
+
raise DahuaError(self.ERROR_DAHUA_API.format(http_response=http_response))
|
|
69
|
+
else:
|
|
70
|
+
raise DahuaError(self.ERROR_EXPECTED_DIGEST_CHALLENGE.format(status_code=http_response.get_status_code()))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def _get_http_client(self) -> PtcpHttpClient:
|
|
74
|
+
if self._http_client is None:
|
|
75
|
+
raise DahuaError(self.ERROR_NOT_CONNECTED)
|
|
76
|
+
else:
|
|
77
|
+
return self._http_client
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _add_header_authentication(self, http_request: HttpRequest, nonce: Nonce, realm: str) -> HttpRequest:
|
|
81
|
+
header_authentication = ApiDahuaAuthenticationUtil.generate_header_authentication(
|
|
82
|
+
http_request,
|
|
83
|
+
self._device,
|
|
84
|
+
nonce,
|
|
85
|
+
realm,
|
|
86
|
+
)
|
|
87
|
+
http_request.add_header(header_authentication)
|
|
88
|
+
|
|
89
|
+
return http_request
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _is_http_response_success(self, http_response: HttpResponse) -> bool:
|
|
93
|
+
return http_response.get_status_code() in self._get_all_http_status_code_success()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _get_all_http_status_code_success() -> list[HttpStatusCode]:
|
|
98
|
+
return [
|
|
99
|
+
HttpStatusCode.OK,
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _parse_api_response(api_request: ApiRequestDahua[T], http_response: HttpResponse) -> T:
|
|
105
|
+
response_class = api_request.get_response_class()
|
|
106
|
+
response_body_dict = ApiDahuaBodyParser.determine_dict(http_response)
|
|
107
|
+
|
|
108
|
+
return response_class.parse(response_body_dict)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from opendahua.common_object.nonce import Nonce
|
|
5
|
+
from opendahua.common_util.error_util import ErrorUtil
|
|
6
|
+
from opendahua.dahua.dahua_device import DahuaDevice
|
|
7
|
+
from opendahua.http.http_header import HttpHeader
|
|
8
|
+
from opendahua.http.http_request import HttpRequest
|
|
9
|
+
from opendahua.http.http_response import HttpResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApiDahuaAuthenticationUtil:
|
|
13
|
+
# Header constants.
|
|
14
|
+
HEADER_KEY_AUTHORIZATION = "Authorization"
|
|
15
|
+
HEADER_KEY_DIGEST_AUTHENTICATION_REQUIRED = "WWW-Authenticate"
|
|
16
|
+
|
|
17
|
+
# Format constants.
|
|
18
|
+
FORMAT_HEADER_DIGEST = (
|
|
19
|
+
"Digest "
|
|
20
|
+
"username=\"{username}\", "
|
|
21
|
+
"realm=\"{realm}\", "
|
|
22
|
+
"nonce=\"{nonce}\", "
|
|
23
|
+
"uri=\"{url_request}\", "
|
|
24
|
+
"qop=\"{qop}\", "
|
|
25
|
+
"nc={number_of_time_nonce_used:08x}, "
|
|
26
|
+
"cnonce=\"{nonce_client}\", "
|
|
27
|
+
"response=\"{response}\", "
|
|
28
|
+
"opaque=\"{opaque}\""
|
|
29
|
+
)
|
|
30
|
+
FORMAT_HA1 = "{username}:{realm}:{password}"
|
|
31
|
+
FORMAT_HA2 = "{http_request_method}:{url_request}"
|
|
32
|
+
FORMAT_RESPONSE = "{ha1}:{nonce}:{number_of_time_nonce_used:08x}:{nonce_client}:{qop}:{ha2}"
|
|
33
|
+
|
|
34
|
+
# Digest constants.
|
|
35
|
+
DIGEST_QUALITY_OF_PROTECTION_AUTHENTICATION = "auth"
|
|
36
|
+
DIGEST_NUMBER_OF_TIME_NONCE_USED = 1
|
|
37
|
+
|
|
38
|
+
# Regex constants.
|
|
39
|
+
REGEX_PATTERN_NONCE = re.compile(r'nonce="([^"]*)"')
|
|
40
|
+
REGEX_PATTERN_REALM = re.compile(r'realm="([^"]*)"')
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def determine_nonce_from_http_response_unauthorized(
|
|
44
|
+
cls,
|
|
45
|
+
http_response_unauthorized: HttpResponse,
|
|
46
|
+
) -> Nonce:
|
|
47
|
+
header_digest_authentication_required = cls._get_header_digest_authentication_required(
|
|
48
|
+
http_response_unauthorized,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
match = cls.REGEX_PATTERN_NONCE.search(header_digest_authentication_required.get_header_value_string())
|
|
52
|
+
|
|
53
|
+
return Nonce(match.group(1))
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def determine_realm_from_http_response_unauthorized(
|
|
57
|
+
cls,
|
|
58
|
+
http_response_unauthorized: HttpResponse,
|
|
59
|
+
) -> str:
|
|
60
|
+
header_digest_authentication_required = cls._get_header_digest_authentication_required(
|
|
61
|
+
http_response_unauthorized,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
match = cls.REGEX_PATTERN_REALM.search(header_digest_authentication_required.get_header_value_string())
|
|
65
|
+
|
|
66
|
+
return match.group(1)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def _get_header_digest_authentication_required(cls, http_response_unauthorized: HttpResponse) -> HttpHeader:
|
|
70
|
+
header_digest_authentication_required = http_response_unauthorized.get_header_or_none(
|
|
71
|
+
cls.HEADER_KEY_DIGEST_AUTHENTICATION_REQUIRED,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if header_digest_authentication_required is None:
|
|
75
|
+
raise ErrorUtil.create_error_unexpected_none_value(HttpHeader)
|
|
76
|
+
else:
|
|
77
|
+
return header_digest_authentication_required
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def generate_header_authentication(
|
|
82
|
+
cls,
|
|
83
|
+
request: HttpRequest,
|
|
84
|
+
device: DahuaDevice,
|
|
85
|
+
nonce: Nonce,
|
|
86
|
+
realm: str,
|
|
87
|
+
) -> HttpHeader:
|
|
88
|
+
quality_of_protection = cls.DIGEST_QUALITY_OF_PROTECTION_AUTHENTICATION
|
|
89
|
+
nonce_client = Nonce.create_random()
|
|
90
|
+
|
|
91
|
+
ha1 = cls._hash_md5(
|
|
92
|
+
cls.FORMAT_HA1.format(username=device.get_username(), realm=realm, password=device.get_password()),
|
|
93
|
+
)
|
|
94
|
+
ha2 = cls._hash_md5(
|
|
95
|
+
cls.FORMAT_HA2.format(
|
|
96
|
+
http_request_method=request.get_method().value,
|
|
97
|
+
url_request=request.get_url().get_url_string(),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
response = cls._hash_md5(
|
|
102
|
+
cls.FORMAT_RESPONSE.format(
|
|
103
|
+
ha1=ha1,
|
|
104
|
+
nonce=nonce.get_nonce_string(),
|
|
105
|
+
number_of_time_nonce_used=cls.DIGEST_NUMBER_OF_TIME_NONCE_USED,
|
|
106
|
+
nonce_client=nonce_client.get_nonce_string(),
|
|
107
|
+
qop=quality_of_protection,
|
|
108
|
+
ha2=ha2,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return HttpHeader(
|
|
113
|
+
key=cls.HEADER_KEY_AUTHORIZATION,
|
|
114
|
+
value=cls.FORMAT_HEADER_DIGEST.format(
|
|
115
|
+
username=device.get_username(),
|
|
116
|
+
realm=realm,
|
|
117
|
+
nonce=nonce.get_nonce_string(),
|
|
118
|
+
url_request=request.get_url().get_url_string(),
|
|
119
|
+
qop=quality_of_protection,
|
|
120
|
+
number_of_time_nonce_used=cls.DIGEST_NUMBER_OF_TIME_NONCE_USED,
|
|
121
|
+
nonce_client=nonce_client.get_nonce_string(),
|
|
122
|
+
response=response,
|
|
123
|
+
opaque="",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _hash_md5(value: str) -> str:
|
|
129
|
+
return hashlib.md5(value.encode()).hexdigest()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from opendahua.common_object.dahua_error import DahuaError
|
|
6
|
+
from opendahua.common_util.error_util import ErrorUtil
|
|
7
|
+
from opendahua.http.http_header import HttpHeader
|
|
8
|
+
from opendahua.http.http_response import HttpResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiDahuaBodyParser:
|
|
12
|
+
# Error constants.
|
|
13
|
+
ERROR_UNEXPECTED_CONTENT_TYPE = "Unexpected content type \"{content_type}\"."
|
|
14
|
+
|
|
15
|
+
# Separator constants.
|
|
16
|
+
SEPARATOR_LINE_BODY = "\r\n"
|
|
17
|
+
SEPARATOR_KEY_VALUE = "="
|
|
18
|
+
SEPARATOR_PART_KEY = "."
|
|
19
|
+
|
|
20
|
+
# TODO: verplaats http logica naar http response zelf
|
|
21
|
+
# Header constants.
|
|
22
|
+
HEADER_CONTENT_TYPE = "Content-type"
|
|
23
|
+
|
|
24
|
+
# Content constants.
|
|
25
|
+
CONTENT_TYPE_TEXT_PLAIN = "text/plain"
|
|
26
|
+
CONTENT_TYPE_TEXT_PLAIN_CHARSET_UTF_8 = "text/plain;charset=utf-8"
|
|
27
|
+
CONTENT_TYPE_APPLICATION_HTTP = "application/http"
|
|
28
|
+
|
|
29
|
+
# Field constants.
|
|
30
|
+
FIELD_DATA = "data"
|
|
31
|
+
|
|
32
|
+
# Regex constants.
|
|
33
|
+
REGEX_PART_KEY_LIST = r"^(?P<key>[a-zA-Z]+)\[(?P<index>\d+)\]$"
|
|
34
|
+
REGEX_MATCH_GROUP_KEY = "key"
|
|
35
|
+
REGEX_MATCH_GROUP_INDEX = "index"
|
|
36
|
+
|
|
37
|
+
# Index constants.
|
|
38
|
+
INDEX_FIRST = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def determine_dict(cls, http_response: HttpResponse) -> dict:
|
|
43
|
+
content_type_response = cls.determine_content_type_response(http_response)
|
|
44
|
+
|
|
45
|
+
# TODO: Iets van should_use_key_value_parsing.
|
|
46
|
+
if content_type_response == cls.CONTENT_TYPE_TEXT_PLAIN or content_type_response == cls.CONTENT_TYPE_TEXT_PLAIN_CHARSET_UTF_8:
|
|
47
|
+
return cls.parse_body_key_value(http_response.get_body().get_http_response_body_string())
|
|
48
|
+
elif content_type_response == cls.CONTENT_TYPE_APPLICATION_HTTP:
|
|
49
|
+
return {cls.FIELD_DATA: http_response.get_body().get_http_response_body_bytes()}
|
|
50
|
+
else:
|
|
51
|
+
raise DahuaError(cls.ERROR_UNEXPECTED_CONTENT_TYPE.format(content_type=content_type_response))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def determine_content_type_response(cls, http_response: HttpResponse) -> str:
|
|
56
|
+
header_content_type = http_response.get_header_or_none(cls.HEADER_CONTENT_TYPE)
|
|
57
|
+
|
|
58
|
+
if header_content_type is None:
|
|
59
|
+
raise ErrorUtil.create_error_unexpected_none_value(HttpHeader)
|
|
60
|
+
else:
|
|
61
|
+
return header_content_type.get_header_value_string()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def parse_body_key_value(cls, body_string: str) -> dict:
|
|
66
|
+
response_dict = {}
|
|
67
|
+
body_string_excluding_trailing_carriage_return = body_string.strip()
|
|
68
|
+
|
|
69
|
+
all_line_body = body_string_excluding_trailing_carriage_return.split(cls.SEPARATOR_LINE_BODY)
|
|
70
|
+
|
|
71
|
+
for line_body in all_line_body:
|
|
72
|
+
cls._parse_key_value(line_body, response_dict)
|
|
73
|
+
|
|
74
|
+
return response_dict
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def _parse_key_value(cls, key_value_string: str, response_dict: dict) -> dict:
|
|
78
|
+
key_string_full, _, _ = key_value_string.partition(cls.SEPARATOR_KEY_VALUE)
|
|
79
|
+
is_key_nested = cls.SEPARATOR_PART_KEY in key_string_full
|
|
80
|
+
|
|
81
|
+
separator = cls.SEPARATOR_PART_KEY if is_key_nested else cls.SEPARATOR_KEY_VALUE
|
|
82
|
+
key, _, remainder_string = key_value_string.partition(separator)
|
|
83
|
+
|
|
84
|
+
match_list = re.search(cls.REGEX_PART_KEY_LIST, key)
|
|
85
|
+
|
|
86
|
+
if match_list:
|
|
87
|
+
key_list = match_list.group(cls.REGEX_MATCH_GROUP_KEY)
|
|
88
|
+
index_list_item = int(match_list.group(cls.REGEX_MATCH_GROUP_INDEX))
|
|
89
|
+
|
|
90
|
+
all_list_item = response_dict.setdefault(key_list, [])
|
|
91
|
+
is_list_item_existing = index_list_item < len(all_list_item)
|
|
92
|
+
|
|
93
|
+
if is_key_nested:
|
|
94
|
+
if is_list_item_existing:
|
|
95
|
+
list_item_dict = all_list_item[index_list_item]
|
|
96
|
+
else:
|
|
97
|
+
list_item_dict = {}
|
|
98
|
+
|
|
99
|
+
list_item_dict_updated = cls._parse_key_value(remainder_string, list_item_dict)
|
|
100
|
+
else:
|
|
101
|
+
value_string = remainder_string
|
|
102
|
+
|
|
103
|
+
list_item_dict_updated = cls._parse_value(value_string)
|
|
104
|
+
|
|
105
|
+
if is_list_item_existing:
|
|
106
|
+
all_list_item[index_list_item] = list_item_dict_updated
|
|
107
|
+
else:
|
|
108
|
+
all_list_item.append(list_item_dict_updated)
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
if is_key_nested:
|
|
112
|
+
value_current = response_dict.get(key, {})
|
|
113
|
+
value_updated = cls._parse_key_value(remainder_string, value_current)
|
|
114
|
+
else:
|
|
115
|
+
value_string = remainder_string
|
|
116
|
+
|
|
117
|
+
value_updated = cls._parse_value(value_string)
|
|
118
|
+
|
|
119
|
+
response_dict[key] = value_updated
|
|
120
|
+
|
|
121
|
+
return response_dict
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def _parse_value(cls, value_string: str) -> Any:
|
|
126
|
+
if value_string.isdigit():
|
|
127
|
+
return int(value_string)
|
|
128
|
+
elif cls._is_datetime_string(value_string):
|
|
129
|
+
return cls._parse_datetime_string(value_string)
|
|
130
|
+
else:
|
|
131
|
+
return value_string
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def _is_datetime_string(cls, value_string: str) -> bool:
|
|
136
|
+
try:
|
|
137
|
+
cls._parse_datetime_string(value_string)
|
|
138
|
+
|
|
139
|
+
return True
|
|
140
|
+
except ValueError:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def _parse_datetime_string(cls, value_string: str) -> datetime:
|
|
146
|
+
try:
|
|
147
|
+
return datetime.fromisoformat(value_string)
|
|
148
|
+
except ValueError:
|
|
149
|
+
# TODO: magic number.
|
|
150
|
+
return datetime.strptime(value_string, "%Y-%m-%d %H:%M:%S")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
class ApiDahuaUtil:
|
|
4
|
+
# Format constants.
|
|
5
|
+
FORMAT_DATETIME_DAHUA_STRING = "%Y-%m-%dT%H:%M:%SZ"
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def determine_datetime_dahua_string(cls, datetime: datetime):
|
|
9
|
+
return datetime.strftime(cls.FORMAT_DATETIME_DAHUA_STRING)
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from opendahua.api_dahua.object.api_dahua_video_stream import ApiDahuaVideoStream
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ApiDahuaMediaFileFindResultItem:
|
|
10
|
+
_channel: int
|
|
11
|
+
_time_start: datetime
|
|
12
|
+
_time_end: datetime
|
|
13
|
+
_path_file: Path
|
|
14
|
+
_file_type: str
|
|
15
|
+
_video_stream: ApiDahuaVideoStream
|
|
16
|
+
|
|
17
|
+
def get_channel_int(self) -> int:
|
|
18
|
+
return self._channel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_time_start(self) -> datetime:
|
|
22
|
+
return self._time_start
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_time_end(self) -> datetime:
|
|
26
|
+
return self._time_end
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_path_file(self) -> Path:
|
|
30
|
+
return self._path_file
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_file_type(self) -> str:
|
|
34
|
+
return self._file_type
|
|
35
|
+
|
|
36
|
+
def get_video_stream(self) -> ApiDahuaVideoStream:
|
|
37
|
+
return self._video_stream
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class ApiDahuaMediaFileFinderIdentifier:
|
|
6
|
+
_media_file_identifier_int: int
|
|
7
|
+
|
|
8
|
+
def get_media_file_identifier_int(self) -> int:
|
|
9
|
+
return self._media_file_identifier_int
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
return str(self.get_media_file_identifier_int())
|
|
13
|
+
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from typing import TypeVar, Generic
|
|
2
|
+
|
|
3
|
+
from opendahua.api.api_request import ApiRequest
|
|
4
|
+
from opendahua.api_dahua.response.api_response_dahua import ApiResponseDahua
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T", bound=ApiResponseDahua)
|
|
7
|
+
|
|
8
|
+
class ApiRequestDahua(ApiRequest[T], Generic[T]):
|
|
9
|
+
pass
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from opendahua.api.api_request import T
|
|
4
|
+
from opendahua.api_dahua.request.api_request_dahua import ApiRequestDahua
|
|
5
|
+
from opendahua.api_dahua.response.api_response_dahua_machine_name_read import ApiResponseDahuaMachineNameRead
|
|
6
|
+
from opendahua.http.http_request_body import HttpRequestBody
|
|
7
|
+
from opendahua.http.http_request_method import HttpRequestMethod
|
|
8
|
+
from opendahua.object.url import Url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiRequestDahuaMachineNameRead(ApiRequestDahua[ApiResponseDahuaMachineNameRead]):
|
|
12
|
+
# Request constants.
|
|
13
|
+
REQUEST_ENDPOINT = "/cgi-bin/magicBox.cgi?action=getMachineName"
|
|
14
|
+
|
|
15
|
+
def determine_endpoint(self) -> Url:
|
|
16
|
+
return Url(self.REQUEST_ENDPOINT)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_request_method(self) -> HttpRequestMethod:
|
|
20
|
+
return HttpRequestMethod.GET
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def determine_body_or_none(self) -> HttpRequestBody | None:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_response_class(self) -> Type[T]:
|
|
28
|
+
return ApiResponseDahuaMachineNameRead
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Type
|
|
3
|
+
|
|
4
|
+
from opendahua.api.api_request import T
|
|
5
|
+
from opendahua.api_dahua.request.api_request_dahua import ApiRequestDahua
|
|
6
|
+
from opendahua.api_dahua.response.api_response_dahua_media_file_download import ApiResponseDahuaMediaFileDownload
|
|
7
|
+
from opendahua.http.http_request_body import HttpRequestBody
|
|
8
|
+
from opendahua.http.http_request_method import HttpRequestMethod
|
|
9
|
+
from opendahua.object.url import Url
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApiRequestDahuaMediaFileDownload(ApiRequestDahua[ApiResponseDahuaMediaFileDownload]):
|
|
13
|
+
# Request constants.
|
|
14
|
+
REQUEST_ENDPOINT = "/cgi-bin/RPC_Loadfile/{path}"
|
|
15
|
+
|
|
16
|
+
# Character constants.
|
|
17
|
+
CHARACTER_LEADING_SLASH = "/"
|
|
18
|
+
|
|
19
|
+
def __init__(self, path: Path):
|
|
20
|
+
self._path: Path = path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def determine_endpoint(self) -> Url:
|
|
24
|
+
return Url(self.REQUEST_ENDPOINT.format(path=str(self._path).strip(self.CHARACTER_LEADING_SLASH)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_request_method(self) -> HttpRequestMethod:
|
|
28
|
+
return HttpRequestMethod.GET
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def determine_body_or_none(self) -> HttpRequestBody | None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_response_class(self) -> Type[T]:
|
|
36
|
+
return ApiResponseDahuaMediaFileDownload
|