diff --git a/agrirouter/auth/auth.py b/agrirouter/auth/auth.py index 6cf1902b..b33527c0 100644 --- a/agrirouter/auth/auth.py +++ b/agrirouter/auth/auth.py @@ -12,10 +12,10 @@ class Authorization(EnvironmentalService): TOKEN_KEY = "token" ERROR_KEY = "error" - def __init__(self, *args, **kwargs): - self._public_key = kwargs.pop("public_key") - self._private_key = kwargs.pop("private_key") - super(Authorization, self).__init__(*args, **kwargs) + def __init__(self, env, public_key, private_key): + self._public_key = public_key + self._private_key = private_key + super(Authorization, self).__init__(env) def get_auth_request_url(self, parameters: AuthUrlParameter) -> str: auth_parameters = parameters.get_parameters() @@ -34,4 +34,3 @@ def verify_auth_response(self, response, public_key=None): def _extract_query_params(query_params: str) -> dict: qp_pairs = parse_qs(query_params) return {k: v[0] for k, v in qp_pairs.items()} - diff --git a/agrirouter/auth/dto.py b/agrirouter/auth/dto.py new file mode 100644 index 00000000..55d1bbbb --- /dev/null +++ b/agrirouter/auth/dto.py @@ -0,0 +1,118 @@ +import json +from typing import Union + +from agrirouter.messaging.exceptions import WrongFieldError + + +class AuthorizationToken: + ACCOUNT = 'account' + REGISTRATION_CODE = 'regcode' + EXPIRES = 'expires' + + def __init__(self, + *, + account: str = None, + regcode: str = None, + expires: str = None + ): + self.account = account + self.regcode = regcode + self.expires = expires + + def json_deserialize(self, data: Union[str, dict]) -> None: + data = data if type(data) == dict else json.loads(data) + for key, value in data.items(): + if key == self.ACCOUNT: + self.account = value + elif key == self.REGISTRATION_CODE: + self.regcode = value + elif key == self.EXPIRES: + self.expires = value + else: + raise WrongFieldError(f"Unknown field {key} for AuthorizationToken class") + + def get_account(self) -> str: + return self.account + + def set_account(self, account: str) -> None: + self.account = account + + def get_regcode(self) -> str: + return self.regcode + + def set_regcode(self, regcode: str) -> None: + self.regcode = regcode + + def get_expires(self) -> str: + return self.expires + + def set_expires(self, expires: str) -> None: + self.expires = expires + + +class AuthorizationResultUrl: + def __init__(self, + *, + state: str = None, + signature: str = None, + token: str = None, + decoded_token: AuthorizationToken = None, + error: str = None + ): + self.state = state + self.signature = signature + self.token = token + self.decoded_token = decoded_token + self.error = error + + def get_state(self) -> str: + return self.state + + def set_state(self, state: str) -> None: + self.state = state + + def get_signature(self) -> str: + return self.signature + + def set_signature(self, signature: str) -> None: + self.signature = signature + + def get_token(self) -> str: + return self.token + + def set_token(self, token: str) -> None: + self.token = token + + def get_error(self) -> str: + return self.error + + def set_error(self, error: str) -> None: + self.error = error + + def get_decoded_token(self) -> AuthorizationToken: + return self.decoded_token + + def set_decoded_token(self, decoded_token: AuthorizationToken) -> None: + self.decoded_token = decoded_token + + +class AuthorizationResult: + def __init__(self, + *, + authorization_url: str = None, + state: str = None, + ): + self.authorization_url = authorization_url + self.state = state + + def get_authorization_url(self) -> str: + return self.authorization_url + + def set_authorization_url(self, authorization_url: str) -> None: + self.authorization_url = authorization_url + + def get_state(self) -> str: + return self.state + + def set_state(self, state: str) -> None: + self.state = state diff --git a/agrirouter/auth/response.py b/agrirouter/auth/response.py index a711c4a6..a4b87f7f 100644 --- a/agrirouter/auth/response.py +++ b/agrirouter/auth/response.py @@ -5,6 +5,7 @@ from cryptography.exceptions import InvalidSignature +from agrirouter.auth.dto import AuthorizationToken, AuthorizationResultUrl from agrirouter.onboarding.signature import verify_signature @@ -17,11 +18,11 @@ class AuthResponse: CRED_KEY = "credentials" def __init__(self, query_params): - self._state = query_params.get(self.STATE_KEY, None) - self._signature = query_params.get(self.SIGNATURE_KEY, None) - self._token = query_params.get(self.TOKEN_KEY, None) - self._error = query_params.get(self.ERROR_KEY, None) - self.is_successful = not bool(self._error) + self.state = query_params.get(self.STATE_KEY, None) + self.signature = query_params.get(self.SIGNATURE_KEY, None) + self.token = query_params.get(self.TOKEN_KEY, None) + self.error = query_params.get(self.ERROR_KEY, None) + self.is_successful = not bool(self.error) self._was_verified = False self._is_valid = False @@ -43,33 +44,56 @@ def verify(self, public_key) -> None: :return: """ - encoded_data = self._state + self._token - unquoted_signature = unquote(self._signature) + encoded_data = self.state + self.token + unquoted_signature = unquote(self.signature) encoded_signature = base64.b64decode(unquoted_signature.encode("utf-8")) - self._was_verified = True + try: verify_signature(encoded_data, encoded_signature, public_key) except InvalidSignature: print("Response is invalid: invalid signature.") self._is_valid = False + finally: + self._was_verified = True self._is_valid = True @staticmethod - def decode_token(token: Union[str, bytes]) -> dict: + def decode_token(token: Union[str, bytes]) -> AuthorizationToken: if type(token) == str: token = token.encode("utf-8") base_64_decoded_token = base64.b64decode(token) decoded_token = base_64_decoded_token.decode("utf-8") - return json.loads(decoded_token) - - def get_auth_result(self) -> dict: - if not self.is_successful: - return {self.ERROR_KEY: self._error} - decoded_token = self.decode_token(self._token) - return { - self.SIGNATURE_KEY: self._signature, - self.STATE_KEY: self._state, - self.TOKEN_KEY: self._token, - self.CRED_KEY: decoded_token - } + + auth_token = AuthorizationToken() + auth_token.json_deserialize(json.loads(decoded_token)) + return auth_token + + def get_auth_result(self) -> AuthorizationResultUrl: + decoded_token = self.decode_token(self.token) + + return AuthorizationResultUrl( + signature=self.signature, + state=self.state, + token=self.token, + decoded_token=decoded_token, + error=self.error + ) + + def get_signature(self): + return self.signature + + def set_signature(self, signature): + self.signature = signature + + def get_state(self): + return self.state + + def set_state(self, state): + self.state = state + + def get_token(self): + return self.token + + def set_token(self, token): + self.token = token diff --git a/agrirouter/environments/environments.py b/agrirouter/environments/environments.py index 503ecd6b..6b05b99e 100644 --- a/agrirouter/environments/environments.py +++ b/agrirouter/environments/environments.py @@ -6,7 +6,7 @@ class BaseEnvironment: _MQTT_URL_TEMPLATE = "ssl://{host}:{port}" _SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE = \ "/application/{application_id}/authorize" \ - "?response_type={response_type}&state={state}&redirect_uri={redirect_uri}" + "?response_type={response_type}&state={state}" _ENV_BASE_URL = "" _API_PREFIX = "" @@ -38,13 +38,13 @@ def get_revoke_url(self) -> str: def get_agrirouter_login_url(self) -> str: return self.get_base_url() + self._AGRIROUTER_LOGIN_URL - def get_secured_onboarding_authorization_url(self, application_id, response_type, state, redirect_uri) -> str: - return self.get_base_url() + self._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( + def get_secured_onboarding_authorization_url(self, application_id, response_type, state, redirect_uri=None) -> str: + auth_url = self.get_base_url() + self._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( application_id=application_id, response_type=response_type, - state=state, - redirect_uri=redirect_uri + state=state ) + return auth_url + f"&redirect_uri={redirect_uri}" if redirect_uri is not None else auth_url def get_mqtt_server_url(self, host, port) -> str: return self._MQTT_URL_TEMPLATE.format(host=host, port=port) diff --git a/agrirouter/messaging/builders.py b/agrirouter/messaging/builders.py new file mode 100644 index 00000000..d7815042 --- /dev/null +++ b/agrirouter/messaging/builders.py @@ -0,0 +1,201 @@ +from typing import List + +from agrirouter.generated.messaging.request.payload.endpoint.capabilities_pb2 import CapabilitySpecification +from agrirouter.generated.messaging.request.payload.endpoint.subscription_pb2 import Subscription +from agrirouter.messaging.enums import CapabilityType + + +class SubscriptionItemBuilder: + + def __init__(self): + self._subscription_items = [] + + def build(self): + return self._subscription_items + + def clear(self): + self._subscription_items = [] + + def with_task_data(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.ISO_11783_TASKDATA_ZIP.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_device_description(self, ddis: List[int] = None, position: bool = None): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.ISO_11783_DEVICE_DESCRIPTION_PROTOBUF.value, + ddis=ddis, + position=position + ) + self._subscription_items.append(subscription_item) + return self + + def with_time_log(self, ddis: List[int] = None, position: bool = None): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.ISO_11783_TIMELOG_PROTOBUF.value, + ddis=ddis, + position=position + ) + self._subscription_items.append(subscription_item) + return self + + def with_bmp(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.IMG_BMP.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_jpg(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.IMG_JPEG.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_png(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.IMG_PNG.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_shape(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.SHP_SHAPE_ZIP.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_pdf(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.DOC_PDF.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_avi(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.VID_AVI.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_mp4(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.VID_MP4.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_wmv(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.VID_WMV.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_gps_info(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.GPS_INFO.value + ) + self._subscription_items.append(subscription_item) + return self + + +class CapabilityBuilder: + + def __init__(self): + self._capabilities = [] + + def build(self) -> list: + return self._capabilities + + def clear(self): + self._capabilities = [] + + def with_task_data(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.ISO_11783_TASKDATA_ZIP.value + self._capabilities.append(capability) + return self + + def with_device_description(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.ISO_11783_DEVICE_DESCRIPTION_PROTOBUF.value + self._capabilities.append(capability) + return self + + def with_time_log(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.ISO_11783_TIMELOG_PROTOBUF.value + self._capabilities.append(capability) + return self + + def with_bmp(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.IMG_BMP.value + self._capabilities.append(capability) + return self + + def with_jpg(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.IMG_JPEG.value + self._capabilities.append(capability) + return self + + def with_png(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.IMG_PNG.value + self._capabilities.append(capability) + return self + + def with_shape(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.SHP_SHAPE_ZIP.value + self._capabilities.append(capability) + return self + + def with_pdf(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.DOC_PDF.value + self._capabilities.append(capability) + return self + + def with_avi(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.VID_AVI.value + self._capabilities.append(capability) + return self + + def with_mp4(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.VID_MP4.value + self._capabilities.append(capability) + return self + + def with_wmv(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.VID_WMV.value + self._capabilities.append(capability) + return self + + def with_gps_info(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.GPS_INFO.value + self._capabilities.append(capability) + return self diff --git a/agrirouter/messaging/certification.py b/agrirouter/messaging/certification.py index 5579b870..c87afc83 100644 --- a/agrirouter/messaging/certification.py +++ b/agrirouter/messaging/certification.py @@ -1,14 +1,12 @@ -import json import os -import pathlib -from pathlib import Path import tempfile -from agrirouter.onboarding.response import BaseOnboardingResonse +from agrirouter.onboarding.response import SoftwareOnboardingResponse -def create_certificate_file(onboard_response: BaseOnboardingResonse): +def create_certificate_file_from_pen(onboard_response: SoftwareOnboardingResponse): + dir_ = tempfile.mkdtemp() prefix = onboard_response.get_sensor_alternate_id() data = onboard_response.get_authentication().get_certificate() diff --git a/agrirouter/messaging/clients/constants.py b/agrirouter/messaging/clients/constants.py new file mode 100644 index 00000000..78de8628 --- /dev/null +++ b/agrirouter/messaging/clients/constants.py @@ -0,0 +1,2 @@ +ASYNC = "ASYNC" +SYNC = "SYNC" diff --git a/agrirouter/messaging/clients/http.py b/agrirouter/messaging/clients/http.py index 08530c77..cf1e400f 100644 --- a/agrirouter/messaging/clients/http.py +++ b/agrirouter/messaging/clients/http.py @@ -1,22 +1,81 @@ +import http.client +import json +import os +import ssl +from urllib.parse import urlparse + +from agrirouter.messaging.certification import create_certificate_file_from_pen +from agrirouter.onboarding.response import SoftwareOnboardingResponse + + class HttpClient: - headers = {"Content-Type": "application/json"} + headers = { + "Content-Type": "application/json", + "Accept": "application/json" + } + + def make_connection(self, certificate_file_path: str, uri: str, onboard_response: SoftwareOnboardingResponse): + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.load_cert_chain( + certfile=certificate_file_path, + keyfile=certificate_file_path, + password=onboard_response.get_authentication().get_secret(), + ) + connection = http.client.HTTPSConnection( + host=self.get_host(uri), + port=self.get_port(uri), + context=context + ) + return connection + + def send_measure(self, onboard_response: SoftwareOnboardingResponse, request_body=None): + return self.send( + method="POST", + uri=onboard_response.get_connection_criteria().get_measures(), + onboard_response=onboard_response, + request_body=request_body + ) + + def send_command(self, onboard_response: SoftwareOnboardingResponse, request_body=None): + return self.send( + method="GET", + uri=onboard_response.get_connection_criteria().get_commands(), + onboard_response=onboard_response, + request_body=request_body + ) - def __init__(self, - on_message_callback: callable, - timeout=20 - ): - self.on_message_callback = on_message_callback - self.timeout = timeout + def send(self, method: str, uri: str, onboard_response: SoftwareOnboardingResponse, request_body=None): + certificate_file_path = create_certificate_file_from_pen(onboard_response) + try: + connection = self.make_connection(certificate_file_path, uri, onboard_response) + if request_body is not None: + connection.request( + method=method, + url=self.get_path(uri), + headers=self.headers, + body=json.dumps(request_body.json_serialize()) + ) + else: + connection.request( + method=method, + url=self.get_path(uri), + headers=self.headers, + ) + response = connection.getresponse() + finally: + os.remove(certificate_file_path) - def publish(self): - pass + return response - def subscribe(self): - pass + @staticmethod + def get_host(uri): + return urlparse(uri).netloc - def unsubscribe(self): - pass + @staticmethod + def get_port(uri): + return urlparse(uri).port if urlparse(uri).port else None - def _start_loop(self): - pass + @staticmethod + def get_path(uri): + return urlparse(uri).path diff --git a/agrirouter/messaging/clients/mqtt.py b/agrirouter/messaging/clients/mqtt.py index 4e8dd555..423897d0 100644 --- a/agrirouter/messaging/clients/mqtt.py +++ b/agrirouter/messaging/clients/mqtt.py @@ -1,16 +1,22 @@ +import time +import ssl from typing import Any, List, Tuple -from paho.mqtt import client as mqtt_client +import paho.mqtt.client as mqtt_client from paho.mqtt.client import MQTTv31, MQTTMessageInfo +from agrirouter.messaging.certification import create_certificate_file_from_pen +from agrirouter.messaging.clients.constants import SYNC, ASYNC + class MqttClient: def __init__(self, - client_id: str = "", + onboard_response, + client_id: str, on_message_callback: callable = None, userdata: Any = None, - clean_session: bool = True + clean_session: bool = False ): # TODO: Implement on_message_callback parameter validation: # must take params as described at https://pypi.org/project/paho-mqtt/#callbacks @@ -22,26 +28,54 @@ def __init__(self, protocol=MQTTv31, transport="tcp" ) + self.mqtt_client.on_message = on_message_callback if on_message_callback else self._get_on_message_callback() - self.mqtt_client.on_connect = self._get_on_connect_callback() + self.mqtt_client.on_connect = self._get_on_connect_callback(onboard_response) self.mqtt_client.on_disconnect = self._get_on_disconnect_callback() self.mqtt_client.on_subscribe = self._get_on_subscribe_callback() self.mqtt_client.on_unsubscribe = self._get_on_unsubscribe_callback() + certificate_file_path = create_certificate_file_from_pen(onboard_response) + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.load_cert_chain( + certfile=certificate_file_path, + keyfile=certificate_file_path, + password=onboard_response.get_authentication().get_secret(), + ) + self.mqtt_client.tls_set_context(context) + + self._mode = None + def connect(self, host: str, port: str) -> None: + self.mqtt_client.connect( + host=host, + port=int(port) + ) + self.mqtt_client.loop() + + self._mode = SYNC + + def connect_async(self, host: str, port: str): self.mqtt_client.connect_async( host=host, - port=port + port=int(port) ) self.mqtt_client.loop_start() + self._mode = ASYNC + + while self.mqtt_client._state == 0: + time.sleep(1) + def disconnect(self): self.mqtt_client.loop_stop() self.mqtt_client.disconnect() - def publish(self, topic, payload, qos=0) -> MQTTMessageInfo: - """ + def receive_outbox_messages(self): + self.mqtt_client.loop() + def publish(self, topic, payload, qos=2) -> MQTTMessageInfo: + """ :param topic: str representing unique name of the topic that the message should be published on :param payload: The actual message to send :param qos: int representing the quality of service level to use. May be [0, 1, 2] @@ -52,6 +86,10 @@ def publish(self, topic, payload, qos=0) -> MQTTMessageInfo: payload=payload, qos=qos ) + if self._mode == SYNC: + self.mqtt_client.loop() + time.sleep(3) + self.mqtt_client.loop() return message_info def subscribe(self, topics: List[Tuple[str, int]]) -> tuple: @@ -67,7 +105,7 @@ def subscribe(self, topics: List[Tuple[str, int]]) -> tuple: :return: tuple """ - result, mid = self.mqtt_client.subscribe(topics) + result, mid = self.mqtt_client.subscribe(topics, qos=2) return result, mid def unsubscribe(self, topics: List[str]) -> tuple: @@ -85,15 +123,12 @@ def unsubscribe(self, topics: List[str]) -> tuple: return result, mid @staticmethod - def _get_on_connect_callback() -> callable: + def _get_on_connect_callback(onboard_response) -> callable: - def on_connect(client, userdata, flags, rc, properties=None): + def on_connect(client: mqtt_client.Client, userdata, flags, rc, properties=None): if rc == 0: - print("Connected to MQTT Broker!") - else: - print(f"Failed to connect, return code: {rc}") - - return client, userdata, flags, rc, properties + client.subscribe(topic=onboard_response.connection_criteria.commands) + time.sleep(3) return on_connect @@ -101,8 +136,6 @@ def on_connect(client, userdata, flags, rc, properties=None): def _get_on_message_callback() -> callable: def on_message(client, userdata, msg): - # print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic") - return client, userdata, msg return on_message @@ -110,29 +143,23 @@ def on_message(client, userdata, msg): @staticmethod def _get_on_subscribe_callback() -> callable: - def on_subscribe(client, userdata, mid, granted_qos, properties=None): - # print(f"Subscribed {userdata} to `{properties}`") - - return client, userdata, mid, granted_qos, properties + def on_subscribe(*args, **kwargs): + return args, kwargs return on_subscribe @staticmethod def _get_on_disconnect_callback() -> callable: - def on_disconnect(client, userdata, rc): - # print(f"Disconnected from from `{properties}`") - - return client, userdata, rc + def on_disconnect(*args, **kwargs): + return args, kwargs return on_disconnect @staticmethod def _get_on_unsubscribe_callback() -> callable: - def on_unsubscribe(client, userdata, mid): - # print(f"Unsubscribed `{userdata}` from `{properties}`") - - return client, userdata, mid + def on_unsubscribe(*args, **kwargs): + return args, kwargs return on_unsubscribe diff --git a/agrirouter/messaging/decode.py b/agrirouter/messaging/decode.py index 839a0e48..0d1a4e12 100644 --- a/agrirouter/messaging/decode.py +++ b/agrirouter/messaging/decode.py @@ -1,5 +1,4 @@ import base64 -from ctypes import Union from google.protobuf.any_pb2 import Any from google.protobuf.internal.decoder import _DecodeVarint @@ -9,6 +8,7 @@ from agrirouter.generated.messaging.response.payload.feed.feed_response_pb2 import HeaderQueryResponse, \ MessageQueryResponse from agrirouter.generated.messaging.response.response_pb2 import ResponseEnvelope, ResponsePayloadWrapper +from agrirouter.messaging.exceptions import DecodeMessageException from agrirouter.messaging.messages import DecodedMessage from agrirouter.utils.type_url import TypeUrl @@ -31,8 +31,10 @@ def decode_response(message: bytes) -> DecodedMessage: input_stream = base64.b64decode(message) response_envelope_buffer, response_payload_buffer = read_properties_buffers_from_input_stream(input_stream) - envelope = ResponseEnvelope().MergeFromString(response_envelope_buffer) - payload = ResponsePayloadWrapper().MergeFromString(response_payload_buffer) + envelope = ResponseEnvelope() + envelope.ParseFromString(response_envelope_buffer) + payload = ResponsePayloadWrapper() + payload.ParseFromString(response_payload_buffer) message = DecodedMessage(envelope, payload) @@ -40,11 +42,21 @@ def decode_response(message: bytes) -> DecodedMessage: def decode_details(details: Any): - if details.type_url == TypeUrl.get_type_url(Messages.__name__): - return Messages().MergeFromString(details.value) - elif details.type_url == TypeUrl.get_type_url(ListEndpointsResponse.__name__): - return ListEndpointsResponse().MergeFromString(details.value) - elif details.type_url == TypeUrl.get_type_url(HeaderQueryResponse.__name__): - return HeaderQueryResponse().MergeFromString(details.value) - elif details.type_url == TypeUrl.get_type_url(MessageQueryResponse.__name__): - return MessageQueryResponse().MergeFromString(details.value) + if details.type_url == TypeUrl.get_type_url(Messages): + messages = Messages() + messages.MergeFromString(details.value) + return messages + elif details.type_url == TypeUrl.get_type_url(ListEndpointsResponse): + list_endpoints_response = ListEndpointsResponse() + list_endpoints_response.MergeFromString(details.value) + return list_endpoints_response + elif details.type_url == TypeUrl.get_type_url(HeaderQueryResponse): + header_query_response = HeaderQueryResponse() + header_query_response.MergeFromString(details.value) + return header_query_response + elif details.type_url == TypeUrl.get_type_url(MessageQueryResponse): + message_query_response = MessageQueryResponse() + message_query_response.MergeFromString(details.value) + return message_query_response + else: + raise DecodeMessageException(f"Could not handle type {details.type_url} while decoding details.") diff --git a/agrirouter/messaging/encode.py b/agrirouter/messaging/encode.py index fe74c229..f1f40513 100644 --- a/agrirouter/messaging/encode.py +++ b/agrirouter/messaging/encode.py @@ -18,23 +18,26 @@ def write_proto_parts_to_buffer(parts: list, buffer: bytes = b""): return buffer -def encode_message(header_parameters: MessageHeaderParameters, payload_parameters: MessagePayloadParameters) -> bytes: +def encode_message(header_parameters: MessageHeaderParameters, payload_parameters: MessagePayloadParameters) -> str: request_envelope = encode_header(header_parameters) request_payload = encode_payload(payload_parameters) raw_data = write_proto_parts_to_buffer([request_envelope, request_payload]) - return base64.b64encode(raw_data) + return base64.b64encode(raw_data).decode() def encode_header(header_parameters: MessageHeaderParameters) -> RequestEnvelope: request_envelope = RequestEnvelope() - request_envelope.application_id = header_parameters.get_application_message_id() \ + request_envelope.application_message_id = header_parameters.get_application_message_id() \ if header_parameters.get_application_message_id() else new_uuid() request_envelope.application_message_seq_no = header_parameters.get_application_message_seq_no() request_envelope.technical_message_type = header_parameters.get_technical_message_type() request_envelope.mode = header_parameters.get_mode() - request_envelope.timestamp = now_as_utc_timestamp() + if header_parameters.get_team_set_context_id() is not None: + request_envelope.team_set_context_id = header_parameters.get_team_set_context_id() + request_envelope.timestamp.FromDatetime(now_as_utc_timestamp()) + return request_envelope @@ -42,5 +45,5 @@ def encode_payload(payload_parameters: MessagePayloadParameters) -> RequestPaylo any_proto_wrapper = Any() any_proto_wrapper.type_url = payload_parameters.get_type_url() any_proto_wrapper.value = payload_parameters.get_value() - request_payload = RequestPayloadWrapper(any_proto_wrapper) + request_payload = RequestPayloadWrapper(details=any_proto_wrapper) return request_payload diff --git a/agrirouter/messaging/enums.py b/agrirouter/messaging/enums.py index 1247118f..53265ec5 100644 --- a/agrirouter/messaging/enums.py +++ b/agrirouter/messaging/enums.py @@ -13,3 +13,18 @@ class TechnicalMessageType(BaseEnum): FEED_MESSAGE_QUERY = "dke:feed_message_query" CLOUD_ONBOARD_ENDPOINTS = "dke:cloud_onboard_endpoints" CLOUD_OFFBOARD_ENDPOINTS = "dke:cloud_offboard_endpoints" + + +class CapabilityType(BaseEnum): + ISO_11783_TASKDATA_ZIP = "iso:11783:-10:taskdata:zip" + ISO_11783_DEVICE_DESCRIPTION_PROTOBUF = "iso:11783:-10:device_description:protobuf" + ISO_11783_TIMELOG_PROTOBUF = "iso:11783:-10:time_log:protobuf" + IMG_BMP = "img:bmp" + IMG_JPEG = "img:jpeg" + IMG_PNG = "img:png" + SHP_SHAPE_ZIP = "shp:shape:zip" + DOC_PDF = "doc:pdf" + VID_AVI = "vid:avi" + VID_MP4 = "vid:mp4" + VID_WMV = "vid:wmv" + GPS_INFO = "gps:info" diff --git a/agrirouter/messaging/exceptions.py b/agrirouter/messaging/exceptions.py index 3df29d4c..2bc8b9a7 100644 --- a/agrirouter/messaging/exceptions.py +++ b/agrirouter/messaging/exceptions.py @@ -7,3 +7,11 @@ class TypeUrlNotFoundError(AgriRouuterBaseException): class WrongFieldError(AgriRouuterBaseException): _message = "Unknown field" + + +class DecodeMessageException(AgriRouuterBaseException): + _message = "Can't decode message" + + +class OutboxException(AgriRouuterBaseException): + _message = "Can't fetch outbox message" diff --git a/agrirouter/messaging/messages.py b/agrirouter/messaging/messages.py index 1e5228fd..7db4e461 100644 --- a/agrirouter/messaging/messages.py +++ b/agrirouter/messaging/messages.py @@ -1,8 +1,8 @@ import json -from datetime import datetime, timezone -from typing import Union, List, Dict +from typing import Union, Dict from agrirouter.messaging.exceptions import WrongFieldError +from agrirouter.utils.utc_time_util import now_as_utc_str class EncodedMessage: @@ -36,12 +36,12 @@ class Message: def __init__(self, content): self.content = content - self.timestamp = datetime.utcnow() + self.timestamp = now_as_utc_str() def json_serialize(self) -> dict: return { self.MESSAGE: self.content, - self.TIMESTAMP: self.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + self.TIMESTAMP: self.timestamp } @@ -52,8 +52,8 @@ def __init__(self, message: str = None): self.message = message def json_deserialize(self, data: Union[Dict[str, str], str]): - messages = data if type(data) == list else json.loads(data) - for key, value in messages.keys(): + messages = data if type(data) == dict else json.loads(data) + for key, value in messages.items(): if key == self.MESSAGE: self.message = value else: @@ -81,15 +81,17 @@ def __init__(self, self.sensor_alternate_id = sensor_alternate_id self.command = command - def json_deserialize(self, data: Union[list, str]): - data = data if type(data) == list else json.loads(data) - for key, value in data.keys(): + def json_deserialize(self, data: Union[dict, str]): + data = data if type(data) == dict else json.loads(data) + for (key, value) in data.items(): if key == self.CAPABILITY_ALTERNATE_ID: self.capability_alternate_id = value elif key == self.SENSOR_ALTERNATE_ID: self.sensor_alternate_id = value elif key == self.COMMAND: - self.command = Command.json_deserialize(value) + command = Command() + command.json_deserialize(value) + self.command = command else: raise WrongFieldError(f"Unknown field `{key}` for {self.__class__}") @@ -110,6 +112,3 @@ def get_command(self) -> Command: def set_command(self, command: Command) -> None: self.command = command - - def json_deserialize(self): - pass diff --git a/agrirouter/messaging/parameters/dto.py b/agrirouter/messaging/parameters/dto.py index 3c1404de..17998bf0 100644 --- a/agrirouter/messaging/parameters/dto.py +++ b/agrirouter/messaging/parameters/dto.py @@ -8,8 +8,8 @@ class Parameters: def __init__(self, *, - application_message_seq_no: str, - application_message_id: int = None, + application_message_seq_no: int, + application_message_id: str = None, team_set_context_id: str ): self.application_message_seq_no = application_message_seq_no @@ -41,9 +41,9 @@ def validate(self): class MessageParameters(Parameters): def __init__(self, *, - application_message_seq_no: str, - application_message_id: int, - team_set_context_id: str, + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, onboarding_response: BaseOnboardingResonse ): super(MessageParameters, self).__init__( @@ -54,7 +54,7 @@ def __init__(self, self.onboarding_response = onboarding_response - def get_onboarding_response(self): + def get_onboarding_response(self) -> BaseOnboardingResonse: return self.onboarding_response @@ -63,7 +63,7 @@ class MessagingParameters(MessageParameters): def __init__(self, *, application_message_seq_no: str = None, - application_message_id: int = None, + application_message_id: str = None, team_set_context_id: str = None, onboarding_response: BaseOnboardingResonse, encoded_messages=None diff --git a/agrirouter/messaging/parameters/service.py b/agrirouter/messaging/parameters/service.py index 0769fa08..758df834 100644 --- a/agrirouter/messaging/parameters/service.py +++ b/agrirouter/messaging/parameters/service.py @@ -1,11 +1,12 @@ -from abc import ABC, abstractmethod from copy import deepcopy from typing import List from agrirouter.generated.commons.chunk_pb2 import ChunkComponent +from agrirouter.generated.messaging.request.payload.endpoint.capabilities_pb2 import CapabilitySpecification from agrirouter.generated.messaging.request.payload.endpoint.subscription_pb2 import Subscription from agrirouter.generated.messaging.request.payload.feed.feed_requests_pb2 import ValidityPeriod from agrirouter.messaging.parameters.dto import MessageParameters, Parameters +from agrirouter.onboarding.response import BaseOnboardingResonse class MessageHeaderParameters(Parameters): @@ -15,10 +16,10 @@ def __init__(self, technical_message_type: str = None, mode: str = None, team_set_context_id: str = None, - application_message_seq_no: str = None, + application_message_seq_no: int = None, recipients: list = None, chunk_component: ChunkComponent = None, - application_message_id: int = None, + application_message_id: str = None, ): super(MessageHeaderParameters, self).__init__( application_message_seq_no=application_message_seq_no, @@ -65,13 +66,20 @@ def get_value(self) -> str: class CloudOnboardParameters(MessageParameters): def __init__(self, - # List[EndpointRegistrationDetails] - must be defined in generated by by proto schemes, - # but they are not + *, onboarding_requests: list = None, - **kwargs + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse ): self.onboarding_requests = onboarding_requests if onboarding_requests else [] - super(CloudOnboardParameters, self).__init__(**kwargs) + super(CloudOnboardParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_onboarding_requests(self) -> list: return self.onboarding_requests @@ -89,11 +97,20 @@ def extend_onboarding_requests(self, onboarding_requests: list) -> None: class CloudOffboardParameters(MessageParameters): def __init__(self, + *, endpoints: List[str] = None, - **kwargs + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse ): self.endpoints = endpoints if endpoints else [] - super(CloudOffboardParameters, self).__init__(**kwargs) + super(CloudOffboardParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_endpoints(self) -> List[str]: return self.endpoints @@ -111,17 +128,26 @@ def extend_endpoints(self, endpoints: List[str]) -> None: class CapabilityParameters(MessageParameters): def __init__(self, - application_id, - certification_version_id, - enable_push_notification, - capability_parameters: list = None, - **kwargs + *, + application_id: str, + certification_version_id: str, + enable_push_notification: int = CapabilitySpecification.PushNotification.Value("DISABLED"), + capability_parameters: List[CapabilitySpecification.Capability] = None, + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse ): self.application_id = application_id self.certification_version_id = certification_version_id self.enable_push_notification = enable_push_notification self.capability_parameters = capability_parameters if capability_parameters else [] - super(CapabilityParameters, self).__init__(**kwargs) + super(CapabilityParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_application_id(self): return self.application_id @@ -155,9 +181,21 @@ def extend_capability_parameters(self, capability_parameters: list): class FeedConfirmParameters(MessageParameters): - def __init__(self, message_ids: list = None, **kwargs): + def __init__(self, + *, + message_ids: list = None, + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse + ): self.message_ids = message_ids if message_ids else [] - super(FeedConfirmParameters, self).__init__(**kwargs) + super(FeedConfirmParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_message_ids(self): return deepcopy(self.message_ids) @@ -174,14 +212,24 @@ def extend_message_ids(self, message_ids): class FeedDeleteParameters(MessageParameters): def __init__(self, + *, message_ids: list = None, receivers: list = None, validity_period: ValidityPeriod = None, - **kwargs): + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, + ): self.message_ids = message_ids if message_ids else [] self.receivers = receivers if receivers else [] self.validity_period = validity_period - super(FeedDeleteParameters, self).__init__(**kwargs) + super(FeedDeleteParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_message_ids(self): return deepcopy(self.message_ids) @@ -216,14 +264,24 @@ def set_validity_period(self, validity_period: ValidityPeriod): class ListEndpointsParameters(MessageParameters): def __init__(self, + *, technical_message_type: str = None, - direction: str = None, + direction: int = None, filtered: bool = False, - **kwargs): + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, + ): self.technical_message_type = technical_message_type self.direction = direction self.filtered = filtered - super(ListEndpointsParameters, self).__init__(**kwargs) + super(ListEndpointsParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_technical_message_type(self) -> str: return self.technical_message_type @@ -231,10 +289,10 @@ def get_technical_message_type(self) -> str: def set_technical_message_type(self, technical_message_type: str): self.technical_message_type = technical_message_type - def get_direction(self) -> str: + def get_direction(self) -> int: return self.direction - def set_direction(self, direction: str): + def set_direction(self, direction: int): self.direction = direction def is_filtered(self): @@ -246,14 +304,24 @@ def set_filtered(self, filtered: bool): class QueryMessageParameters(MessageParameters): def __init__(self, + *, senders: list = None, message_ids: list = None, validity_period: ValidityPeriod = None, - **kwargs): + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, + ): self.senders = senders self.message_ids = message_ids self.validity_period = validity_period - super(QueryMessageParameters, self).__init__(**kwargs) + super(QueryMessageParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_senders(self) -> list: return self.senders @@ -288,14 +356,24 @@ def set_validity_period(self, validity_period: list) -> None: class QueryHeaderParameters(MessageParameters): def __init__(self, + *, senders: list = None, message_ids: list = None, validity_period: ValidityPeriod = None, - **kwargs): + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, + ): self.senders = senders self.message_ids = message_ids self.validity_period = validity_period - super(QueryHeaderParameters, self).__init__(**kwargs) + super(QueryHeaderParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_senders(self) -> list: return self.senders @@ -330,10 +408,20 @@ def set_validity_period(self, validity_period: list) -> None: class SubscriptionParameters(MessageParameters): def __init__(self, + *, + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, subscription_items: List[Subscription.MessageTypeSubscriptionItem] = None, - **kwargs): + ): self.subscription_items = subscription_items if subscription_items else [] - super(SubscriptionParameters, self).__init__(**kwargs) + super(SubscriptionParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_subscription_items(self) -> List[Subscription.MessageTypeSubscriptionItem]: return self.subscription_items diff --git a/agrirouter/messaging/request.py b/agrirouter/messaging/request.py index 4de0a69e..14766318 100644 --- a/agrirouter/messaging/request.py +++ b/agrirouter/messaging/request.py @@ -1,7 +1,5 @@ from typing import List -from agrirouter.messaging.messages import Message - class MessageRequest: SENSOR_ALTERNATE_ID = "sensorAlternateId" @@ -11,7 +9,7 @@ class MessageRequest: def __init__(self, sensor_alternate_id: str, capability_alternate_id: str, - messages: List[Message] + messages: List[dict] ): self.sensor_alternate_id = sensor_alternate_id self.capability_alternate_id = capability_alternate_id diff --git a/agrirouter/messaging/result.py b/agrirouter/messaging/result.py index ba2df495..d08c7f4b 100644 --- a/agrirouter/messaging/result.py +++ b/agrirouter/messaging/result.py @@ -26,7 +26,13 @@ def __init__(self, def json_deserialize(self, data: Union[list, str]): messages = data if type(data) == list else json.loads(data) - self.set_messages([OutboxMessage.json_deserialize(message) for message in messages]) + outbox_message_list = [] + for message in messages: + outbox_message = OutboxMessage() + outbox_message.json_deserialize(message) + outbox_message_list.append(outbox_message) + + self.set_messages(outbox_message_list) def get_status_code(self) -> int: return self.status_code diff --git a/agrirouter/messaging/services/cloud.py b/agrirouter/messaging/services/cloud.py index cd40a465..217462e2 100644 --- a/agrirouter/messaging/services/cloud.py +++ b/agrirouter/messaging/services/cloud.py @@ -28,7 +28,7 @@ def encode(parameters: CloudOnboardParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(OnboardingRequest.__name__), + type_url=TypeUrl.get_type_url(OnboardingRequest), value=onboarding_request.SerializeToString() ) @@ -58,7 +58,7 @@ def encode(parameters: CloudOffboardParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(OffboardingRequest.__name__), + type_url=TypeUrl.get_type_url(OffboardingRequest), value=offboarding_request.SerializeToString() ) diff --git a/agrirouter/messaging/services/commons.py b/agrirouter/messaging/services/commons.py index 3e368632..8f4f6db5 100644 --- a/agrirouter/messaging/services/commons.py +++ b/agrirouter/messaging/services/commons.py @@ -1,13 +1,13 @@ -import os +import json from abc import ABC, abstractmethod -import requests - -from agrirouter.messaging.certification import create_certificate_file +from agrirouter.messaging.clients.http import HttpClient from agrirouter.messaging.clients.mqtt import MqttClient from agrirouter.messaging.messages import Message from agrirouter.messaging.request import MessageRequest from agrirouter.messaging.result import MessagingResult +from agrirouter.onboarding.exceptions import BadMessagingResult +from agrirouter.onboarding.response import SoftwareOnboardingResponse class AbstractMessagingClient(ABC): @@ -17,10 +17,10 @@ def create_message_request(parameters) -> MessageRequest: messages = [] for encoded_message in parameters.get_encoded_messages(): message = Message(encoded_message) - messages.append(message) + messages.append(message.json_serialize()) message_request = MessageRequest( - parameters.get_sensor_alternate_id(), - parameters.get_capability_alternate_id(), + parameters.get_onboarding_response().get_sensor_alternate_id(), + parameters.get_onboarding_response().get_capability_alternate_id(), messages ) return message_request @@ -32,59 +32,50 @@ def send(self, parameters): class HttpMessagingService(AbstractMessagingClient): + def __init__(self): + self.client = HttpClient() + def send(self, parameters) -> MessagingResult: request = self.create_message_request(parameters) - cert_file_path = create_certificate_file(parameters.get_onboarding_response()) - try: - response = requests.post( - url=parameters.get_onboarding_response().get_connection_criteria().get_measures(), - headers={"Content-type": "application/json"}, - data=request.json_serialize(), - cert=( - cert_file_path, - parameters.get_onboarding_response().get_authentication().get_secret() - ), - ) - finally: - os.remove(cert_file_path) - result = MessagingResult([parameters.get_message_id()]) + response = self.client.send_measure(parameters.get_onboarding_response(), request) + if response.status != 200: + raise BadMessagingResult(f"Messaging Request failed with status code {response.status}") + result = MessagingResult([parameters.get_application_message_id()]) return result - def subscribe(self): - pass - - def unsubscribe(self): - pass - class MqttMessagingService(AbstractMessagingClient): def __init__(self, - onboarding_response, + onboarding_response: SoftwareOnboardingResponse, on_message_callback: callable = None, + client_async: bool = True ): self.onboarding_response = onboarding_response self.client = MqttClient( - client_id=self.onboarding_response.get_client_id(), + onboard_response=onboarding_response, + client_id=onboarding_response.get_connection_criteria().get_client_id(), on_message_callback=on_message_callback, ) - self.client.connect( - self.onboarding_response.get_connection_criteria().get_host(), - self.onboarding_response.get_connection_criteria().get_port() - ) + if client_async: + self.client.connect_async( + self.onboarding_response.get_connection_criteria().get_host(), + self.onboarding_response.get_connection_criteria().get_port() + ) + else: + self.client.connect( + self.onboarding_response.get_connection_criteria().get_host(), + self.onboarding_response.get_connection_criteria().get_port() + ) def send(self, parameters, qos: int = 0) -> MessagingResult: - mqtt_payload = self.create_message_request(parameters) + message_request = self.create_message_request(parameters) + mqtt_payload = message_request.json_serialize() self.client.publish( - self.onboarding_response.get_connection_criteria().get_measures(), mqtt_payload, + topic=self.onboarding_response.get_connection_criteria().get_measures(), + payload=json.dumps(mqtt_payload), qos=qos ) - result = MessagingResult([parameters.get_message_id()]) + result = MessagingResult([parameters.get_application_message_id()]) return result - - def subscribe(self): - pass - - def unsubscribe(self): - pass diff --git a/agrirouter/messaging/services/http/outbox.py b/agrirouter/messaging/services/http/outbox.py index b81bfcbf..7985f914 100644 --- a/agrirouter/messaging/services/http/outbox.py +++ b/agrirouter/messaging/services/http/outbox.py @@ -1,30 +1,21 @@ -import os - -import requests - +from agrirouter.messaging.clients.http import HttpClient +from agrirouter.messaging.exceptions import OutboxException from agrirouter.messaging.result import OutboxResponse -from agrirouter.messaging.certification import create_certificate_file - class OutboxService: + def __init__(self): + self.client = HttpClient() + def fetch(self, onboarding_response) -> OutboxResponse: - cert_file_path = create_certificate_file(onboarding_response) - try: - response = requests.get( - url=onboarding_response.get_connection_criteria().get_commands(), - headers={"Content-type": "application/json"}, - cert=( - cert_file_path, - onboarding_response.get_authentication().get_secret() - ), - ) - finally: - os.remove(cert_file_path) + response = self.client.send_command(onboarding_response, None) - outbox_response = OutboxResponse(status_code=response.status_code) - outbox_response.json_deserialize(response.json()["contents"]) + if response.status == 200: + outbox_response = OutboxResponse(status_code=response.status) + response_body = response.read() + outbox_response.json_deserialize(response_body) + else: + raise OutboxException(f"Could not fetch messages from outbox. Status code was {response.status}") return outbox_response - diff --git a/agrirouter/messaging/services/messaging.py b/agrirouter/messaging/services/messaging.py index e071b028..81f60d6f 100644 --- a/agrirouter/messaging/services/messaging.py +++ b/agrirouter/messaging/services/messaging.py @@ -6,7 +6,7 @@ from agrirouter.messaging.encode import encode_message from agrirouter.messaging.enums import TechnicalMessageType from agrirouter.messaging.messages import EncodedMessage -from agrirouter.messaging.parameters.dto import MessageParameters, MessagingParameters +from agrirouter.messaging.parameters.dto import MessagingParameters from agrirouter.messaging.parameters.service import MessageHeaderParameters, MessagePayloadParameters, \ CapabilityParameters, FeedConfirmParameters, FeedDeleteParameters, ListEndpointsParameters, \ SubscriptionParameters, QueryHeaderParameters, QueryMessageParameters @@ -52,10 +52,10 @@ def encode(parameters: CapabilityParameters) -> EncodedMessage: enable_push_notifications=parameters.get_enable_push_notification() ) if parameters.get_capability_parameters(): - capability_specification.capabilities = parameters.get_capability_parameters() + capability_specification.capabilities.extend(parameters.get_capability_parameters()) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(CapabilitySpecification.__name__), + type_url=TypeUrl.get_type_url(CapabilitySpecification), value=capability_specification.SerializeToString() ) @@ -85,7 +85,7 @@ def encode(parameters: FeedConfirmParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(MessageConfirm.__name__), + type_url=TypeUrl.get_type_url(MessageConfirm), value=message_confirm.SerializeToString() ) @@ -115,7 +115,7 @@ def encode(parameters: FeedDeleteParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(MessageConfirm.__name__), + type_url=TypeUrl.get_type_url(MessageConfirm), value=message_confirm.SerializeToString() ) @@ -147,7 +147,7 @@ def encode(parameters: ListEndpointsParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(ListEndpointsQuery.__name__), + type_url=TypeUrl.get_type_url(ListEndpointsQuery), value=list_endpoints_query.SerializeToString() ) @@ -179,7 +179,7 @@ def encode(parameters: QueryMessageParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(MessageQuery.__name__), + type_url=TypeUrl.get_type_url(MessageQuery), value=message_query.SerializeToString() ) @@ -211,7 +211,7 @@ def encode(parameters: QueryHeaderParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(MessageQuery.__name__), + type_url=TypeUrl.get_type_url(MessageQuery), value=message_query.SerializeToString() ) @@ -241,7 +241,7 @@ def encode(parameters: SubscriptionParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(Subscription.__name__), + type_url=TypeUrl.get_type_url(Subscription), value=subscription.SerializeToString() ) diff --git a/agrirouter/onboarding/dto.py b/agrirouter/onboarding/dto.py new file mode 100644 index 00000000..d2b34bed --- /dev/null +++ b/agrirouter/onboarding/dto.py @@ -0,0 +1,196 @@ +import json +from typing import Union + +from agrirouter.messaging.exceptions import WrongFieldError + + +class ConnectionCriteria: + CLIENT_ID = 'clientId' + COMMANDS = 'commands' + GATEWAY_ID = 'gatewayId' + HOST = 'host' + MEASURES = 'measures' + PORT = 'port' + + def __init__(self, + *, + gateway_id: str = None, + measures: str = None, + commands: str = None, + host: str = None, + port: str = None, + client_id: str = None + ): + self.gateway_id = gateway_id + self.measures = measures + self.commands = commands + self.host = host + self.port = port + self.client_id = client_id + + def json_serialize(self) -> dict: + return { + self.GATEWAY_ID: self.gateway_id, + self.MEASURES: self.measures, + self.COMMANDS: self.commands, + self.HOST: self.host, + self.PORT: self.port, + self.CLIENT_ID: self.client_id + } + + def json_deserialize(self, data: Union[str, dict]) -> None: + data = data if type(data) == dict else json.loads(data) + for key, value in data.items(): + if key == self.GATEWAY_ID: + self.gateway_id = value + elif key == self.MEASURES: + self.measures = value + elif key == self.COMMANDS: + self.commands = value + elif key == self.HOST: + self.host = value + elif key == self.PORT: + self.port = value + elif key == self.CLIENT_ID: + self.client_id = value + else: + raise WrongFieldError(f"Unknown field {key} for Connection Criteria class") + + def get_gateway_id(self) -> str: + return self.gateway_id + + def set_gateway_id(self, gateway_id: str) -> None: + self.gateway_id = gateway_id + + def get_measures(self) -> str: + return self.measures + + def set_measures(self, measures: str) -> None: + self.measures = measures + + def get_commands(self) -> str: + return self.commands + + def set_commands(self, commands: str) -> None: + self.commands = commands + + def get_host(self) -> str: + return self.host + + def set_host(self, host: str) -> None: + self.host = host + + def get_port(self) -> str: + return self.port + + def set_port(self, port: str) -> None: + self.port = port + + def get_client_id(self) -> str: + return self.client_id + + def set_client_id(self, client_id: str) -> None: + self.client_id = client_id + + def __str__(self): + return str(self.json_serialize()) + + def __repr__(self): + return str(self.json_serialize()) + + +class Authentication: + TYPE = 'type' + SECRET = 'secret' + CERTIFICATE = 'certificate' + + def __init__(self, + *, + type: str = None, + secret: str = None, + certificate: str = None, + ): + self.type = type + self.secret = secret + self.certificate = certificate + + def json_serialize(self) -> dict: + return { + self.TYPE: self.type, + self.SECRET: self.secret, + self.CERTIFICATE: self.certificate, + } + + def json_deserialize(self, data: Union[str, dict]) -> None: + data = data if type(data) == dict else json.loads(data) + for key, value in data.items(): + if key == self.TYPE: + self.type = value + elif key == self.SECRET: + self.secret = value + elif key == self.CERTIFICATE: + self.certificate = value + else: + raise WrongFieldError(f"Unknown field {key} for Authentication class") + + def get_type(self) -> str: + return self.type + + def set_type(self, type: str) -> None: + self.type = type + + def get_secret(self) -> str: + return self.secret + + def set_secret(self, secret: str) -> None: + self.secret = secret + + def get_certificate(self) -> str: + return self.certificate + + def set_certificate(self, certificate: str) -> None: + self.certificate = certificate + + def __str__(self): + return str(self.json_serialize()) + + def __repr__(self): + return str(self.json_serialize()) + + +class ErrorResponse: + def __init__(self, + *, + code, + message, + target, + details + ): + self.code = code + self.message = message + self.target = target + self.details = details + + def get_code(self) -> str: + return self.code + + def set_code(self, code: str) -> None: + self.code = code + + def get_message(self) -> str: + return self.message + + def set_message(self, message: str) -> None: + self.message = message + + def get_target(self) -> str: + return self.target + + def set_target(self, target: str) -> None: + self.target = target + + def get_details(self) -> str: + return self.details + + def set_details(self, details: str) -> None: + self.details = details diff --git a/agrirouter/onboarding/exceptions.py b/agrirouter/onboarding/exceptions.py index 490e51e3..fb3b024a 100644 --- a/agrirouter/onboarding/exceptions.py +++ b/agrirouter/onboarding/exceptions.py @@ -21,3 +21,7 @@ class RequestNotSigned(AgriRouuterBaseException): Details on: https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/ integration/onboarding.html#signing-requests """ + + +class BadMessagingResult(AgriRouuterBaseException): + _message = "Messaging Request failed" diff --git a/agrirouter/onboarding/headers.py b/agrirouter/onboarding/headers.py index a1e0d61c..7843e2ad 100644 --- a/agrirouter/onboarding/headers.py +++ b/agrirouter/onboarding/headers.py @@ -1,28 +1,7 @@ -import base64 -from abc import ABC, abstractmethod - from agrirouter.constants.media_types import ContentTypes -class BaseOnboardingHeader(ABC): - @abstractmethod - def __init__(self, *args, **kwargs): - self._set_params(*args, **kwargs) - - @abstractmethod - def get_header(self) -> dict: - ... - - @abstractmethod - def _set_params(self, *args, **kwargs): - ... - - @abstractmethod - def sign(self, *args, **kwargs): - ... - - -class SoftwareOnboardingHeader(BaseOnboardingHeader): +class SoftwareOnboardingHeader: def __init__(self, reg_code, application_id, @@ -35,10 +14,8 @@ def __init__(self, def get_header(self) -> dict: return self.params - def sign(self, signature: bytes): - print(signature) - self.params["X-Agrirouter-Signature"] = base64.b64encode(signature).decode() - print(self.params["X-Agrirouter-Signature"]) + def sign(self, signature: str): + self.params["X-Agrirouter-Signature"] = signature def _set_params(self, reg_code: str, application_id: str, signature: str, content_type: str): header = dict() @@ -48,7 +25,3 @@ def _set_params(self, reg_code: str, application_id: str, signature: str, conten header["X-Agrirouter-Signature"] = signature if signature else "" self.params = header - - -class CUOnboardingHeader(BaseOnboardingHeader): - pass diff --git a/agrirouter/onboarding/onboarding.py b/agrirouter/onboarding/onboarding.py index 155d08dc..3119e181 100644 --- a/agrirouter/onboarding/onboarding.py +++ b/agrirouter/onboarding/onboarding.py @@ -1,15 +1,12 @@ -import json - import requests from agrirouter.environments.environmental_services import EnvironmentalService from agrirouter.onboarding.exceptions import RequestNotSigned -from agrirouter.onboarding.headers import SoftwareOnboardingHeader, CUOnboardingHeader -from agrirouter.onboarding.parameters import SoftwareOnboardingParameter, BaseOnboardingParameter, CUOnboardingParameter -from agrirouter.onboarding.request import SoftwareOnboardingRequest, BaseOnboardingRequest, CUOnboardingRequest -from agrirouter.onboarding.request_body import SoftwareOnboardingBody, CUOnboardingBody -from agrirouter.onboarding.response import SoftwareVerifyOnboardingResponse, SoftwareOnboardingResponse, \ - CUOnboardingResponse +from agrirouter.onboarding.headers import SoftwareOnboardingHeader +from agrirouter.onboarding.parameters import SoftwareOnboardingParameter +from agrirouter.onboarding.request import SoftwareOnboardingRequest +from agrirouter.onboarding.request_body import SoftwareOnboardingBody +from agrirouter.onboarding.response import SoftwareVerifyOnboardingResponse, SoftwareOnboardingResponse class SoftwareOnboarding(EnvironmentalService): @@ -19,7 +16,7 @@ def __init__(self, *args, **kwargs): self._private_key = kwargs.pop("private_key") super(SoftwareOnboarding, self).__init__(*args, **kwargs) - def _create_request(self, params: BaseOnboardingParameter, url: str) -> SoftwareOnboardingRequest: + def _create_request(self, params: SoftwareOnboardingParameter, url: str) -> SoftwareOnboardingRequest: body_params = params.get_body_params() request_body = SoftwareOnboardingBody(**body_params) @@ -28,59 +25,25 @@ def _create_request(self, params: BaseOnboardingParameter, url: str) -> Software return SoftwareOnboardingRequest(header=request_header, body=request_body, url=url) - def _perform_request(self, params: BaseOnboardingParameter, url: str) -> requests.Response: + def _perform_request(self, params: SoftwareOnboardingParameter, url: str) -> requests.Response: request = self._create_request(params, url) - request.sign(self._private_key) + request.sign(self._private_key, self._public_key) if request.is_signed: return requests.post( url=request.get_url(), - data=json.dumps(request.get_data()), + data=request.get_body_content(), headers=request.get_header() ) raise RequestNotSigned - def verify(self, params: SoftwareOnboardingParameter) -> SoftwareOnboardingResponse: + def verify(self, params: SoftwareOnboardingParameter) -> SoftwareVerifyOnboardingResponse: url = self._environment.get_verify_onboard_request_url() http_response = self._perform_request(params=params, url=url) - return SoftwareOnboardingResponse(http_response) + return SoftwareVerifyOnboardingResponse(http_response) def onboard(self, params: SoftwareOnboardingParameter) -> SoftwareOnboardingResponse: url = self._environment.get_secured_onboard_url() http_response = self._perform_request(params=params, url=url) return SoftwareOnboardingResponse(http_response) - - -class CUOnboarding(EnvironmentalService): - - def __init__(self, *args, **kwargs): - self._public_key = kwargs.pop("public_key") - self._private_key = kwargs.pop("private_key") - super(CUOnboarding, self).__init__(*args, **kwargs) - - def _create_request(self, params: CUOnboardingParameter, url: str) -> CUOnboardingRequest: - body_params = params.get_body_params() - request_body = CUOnboardingBody(**body_params) - - header_params = params.get_header_params() - request_header = CUOnboardingHeader(**header_params) - - return CUOnboardingRequest(header=request_header, body=request_body, url=url) - - def _perform_request(self, params: CUOnboardingParameter, url: str) -> requests.Response: - request = self._create_request(params, url) - request.sign(self._private_key) - if request.is_signed: - return requests.post( - url=request.get_url(), - data=request.get_data(), - headers=request.get_header() - ) - raise RequestNotSigned - - def onboard(self, params: CUOnboardingParameter) -> CUOnboardingResponse: - url = self._environment.get_onboard_url() - http_response = self._perform_request(params=params, url=url) - - return CUOnboardingResponse(http_response) diff --git a/agrirouter/onboarding/parameters.py b/agrirouter/onboarding/parameters.py index 1ca75312..1895c9fe 100644 --- a/agrirouter/onboarding/parameters.py +++ b/agrirouter/onboarding/parameters.py @@ -1,25 +1,9 @@ -from abc import ABC, abstractmethod -from datetime import datetime - from agrirouter.constants.media_types import ContentTypes from agrirouter.onboarding.enums import CertificateTypes +from agrirouter.utils.utc_time_util import now_as_utc_str -class BaseOnboardingParameter(ABC): - @abstractmethod - def __init__(self, *args, **kwargs): - ... - - @abstractmethod - def get_header_params(self, *args, **kwargs): - ... - - @abstractmethod - def get_body_params(self, *args, **kwargs): - ... - - -class SoftwareOnboardingParameter(BaseOnboardingParameter): +class SoftwareOnboardingParameter: def __init__(self, *, id_, @@ -39,8 +23,7 @@ def __init__(self, self.certification_version_id = certification_version_id self.gateway_id = str(gateway_id) self.certificate_type = certificate_type - self.utc_timestamp = str(utc_timestamp) if utc_timestamp \ - else datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") + self.utc_timestamp = str(utc_timestamp) if utc_timestamp else now_as_utc_str() self.time_zone = str(time_zone) self.reg_code = reg_code @@ -61,38 +44,3 @@ def get_body_params(self): "utc_timestamp": self.utc_timestamp, "time_zone": self.time_zone, } - - -class CUOnboardingParameter(BaseOnboardingParameter): - def __init__(self, - id_, - application_id, - certification_version_id, - gateway_id, - reg_code, - content_type=ContentTypes.APPLICATION_JSON.value, - certificate_type=CertificateTypes.P12.value, - ): - - self.id_ = id_ - self.application_id = application_id - self.content_type = content_type - self.certification_version_id = certification_version_id - self.gateway_id = gateway_id - self.certificate_type = certificate_type - self.reg_code = reg_code - - def get_header_params(self): - return { - "content_type": self.content_type, - "reg_code": self.reg_code, - } - - def get_body_params(self): - return { - "id_": self.id_, - "application_id": self.application_id, - "certification_version_id": self.certification_version_id, - "gateway_id": self.gateway_id, - "certificate_type": self.certificate_type, - } diff --git a/agrirouter/onboarding/request.py b/agrirouter/onboarding/request.py index 1e7113b0..0728f149 100644 --- a/agrirouter/onboarding/request.py +++ b/agrirouter/onboarding/request.py @@ -1,10 +1,10 @@ -from agrirouter.onboarding.headers import SoftwareOnboardingHeader, BaseOnboardingHeader -from agrirouter.onboarding.request_body import SoftwareOnboardingBody, BaseOnboardingBody -from agrirouter.onboarding.signature import create_signature +from agrirouter.onboarding.headers import SoftwareOnboardingHeader +from agrirouter.onboarding.request_body import SoftwareOnboardingBody +from agrirouter.onboarding.signature import create_signature, verify_signature -class BaseOnboardingRequest: - def __init__(self, header: BaseOnboardingHeader, body: BaseOnboardingBody, url: str): +class SoftwareOnboardingRequest: + def __init__(self, header: SoftwareOnboardingHeader, body: SoftwareOnboardingBody, url: str): self.header = header self.body = body self.url = url @@ -18,8 +18,13 @@ def get_data(self): def get_header(self): return self.header.get_header() - def sign(self, private_key): - signature = create_signature(self.body.json(), private_key) + def get_body_content(self): + return self.body.json().replace("\n", "") + + def sign(self, private_key, public_key): + body = self.get_body_content() + signature = create_signature(body, private_key) + verify_signature(body, bytes.fromhex(signature), public_key) self.header.sign(signature) @property @@ -28,17 +33,3 @@ def is_signed(self): if header_has_signature: return True return False - - -class SoftwareOnboardingRequest(BaseOnboardingRequest): - """ - Request must be used to onboard Farming Software or Telemetry Platform - """ - pass - - -class CUOnboardingRequest(BaseOnboardingRequest): - """ - Request must be used to onboard CUs - """ - pass diff --git a/agrirouter/onboarding/request_body.py b/agrirouter/onboarding/request_body.py index 951ab01d..a730cf6e 100644 --- a/agrirouter/onboarding/request_body.py +++ b/agrirouter/onboarding/request_body.py @@ -1,30 +1,11 @@ import json -from abc import ABC, abstractmethod from datetime import datetime from agrirouter.onboarding.enums import CertificateTypes, GateWays from agrirouter.onboarding.exceptions import WrongCertificationType, WrongGateWay -class BaseOnboardingBody(ABC): - @abstractmethod - def __init__(self, *args, **kwargs): - ... - - @abstractmethod - def get_parameters(self, *args, **kwargs) -> dict: - ... - - @abstractmethod - def _set_params(self, *args, **kwargs): - ... - - @abstractmethod - def json(self, *args, **kwargs): - ... - - -class SoftwareOnboardingBody(BaseOnboardingBody): +class SoftwareOnboardingBody: def __init__(self, *, id_, @@ -75,7 +56,7 @@ def _set_params(self, } def json(self) -> str: - return json.dumps(self.get_parameters()) + return json.dumps(self.get_parameters(), separators=(',', ':')) @staticmethod def _validate_certificate_type(certificate_type: str) -> None: @@ -86,21 +67,3 @@ def _validate_certificate_type(certificate_type: str) -> None: def _validate_gateway_id(gateway_id: str) -> None: if gateway_id not in GateWays.values_list(): raise WrongGateWay - - -class CUOnboardingBody(BaseOnboardingBody): - - def __init__(self, *args, **kwargs): - ... - - def get_parameters(self, *args, **kwargs) -> dict: - ... - - def _set_params(self, *args, **kwargs): - ... - - def json(self, new_lines: bool = True) -> str: - result = json.dumps(self.get_parameters(), indent="") - if not new_lines: - return result.replace("\n", "") - return result diff --git a/agrirouter/onboarding/response.py b/agrirouter/onboarding/response.py index cab6bca0..44a4f5b5 100644 --- a/agrirouter/onboarding/response.py +++ b/agrirouter/onboarding/response.py @@ -1,64 +1,165 @@ +import json +from typing import Union + from requests import Response +from agrirouter.messaging.exceptions import WrongFieldError +from agrirouter.onboarding.dto import ErrorResponse, ConnectionCriteria, Authentication + class BaseOnboardingResonse: def __init__(self, http_response: Response): - self.response: Response = http_response - - def get_connection_criteria(self) -> dict: - response_data = self.data() - return response_data.get("connectionCriteria") - def get_sensor_alternate_id(self): - response_data = self.data() - return response_data.get("sensorAlternateId") - - def get_authentication(self): - response_data = self.data() - return response_data.get("authentication") - - @property - def data(self): - return self.response.json() + self._status_code = http_response.status_code + self._text = http_response.text @property def status_code(self): - return self.response.status_code + return self._status_code @property def text(self): - return self.response.text + return self._text class SoftwareVerifyOnboardingResponse(BaseOnboardingResonse): """ Response from verify request used for Farming Software or Telemetry Platform before onboarding """ - pass + def __init__(self, http_response: Response = None): + if http_response: + super(SoftwareVerifyOnboardingResponse, self).__init__(http_response) + response_body = http_response.json() + else: + self._text = None + self._status_code = None + response_body = {} -class SoftwareOnboardingResponse(BaseOnboardingResonse): - """ - Response from onboarding request used for Farming Software or Telemetry Platform - """ + self.account_id = response_body.get("accountId", None) - def get_connection_criteria(self) -> dict: - response_data = self.data() - return response_data.get("connectionCriteria") + self.error = ErrorResponse( + code=response_body.get("error").get("code"), + message=response_body.get("error").get("message"), + target=response_body.get("error").get("target"), + details=response_body.get("error").get("details"), + ) if response_body.get("error", None) else None - def get_sensor_alternate_id(self): - response_data = self.data() - return response_data.get("sensorAlternateId") + def get_account_id(self) -> str: + return self.account_id - def get_authentication(self): - response_data = self.data() - return response_data.get("authentication") + def set_account_id(self, account_id: str): + self.account_id = account_id -class CUOnboardingResponse(BaseOnboardingResonse): +class SoftwareOnboardingResponse(BaseOnboardingResonse): """ - Response from onboarding request used for CUs + Response from onboarding request used for Farming Software or Telemetry Platform """ - pass + DEVICE_ALTERNATE_ID = "deviceAlternateId" + CAPABILITY_ALTERNATE_ID = "capabilityAlternateId" + SENSOR_ALTERNATE_ID = "sensorAlternateId" + CONNECTION_CRITERIA = "connectionCriteria" + AUTHENTICATION = "authentication" + + def __init__(self, http_response: Response = None): + if http_response: + super(SoftwareOnboardingResponse, self).__init__(http_response) + response_body = http_response.json() + else: + self._text = None + self._status_code = None + response_body = {} + + self.connection_criteria = ConnectionCriteria( + gateway_id=response_body.get("connectionCriteria").get("gatewayId"), + measures=response_body.get("connectionCriteria").get("measures"), + commands=response_body.get("connectionCriteria").get("commands"), + host=response_body.get("connectionCriteria").get("host"), + port=response_body.get("connectionCriteria").get("port"), + client_id=response_body.get("connectionCriteria").get("clientId") + ) if response_body.get("connectionCriteria", None) else None + + self.authentication = Authentication( + type=response_body.get("authentication").get("type"), + secret=response_body.get("authentication").get("secret"), + certificate=response_body.get("authentication").get("certificate") + ) if response_body.get("authentication", None) else None + + self.capability_alternate_id = response_body.get("capabilityAlternateId", None) + self.device_alternate_id = response_body.get("deviceAlternateId", None) + self.sensor_alternate_id = response_body.get("sensorAlternateId", None) + + self.error = ErrorResponse( + code=response_body.get("error").get("code"), + message=response_body.get("error").get("message"), + target=response_body.get("error").get("target"), + details=response_body.get("error").get("details"), + ) if response_body.get("error", None) else None + + def get_connection_criteria(self) -> ConnectionCriteria: + return self.connection_criteria + + def set_connection_criteria(self, connection_criteria: ConnectionCriteria): + self.connection_criteria = connection_criteria + + def get_authentication(self) -> Authentication: + return self.authentication + + def set_authentication(self, authentication: Authentication): + self.authentication = authentication + + def get_sensor_alternate_id(self) -> str: + return self.sensor_alternate_id + + def set_sensor_alternate_id(self, sensor_alternate_id: str): + self.sensor_alternate_id = sensor_alternate_id + + def get_device_alternate_id(self) -> str: + return self.device_alternate_id + + def set_device_alternate_id(self, device_alternate_id: str): + self.device_alternate_id = device_alternate_id + + def get_capability_alternate_id(self) -> str: + return self.capability_alternate_id + + def set_capability_alternate_id(self, capability_alternate_id: str): + self.capability_alternate_id = capability_alternate_id + + def json_serialize(self): + return { + self.DEVICE_ALTERNATE_ID: self.device_alternate_id, + self.CAPABILITY_ALTERNATE_ID: self.capability_alternate_id, + self.SENSOR_ALTERNATE_ID: self.sensor_alternate_id, + self.CONNECTION_CRITERIA: self.connection_criteria.json_serialize(), + self.AUTHENTICATION: self.authentication.json_serialize() + } + + def json_deserialize(self, data: Union[dict, str]): + data_dict = data if type(data) == dict else json.loads(data) + for (key, value) in data_dict.items(): + if key == self.DEVICE_ALTERNATE_ID: + self.device_alternate_id = value + elif key == self.CAPABILITY_ALTERNATE_ID: + self.capability_alternate_id = value + elif key == self.SENSOR_ALTERNATE_ID: + self.sensor_alternate_id = value + elif key == self.CONNECTION_CRITERIA: + connection_criteria = ConnectionCriteria() + connection_criteria.json_deserialize(value) + self.connection_criteria = connection_criteria + elif key == self.AUTHENTICATION: + authentication = Authentication() + authentication.json_deserialize(value) + self.authentication = authentication + else: + raise WrongFieldError(f"Unknown field `{key}` for {self.__class__}") + + def __str__(self): + return str(self.json_serialize()) + + def __repr__(self): + return str(self.json_serialize()) diff --git a/agrirouter/onboarding/signature.py b/agrirouter/onboarding/signature.py index 6bedc3bd..64174adc 100644 --- a/agrirouter/onboarding/signature.py +++ b/agrirouter/onboarding/signature.py @@ -2,20 +2,24 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding -from pprint import pprint SIGNATURE_ALGORITHM = "SHA256withRSA" -def create_signature(request_body: str, private_key: str) -> bytes: +def to_hex(sign: bytes): + return sign.hex() + + +def create_signature(request_body: str, private_key: str) -> str: private_key_bytes = bytearray(private_key.encode('utf-8')) private_key_data = load_pem_private_key(private_key_bytes, None) signature = private_key_data.sign( - request_body.encode('utf-8'), + request_body.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256() ) - return signature + + return to_hex(signature) def verify_signature(request_body: str, signature: bytes, public_key: str) -> None: diff --git a/agrirouter/revoking/headers.py b/agrirouter/revoking/headers.py index 0c1770f2..50652d5a 100644 --- a/agrirouter/revoking/headers.py +++ b/agrirouter/revoking/headers.py @@ -23,4 +23,4 @@ def _set_params(self, application_id: str, signature: str, content_type: str): if signature: header["X-Agrirouter-Signature"] = signature - self.params = header \ No newline at end of file + self.params = header diff --git a/agrirouter/revoking/parameters.py b/agrirouter/revoking/parameters.py index 95a73332..cf706b02 100644 --- a/agrirouter/revoking/parameters.py +++ b/agrirouter/revoking/parameters.py @@ -8,7 +8,7 @@ def __init__(self, account_id, endpoint_ids, utc_timestamp, - timestamp, + time_zone, content_type=ContentTypes.APPLICATION_JSON.value ): @@ -17,7 +17,7 @@ def __init__(self, self.account_id = account_id self.endpoint_ids = endpoint_ids self.utc_timestamp = utc_timestamp - self.timestamp = timestamp + self.time_zone = time_zone def get_header_params(self): return { @@ -30,5 +30,5 @@ def get_body_params(self): "account_id": self.account_id, "endpoint_ids": self.endpoint_ids, "utc_timestamp": self.utc_timestamp, - "timestamp": self.timestamp, + "time_zone": self.time_zone, } diff --git a/agrirouter/revoking/request.py b/agrirouter/revoking/request.py index d45b7787..1fea09e1 100644 --- a/agrirouter/revoking/request.py +++ b/agrirouter/revoking/request.py @@ -18,8 +18,12 @@ def get_data(self): def get_header(self): return self.header.get_header() + def get_body_content(self): + return self.body.json().replace("\n", "") + def sign(self, private_key): - signature = create_signature(self.body.json(new_lines=False), private_key) + body = self.get_body_content() + signature = create_signature(body, private_key) self.header.sign(signature) @property @@ -28,10 +32,3 @@ def is_signed(self) -> bool: if header_has_signature: return True return False - - @property - def is_valid(self) -> bool: - if not self.is_signed: - return False - signature = self.get_header().get("X-Agrirouter-Signature") - # return validate_signature(signature) diff --git a/agrirouter/revoking/request_body.py b/agrirouter/revoking/request_body.py index 6ec70b2d..7bdea6e5 100644 --- a/agrirouter/revoking/request_body.py +++ b/agrirouter/revoking/request_body.py @@ -26,14 +26,11 @@ def _set_params(self, ) -> None: self.params = { - "account_id": account_id, - "endpoint_ids": endpoint_ids, + "accountId": account_id, + "endpointIds": endpoint_ids, "UTCTimestamp": utc_timestamp, "timeZone": time_zone, } - def json(self, new_lines: bool = True) -> str: - result = json.dumps(self.get_parameters(), indent="") - if not new_lines: - return result.replace("\n", "") - return result + def json(self) -> str: + return json.dumps(self.get_parameters(), separators=(',', ':')) diff --git a/agrirouter/revoking/revoking.py b/agrirouter/revoking/revoking.py index aaa8f871..b835bc8f 100644 --- a/agrirouter/revoking/revoking.py +++ b/agrirouter/revoking/revoking.py @@ -29,9 +29,9 @@ def _perform_request(self, params: RevokingParameter, url: str) -> requests.Resp request = self._create_request(params, url) request.sign(self._private_key) if request.is_signed: - return requests.post( + return requests.delete( url=request.get_url(), - data=request.get_data(), + json=request.get_data(), headers=request.get_header() ) raise RequestNotSigned @@ -40,4 +40,4 @@ def revoke(self, params: RevokingParameter) -> RevokingResponse: url = self._environment.get_revoke_url() http_response = self._perform_request(params=params, url=url) - return RevokingResponse(http_response) \ No newline at end of file + return RevokingResponse(http_response) diff --git a/agrirouter/utils/type_url.py b/agrirouter/utils/type_url.py index 71f4c6df..2457b9da 100644 --- a/agrirouter/utils/type_url.py +++ b/agrirouter/utils/type_url.py @@ -17,29 +17,29 @@ class TypeUrl: @classmethod def get_type_url(cls, class_): - if class_.__name__ == Messages.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == ListEndpointsResponse.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == HeaderQueryResponse.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == MessageQueryResponse.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == MessageDelete.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == MessageConfirm.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == OnboardingResponse.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == OnboardingRequest.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == CapabilitySpecification.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == Subscription.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == MessageQuery.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == ListEndpointsQuery.__name__: - return cls.prefix + class_.__name__ + if class_ == Messages: + return cls.prefix + Messages.DESCRIPTOR.full_name + elif class_ == ListEndpointsResponse: + return cls.prefix + ListEndpointsResponse.DESCRIPTOR.full_name + elif class_ == HeaderQueryResponse: + return cls.prefix + HeaderQueryResponse.DESCRIPTOR.full_name + elif class_ == MessageQueryResponse: + return cls.prefix + MessageQueryResponse.DESCRIPTOR.full_name + elif class_ == MessageDelete: + return cls.prefix + MessageDelete.DESCRIPTOR.full_name + elif class_ == MessageConfirm: + return cls.prefix + MessageConfirm.DESCRIPTOR.full_name + elif class_ == OnboardingResponse: + return cls.prefix + OnboardingResponse.DESCRIPTOR.full_name + elif class_ == OnboardingRequest: + return cls.prefix + OnboardingRequest.DESCRIPTOR.full_name + elif class_ == CapabilitySpecification: + return cls.prefix + CapabilitySpecification.DESCRIPTOR.full_name + elif class_ == Subscription: + return cls.prefix + Subscription.DESCRIPTOR.full_name + elif class_ == MessageQuery: + return cls.prefix + MessageQuery.DESCRIPTOR.full_name + elif class_ == ListEndpointsQuery: + return cls.prefix + ListEndpointsQuery.DESCRIPTOR.full_name else: raise TypeUrlNotFoundError(f"The {class_} type url not found") diff --git a/agrirouter/utils/utc_time_util.py b/agrirouter/utils/utc_time_util.py index b86f3afb..b87ca808 100644 --- a/agrirouter/utils/utc_time_util.py +++ b/agrirouter/utils/utc_time_util.py @@ -2,5 +2,9 @@ def now_as_utc_timestamp(): + return datetime.utcnow() + + +def now_as_utc_str(): timestamp = datetime.utcnow() return timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") diff --git a/agrirouter/utils/uuid_util.py b/agrirouter/utils/uuid_util.py index 0c9bd872..8e500ead 100644 --- a/agrirouter/utils/uuid_util.py +++ b/agrirouter/utils/uuid_util.py @@ -2,4 +2,4 @@ def new_uuid(): - return uuid.uuid4() + return str(uuid.uuid4()) diff --git a/example_script.py b/example_script.py new file mode 100644 index 00000000..50e4ae3f --- /dev/null +++ b/example_script.py @@ -0,0 +1,310 @@ +from pprint import pprint + +from google.protobuf.timestamp_pb2 import Timestamp + +from agrirouter.generated.messaging.request.payload.account.endpoints_pb2 import ListEndpointsQuery +from agrirouter.generated.messaging.request.payload.feed.feed_requests_pb2 import ValidityPeriod +from agrirouter.onboarding.response import SoftwareOnboardingResponse +import time + +public_key = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzGt41/+kSOTlO1sJvLIN +6RAFaOn6GiCNX/Ju0oVT4VMDHfuQMI5t9+ZgBxFmUhtY5/eykQfYJVGac/cy5xyk +F/1xpMzltK7pfo7XZpfHjkHLPUOeaHW0zE+g2vopQOARKE5LSguCBUhdtfFuiheR +IP0EU+MtEQDhlfiqYLAJkAvZHluCH9q6hawn0t/G873jlzsrXBqIgKboXqyz1lRE +SvMyqX04Xwaq1CgAZjHXBVWvbuOriCR0P2n13/nkCgBgLd/ORwVilb4GQDXkkCSg +uOVcRU3s/KG/OVJTonHVlLvDzBA5GLrpZMpzC4EfzXBM98s4Vj6IOAIQeY84Sppj +qwIDAQAB +-----END PUBLIC KEY-----""" + +private_key = """-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDMa3jX/6RI5OU7 +Wwm8sg3pEAVo6foaII1f8m7ShVPhUwMd+5Awjm335mAHEWZSG1jn97KRB9glUZpz +9zLnHKQX/XGkzOW0rul+jtdml8eOQcs9Q55odbTMT6Da+ilA4BEoTktKC4IFSF21 +8W6KF5Eg/QRT4y0RAOGV+KpgsAmQC9keW4If2rqFrCfS38bzveOXOytcGoiApuhe +rLPWVERK8zKpfThfBqrUKABmMdcFVa9u46uIJHQ/afXf+eQKAGAt385HBWKVvgZA +NeSQJKC45VxFTez8ob85UlOicdWUu8PMEDkYuulkynMLgR/NcEz3yzhWPog4AhB5 +jzhKmmOrAgMBAAECggEAEEr6mUCzb+nqiWYSqxsH980CmV+Yww9YJU8V3SqqSlnK +9E9SKUSY6DrQ6Y9N9/pdBjQcY+nbpPHRnS+VO41xWMYnEisQneuZCbDJ40/ypFiD +IfFrRUkobWZlXD63Hggd5fgDkTXEmbYwXemN1WzWcOopt6PyOho3YLQupEEzqerb +XkzBFWwWO9589fbWnlaSoJPtgA8gFxeJJkU3kG10Epj6wV17yo6DuyVZpemGPTUL +uVl7yNx9O/Lp8UXRlBtSEEBQqoJaGy9mzVZyobXNKvdlZxwlkbJQpZB/m4dzqbyn +Wv+lSJdmbOnOzc67FfRqHf/irIdg6aInJd6WxZ3rPQKBgQDlxrcePlzpNwJOWtXb +sneHU50Lx73u183q5dtKlH/FudhOgP4aot6+q1KDu3b9rRakGJUKQYoLgGNwhl/7 +5CF0iKQE+5JZ5R9YpwFoDuALjPfic5vFN76G851ccz5pfThLjCMV1NgKJskaefP0 +OdV+UW9qOIxR8UAMntWTTrQzFwKBgQDjv+2Kz1/KsXSPaw+mJKsmUnC2YbqeAr+9 +Dwm7Hr0RZWkkS2EjqcMxvq0D8bYcuJvrlZFmB/r6Ly0MKlfsUT+64LAQnKHhlCUi +vlE7VuDOR16lC4ZCPeWtjrL45fpj+Lhe54m7rCT8F+Ocdxv2yNQrSBbQ6epOVuDz +XJaSRt/AjQKBgQCrBZPIS+yFnO73eP6SLixvKhnK6dmBi1h1zK3CvfK4LZJFJBd9 +pdoampOo/wAa4hjm/HD6GDvyQZZB65JHfs4z2XwTRVfx1urU5kDSvbeegUcDYr7/ +NHV4JpzqcdBzXcNn359BoZFHRQUL0tdz4RP5mA1QR1SRrPnaKuKWaM8Q8wKBgQC5 +mY9br+PAqxzyQ61dGETh1g1ElCAg5NyclcS4WTR7GMm2ajefeJk50MnujOx8O3XV +Zu422AoQGKH9aAR+8Teec70HzJ2f17rrtW09jm9lq4PVvK6NDSQ/bCst6z1Ce07F +CKuV5ZO+XTmAKREA7Gj7XKQ7XGU1sldf+/Q5AMkXgQKBgQC4lXL9zLV/vfWUTPSR +qlGcS2+WYtjWPapDZa+7zlxGdPgOTri4nJO69Bs9ReLlzsYKSBihfpWPxcl9sS65 +KFrlBkR/vzKYjCFXB6cmMP61mUrgGQRoYJQBetAyEiXZL3zjt1R/Dndk0kHkVmHr +HjmgzBRxXFy5uph6Ue6dxyszaA== +-----END PRIVATE KEY-----""" + + +onboarding_response_mqtt_data = { + "deviceAlternateId": "2145df0e-3451-46cb-bf23-23191af66fce", + "capabilityAlternateId": "523e4623-68d2-43d4-a0cc-e2ada2f68b5e", + "sensorAlternateId": "1489638c-7bed-4205-ad77-8d11efdc779f", + "connectionCriteria": { + "gatewayId": "2", + "host": "dke-qa.eu10.cp.iot.sap", + "port": 8883, + "clientId": "2145df0e-3451-46cb-bf23-23191af66fce", + "measures": "measures/2145df0e-3451-46cb-bf23-23191af66fce", + "commands": "commands/2145df0e-3451-46cb-bf23-23191af66fce" + }, + "authentication": { + "type": "PEM", + "secret": "JNKdNg8R0lwmFgvrUfOCc7inebr0h?!7Z9wL", + "certificate": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIE6zAdBgoqhkiG9w0BDAEDMA8ECMkL85F+LbPbAgMCAAAEggTI1CmRlnDUStBv\nTycvaRVFMCk1OuynhiOYRF6HBFFXBCxWKZa3WqTShLdf9iCel/NgtdZIiQsoD1LL\nMxVyh8pWAfLQ+pDJLvM6suQjHALt8dW5iTeCZ7R1gzFvPJ+xnDGFFytN7HmGSvHM\nQbcCOuEeIu8U6ENa6/+WmUwK9/ZMkLNqDHVKEGpI+lSJs8JWEE+S3Klmsxuq0dvz\nh6o3V7RKFwMfUZOQLHezGBDjLfEBdP+d2G87CY+LSzinL8pFhLwyrXFKfYWYoT0m\n5PkDdjfiVq3SJIUoQWnGrjaVVw4TV3WSxmhQnWbDwOQydr8DAiBxDMYoeK3rePpC\nwh6KATnBrovq1icqjonYDE0T+3Rs2SUbG+3+m9Zj4j46L2Sh9bUB6qxdw74Ck2/z\nAzJ1N+tB+RL7UvOpMOhmndMBl5qpx9dFFy8Z/N7w4YTQLZLN7chD8ApeFhCgvppt\nAGh8/VeWO54OC9ZOSHpxEl7sJz97jaHYNbw/lGbDk7cOZezwpA0NCWZ/Bb1vRDzy\n8EDX9s1hOA3jiy2T1RSyk2Rj/12pWdKtdSO8lMhMKC0B32Zr1F8rBJKDVzqFWuTt\nn+pXOKedyOA/ggyvYJdsltP8O4XB2oBN3WBdFK7Y1FG/tN30LsaqcnFTxab5v1Pp\ngq2dHu6Xy0TCMAw/DH3RmGXlGnDDWu86Zad7TjjrEZvpSIv4TTSCqqTvc4IN0xFX\nbKZCrY6JSkJWWnDMKrsRYOijUDvpAbYwZuTV9PAljYbt5YX778qxV9O0fNBQdaww\nNlfxU93jgr4g3E9nIzRxLu9S98hPbxKUnVYiQmYvP7vJUcUSo5F0LmUU/nvHY1pi\nr4tZDp8Xu1aZy7cOd3sTbf/68IjiZMZlF5/PVlOFOo40yGqW600j/qEqXoY/492h\nONXUCpHKaG/Pkjtg9THuYoaw1773gxYYsYLt+c6NkQCCsydOr2BMZQ4Qy4bZV67D\n2RNDeZzSBY6jEX6dnfY0FJqIsSiw28Ek5NXx0HTEGN8txPkx/1dfu3RfZnzUqT/0\nmS9xcWVYRmlip3vm48fMecqP/DNIHyjVLC39SsFdeXa+De76z/S3+or0t7HGlUim\nNVkIcWqm/sD2ia8hYberaRRTbUQ1iObNToIg8dA/xna6D61sYK8jkf1GVPpKsCTA\nOVW5u9XrE1f5YQEovE9kFgvtzs0u6jSeI9edqVadH1u6hX4QWQSTrcTb3raqAKpK\nl67cQ96eXI1WQPSdPhQPTjqzOPZDbot3qMkGFijHar7FdQjDx/cNhqhvxv0LWsvl\njgep1czUFoo1BS3wTUiO0qyloNGOQdgmlTOHbMFk1wgoNyAohfZtfn6LH/zlJnE3\nQ0YkUKgAG+1N/PmkQFO0k5qAflUV7h+HAzT1ZAZcscjHNbQFDc0Zjq9nE9sfhxE8\nOFpnF9Jp3fQVekyyC/dsCxtJdYfhxqYe+BzZu0SlsLCmc1JoK5lkiXQwv6+cFpKW\nwfHMTTrCoOetJyiF7oJX+t4adzmLmnujiw5izxObWQJ7avHC1oYNHfRejrOtlu34\n0nDPRFiSDyEbDCBXPe9dIafqjJVLQGFOeXC8/VN9cGSZp2JV8rqumWOr9E+Wd5zU\n8MRZpevo0i3rPgdyFRpw\n-----END ENCRYPTED PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIIEaDCCA1CgAwIBAgIPANHZYxYlOc+wEAEDDWDpMA0GCSqGSIb3DQEBCwUAMFYx\nCzAJBgNVBAYTAkRFMSMwIQYDVQQKExpTQVAgSW9UIFRydXN0IENvbW11bml0eSBJ\nSTEiMCAGA1UEAxMZU0FQIEludGVybmV0IG9mIFRoaW5ncyBDQTAeFw0yMTExMTIw\nNzMyMjNaFw0yMjExMTIwNzMyMjNaMIG1MQswCQYDVQQGEwJERTEcMBoGA1UEChMT\nU0FQIFRydXN0IENvbW11bml0eTEVMBMGA1UECxMMSW9UIFNlcnZpY2VzMXEwbwYD\nVQQDFGhkZXZpY2VBbHRlcm5hdGVJZDoyMTQ1ZGYwZS0zNDUxLTQ2Y2ItYmYyMy0y\nMzE5MWFmNjZmY2V8Z2F0ZXdheUlkOjJ8dGVuYW50SWQ6MTExNjkwMzQ5MHxpbnN0\nYW5jZUlkOmRrZS1xYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJeF\naxjV7Xk1R2dFjadN6WsUkrmcVu44vZRCJEbR7Chkg1xcXT6cgIlokO/V4lTgaD6i\neCMKMFegjXzEJQy0dyIWncozcmt6HJFxpdjVQtdCtDtCWykGscNDgvv5ukykOOKI\nMzWJ4d2cJRlostpNe4FYZoPp6cArSHTl9DvfYqjZ/ykeTa1w157dgVxPxezHrJMl\n+z2XgO37mq6CJLw8J6W8RBHbCADgB8c6qGHgJnBURyxnoHHi/yqdIKC6cOs8NAnc\nyVmnvLDu8RUWu9pWkqFHhMvSqdkUCTYORZ9mUTm/Kmv6ss2NaYT4uUBZTskwnAa9\nFLdj+DV2NG0OQl3NYr8CAwEAAaOB0jCBzzBIBgNVHR8EQTA/MD2gO6A5hjdodHRw\nczovL3Rjcy5teXNhcC5jb20vY3JsL1RydXN0Q29tbXVuaXR5SUkvU0FQSW9UQ0Eu\nY3JsMAwGA1UdEwEB/wQCMAAwJQYDVR0SBB4wHIYaaHR0cDovL3NlcnZpY2Uuc2Fw\nLmNvbS9UQ1MwDgYDVR0PAQH/BAQDAgbAMB0GA1UdDgQWBBSRf8DUjowgQ+6amVIs\njd7zM7VWqjAfBgNVHSMEGDAWgBSVt7P1WN7VtLNYRuDypsl4Tr0tdTANBgkqhkiG\n9w0BAQsFAAOCAQEARzSc9GLpSU3pRJPIfgadHrZ+2KQsPsQ1/fLlASlt4V1Rlxn7\n/tn0gk3sP0X5/TrkO+N0kx1qrLarxWSDiVfaXoPa6Lit30SBPnPLUPPPZeTJOz5r\nTW9PkPPuC39GlM1biVoil2cLZrTr9DMSUoBvR4IVKQoJveQsLwn7Ea+SDPE0uvZV\nbDN6UPGZ2yIiCXO1MODJ6r3A4EDD2MArGgfhGdbvJNAY36ShFJhzfzi0t8linEAA\nxh0vcaEEIkVeEiwiguyGWB69X88cjZ0Q5cCf0r6iu3oQnB57uM5TW12OwXQN1NpQ\neK3EMFSoM6BYJu/3B8TXhNmpNBvD7KYozw9XaA==\n-----END CERTIFICATE-----\n" + } +} + + + + +import agrirouter as ar +from agrirouter.onboarding.enums import GateWays +from agrirouter.messaging.enums import CapabilityType +from agrirouter.generated.messaging.request.payload.endpoint.subscription_pb2 import Subscription +from agrirouter.messaging.services.commons import HttpMessagingService, MqttMessagingService +from agrirouter import ListEndpointsParameters, ListEndpointsService, SubscriptionService, SubscriptionParameters, \ + QueryHeaderService, QueryHeaderParameters +from agrirouter.utils.uuid_util import new_uuid + + +application_id = "8c947a45-c57d-42d2-affc-206e21d63a50" # # store here your application id. You can find it in AR UI + + +def example_auth(): + print("Authorization...\n") + + auth_params = ar.AuthUrlParameter(application_id=application_id, response_type="onboard") + auth_client = ar.Authorization("QA", public_key=public_key, private_key=private_key) + auth_url = auth_client.get_auth_request_url( + auth_params) # use this url to authorize the user as described at https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/integration/authorization.html#perform-authorization + print(f"auth_url={auth_url}") + + auth_result_url = input( + "Enter auth_url (the url the user was redirected to after his authorization, see above): ") # the url the user was redirected to after his authorization. + auth_response = auth_client.extract_auth_response( + auth_result_url) # auth_response contains the results of the auth process + auth_client.verify_auth_response(auth_response) # you may verify auth_response to ensure answer was from AR + + print( + f"auth_response is successful: {auth_response.is_successful}") # True if user accepted application, False if he rejected + + print( + f"auth_response is valid: {auth_response.is_valid}") # Result of verification, if False, response was not validated by public key. Doesn't indicate the auth was successfull. Accessible only after response verifying + + # Get dict containing data from auth process you will use for futher communication. + # If auth was rejected, contains {"error"} key. + # If auth was accepted, contains {signature, state, token, credentials{account, expires, regcode}} keys + # Even if response verifying was not processed or failed, the results will be returned. But in that case you act on your risk. + auth_data = auth_response.get_auth_result() + print(f"auth_data: {auth_data}") + + return auth_data + + +def example_onboarding(gateway_id): + + auth_data = example_auth() + + print("Onboarding...\n") + + id_ = "urn:myapp:snr00003234" # just unique + certification_version_id = "edd5d6b7-45bb-4471-898e-ff9c2a7bf56f" # get from AR UI + time_zone = "+03:00" + + onboarding_client = ar.SoftwareOnboarding("QA", public_key=public_key, private_key=private_key) + onboarding_parameters = ar.SoftwareOnboardingParameter(id_=id_, application_id=application_id, + certification_version_id=certification_version_id, + gateway_id=gateway_id, time_zone=time_zone, + reg_code=auth_data.get_decoded_token().regcode) + onboarding_verifying_response = onboarding_client.verify(onboarding_parameters) + print(f"onboarding_verifying_response.status_code: {onboarding_verifying_response.status_code}") + print(f"onboarding_verifying_response.text: {onboarding_verifying_response.text}") + onboarding_response = onboarding_client.onboard(onboarding_parameters) + print(f"onboarding_response.status_code: {onboarding_response.status_code}") + print(f"onboarding_response.text: {onboarding_response.text}") + + return onboarding_response + + +def example_list_endpoints_mqtt(onboarding_response_data, foo): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = MqttMessagingService( + onboarding_response=onboarding_response, + on_message_callback=foo + + ) + list_endpoint_parameters = ListEndpointsParameters( + technical_message_type=CapabilityType.ISO_11783_TASKDATA_ZIP.value, + direction=ListEndpointsQuery.Direction.Value("SEND_RECEIVE"), + filtered=False, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + list_endpoint_service = ListEndpointsService(messaging_service) + + messaging_result = list_endpoint_service.send(list_endpoint_parameters) + print("Sent message: ", messaging_result) + + # Is needed for waiting of messaging responses from outbox + while True: + time.sleep(1) + + +def example_list_endpoints_http(onboarding_response_data): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = HttpMessagingService() + list_endpoint_parameters = ListEndpointsParameters( + technical_message_type=CapabilityType.ISO_11783_TASKDATA_ZIP.value, + direction=2, + filtered=False, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + list_endpoint_service = ListEndpointsService(messaging_service) + + messaging_result = list_endpoint_service.send(list_endpoint_parameters) + print("Sent message: ", messaging_result) + + return messaging_result + + +def example_subscription_http(onboarding_response_data): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = HttpMessagingService() + subscription_service = SubscriptionService(messaging_service) + tmt = CapabilityType.ISO_11783_TASKDATA_ZIP.value + subscription_item = Subscription.MessageTypeSubscriptionItem(technical_message_type=tmt) + subscription_parameters = SubscriptionParameters( + subscription_items=[subscription_item], + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + + messaging_result = subscription_service.send(subscription_parameters) + print("Sent message: ", messaging_result) + + return messaging_result + + +def example_subscription_mqtt(onboarding_response_data, on_msg_callback): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = MqttMessagingService(onboarding_response, on_message_callback=on_msg_callback) + subscription_service = SubscriptionService(messaging_service) + tmt = CapabilityType.ISO_11783_TASKDATA_ZIP.value + subscription_item = Subscription.MessageTypeSubscriptionItem(technical_message_type=tmt) + subscription_parameters = SubscriptionParameters( + subscription_items=[subscription_item], + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + + messaging_result = subscription_service.send(subscription_parameters) + print("Sent message: ", messaging_result) + + # Is needed for waiting of messaging responses from outbox + while True: + time.sleep(1) + + +def example_query_header_message_http(onboarding_response_data): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = HttpMessagingService() + query_header_service = QueryHeaderService(messaging_service) + sent_from = Timestamp() + sent_to = Timestamp() + validity_period = ValidityPeriod(sent_from=sent_from, sent_to=sent_to) + query_header_parameters = QueryHeaderParameters( + message_ids=[new_uuid(), new_uuid()], + senders=[new_uuid(), new_uuid()], + validity_period=validity_period, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + messaging_result = query_header_service.send(query_header_parameters) + print("Sent message: ", messaging_result) + + return messaging_result + + +def example_query_header_message_mqtt(onboarding_response_data, on_msg_callback): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = MqttMessagingService(onboarding_response, on_message_callback=on_msg_callback) + query_header_service = QueryHeaderService(messaging_service) + sent_from = Timestamp() + sent_to = Timestamp() + validity_period = ValidityPeriod(sent_from=sent_from, sent_to=sent_to) + query_header_parameters = QueryHeaderParameters( + message_ids=[new_uuid(), new_uuid()], + senders=[new_uuid(), new_uuid()], + validity_period=validity_period, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + messaging_result = query_header_service.send(query_header_parameters) + print("Sent message: ", messaging_result) + + # Is needed for waiting of messaging responses from outbox + while True: + time.sleep(1) + + +def on_message_callback(client, userdata, msg): + + # Define here the way receiving messages will be processed + + from agrirouter.messaging.decode import decode_response + from agrirouter.messaging.decode import decode_details + from agrirouter.messaging.messages import OutboxMessage + + outbox_message = OutboxMessage() + outbox_message.json_deserialize(msg.payload.decode().replace("'", '"')) + + print(outbox_message.command.message) + + decoded_message = decode_response(outbox_message.command.message) + print(decoded_message.response_envelope) + + try: + decoded_details = decode_details(decoded_message.response_payload.details) + print(decoded_details) + except Exception as exc: + print("Error in decoding details: ", exc) + + +if __name__ == "__main__": + onboarding_response_mqtt = example_onboarding(GateWays.MQTT.value) + example_list_endpoints_mqtt(onboarding_response_mqtt.json_serialize(), on_message_callback) + + # or for http + # onboarding_response_mqtt = example_onboarding(GateWays.REST.value) + # example_list_endpoints_http(onboarding_response_mqtt.json_serialize()) diff --git a/examples.txt b/examples.txt index f0fa895a..3ca0f065 100644 --- a/examples.txt +++ b/examples.txt @@ -1,3 +1,44 @@ + +public_key = "-----BEGIN PUBLIC KEY-----\n" \ + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFlVFgYRIkGL6Ay/av2e\n" \ + "S2yIag7XHRWFgVFewPegFyWjUQPSe5t2unrNOUu6Ucp7/ck/Ivm4c/6g39fDDzmq\n" \ + "i4JU8OfMpVbUxpiJSGa/OSiXnDuWkJyjdac/C8ip0EpOCFjAWdE+pnGhDny1XAwp\n" \ + "i4t0/WtO8U+IOYtjxpyyOp3daX97C7ihM1I6eOecVN6Caz9B38EnPg12UGA5NkZO\n" \ + "pnz4BHMwYUZqgxaeOPlh4MquAnF5fdjOV3TkmFWkbP1un3BJkU6owcadbjN5DQCG\n" \ + "jguFzX8VVfJEgn2VtIFbbhqsRivvNDmWst1XNZ0GRpviFFQRymz1WroV0lB9P9vK\n" \ + "mwIDAQAB\n" \ + "-----END PUBLIC KEY-----" + +private_key = "-----BEGIN PRIVATE KEY-----\n" \ + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8WVUWBhEiQYvo\n" \ + "DL9q/Z5LbIhqDtcdFYWBUV7A96AXJaNRA9J7m3a6es05S7pRynv9yT8i+bhz/qDf\n" \ + "18MPOaqLglTw58ylVtTGmIlIZr85KJecO5aQnKN1pz8LyKnQSk4IWMBZ0T6mcaEO\n" \ + "fLVcDCmLi3T9a07xT4g5i2PGnLI6nd1pf3sLuKEzUjp455xU3oJrP0HfwSc+DXZQ\n" \ + "YDk2Rk6mfPgEczBhRmqDFp44+WHgyq4CcXl92M5XdOSYVaRs/W6fcEmRTqjBxp1u\n" \ + "M3kNAIaOC4XNfxVV8kSCfZW0gVtuGqxGK+80OZay3Vc1nQZGm+IUVBHKbPVauhXS\n" \ + "UH0/28qbAgMBAAECggEANVX8vMBaEL/L/RnDCOqp7UTeOl5adx91j2G5+d4FhRiA\n" \ + "73usGpmzHOqSe/OgXvH+e6cGDIL3w00rREgGsiSL0XbGU/PoJTf6CAUA9zI1W1vN\n" \ + "1w2evPPGbBZAybb4s4WfJEjxq12QJrUNvRr+hoLhLuV+axb8o2P4uQbqab9Mz0ER\n" \ + "lczCbHi4VDs1fwmNR3o47T1J4Qffzv1nMlor3pSrDzRDebic7/DC5JFkYZNGUtHk\n" \ + "jKDF5Uv7Vzxgb4Of+i3JA5mRMqvG33pdenvvetwl9X69WOiC29bVlymSHyybBE4A\n" \ + "ItfCAHIiY3nUL7UqzoIXpsyPs3ftkiy3Hn7isVSpLQKBgQDjadkGlqIgXCKZ8RS6\n" \ + "a4iLTTTlh8Ur+vMrejBLPul1oxz2dRWZy8zykfNN2MPz7q2xT8wXGuxgj+jei/fi\n" \ + "Gk08+UudMhV5Dtshb3fFq0NFCBe1ZUEX/wAcKC4Ed9xuuHpe7HOKAG0AsnzS8MPC\n" \ + "lcMiL1/vz0GuRbsiyMY6hXweZQKBgQDUBmQNqOBWDTQkO/8MFHopo6Ju9iNvZ4fC\n" \ + "u4SWqL+5BO3nnQHAQyslsj8FNilqhgMI+zaFFbZMZPv5opBSaAR0CQanKxMe3c9I\n" \ + "XYkAJH2+M0fpp80LtxwShD411UDhIypzumfKe8vUXRW/8TWfl6VidfEVjxw6Rc2D\n" \ + "g9btI4k0/wKBgQC42plnGZq/4yTdLXJD9pUPZrrQuQQ1M8/mT3RiNclfri8kxxe/\n" \ + "5EG8C5dSeBkQd7sInmyve1sZQuFvxSbBy89s+NfV95gsxz6odwtMymHsAyACe0Pm\n" \ + "VYmpWZ/OUgAEoEAYWOuyCZaRMoT0knEOAt6TMx8wt7AUEOqE497+QvMZYQKBgQC6\n" \ + "ARlJenvEQjUaDKBFYrmBShK4MasIktThG0zINyZrFE35wR3GI6b4nRT4Z3mSABst\n" \ + "h+Vef5u8DWOYrurZwHMXsMtrYDiX/ZNZMuV7gIfnkmlmLFWQD4XLIMTKyVjvqcAW\n" \ + "YtOnKU+58CeiieO3LHxkkn97oF7tKEuRMtock+5M1QKBgC2fquqxXMrBEIoMGCZs\n" \ + "ooU5V9gOjFVKC52VWnTNgmOWTqgZuqxPJtCTN5wPvhOSggQuHPwBHa9ioshJ0dGE\n" \ + "6jdxGaJjAc82q2KZu9VEqoH/Xa2aS8dPEHwfJtzUVTia6WkrFtMFNaDMFd6byWDQ\n" \ + "ai+T4i2J3/SDL0BfsFWdQuje\n" \ + "-----END PRIVATE KEY-----" + + >>> private_key = ... # store here your private key you get in AR UI during application creation >>> public_key = ... # store here your public key you get in AR UI during application creation >>> application_id = "8c947a45-c57d-4fd2-affc-206e2sdg3a50" # # store here your application id. You can find it in AR UI @@ -12,13 +53,13 @@ >>> auth_client = ar.Authorization("QA", public_key=public_key, private_key=private_key) >>> auth_url = auth_client.get_auth_request_url(auth_params) # use this url to authorize the user as described at https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/integration/authorization.html#perform-authorization ->>> auth_result_url = ... # the url the user was redirected after his authorization. ->>> auth_response = auth_client.extract_auth_response(auth_result_url) # auth_response containing the results of the auth process +>>> auth_result_url = ... # the url the user was redirected to after his authorization. +>>> auth_response = auth_client.extract_auth_response(auth_result_url) # auth_response contains the results of the auth process >>> auth_client.verify_auth_response(auth_response) # you may verify auth_response to ensure answer was from AR ->>> auth_response.is_successfull # True if user accepted application, False if he rejected +>>> auth_response.is_successful # True if user accepted application, False if he rejected True ->>> auth_response.is_valid # Result of verification, if False, response was not validated by public key. Doesn't indicate was the auth successfull. Accessible only after response verifying +>>> auth_response.is_valid # Result of verification, if False, response was not validated by public key. Doesn't indicate the auth was successfull. Accessible only after response verifying True >>> # Get dict containing data from auth process you will use for futher communication. @@ -45,15 +86,16 @@ True >>> id_ = "mydeviceid" >>> certification_version_id = ... # get from AR UI ->>> utc_timestamp = "2018-06-20T07:29:23.457Z" >>> time_zone = "+03:00" >>> onboarding_client = ar.SoftwareOnboarding("QA", public_key=public_key, private_key=private_key) ->>> onboarding_parameters = ar.SoftwareOnboardingParameter(id_=id_, application_id=application_id, certification_version_id=certification_version_id, gateway_id=GateWays.REST.value, utc_timestamp=utc_timestamp, time_zone=time_zone, reg_code=auth_data["credentials"]["regcode"]) +>>> onboarding_parameters = ar.SoftwareOnboardingParameter(id_=id_, application_id=application_id, certification_version_id=certification_version_id, gateway_id=GateWays.REST.value, time_zone=time_zone, reg_code=auth_data.get_decoded_token().regcode) >>> onboarding_verifying_response = onboarding_client.verify(onboarding_parameters) +>>> onboarding_verifying_response.status_code +>>> onboarding_verifying_response.text >>> onboarding_response = onboarding_client.onboard(onboarding_parameters) ->>> onboarding_response.status_code() ->>> onboarding_response.data() # or onboarding_response.text() +>>> onboarding_response.status_code +>>> onboarding_response.text { "authentication": { @@ -69,4 +111,44 @@ True }, "deviceAlternateId": "c067272a-d3a7-4dcf-ab58-5c45ba66ad60", "sensorAlternateId": "5564ce96-385f-448a-9502-9ea3c940a259" -} \ No newline at end of file +} + + +>>> ########################## +>>> # Messaging + + +>>> from agrirouter.messaging.enums import CapabilityTypeDefinitions +>>> from agrirouter.generated.messaging.request.payload.endpoint.subscription_pb2 import Subscription +>>> from agrirouter.messaging.services.commons import HttpMessagingService, MqttMessagingService +>>> from agrirouter import ListEndpointsParameters, ListEndpointsService, SubscriptionService, SubscriptionParameters +>>> from agrirouter.utils.uuid_util import new_uuid + +>>> # List Endpoints + +>>> messaging_service = HttpMessagingService() +>>> list_endpoint_parameters = ListEndpointsParameters( + technical_message_type=CapabilityTypeDefinitions.ISO_11783_TASKDATA_ZIP.value, + direction=2, + filtered=False, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) +>>> list_endpoint_service = ListEndpointsService(messaging_service) +>>> list_endpoint_service.send(list_endpoint_parameters) + +>>> # Subscription + +>>> messaging_service = HttpMessagingService() +>>> subscription_service = SubscriptionService(messaging_service) + +>>> tmt = CapabilityTypeDefinitions.ISO_11783_TASKDATA_ZIP.value +>>> subscription_item = Subscription.MessageTypeSubscriptionItem(technical_message_type=tmt) +>>> subscription_parameters = SubscriptionParameters( + subscription_items=[subscription_item], + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, +) +>>> subscription_service.send(subscription_parameters) diff --git a/tests/auth_test/test_auth.py b/tests/auth_test/test_auth.py new file mode 100644 index 00000000..8ab7cf28 --- /dev/null +++ b/tests/auth_test/test_auth.py @@ -0,0 +1,47 @@ +"""Tests agrirouter/auth/auth.py""" + +from agrirouter import AuthUrlParameter +from agrirouter.auth.auth import Authorization +from tests.constants import ( + public_key, + private_key, + auth_result_url, + ENV, + application_id, +) +from re import search + + +class TestAuthorization: + def test_extract_auth_response(self): + auth_client = Authorization(ENV, public_key=public_key, private_key=private_key) + assert search( + "