def build_request(request, headers, device): """ Helper method to organized required headers into the CoAP Request. :param request: Request being build :param headers: Data from AMQP message which contains data to forward OpenC2 Command. :param device: Device specific data from headers sent by O.I.F. """ orc_host, orc_port = headers["transport"]["socket"].split( ":", 1) # location of orchestrator-side CoAP server request.source = (orc_host, safe_cast(orc_port, int, 5683)) dev_host, dev_port = device["socket"].split( ":", 1) # location of device-side CoAP server request.destination = (dev_host, safe_cast(dev_port, int, 5683)) encoding = f"application/{device['encoding']}" # Content Serialization request.content_type = defines.Content_types[ encoding] # using application/json, TODO: add define to openc2+json request.mid = int("0x" + headers["correlationID"], 16) # 16-bit correlationID request.timestamp = headers[ "date"] # time message was sent from orchestrator # Add OIF-unique value used for routing to the desired actuator profile = Option() profile.number = 8 profile.value = device.get("profile", "")[0] request.add_option(profile) source_socket = Option() source_socket.number = 3 source_socket.value = headers["transport"]["socket"] request.add_option(source_socket) return request
def __init__(self, hostname='127.0.0.1', port=5672, auth=_auth, exchange=_exchange, consumer_key=_consumerKey, producer_exchange=_producerExchange, callbacks=None): """ Message Queue - holds a consumer class and producer class for ease of use :param hostname: server ip/hostname to connect :param port: port the AMQP Queue is listening :param exchange: name of the default exchange :param consumer_key: key to consumer :param producer_exchange: ... :param callbacks: list of functions to call on message receive """ self._exchange = exchange if isinstance(exchange, str) else self._exchange self._consumerKey = consumer_key if isinstance( consumer_key, str) else self._consumerKey self._producerExchange = producer_exchange if isinstance( producer_exchange, str) else self._producerExchange self._publish_opts = dict(host=hostname, port=safe_cast(port, int)) self._consume_opts = dict(host=hostname, port=safe_cast(port, int), exchange=self._exchange, routing_key=self._consumerKey, callbacks=callbacks) self.producer = Producer(**self._publish_opts) self.consumer = Consumer(**self._consume_opts)
def mqtt_publish(recipients: List[str], source: dict, device: dict, body: Union[bytes, str], topic: str, auth: Auth, headers: dict) -> None: # pylint: disable=unbalanced-tuple-unpacking (orc_id, corr_id) = destructure(source, "orchestratorID", "correlationID") # pylint: disable=unbalanced-tuple-unpacking (fmt, encoding, broker_socket) = destructure(device, ("format", "broadcast"), ("encoding", "json"), ("socket", "localhost:1883")) (host, port) = broker_socket.split(":", 1) payload = Message(recipients=recipients, origin=f"{orc_id}@{broker_socket}", msg_type=MessageType.Request, request_id=uuid.UUID(corr_id), content_type=SerialFormats(encoding) if encoding in SerialFormats else SerialFormats.JSON, content=json.loads(body)) print(f"Sending {broker_socket} topic: {topic} -> {payload}") publish_props = Properties(PacketTypes.PUBLISH) publish_props.PayloadFormatIndicator = int( SerialFormats.is_binary(payload.content_type) is False) publish_props.ContentType = "application/openc2" # Content-Type publish_props.UserProperty = ("msgType", payload.msg_type) # User Property publish_props.UserProperty = ("encoding", payload.content_type ) # User Property try: publish_single(config=FrozenDict(MQTT_HOST=host, MQTT_PORT=safe_cast(port, int, 1883), USERNAME=auth.username, PASSWORD=auth.password, TLS_SELF_SIGNED=safe_cast( os.environ.get( "MQTT_TLS_SELF_SIGNED", 0), int, 0), CAFILE=auth.caCert, CLIENT_CERT=auth.clientCert, CLIENT_KEY=auth.clientKey), topic=topic, payload=payload.serialize(), properties=publish_props) print(f"Placed payload onto topic {topic} Payload Sent: {payload}") except Exception as e: print( f"There was an error sending command to {broker_socket} topic: {topic} -> {e}" ) send_error_response(e, headers)
def send_coap(body, message): """ AMQP Callback when we receive a message from internal buffer to be published :param body: Contains the message to be sent. :param message: Contains data about the message as well as headers """ # Set destination and build requests for multiple potential endpoints. for device in message.headers.get("destination", {}): host, port = device["socket"].split(":", 1) encoding = device["encoding"] # Check necessary headers exist if host and port and encoding: path = "transport" client = CoapClient(server=(host, safe_cast(port, int, 5683))) request = client.mk_request(defines.Codes.POST, path) response = client.post(path=path, payload=encode_msg(json.loads(body), encoding), request=build_request( request, message.headers.get("source", {}), device)) if response: print(f"Response from device: {response}") client.stop() else: # send error back to orch print(f"Error: not enough data - {host}, {port}, {encoding}")
def build_request(request, headers): """ Helper method to organized required headers into the CoAP Request. :param request: Request being build :param headers: Data from AMQP message which contains data to forward OpenC2 Command. """ dev_host, dev_port = headers["socket"].split( ":", 1) # location of device-side CoAP server request.source = (dev_host, safe_cast(dev_port, int, 5683)) orc_host, orc_port = headers["socket"].split( ":", 1) # location of orchestrator-side CoAP server request.destination = (orc_host, safe_cast(orc_port, int, 5683)) encoding = f"application/{headers['encoding']}" # Content Serialization request.content_type = defines.Content_types[ encoding] # using application/json, TODO: add define to openc2+json request.mid = headers["correlationID"] # 16-bit value - correlationID return request
def _check_subscribe(self, data: FrozenDict) -> None: socket = "{host}:{port}".format(**data) client = self._clients.setdefault( socket, mqtt.Client(client_id=self.client_id, userdata=self.topics, protocol=mqtt.MQTTv5, transport="tcp")) if client.is_connected(): print(f"Update connection: {socket}") # TODO: Update connection?? # Set topics # TODO: Set topics based on prefix # topics = ["+/+/oc2/rsp", "+/oc2/rsp", "oc2/rsp"] # client.user_data_set(topics) else: print(f"Create connection: {socket}") # Auth with Auth(data) as auth: if username := auth.username: client.username_pw_set(username=username, password=auth.password) # TLS if auth.caCert and auth.clientCert and auth.clientKey: client.tls_set(ca_certs=auth.caCert, certfile=auth.clientCert, keyfile=auth.clientKey, tls_version=ssl.PROTOCOL_TLSv1_2) # Set callbacks client.on_connect = mqtt_on_connect client.on_message = mqtt_on_message if self.debug: client.on_log = mqtt_on_log try: client.connect( host=data["host"], port=safe_cast(data["port"], int, 1883), keepalive=300, clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY) except Exception as e: print(f"MQTT Error: {e}") print(f"Connect to MQTT broker: {socket}") client.loop_start()
def send_coap(body, message): """ AMQP Callback when we receive a message from internal buffer to be published :param body: Contains the message to be sent. :param message: Contains data about the message as well as headers """ host, port = message.headers["socket"].split(":", 1) path = "transport" client = CoapClient(server=(host, safe_cast(port, int, 5683))) request = client.mk_request(defines.Codes.POST, path) response = client.post(path=path, payload=body, request=build_request(request, message.headers)) if response: print(f"Response from orchestrator: {response}") client.stop()
def _socketMsg(self, action, act_args, *args, **kwargs): auth = kwargs.get("headers", {}).get("Authorization", "") token = re.sub(r"^JWT\s+", "", auth) if auth.startswith("JWT") else "" url = f"api{act_args['url'].format(*args)}" url_params = act_args.get('params', {}) if len(url_params) > 0: url += f"?{'&'.join(f'{k}={v}' for k, v in url_params.items())}" rtn = dict( body={}, method=act_args["method"], status_code=500, url=f"{self._root_url}{url}", # Extra Options meta={}) try: self._webSocket.send( json.dumps( dict(endpoint=url, method=act_args["method"], jwt=token, data=kwargs.get("body", {}), types=dict( success=f"@@socket/{action.upper()}_SUCCESS", failure=f"@@socket/{action.upper()}_FAILURE")))) try: rslt = json.loads(self._webSocket.recv()) except ValueError as e: rslt = {} rtn.update( body=rslt.get('payload', {}), status_code=safe_cast( rslt.get('meta', {}).get("status_code", 200), int, 200), # Extra Options meta=rslt.get('meta', {})) return FrozenDict(rtn) except Exception as e: print(e) rtn.update(status_code=500, ) return FrozenDict(rtn)
def _check_subscribe(self, data: FrozenDict) -> None: socket = '{host}:{port}'.format(**data) client = self.mqtt_clients.setdefault( socket, mqtt.Client( # TODO: add orc_id ?? client_id=f"oif-orchestrator-subscribe" # clean_session=None )) if client.is_connected(): # TODO: Update connection?? pass else: # Auth with Auth(data) as auth: # prefix = data.get('prefix', None) if username := auth.username: client.username_pw_set(username=username, password=auth.password) # TLS if auth.caCert and auth.clientCert and auth.clientKey: client.tls_set(ca_certs=auth.caCert, certfile=auth.clientCert, keyfile=auth.clientKey) client.connect( host=data['host'], port=safe_cast(data['port'], int, 1883), # keepalive=60, # clean_start=MQTT_CLEAN_START_FIRST_ONLY ) # Set topics # TODO: Set topics based on prefix topics = ['+/+/oc2/rsp', '+/oc2/rsp', 'oc2/rsp'] client.user_data_set(topics) # Set callbacks client.on_connect = mqtt_on_connect client.on_message = mqtt_on_message print(f'Connect - {socket}') client.loop_start()
def __init__(self, root: str = _ROOT_DIR, act_id: str = _ACT_ID) -> None: """ Initialize and start the Actuator Process :param root: rood directory of actuator - default CWD :param act_id: id of the actuator - default UUIDv4 """ config_file = os.path.join(root, "config.json") schema_file = os.path.join(root, "schema.json") config = general.safe_load(config_file) if "actuator_id" not in config.keys(): config.setdefault("actuator_id", act_id) json.dump(config, open(config_file, "w"), indent=4) # Initialize etcd client self.etcdClient = etcd.Client(host=os.environ.get('ETCD_HOST', 'etcd'), port=safe_cast( os.environ.get('ETCD_PORT', 4001), int, 4001)) schema = general.safe_load(schema_file) self._config = FrozenDict(**config, schema=schema) self._dispatch = dispatch.Dispatch( act=self, dispatch_transform=self._dispatch_transform) self._dispatch.register(exceptions.action_not_implemented, "default") self._pairs = None # Get valid Actions & Targets from the schema self._profile = self._config.schema.get("title", "N/A").replace(" ", "_").lower() self._validator = general.ValidatorJSON(schema) schema_defs = self._config.schema.get("definitions", {}) self._prefix = '/actuator' profiles = self.nsid if len(self.nsid) > 0 else [self._profile] for profile in profiles: self.etcdClient.write(f"{self._prefix}/{profile}", self._config.actuator_id) self._valid_actions = tuple( a["const"] for a in schema_defs.get("Action", {}).get("oneOf", [])) self._valid_targets = tuple( schema_defs.get("Target", {}).get("properties", {}).keys())
def __init__(self, root=ROOT_DIR, act_id=ACT_ID, enable_etcd=True) -> None: """ Initialize and start the Actuator Process :param root: rood directory of actuator - default CWD :param act_id: id of the actuator - default UUIDv4 """ config_file = os.path.join(root, "config.json") schema_file = os.path.join(root, "schema.json") # Set config config = general.safe_load(config_file) if "actuator_id" not in config.keys(): config.setdefault("actuator_id", act_id) with open(config_file, "w", encoding="UTF-8") as f: json.dump(config, f, indent=4) schema = general.safe_load(schema_file) self._config = FrozenDict( **config, schema=schema ) # Configure Action/Target functions self._dispatch = dispatch.Dispatch(namespace="root", dispatch_transform=self._dispatch_transform, act=self) self._dispatch.register(exceptions.action_not_implemented, "default") # Get valid Actions & Targets from the schema self._validator = general.ValidatorJSON(schema) self._profile = self._config.schema.get("title", "N/A").replace(" ", "_").lower() schema_defs = self._config.schema.get("definitions", {}) self._valid_actions = tuple(a["const"] for a in schema_defs.get("Action", {}).get("oneOf", [])) self._valid_targets = tuple(schema_defs.get("Target", {}).get("properties", {}).keys()) # Initialize etcd client and set profiles if enable_etcd: self._etcd = etcd.Client( host=os.environ.get('ETCD_HOST', 'etcd'), port=safe_cast(os.environ.get('ETCD_PORT', 4001), int, 4001) ) profiles = self.nsid if len(self.nsid) > 0 else [self._profile] for profile in profiles: self._etcd.write(f"{self._prefix}/{profile}", self._config.actuator_id)
def publish_single(config: FrozenDict, topic, payload, client_id="", properties: Properties = None): msg = { "topic": topic, "payload": payload, "qos": 1, "retain": False, "properties": properties } client = mqtt.Client(client_id=client_id, userdata=[msg], protocol=mqtt.MQTTv5, transport="tcp") # set auth if config.USERNAME: client.username_pw_set(username=config.USERNAME, password=config.PASSWORD) # check that certs exist if config.CAFILE and config.CLIENT_CERT and config.CLIENT_KEY: client.tls_insecure_set(safe_cast(config.TLS_SELF_SIGNED, bool, False)) client.tls_set(ca_certs=config.CAFILE, certfile=config.CLIENT_CERT, keyfile=config.CLIENT_KEY, tls_version=ssl.PROTOCOL_TLSv1_2) # set callbacks client.on_connect = _on_connect client.on_publish = _on_publish client.connect(host=config.MQTT_HOST, port=config.MQTT_PORT, keepalive=300, clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY) client.loop_forever()
except etcd.EtcdKeyNotFound: dev_uuid = uuid.uuid4() etcd_client.write('/device/id', str(dev_uuid)) dev_id = f'device_{str(dev_uuid).replace("-", "")[:15]}' # begin listening to a single MQTT socket print(f"Initializing Device: {dev_uuid}...") client = mqtt.Client( client_id=dev_id, # clean_session=None ) # check that certs exist if Config.TLS_ENABLED: client.tls_insecure_set(safe_cast(Config.TLS_SELF_SIGNED, bool, False)) client.tls_set( ca_certs=Config.CAFILE, certfile=Config.CLIENT_CERT, keyfile=Config.CLIENT_KEY ) if Config.USERNAME: client.username_pw_set( username=Config.USERNAME, password=Config.PASSWORD ) client.connect( host=Config.MQTT_HOST, port=Config.MQTT_PORT,
MESSAGE_QUEUE = None # Security CRYPTO = Fernet(os.environ['TRANSPORT_SECRET'] ) if 'TRANSPORT_SECRET' in os.environ else None # First key will be used to encrypt all new data # Decryption of existing values will be attempted with all given keys in order FERNET_KEYS = [ k.decode('utf-8') if isinstance(k, bytes) else str(k) for k in [ # Key Generation - URLSAFE_BASE64_ENCRYPT(RANDOM_32_BITS) '4k1wW0AwvNpOYLUazdXtpwLBc6MOaflTKV4UkkzVhS8=', base64.urlsafe_b64encode(SECRET_KEY[:32].encode('utf-8')) ] if k ] # ETCD ETCD = { 'host': os.environ.get('ETCD_HOST', 'localhost'), 'port': safe_cast(os.environ.get('ETCD_PORT', 4001), int, 4001) } ETCD_CLIENT = None # App stats function STATS_FUN = 'app_stats' # GUI Configuration ADMIN_GUI = True
# check that certs exist if os.environ.get('MQTT_TLS_ENABLED', False): client.tls_set( ca_certs=os.environ.get('MQTT_CAFILE', None), certfile=os.environ.get('MQTT_CLIENT_CERT', None), keyfile=os.environ.get('MQTT_CLIENT_KEY', None) ) client.username_pw_set( os.environ.get('MQTT_DEFAULT_USERNAME', 'guest'), os.environ.get('MQTT_DEFAULT_PASS', 'guest') ) client.tls_insecure_set(os.environ.get('MQTT_TLS_SELF_SIGNED', 0)) client.connect( os.environ.get('MQTT_HOST', 'queue'), safe_cast(os.environ.get('MQTT_PORT', 1883), int, 1883) ) topics = [] if "TRANSPORT_TOPICS" in os.environ: time.sleep(2) transports = [t.lower().strip() for t in os.environ.get("TRANSPORT_TOPICS", "").split(",")] topics.extend([t.lower().strip() for t in consumer.get_queues() if t not in transports]) if "MQTT_TOPICS" in os.environ: topics.extend([t.lower().strip() for t in os.environ.get("MQTT_TOPICS", "").split(",") if t not in topics]) client.user_data_set(topics) client.on_connect = Callbacks.on_connect client.on_message = Callbacks.on_message client.loop_start()
import json import os import requests import uuid import kombu from typing import Union from sb_utils import Auth, Consumer, EtcdCache, FrozenDict, Message, MessageType, Producer, SerialFormats, safe_cast, safe_json # Gather transport from etcd transport_cache = EtcdCache( host=os.environ.get("ETCD_HOST", "localhost"), port=safe_cast(os.environ.get("ETCD_PORT", 2379), int, 2379), # Add base of 'orchestrator' ?? base='transport/HTTPS', callbacks=[lambda d: print(f"Update - {d}")]) def process_message(body: Union[dict, str], message: kombu.Message) -> None: """ Callback when we receive a message from internal buffer to publish to waiting flask. :param body: Contains the message to be sent. :param message: Contains data about the message as well as headers """ producer = Producer() body = body if isinstance(body, dict) else safe_json(body) rcv_headers = message.headers orc_socket = rcv_headers["source"]["transport"]["socket"] # orch IP:port orc_id = rcv_headers["source"]["orchestratorID"] # orchestrator ID
import os from sb_utils import FrozenDict, safe_cast Config = FrozenDict( TLS_ENABLED=os.environ.get('MQTT_TLS_ENABLED', False), TLS_SELF_SIGNED=safe_cast(os.environ.get('MQTT_TLS_SELF_SIGNED', 0), int, 0), CAFILE=os.environ.get('MQTT_CAFILE', None), CLIENT_CERT=os.environ.get('MQTT_CLIENT_CERT', None), CLIENT_KEY=os.environ.get('MQTT_CLIENT_KEY', None), USERNAME=os.environ.get('MQTT_DEFAULT_USERNAME', None), PASSWORD=os.environ.get('MQTT_DEFAULT_PASSWORD', None), MQTT_PREFIX=os.environ.get('MQTT_PREFIX', ''), MQTT_HOST=os.environ.get('MQTT_HOST', 'queue'), MQTT_PORT=safe_cast(os.environ.get('MQTT_PORT', 1883), int, 1883), # TODO: find alternatives?? TRANSPORT_TOPICS=[ t.lower().strip() for t in os.environ.get("MQTT_TRANSPORT_TOPICS", "").split(",") ], TOPICS=[ t.lower().strip() for t in os.environ.get("MQTT_TOPICS", "").split(",") ], # ETCD Options ETCD_HOST=os.environ.get('ETCD_HOST', 'etcd'), ETCD_PORT=safe_cast(os.environ.get('ETCD_PORT', 2379), int, 2379))
def send_mqtt(body, message): """ AMQP Callback when we receive a message from internal buffer to be publishedorc_server :param body: Contains the message to be sent. :param message: Contains data about the message as well as headers """ headers = message.headers source = headers.get('source', {}) destinations = headers.get("destination", []) # iterate through all devices within the list of destinations for device in destinations: # check that all necessary parameters exist for device key_diff = Callbacks.required_device_keys.difference( {*device.keys()}) if len(key_diff) != 0: err_msg = f"Missing required header data to successfully transport message - {', '.join(key_diff)}" print(err_msg) return send_error_response(err_msg, headers) orc_id = source.get('orchestratorID', '') corr_id = source.get('correlationID', '') prefix = device.get('prefix', '') fmt = device.get('format', '') encoding = device.get("encoding", "json") ip, port = device.get("socket", "localhost:1883").split(":") broker_socket = f'{ip}:{port}' with Auth(device.get("auth", {})) as auth: # iterate through actuator profiles to send message to for actuator in device.get("profile", []): topic = getTopic(fmt=fmt, prefix=f"{prefix}/" if prefix else '', device_id=device.get('deviceID', ''), profile=actuator) payload = Message( recipients=[f"{actuator}@{broker_socket}"], origin=f"{orc_id}@{broker_socket}", msg_type=MessageType.Request, request_id=uuid.UUID(corr_id), serialization=SerialFormats(encoding) if encoding in SerialValues else SerialFormats.JSON, content=json.loads(body)).pack() print(f"Sending {ip}:{port} topic: {topic} -> {payload}") try: publish.single( topic, client_id=f"oif-{orc_id[:5]}", payload=payload, qos=1, retain=False, hostname=ip, port=safe_cast(port, int, 1883), keepalive=60, will=None, # Authentication auth=dict(username=auth.username, password=auth.password or None) if auth.username else None, tls=dict(ca_certs=auth.caCert, certfile=auth.clientCert, keyfile=auth.clientKey) if auth.caCert and auth.clientCert and auth.clientKey else None) print( f"Placed payload onto topic {topic} Payload Sent: {payload}" ) except Exception as e: print( f"There was an error sending command to {ip}:{port} topic: {actuator} -> {e}" ) send_error_response(e, headers)
def send_mqtt(body, message): """ AMQP Callback when we receive a message from internal buffer to be published :param body: Contains the message to be sent. :param message: Contains data about the message as well as headers """ # check for certs if TLS is enabled if os.environ.get("MQTT_TLS_ENABLED", False) and os.listdir("/opt/transport/MQTT/certs"): tls = dict(ca_certs=os.environ.get("MQTT_CAFILE", None), certfile=os.environ.get("MQTT_CLIENT_CERT", None), keyfile=os.environ.get("MQTT_CLIENT_KEY", None)) else: tls = None # iterate through all devices within the list of destinations for device in message.headers.get("destination", []): # check that all necessary parameters exist for device key_diff = Callbacks.required_device_keys.difference( {*device.keys()}) if len(key_diff) == 0: encoding = device.get("encoding", "json") ip, port = device.get("socket", "localhost:1883").split(":") # iterate through actuator profiles to send message to for actuator in device.get("profile", []): payload = { "header": format_header(message.headers, device, actuator), "body": encode_msg(json.loads(body), encoding) } print(f"Sending {ip}:{port} - {payload}") try: publish.single(actuator, payload=json.dumps(payload), qos=1, hostname=ip, port=safe_cast(port, int, 1883), will={ "topic": actuator, "payload": json.dumps(payload), "qos": 1 }, tls=tls) print( f"Placed payload onto topic {actuator} Payload Sent: {payload}" ) except Exception as e: print( f"There was an error sending command to {ip}:{port} - {e}" ) send_error_response(e, payload["header"]) return get_response( ip, port, message.headers.get("source", {}).get("orchestratorID", "")) else: err_msg = f"Missing required header data to successfully transport message - {', '.join(key_diff)}" send_error_response(err_msg, payload["header"])
def send_mqtt(body, message): """ AMQP Callback when we receive a message from internal buffer to be published to MQTT Broker :param body: Contains the message to be sent. :param message: Contains data about the message as well as headers """ # check for certs if TLS is enabled if os.environ.get("MQTT_TLS_ENABLED", False) and os.listdir("/opt/transport/MQTT/certs"): tls = dict(ca_certs=os.environ.get("MQTT_CAFILE", None), certfile=os.environ.get("MQTT_CLIENT_CERT", None), keyfile=os.environ.get("MQTT_CLIENT_KEY", None)) else: tls = None # build message for MQTT encoding = message.headers.get("encoding", "json") broker_socket = message.headers.get("socket", "localhost:1883") content_type = f"application/openc2-cmd+{encoding};version=1.0" source = f"{message.headers.get('profile', '')}@{broker_socket}" dest = f"{message.headers.get('orchestratorID', '')}@{broker_socket}" corr_id = message.headers.get("correlationID", "") payload = { "header": { "to": dest, "from": source, "correlationID": corr_id, "content_type": content_type }, "body": body } # Transport is running on device side, send response to orchestrator key_diff = Callbacks.required_header_keys.difference( {*message.headers.keys()}) if len(key_diff) == 0: ip, port = broker_socket.split(":")[0:2] topic = message.headers.get("orchestratorID") + "/response" try: publish.single( topic, payload=json.dumps(payload), qos=1, hostname=ip, port=safe_cast(port, int, 1883), will={ "topic": topic, "payload": json.dumps(payload), "qos": 1 }, tls=tls, ) print(f"Sent: {payload}") except Exception as e: print(f"An error occurred - {e}") pass else: print( f"Missing required header data to successfully transport message - {', '.join(key_diff)}" ) print(message.headers)