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

"""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