Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use one base class for plain_coap and coap #86

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 95 additions & 71 deletions pyairctrl/coap_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from coapthon import defines
from coapthon.client.helperclient import HelperClient
from coapthon.messages.request import Request
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad

Expand All @@ -24,13 +25,28 @@ class NotSupportedException(Exception):
pass


class HTTPAirClientBase(ABC):
class CoAPAirClientBase(ABC):
STATUS_PATH = "/sys/dev/status"
CONTROL_PATH = "/sys/dev/control"
SYNC_PATH = "/sys/dev/sync"

def __init__(self, host, port, debug=False):
self.logger = logging.getLogger(self.__class__.__name__)
self.logger.setLevel("WARN")
self.server = host
self.port = port
self.debug = debug
self.client = self._create_coap_client(self.server, self.port)
self.response = None
self._initConnection()

def __del__(self):
if self.response:
self.client.cancel_observing(self.response, True)
self.client.stop()

def _create_coap_client(self, host, port):
return HelperClient(server=(host, port))

def get_status(self, debug=False):
if debug:
Expand All @@ -48,20 +64,65 @@ def set_values(self, values, debug=False):

return result

@abstractmethod
def _get(self):
payload = None

try:
request = self.client.mk_request(defines.Codes.GET, self.STATUS_PATH)
request.observe = 0
self.response = self.client.send_request(request, None, 2)
if self.response:
payload = self._transform_payload_after_receiving(self.response.payload)
except Exception as e:
print("Unexpected error:{}".format(e))

if payload:
try:
return json.loads(payload, object_pairs_hook=OrderedDict)["state"][
"reported"
]
except json.decoder.JSONDecodeError:
print("JSONDecodeError, you may have choosen the wrong coap protocol!")

return {}

def _set(self, key, payload):
try:
payload = self._transform_payload_before_sending(json.dumps(payload))
response = self.client.post(self.CONTROL_PATH, payload)

if self.debug:
print(response)
return response.payload == '{"status":"success"}'
except Exception as e:
print("Unexpected error:{}".format(e))

def _send_empty_message(self):
request = Request()
request.destination = server = (self.server, self.port)
request.code = defines.Codes.EMPTY.number
self.client.send_empty(request)

@abstractmethod
def _initConnection(self):
pass

@abstractmethod
def _set(self, key, value):
def _transform_payload_after_receiving(self, payload):
pass

@abstractmethod
def _transform_payload_before_sending(self, payload):
pass

def get_firmware(self):
status = self._get()
# TODO Really transmit full status here?
return status

def get_filters(self):
status = self._get()
# TODO Really transmit full status here?
return status

def get_wifi(self):
Expand All @@ -71,50 +132,43 @@ def set_wifi(self, ssid, pwd):
raise NotSupportedException


class CoAPAirClient(HTTPAirClientBase):
class CoAPAirClient(CoAPAirClientBase):
SECRET_KEY = "JiangPan"

def __init__(self, host, port=5683, debug=False):
super().__init__(host, port, debug)
self.client = self._create_coap_client(self.server, self.port)
self.response = None
self._sync()

def __del__(self):
# TODO call a close method explicitly instead
if self.response:
self.client.cancel_observing(self.response, True)
self.client.stop()

def _create_coap_client(self, host, port):
return HelperClient(server=(host, port))

def _sync(self):
def _initConnection(self):
self.syncrequest = binascii.hexlify(os.urandom(4)).decode("utf8").upper()
resp = self.client.post("/sys/dev/sync", self.syncrequest, timeout=5)
resp = self.client.post(self.SYNC_PATH, self.syncrequest, timeout=5)
if resp:
self.client_key = resp.payload
else:
self.client.stop()
raise Exception("sync timeout")

def _decrypt_payload(self, encrypted_payload):
encoded_counter = encrypted_payload[0:8]
aes = self._handle_AES(encoded_counter)
encoded_message = encrypted_payload[8:-64].upper()
digest = encrypted_payload[-64:]
calculated_digest = self._create_digest(encoded_counter, encoded_message)
if digest != calculated_digest:
raise WrongDigestException
decoded_message = aes.decrypt(bytes.fromhex(encoded_message))
unpaded_message = unpad(decoded_message, 16, style="pkcs7")
return unpaded_message.decode("utf8")

def _encrypt_payload(self, payload):
def _transform_payload_after_receiving(self, encrypted_payload):
try:
encoded_counter = encrypted_payload[0:8]
aes = self._handle_AES(encoded_counter)
encoded_message = encrypted_payload[8:-64].upper()
digest = encrypted_payload[-64:]
calculated_digest = self._create_digest(encoded_counter, encoded_message)
if digest != calculated_digest:
raise WrongDigestException
decoded_message = aes.decrypt(bytes.fromhex(encoded_message))
unpaded_message = unpad(decoded_message, 16, style="pkcs7")
return unpaded_message.decode("utf8")
except WrongDigestException:
print("Message from device got corrupted")

def _transform_payload_before_sending(self, payload):
self._update_client_key()
aes = self._handle_AES(self.client_key)
paded_message = pad(bytes(payload.encode("utf8")), 16, style="pkcs7")
encoded_message = binascii.hexlify(aes.encrypt(paded_message)).decode("utf8").upper()
encoded_message = (
binascii.hexlify(aes.encrypt(paded_message)).decode("utf8").upper()
)
digest = self._create_digest(self.client_key, encoded_message)
return self.client_key + encoded_message + digest

Expand All @@ -138,45 +192,15 @@ def _handle_AES(self, id):
bytes(secret_key.encode("utf8")), AES.MODE_CBC, bytes(iv.encode("utf8"))
)

def _get(self):
path = "/sys/dev/status"
decrypted_payload = None

try:
request = self.client.mk_request(defines.Codes.GET, path)
request.observe = 0
self.response = self.client.send_request(request, None, 2)
encrypted_payload = self.response.payload
decrypted_payload = self._decrypt_payload(encrypted_payload)
except WrongDigestException:
print("Message from device got corrupted")
except Exception as e:
print("Unexpected error:{}".format(e))

if decrypted_payload is not None:
return json.loads(decrypted_payload, object_pairs_hook=OrderedDict)[
"state"
]["reported"]
else:
return {}

def _set(self, key, value):
path = "/sys/dev/control"
try:
payload = {
"state": {
"desired": {
"CommandType": "app",
"DeviceId": "",
"EnduserId": "",
key: value,
}
payload = {
"state": {
"desired": {
"CommandType": "app",
"DeviceId": "",
"EnduserId": "",
key: value,
}
}
encrypted_payload = self._encrypt_payload(json.dumps(payload))
response = self.client.post(path, encrypted_payload)
if self.debug:
print(response)
return response.payload == '{"status":"success"}'
except Exception as e:
print("Unexpected error:{}".format(e))
}
return super()._set(key, payload)
131 changes: 33 additions & 98 deletions pyairctrl/plain_coap_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,42 @@
import time

from collections import OrderedDict
from coapthon import defines
from coapthon.client.helperclient import HelperClient
from coapthon.messages.request import Request
from coapthon.utils import generate_random_token
from .coap_client import CoAPAirClientBase


class NotSupportedException(Exception):
pass
class PlainCoAPAirClient(CoAPAirClientBase):
def __init__(self, host, port=5683, debug=False):
super().__init__(host, port, debug)
# TODO is this really needed for _get?
# request.type = defines.Types["ACK"]
# request.token = generate_random_token(4)

def _set(self, key, value):
payload = {"state": {"desired": {key: value}}}
return super()._set(key, payload)

def _transform_payload_after_receiving(self, payload):
return payload

def _transform_payload_before_sending(self, payload):
return payload

def _initConnection(self):
try:
ownIp = self._get_ip()

header = self._create_icmp_header()
data = self._create_icmp_data(ownIp, self.port, self.server, self.port)
packet = header + data
packet = self._create_icmp_header(self._checksum_icmp(packet)) + data

class PlainCoAPAirClient:
def __init__(self, host, port=5683):
self.coapthon_logger = logging.getLogger("coapthon")
self.coapthon_logger.setLevel("WARN")
self.server = host
self.port = port
self._send_over_socket(self.server, packet)

def _create_coap_client(self, host, port):
return HelperClient(server=(host, port))
# that is needed to give device time to open coap port, otherwise it may not respond properly
time.sleep(0.5)
self._send_empty_message()
finally:
pass

def _send_over_socket(self, destination, packet):
protocol = socket.getprotobyname("icmp")
Expand All @@ -44,57 +61,6 @@ def _send_over_socket(self, destination, packet):
finally:
s.close()

def _get(self):
path = "/sys/dev/status"
response = None
try:
client = self._create_coap_client(self.server, self.port)
self._send_hello_sequence(client)
request = client.mk_request(defines.Codes.GET, path)
request.destination = server = (self.server, self.port)
request.type = defines.Types["ACK"]
request.token = generate_random_token(4)
request.observe = 0
response = client.send_request(request, None, 2)
finally:
if response:
client.cancel_observing(response, True)
client.stop()

if response:
return json.loads(response.payload, object_pairs_hook=OrderedDict)["state"]["reported"]
else:
return {}

def _set(self, key, value):
path = "/sys/dev/control"
try:
client = self._create_coap_client(self.server, self.port)
self._send_hello_sequence(client)
payload = {"state": {"desired": {key: value}}}
response = client.post(path, json.dumps(payload))
return response.payload == '{"status":"success"}'
finally:
client.stop()

def _send_hello_sequence(self, client):
ownIp = self._get_ip()

header = self._create_icmp_header()
data = self._create_icmp_data(ownIp, self.port, self.server, self.port)
packet = header + data
packet = self._create_icmp_header(self._checksum_icmp(packet)) + data

self._send_over_socket(self.server, packet)

# that is needed to give device time to open coap port, otherwise it may not respond properly
time.sleep(0.5)

request = Request()
request.destination = server = (self.server, self.port)
request.code = defines.Codes.EMPTY.number
client.send_empty(request)

def _get_ip(self):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
Expand Down Expand Up @@ -188,7 +154,7 @@ def _create_tcp_data(self, srcIp, dstIp, checksum=0):
tcp = struct.pack(
"!BBHHHBBH4s4s",
ip_ver, # IP Version
ip_dfc, # Differentiate Service Feild
ip_dfc, # Differentiate Service Field
ip_tol, # Total Length
ip_idf, # Identification
ip_flg, # Flags
Expand All @@ -211,35 +177,4 @@ def _create_udp_data(self, srcPort, dstPort):
def _create_icmp_data(self, srcIp, srcPort, dstIp, dstPort):
return self._create_tcp_data(srcIp, dstIp) + self._create_udp_data(
srcPort, dstPort
)

def set_values(self, values, debug=False):
if debug:
self.coapthon_logger.setLevel("DEBUG")
result = True
for key in values:
result = result and self._set(key, values[key])

return result

def get_status(self, debug=False):
if debug:
self.coapthon_logger.setLevel("DEBUG")
status = self._get()
return status

def get_wifi(self):
raise NotSupportedException

def set_wifi(self, ssid, pwd):
raise NotSupportedException

def get_firmware(self):
status = self._get()
# TODO Really transmit full status here?
return status

def get_filters(self):
status = self._get()
# TODO Really transmit full status here?
return status
)
Loading