You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
160 lines
5.3 KiB
160 lines
5.3 KiB
"""Ezviz CAS API Functions.""" |
|
|
|
import random |
|
import socket |
|
import ssl |
|
from io import BytesIO |
|
from itertools import cycle |
|
|
|
import xmltodict |
|
from Crypto.Cipher import AES |
|
from pyezviz.constants import FEATURE_CODE, XOR_KEY |
|
from pyezviz.exceptions import InvalidHost |
|
|
|
|
|
def xor_enc_dec(msg, xor_key=XOR_KEY): |
|
"""Xor encodes camera serial""" |
|
with BytesIO(msg) as stream: |
|
xor_msg = bytes(a ^ b for a, b in zip(stream.read(), cycle(xor_key))) |
|
return xor_msg |
|
|
|
|
|
class EzvizCAS: |
|
"""Ezviz CAS server client.""" |
|
|
|
def __init__(self, token): |
|
"""Initialize the client object.""" |
|
self._session = None |
|
self._token = token or { |
|
"session_id": None, |
|
"rf_session_id": None, |
|
"username": None, |
|
"api_url": "apiieu.ezvizlife.com", |
|
} |
|
self._service_urls = token["service_urls"] |
|
|
|
def cas_get_encryption(self, devserial): |
|
"""Fetch encryption code from ezviz cas server""" |
|
|
|
# Random hex 64 characters long. |
|
rand_hex = random.randrange(10 ** 80) |
|
rand_hex = "%064x" % rand_hex |
|
rand_hex = rand_hex[:64] |
|
|
|
payload = ( |
|
f"\x9e\xba\xac\xe9\x01\x00\x00\x00\x00\x00" |
|
f"\x00\x02" # Check or order? |
|
f"\x00\x00\x00\x00\x00\x00 " |
|
f"\x01" # Check or order? |
|
f"\x00\x00\x00\x00\x00\x00\x02\t\x00\x00\x00\x00" |
|
f'<?xml version="1.0" encoding="utf-8"?>\n<Request>\n\t' |
|
f'<ClientID>{self._token["session_id"]}</ClientID>' |
|
f"\n\t<Sign>{FEATURE_CODE}</Sign>\n\t" |
|
f"<DevSerial>{devserial}</DevSerial>" |
|
f"\n\t<ClientType>0</ClientType>\n</Request>\n" |
|
).encode("latin1") |
|
|
|
payload_end_padding = rand_hex.encode("latin1") |
|
|
|
context = ssl.SSLContext(cert_reqs=ssl.CERT_NONE) |
|
|
|
# Create a TCP/IP socket |
|
my_socket = socket.create_connection( |
|
(self._service_urls["sysConf"][15], self._service_urls["sysConf"][16]) |
|
) |
|
my_socket = context.wrap_socket( |
|
my_socket, server_hostname=self._service_urls["sysConf"][15] |
|
) |
|
|
|
# Get CAS Encryption Key |
|
try: |
|
my_socket.send(payload + payload_end_padding) |
|
response = my_socket.recv(1024) |
|
print(f"Get Encryption Key: {response}") |
|
|
|
except (socket.gaierror, ConnectionRefusedError) as err: |
|
raise InvalidHost("Invalid IP or Hostname") from err |
|
|
|
finally: |
|
my_socket.close() |
|
|
|
# Trim header, tail and convert xml to dict. |
|
response = response[32::] |
|
response = response[:-32:] |
|
response = xmltodict.parse(response) |
|
|
|
return response |
|
|
|
def set_camera_defence_state(self, serial, enable=1): |
|
"""Enable alarm notifications.""" |
|
|
|
# Random hex 64 characters long. |
|
rand_hex = random.randrange(10 ** 80) |
|
rand_hex = "%064x" % rand_hex |
|
rand_hex = rand_hex[:64] |
|
|
|
payload = ( |
|
f"\x9e\xba\xac\xe9\x01\x00\x00\x00\x00\x00" |
|
f"\x00\x14" # Check or order? |
|
f"\x00\x00\x00\x00\x00\x00 " |
|
f"\x05" |
|
f"\x00\x00\x00\x00\x00\x00\x02\xd0\x00\x00\x01\xe0" |
|
f'<?xml version="1.0" encoding="utf-8"?>\n<Request>\n\t' |
|
f'<Verify ClientSession="{self._token["session_id"]}" ' |
|
f'ToDevice="{serial}" ClientType="0" />\n\t' |
|
f'<Message Length="240" />\n</Request>\n' |
|
f"\x9e\xba\xac\xe9\x01\x00\x00\x00\x00\x00" |
|
f"\x00\x13" |
|
f"\x00\x00\x00\x00\x00\x000\x0f\xff\xff\xff\xff" |
|
f"\x00\x00\x00\xb0\x00\x00\x00\x00" |
|
).encode("latin1") |
|
|
|
payload_end_padding = rand_hex.encode("latin1") |
|
|
|
# xor camera serial |
|
xor_cam_serial = xor_enc_dec(serial.encode("latin1")) |
|
|
|
defence_msg_string = ( |
|
f'{xor_cam_serial.decode()}2+,*xdv.0" ' |
|
f'encoding="utf-8"?>\n' |
|
f"<Request>\n" |
|
f"\t<OperationCode>ABCDEFG</OperationCode>\n" |
|
f'\t<Defence Type="Global" Status="{enable}" Actor="V" Channel="0" />\n' |
|
f"</Request>\n" |
|
f"\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10" |
|
).encode("latin1") |
|
|
|
context = ssl.SSLContext(cert_reqs=ssl.CERT_NONE) |
|
|
|
# Create a TCP/IP socket |
|
my_socket = socket.create_connection( |
|
(self._service_urls["sysConf"][15], self._service_urls["sysConf"][16]) |
|
) |
|
my_socket = context.wrap_socket( |
|
my_socket, server_hostname=self._service_urls["sysConf"][15] |
|
) |
|
|
|
cas_client = self.cas_get_encryption(serial) |
|
|
|
aes_key = cas_client["Response"]["Session"]["@Key"].encode("latin1") |
|
iv_value = ( |
|
f"{serial}{cas_client['Response']['Session']['@OperationCode']}".encode( |
|
"latin1" |
|
) |
|
) |
|
|
|
# Message encryption |
|
cipher = AES.new(aes_key, AES.MODE_CBC, iv_value) |
|
|
|
try: |
|
defence_msg_string = cipher.encrypt(defence_msg_string) |
|
my_socket.send(payload + defence_msg_string + payload_end_padding) |
|
print(f"Set camera response: {my_socket.recv()}") |
|
|
|
except (socket.gaierror, ConnectionRefusedError) as err: |
|
raise InvalidHost("Invalid IP or Hostname") from err |
|
|
|
finally: |
|
my_socket.close() |
|
|
|
return True
|
|
|