From 2e05b588168fd60eda49faebb776d1ce67acd838 Mon Sep 17 00:00:00 2001 From: bdoona Date: Thu, 29 Feb 2024 11:44:30 +1100 Subject: [PATCH 1/6] Add TLS connectivity --- chanmqttproxy/channelsmqttproxy.py | 99 ++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/chanmqttproxy/channelsmqttproxy.py b/chanmqttproxy/channelsmqttproxy.py index b326dbc..8724755 100644 --- a/chanmqttproxy/channelsmqttproxy.py +++ b/chanmqttproxy/channelsmqttproxy.py @@ -5,14 +5,33 @@ import os import signal import socket +import ssl from gmqtt import Client as MQTTClient +from gmqtt.mqtt.handler import MQTTConnectError from gmqtt.mqtt.constants import MQTTv311, MQTTv50 LOGGER = logging.getLogger(__name__) class ChannelsMQTTProxy: + @staticmethod + def strtobool(val): + """ + FROM: https://stackoverflow.com/a/18472142 + Convert a string representation of truth to true (1) or false (0). + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + val = val.lower() + if val in ('y', 'yes', 't', 'true', 'on', '1'): + return 1 + elif val in ('n', 'no', 'f', 'false', 'off', '0'): + return 0 + else: + raise ValueError("invalid truth value %r" % (val,)) + def __init__(self, channel_layer, settings): self.channel_layer = channel_layer @@ -20,9 +39,42 @@ def __init__(self, channel_layer, settings): # Creating the client does not connect. self.mqtt = MQTTClient( f"ChannelsMQTTProxy@{socket.gethostname()}.{os.getpid()}") - self.mqtt.set_auth_credentials(username=settings.MQTT_USER, - password=settings.MQTT_PASSWORD) + try: + self.mqtt.set_auth_credentials(username=settings.MQTT_USER, + password=settings.MQTT_PASSWORD) + except AttributeError: + # Settings are not defined. Try anonymous connection + pass self.mqtt_host = settings.MQTT_HOST + try: + self.mqtt_port = int(settings.MQTT_PORT) + except AttributeError: + # Setting not defined. Use default unsecured. + self.mqtt_port = 1883 + # Set ssl + try: + self.mqtt_usessl = self.strtobool(settings.MQTT_USE_SSL) + except AttributeError: + # Setting is not defined. Assume false + self.mqtt_usessl = False + + try: + self.mqtt_ssl_ca = settings.MQTT_SSL_CA + self.mqtt_ssl_cert = settings.MQTT_SSL_CERT + self.mqtt_ssl_key = settings.MQTT_SSL_KEY + try: + self.mqtt_ssl_verify = \ + self.strtobool(settings.MQTT_SSL_VERIFY) + except AttributeError: + # Assume True on error + self.mqtt_ssl_verify = True + except AttributeError: + # Setting is not defined. Set safe values. + self.mqtt_ssl_ca = None + self.mqtt_ssl_cert = None + self.mqtt_ssl_key = None + self.mqtt_ssl_verify = True + try: self.mqtt_version = settings.MQTT_VERSION except AttributeError: @@ -67,7 +119,46 @@ async def connect(self): while not self.mqtt.is_connected: try: - await self.mqtt.connect(self.mqtt_host, version=version) + LOGGER.debug((f'Connecting to mqtt' + f'{"s" if self.mqtt_usessl else ""}' + f'://{self.mqtt_host}:{self.mqtt_port} ' + f'using v{self.mqtt_version}')) + use_ssl = self.mqtt_usessl + if (self.mqtt_usessl) and (self.mqtt_ssl_ca is not None): + LOGGER.debug((f'Using CA: {self.mqtt_ssl_ca}' + f' Cert: {self.mqtt_ssl_cert}' + f' Key: {self.mqtt_ssl_key}' + f' Verify: {self.mqtt_ssl_verify}')) + try: + use_ssl = ssl.create_default_context( + ssl.Purpose.SERVER_AUTH, + cafile = self.mqtt_ssl_ca) + use_ssl.check_hostname = self.mqtt_ssl_verify + if self.mqtt_ssl_verify: + use_ssl.verify_mode = ssl.CERT_REQUIRED + else: + use_ssl.verify_mode =ssl.CERT_NONE + use_ssl.load_cert_chain( + certfile=self.mqtt_ssl_cert, + keyfile=self.mqtt_ssl_key) + except Exception as e: + LOGGER.warn(f'Error initialising ssl: ' + f'{e}. Retrying.') + await asyncio.sleep(1) + continue + await self.mqtt.connect( + self.mqtt_host, + port=self.mqtt_port, + ssl=use_ssl, + version=version) + except MQTTConnectError as e: + # Mqtt server returned an error. + # Back off as to not spam the server + LOGGER.warn(f"MQTT Error trying to connect: {e}. Retrying.") + # Close the connection since it is running and gmqtt will + # still retry to complete the connection. + await self.mqtt.disconnect() + await asyncio.sleep(30) except Exception as e: LOGGER.warn(f"Error trying to connect: {e}. Retrying.") await asyncio.sleep(1) @@ -81,10 +172,10 @@ async def finish(self): LOGGER.debug("MQTT client disconnected") def _on_connect(self, _client, _flags, _rc, _properties): + LOGGER.debug('Connected') for s in self.subscriptions.keys(): LOGGER.debug(f"Re-subscribing to {s}") self.mqtt.subscribe(s) - LOGGER.debug('Connected and subscribed') def _on_disconnect(self, _client, _packet, _exc=None): LOGGER.debug('Disconnected') From 0b53fc804cdc679dc1ade31bbf9364f661b38ae8 Mon Sep 17 00:00:00 2001 From: bdoona Date: Thu, 29 Feb 2024 11:58:46 +1100 Subject: [PATCH 2/6] Update README.md to include secure settings --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 8fde07e..7793441 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,30 @@ The complete code for the Channels Chat tutorial application (up to part 3) with the channels-mqtt-proxy additions is here: https://github.com/lbt/channels-mqtt-proxy/tree/main/examples + +### Secure connections + +Additional settings in `site/settings.py` to connect to a MQTT broker with a secure connection: + +```python +# Local mqtt settings +MQTT_HOST = "mqtt.example.com" +MQTT_USER = "mqtt-test" +MQTT_PASSWORD = "mqtt-test" +MQTT_VERSION = 311 # defaults to 50 + +# TLS settings +MQTT_PORT = 8883 # mqtts port +MQTT_USE_SSL = True # Enable ssl connection +## Stop here if your certificate has been properly signed. + +## Settings for self-signed certificates +MQTT_SSL_CA = ca.crt +MQTT_SSL_CERT = client.crt +MQTT_SSL_KEY = client.key +MQTT_SSL_VERIFY = False +``` + ## Usage Now run both of these (in different consoles) From 03abc6b1001b243debe0be8244092bf79e397c89 Mon Sep 17 00:00:00 2001 From: bdoona Date: Thu, 29 Feb 2024 12:14:33 +1100 Subject: [PATCH 3/6] Update channelsmqttproxy.py - fixed boolean response --- chanmqttproxy/channelsmqttproxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chanmqttproxy/channelsmqttproxy.py b/chanmqttproxy/channelsmqttproxy.py index 8724755..05d79c3 100644 --- a/chanmqttproxy/channelsmqttproxy.py +++ b/chanmqttproxy/channelsmqttproxy.py @@ -26,9 +26,9 @@ def strtobool(val): """ val = val.lower() if val in ('y', 'yes', 't', 'true', 'on', '1'): - return 1 + return True elif val in ('n', 'no', 'f', 'false', 'off', '0'): - return 0 + return False else: raise ValueError("invalid truth value %r" % (val,)) From 8cbc68bd5280e70a0c87fed8ec681d417879ac5b Mon Sep 17 00:00:00 2001 From: bdoona Date: Mon, 4 Mar 2024 11:43:16 +1100 Subject: [PATCH 4/6] Removed type conversions for settings All submitted setting variables must be of the correct type. References: * https://github.com/lbt/channels-mqtt-proxy/pull/4#discussion_r1509992331 * https://github.com/lbt/channels-mqtt-proxy/pull/4#discussion_r1510431358 --- chanmqttproxy/channelsmqttproxy.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/chanmqttproxy/channelsmqttproxy.py b/chanmqttproxy/channelsmqttproxy.py index 05d79c3..5f3996b 100644 --- a/chanmqttproxy/channelsmqttproxy.py +++ b/chanmqttproxy/channelsmqttproxy.py @@ -16,21 +16,6 @@ class ChannelsMQTTProxy: @staticmethod - def strtobool(val): - """ - FROM: https://stackoverflow.com/a/18472142 - Convert a string representation of truth to true (1) or false (0). - True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values - are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if - 'val' is anything else. - """ - val = val.lower() - if val in ('y', 'yes', 't', 'true', 'on', '1'): - return True - elif val in ('n', 'no', 'f', 'false', 'off', '0'): - return False - else: - raise ValueError("invalid truth value %r" % (val,)) def __init__(self, channel_layer, settings): self.channel_layer = channel_layer @@ -47,13 +32,13 @@ def __init__(self, channel_layer, settings): pass self.mqtt_host = settings.MQTT_HOST try: - self.mqtt_port = int(settings.MQTT_PORT) + self.mqtt_port = settings.MQTT_PORT except AttributeError: # Setting not defined. Use default unsecured. self.mqtt_port = 1883 # Set ssl try: - self.mqtt_usessl = self.strtobool(settings.MQTT_USE_SSL) + self.mqtt_usessl = settings.MQTT_USE_SSL except AttributeError: # Setting is not defined. Assume false self.mqtt_usessl = False @@ -63,8 +48,7 @@ def __init__(self, channel_layer, settings): self.mqtt_ssl_cert = settings.MQTT_SSL_CERT self.mqtt_ssl_key = settings.MQTT_SSL_KEY try: - self.mqtt_ssl_verify = \ - self.strtobool(settings.MQTT_SSL_VERIFY) + self.mqtt_ssl_verify = settings.MQTT_SSL_VERIFY except AttributeError: # Assume True on error self.mqtt_ssl_verify = True From 4c2b4e0f08646841b56e5ec18ada1cca9d5d119b Mon Sep 17 00:00:00 2001 From: bdoona Date: Mon, 4 Mar 2024 11:59:03 +1100 Subject: [PATCH 5/6] Fixed lint messages * Use lazy % formatting in logging functions Pylint (W1203:logging-fstring-interpolation) * Using deprecated method warn() Pylint (W4902:deprecated-method) * Catching too general exception Exception Pylint (W0718:broad-exception-caught) --- chanmqttproxy/channelsmqttproxy.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/chanmqttproxy/channelsmqttproxy.py b/chanmqttproxy/channelsmqttproxy.py index 5f3996b..39edc67 100644 --- a/chanmqttproxy/channelsmqttproxy.py +++ b/chanmqttproxy/channelsmqttproxy.py @@ -15,8 +15,6 @@ class ChannelsMQTTProxy: - @staticmethod - def __init__(self, channel_layer, settings): self.channel_layer = channel_layer @@ -103,16 +101,18 @@ async def connect(self): while not self.mqtt.is_connected: try: - LOGGER.debug((f'Connecting to mqtt' - f'{"s" if self.mqtt_usessl else ""}' - f'://{self.mqtt_host}:{self.mqtt_port} ' - f'using v{self.mqtt_version}')) + LOGGER.debug('Connecting to mqtt%s://%s:%s using v%s', + "s" if self.mqtt_usessl else "", + self.mqtt_host, + self.mqtt_port, + self.mqtt_version) use_ssl = self.mqtt_usessl if (self.mqtt_usessl) and (self.mqtt_ssl_ca is not None): - LOGGER.debug((f'Using CA: {self.mqtt_ssl_ca}' - f' Cert: {self.mqtt_ssl_cert}' - f' Key: {self.mqtt_ssl_key}' - f' Verify: {self.mqtt_ssl_verify}')) + LOGGER.debug('Using CA: %s Cert: %s Key: %s Verify: %s', + self.mqtt_ssl_ca, + self.mqtt_ssl_cert, + self.mqtt_ssl_key, + self.mqtt_ssl_verify) try: use_ssl = ssl.create_default_context( ssl.Purpose.SERVER_AUTH, @@ -125,9 +125,8 @@ async def connect(self): use_ssl.load_cert_chain( certfile=self.mqtt_ssl_cert, keyfile=self.mqtt_ssl_key) - except Exception as e: - LOGGER.warn(f'Error initialising ssl: ' - f'{e}. Retrying.') + except ssl.SSLError as e: + LOGGER.error('Error initialising ssl: %s. Retrying.',e) await asyncio.sleep(1) continue await self.mqtt.connect( @@ -138,7 +137,7 @@ async def connect(self): except MQTTConnectError as e: # Mqtt server returned an error. # Back off as to not spam the server - LOGGER.warn(f"MQTT Error trying to connect: {e}. Retrying.") + LOGGER.info('MQTT Error trying to connect: %s. Retrying.',e) # Close the connection since it is running and gmqtt will # still retry to complete the connection. await self.mqtt.disconnect() From 8e4a04d6b0a6d493f4f9a1433e95f79d84bd0eae Mon Sep 17 00:00:00 2001 From: bdoona Date: Mon, 4 Mar 2024 12:16:28 +1100 Subject: [PATCH 6/6] Additional details for TLS settings Reference: * # https://github.com/lbt/channels-mqtt-proxy/pull/4#discussion_r1509993090 --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7793441..257f330 100644 --- a/README.md +++ b/README.md @@ -186,15 +186,17 @@ MQTT_PASSWORD = "mqtt-test" MQTT_VERSION = 311 # defaults to 50 # TLS settings -MQTT_PORT = 8883 # mqtts port -MQTT_USE_SSL = True # Enable ssl connection -## Stop here if your certificate has been properly signed. - -## Settings for self-signed certificates -MQTT_SSL_CA = ca.crt -MQTT_SSL_CERT = client.crt -MQTT_SSL_KEY = client.key -MQTT_SSL_VERIFY = False +MQTT_USE_SSL = True # enable ssl connection (bool) +MQTT_PORT = 8883 # override the port to connect to (int) +## Stop here if your server certificate has been properly signed. + +## Optional +## Settings to connect to a server with self-signed certificates +MQTT_SSL_VERIFY = False # set to False to connect to a server + # with a self signed certificate +MQTT_SSL_CA = "" # ca file from server +MQTT_SSL_CERT = "" # client specific cert file +MQTT_SSL_KEY = "" # client specific key file ``` ## Usage