From 5ff8d16bcb16af0ed308e76c56312b3d27d56aa6 Mon Sep 17 00:00:00 2001 From: Louis King Date: Sun, 7 Dec 2025 21:15:05 +0000 Subject: [PATCH] Added support for MQTT TLS --- .env.example | 5 +++++ docker-compose.yml | 5 +++++ src/meshcore_hub/api/app.py | 3 +++ src/meshcore_hub/api/cli.py | 9 ++++++++ src/meshcore_hub/api/dependencies.py | 2 ++ src/meshcore_hub/collector/cli.py | 13 +++++++++++ src/meshcore_hub/collector/subscriber.py | 6 +++++ src/meshcore_hub/common/config.py | 3 +++ src/meshcore_hub/common/mqtt.py | 9 ++++++++ src/meshcore_hub/interface/cli.py | 28 ++++++++++++++++++++++++ src/meshcore_hub/interface/receiver.py | 6 +++++ src/meshcore_hub/interface/sender.py | 6 +++++ 12 files changed, 95 insertions(+) diff --git a/.env.example b/.env.example index 307110f..9b46c01 100644 --- a/.env.example +++ b/.env.example @@ -52,6 +52,11 @@ MQTT_USERNAME= MQTT_PASSWORD= MQTT_PREFIX=meshcore +# Enable TLS/SSL for MQTT connection (default: false) +# When enabled, uses TLS with system CA certificates (e.g., for Let's Encrypt) +# Set to true for secure MQTT connections (port 8883) +MQTT_TLS=false + # External port mappings for local MQTT broker (--profile mqtt only) MQTT_EXTERNAL_PORT=1883 MQTT_WS_PORT=9001 diff --git a/docker-compose.yml b/docker-compose.yml index cb074b0..5fdb796 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,7 @@ services: - MQTT_USERNAME=${MQTT_USERNAME:-} - MQTT_PASSWORD=${MQTT_PASSWORD:-} - MQTT_PREFIX=${MQTT_PREFIX:-meshcore} + - MQTT_TLS=${MQTT_TLS:-false} - SERIAL_PORT=${SERIAL_PORT:-/dev/ttyUSB0} - SERIAL_BAUD=${SERIAL_BAUD:-115200} - NODE_ADDRESS=${NODE_ADDRESS:-} @@ -81,6 +82,7 @@ services: - MQTT_USERNAME=${MQTT_USERNAME:-} - MQTT_PASSWORD=${MQTT_PASSWORD:-} - MQTT_PREFIX=${MQTT_PREFIX:-meshcore} + - MQTT_TLS=${MQTT_TLS:-false} - SERIAL_PORT=${SERIAL_PORT_SENDER:-/dev/ttyUSB1} - SERIAL_BAUD=${SERIAL_BAUD:-115200} - NODE_ADDRESS=${NODE_ADDRESS_SENDER:-} @@ -112,6 +114,7 @@ services: - MQTT_USERNAME=${MQTT_USERNAME:-} - MQTT_PASSWORD=${MQTT_PASSWORD:-} - MQTT_PREFIX=${MQTT_PREFIX:-meshcore} + - MQTT_TLS=${MQTT_TLS:-false} - MOCK_DEVICE=true - NODE_ADDRESS=${NODE_ADDRESS:-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef} command: ["interface", "receiver", "--mock"] @@ -145,6 +148,7 @@ services: - MQTT_USERNAME=${MQTT_USERNAME:-} - MQTT_PASSWORD=${MQTT_PASSWORD:-} - MQTT_PREFIX=${MQTT_PREFIX:-meshcore} + - MQTT_TLS=${MQTT_TLS:-false} - DATA_HOME=/data - SEED_HOME=/seed # Explicitly unset to use DATA_HOME-based default path @@ -197,6 +201,7 @@ services: - MQTT_USERNAME=${MQTT_USERNAME:-} - MQTT_PASSWORD=${MQTT_PASSWORD:-} - MQTT_PREFIX=${MQTT_PREFIX:-meshcore} + - MQTT_TLS=${MQTT_TLS:-false} - DATA_HOME=/data # Explicitly unset to use DATA_HOME-based default path - DATABASE_URL= diff --git a/src/meshcore_hub/api/app.py b/src/meshcore_hub/api/app.py index fa9191d..a1bba91 100644 --- a/src/meshcore_hub/api/app.py +++ b/src/meshcore_hub/api/app.py @@ -52,6 +52,7 @@ def create_app( mqtt_host: str = "localhost", mqtt_port: int = 1883, mqtt_prefix: str = "meshcore", + mqtt_tls: bool = False, cors_origins: list[str] | None = None, ) -> FastAPI: """Create and configure the FastAPI application. @@ -63,6 +64,7 @@ def create_app( mqtt_host: MQTT broker host mqtt_port: MQTT broker port mqtt_prefix: MQTT topic prefix + mqtt_tls: Enable TLS/SSL for MQTT connection cors_origins: Allowed CORS origins Returns: @@ -85,6 +87,7 @@ def create_app( app.state.mqtt_host = mqtt_host app.state.mqtt_port = mqtt_port app.state.mqtt_prefix = mqtt_prefix + app.state.mqtt_tls = mqtt_tls # Configure CORS if cors_origins is None: diff --git a/src/meshcore_hub/api/cli.py b/src/meshcore_hub/api/cli.py index 62d987b..cd30211 100644 --- a/src/meshcore_hub/api/cli.py +++ b/src/meshcore_hub/api/cli.py @@ -67,6 +67,13 @@ import click envvar="MQTT_TOPIC_PREFIX", help="MQTT topic prefix", ) +@click.option( + "--mqtt-tls", + is_flag=True, + default=False, + envvar="MQTT_TLS", + help="Enable TLS/SSL for MQTT connection", +) @click.option( "--cors-origins", type=str, @@ -92,6 +99,7 @@ def api( mqtt_host: str, mqtt_port: int, mqtt_prefix: str, + mqtt_tls: bool, cors_origins: str | None, reload: bool, ) -> None: @@ -171,6 +179,7 @@ def api( mqtt_host=mqtt_host, mqtt_port=mqtt_port, mqtt_prefix=mqtt_prefix, + mqtt_tls=mqtt_tls, cors_origins=origins_list, ) diff --git a/src/meshcore_hub/api/dependencies.py b/src/meshcore_hub/api/dependencies.py index bd16067..696b5e7 100644 --- a/src/meshcore_hub/api/dependencies.py +++ b/src/meshcore_hub/api/dependencies.py @@ -57,6 +57,7 @@ def get_mqtt_client(request: Request) -> MQTTClient: mqtt_host = getattr(request.app.state, "mqtt_host", "localhost") mqtt_port = getattr(request.app.state, "mqtt_port", 1883) mqtt_prefix = getattr(request.app.state, "mqtt_prefix", "meshcore") + mqtt_tls = getattr(request.app.state, "mqtt_tls", False) # Use unique client ID to allow multiple API instances unique_id = uuid.uuid4().hex[:8] @@ -65,6 +66,7 @@ def get_mqtt_client(request: Request) -> MQTTClient: port=mqtt_port, prefix=mqtt_prefix, client_id=f"meshcore-api-{unique_id}", + tls=mqtt_tls, ) client = MQTTClient(config) diff --git a/src/meshcore_hub/collector/cli.py b/src/meshcore_hub/collector/cli.py index 655d56f..ded2af3 100644 --- a/src/meshcore_hub/collector/cli.py +++ b/src/meshcore_hub/collector/cli.py @@ -47,6 +47,13 @@ if TYPE_CHECKING: envvar="MQTT_PREFIX", help="MQTT topic prefix", ) +@click.option( + "--mqtt-tls", + is_flag=True, + default=False, + envvar="MQTT_TLS", + help="Enable TLS/SSL for MQTT connection", +) @click.option( "--data-home", type=str, @@ -82,6 +89,7 @@ def collector( mqtt_username: str | None, mqtt_password: str | None, prefix: str, + mqtt_tls: bool, data_home: str | None, seed_home: str | None, database_url: str | None, @@ -125,6 +133,7 @@ def collector( ctx.obj["mqtt_username"] = mqtt_username ctx.obj["mqtt_password"] = mqtt_password ctx.obj["prefix"] = prefix + ctx.obj["mqtt_tls"] = mqtt_tls ctx.obj["data_home"] = data_home or settings.data_home ctx.obj["seed_home"] = settings.effective_seed_home ctx.obj["database_url"] = effective_db_url @@ -139,6 +148,7 @@ def collector( mqtt_username=mqtt_username, mqtt_password=mqtt_password, prefix=prefix, + mqtt_tls=mqtt_tls, database_url=effective_db_url, log_level=log_level, data_home=data_home or settings.data_home, @@ -152,6 +162,7 @@ def _run_collector_service( mqtt_username: str | None, mqtt_password: str | None, prefix: str, + mqtt_tls: bool, database_url: str, log_level: str, data_home: str, @@ -236,6 +247,7 @@ def _run_collector_service( mqtt_username=mqtt_username, mqtt_password=mqtt_password, mqtt_prefix=prefix, + mqtt_tls=mqtt_tls, database_url=database_url, webhook_dispatcher=webhook_dispatcher, ) @@ -254,6 +266,7 @@ def run_cmd(ctx: click.Context) -> None: mqtt_username=ctx.obj["mqtt_username"], mqtt_password=ctx.obj["mqtt_password"], prefix=ctx.obj["prefix"], + mqtt_tls=ctx.obj["mqtt_tls"], database_url=ctx.obj["database_url"], log_level=ctx.obj["log_level"], data_home=ctx.obj["data_home"], diff --git a/src/meshcore_hub/collector/subscriber.py b/src/meshcore_hub/collector/subscriber.py index 0b4e4e2..eb01065 100644 --- a/src/meshcore_hub/collector/subscriber.py +++ b/src/meshcore_hub/collector/subscriber.py @@ -293,6 +293,7 @@ def create_subscriber( mqtt_username: Optional[str] = None, mqtt_password: Optional[str] = None, mqtt_prefix: str = "meshcore", + mqtt_tls: bool = False, database_url: str = "sqlite:///./meshcore.db", webhook_dispatcher: Optional["WebhookDispatcher"] = None, ) -> Subscriber: @@ -304,6 +305,7 @@ def create_subscriber( mqtt_username: MQTT username mqtt_password: MQTT password mqtt_prefix: MQTT topic prefix + mqtt_tls: Enable TLS/SSL for MQTT connection database_url: Database connection URL webhook_dispatcher: Optional webhook dispatcher for event forwarding @@ -319,6 +321,7 @@ def create_subscriber( password=mqtt_password, prefix=mqtt_prefix, client_id=f"meshcore-collector-{unique_id}", + tls=mqtt_tls, ) mqtt_client = MQTTClient(mqtt_config) @@ -342,6 +345,7 @@ def run_collector( mqtt_username: Optional[str] = None, mqtt_password: Optional[str] = None, mqtt_prefix: str = "meshcore", + mqtt_tls: bool = False, database_url: str = "sqlite:///./meshcore.db", webhook_dispatcher: Optional["WebhookDispatcher"] = None, ) -> None: @@ -353,6 +357,7 @@ def run_collector( mqtt_username: MQTT username mqtt_password: MQTT password mqtt_prefix: MQTT topic prefix + mqtt_tls: Enable TLS/SSL for MQTT connection database_url: Database connection URL webhook_dispatcher: Optional webhook dispatcher for event forwarding """ @@ -362,6 +367,7 @@ def run_collector( mqtt_username=mqtt_username, mqtt_password=mqtt_password, mqtt_prefix=mqtt_prefix, + mqtt_tls=mqtt_tls, database_url=database_url, webhook_dispatcher=webhook_dispatcher, ) diff --git a/src/meshcore_hub/common/config.py b/src/meshcore_hub/common/config.py index 1b936b9..42942fa 100644 --- a/src/meshcore_hub/common/config.py +++ b/src/meshcore_hub/common/config.py @@ -52,6 +52,9 @@ class CommonSettings(BaseSettings): default=None, description="MQTT password (optional)" ) mqtt_prefix: str = Field(default="meshcore", description="MQTT topic prefix") + mqtt_tls: bool = Field( + default=False, description="Enable TLS/SSL for MQTT connection" + ) class InterfaceSettings(CommonSettings): diff --git a/src/meshcore_hub/common/mqtt.py b/src/meshcore_hub/common/mqtt.py index 82b80c2..fcc9f90 100644 --- a/src/meshcore_hub/common/mqtt.py +++ b/src/meshcore_hub/common/mqtt.py @@ -23,6 +23,7 @@ class MQTTConfig: client_id: Optional[str] = None keepalive: int = 60 clean_session: bool = True + tls: bool = False class TopicBuilder: @@ -131,6 +132,11 @@ class MQTTClient: self._connected = False self._message_handlers: dict[str, list[MessageHandler]] = {} + # Set up TLS if enabled + if config.tls: + self._client.tls_set() + logger.debug("TLS/SSL enabled for MQTT connection") + # Set up authentication if provided if config.username: self._client.username_pw_set(config.username, config.password) @@ -344,6 +350,7 @@ def create_mqtt_client( password: Optional[str] = None, prefix: str = "meshcore", client_id: Optional[str] = None, + tls: bool = False, ) -> MQTTClient: """Create and configure an MQTT client. @@ -354,6 +361,7 @@ def create_mqtt_client( password: MQTT password (optional) prefix: Topic prefix client_id: Client identifier (optional) + tls: Enable TLS/SSL connection (optional) Returns: Configured MQTTClient instance @@ -365,5 +373,6 @@ def create_mqtt_client( password=password, prefix=prefix, client_id=client_id, + tls=tls, ) return MQTTClient(config) diff --git a/src/meshcore_hub/interface/cli.py b/src/meshcore_hub/interface/cli.py index 7ec191f..46a47ed 100644 --- a/src/meshcore_hub/interface/cli.py +++ b/src/meshcore_hub/interface/cli.py @@ -93,6 +93,13 @@ def interface() -> None: envvar="MQTT_PREFIX", help="MQTT topic prefix", ) +@click.option( + "--mqtt-tls", + is_flag=True, + default=False, + envvar="MQTT_TLS", + help="Enable TLS/SSL for MQTT connection", +) @click.option( "--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), @@ -112,6 +119,7 @@ def run( mqtt_username: str | None, mqtt_password: str | None, prefix: str, + mqtt_tls: bool, log_level: str, ) -> None: """Run the interface component. @@ -153,6 +161,7 @@ def run( mqtt_username=mqtt_username, mqtt_password=mqtt_password, mqtt_prefix=prefix, + mqtt_tls=mqtt_tls, ) elif mode_upper == "SENDER": from meshcore_hub.interface.sender import run_sender @@ -168,6 +177,7 @@ def run( mqtt_username=mqtt_username, mqtt_password=mqtt_password, mqtt_prefix=prefix, + mqtt_tls=mqtt_tls, ) else: click.echo(f"Unknown mode: {mode}", err=True) @@ -245,6 +255,13 @@ def run( envvar="MQTT_PREFIX", help="MQTT topic prefix", ) +@click.option( + "--mqtt-tls", + is_flag=True, + default=False, + envvar="MQTT_TLS", + help="Enable TLS/SSL for MQTT connection", +) def receiver( port: str, baud: int, @@ -256,6 +273,7 @@ def receiver( mqtt_username: str | None, mqtt_password: str | None, prefix: str, + mqtt_tls: bool, ) -> None: """Run interface in RECEIVER mode. @@ -280,6 +298,7 @@ def receiver( mqtt_username=mqtt_username, mqtt_password=mqtt_password, mqtt_prefix=prefix, + mqtt_tls=mqtt_tls, ) @@ -354,6 +373,13 @@ def receiver( envvar="MQTT_PREFIX", help="MQTT topic prefix", ) +@click.option( + "--mqtt-tls", + is_flag=True, + default=False, + envvar="MQTT_TLS", + help="Enable TLS/SSL for MQTT connection", +) def sender( port: str, baud: int, @@ -365,6 +391,7 @@ def sender( mqtt_username: str | None, mqtt_password: str | None, prefix: str, + mqtt_tls: bool, ) -> None: """Run interface in SENDER mode. @@ -389,4 +416,5 @@ def sender( mqtt_username=mqtt_username, mqtt_password=mqtt_password, mqtt_prefix=prefix, + mqtt_tls=mqtt_tls, ) diff --git a/src/meshcore_hub/interface/receiver.py b/src/meshcore_hub/interface/receiver.py index 08b15fb..2383ee5 100644 --- a/src/meshcore_hub/interface/receiver.py +++ b/src/meshcore_hub/interface/receiver.py @@ -290,6 +290,7 @@ def create_receiver( mqtt_username: Optional[str] = None, mqtt_password: Optional[str] = None, mqtt_prefix: str = "meshcore", + mqtt_tls: bool = False, ) -> Receiver: """Create a configured receiver instance. @@ -304,6 +305,7 @@ def create_receiver( mqtt_username: MQTT username mqtt_password: MQTT password mqtt_prefix: MQTT topic prefix + mqtt_tls: Enable TLS/SSL for MQTT connection Returns: Configured Receiver instance @@ -324,6 +326,7 @@ def create_receiver( password=mqtt_password, prefix=mqtt_prefix, client_id=f"meshcore-receiver-{device.public_key[:12] if device.public_key else 'unknown'}", + tls=mqtt_tls, ) mqtt_client = MQTTClient(mqtt_config) @@ -341,6 +344,7 @@ def run_receiver( mqtt_username: Optional[str] = None, mqtt_password: Optional[str] = None, mqtt_prefix: str = "meshcore", + mqtt_tls: bool = False, ) -> None: """Run the receiver (blocking). @@ -357,6 +361,7 @@ def run_receiver( mqtt_username: MQTT username mqtt_password: MQTT password mqtt_prefix: MQTT topic prefix + mqtt_tls: Enable TLS/SSL for MQTT connection """ receiver = create_receiver( port=port, @@ -369,6 +374,7 @@ def run_receiver( mqtt_username=mqtt_username, mqtt_password=mqtt_password, mqtt_prefix=mqtt_prefix, + mqtt_tls=mqtt_tls, ) # Set up signal handlers diff --git a/src/meshcore_hub/interface/sender.py b/src/meshcore_hub/interface/sender.py index 949da3a..b23947f 100644 --- a/src/meshcore_hub/interface/sender.py +++ b/src/meshcore_hub/interface/sender.py @@ -293,6 +293,7 @@ def create_sender( mqtt_username: Optional[str] = None, mqtt_password: Optional[str] = None, mqtt_prefix: str = "meshcore", + mqtt_tls: bool = False, ) -> Sender: """Create a configured sender instance. @@ -307,6 +308,7 @@ def create_sender( mqtt_username: MQTT username mqtt_password: MQTT password mqtt_prefix: MQTT topic prefix + mqtt_tls: Enable TLS/SSL for MQTT connection Returns: Configured Sender instance @@ -327,6 +329,7 @@ def create_sender( password=mqtt_password, prefix=mqtt_prefix, client_id=f"meshcore-sender-{device.public_key[:12] if device.public_key else 'unknown'}", + tls=mqtt_tls, ) mqtt_client = MQTTClient(mqtt_config) @@ -344,6 +347,7 @@ def run_sender( mqtt_username: Optional[str] = None, mqtt_password: Optional[str] = None, mqtt_prefix: str = "meshcore", + mqtt_tls: bool = False, ) -> None: """Run the sender (blocking). @@ -360,6 +364,7 @@ def run_sender( mqtt_username: MQTT username mqtt_password: MQTT password mqtt_prefix: MQTT topic prefix + mqtt_tls: Enable TLS/SSL for MQTT connection """ sender = create_sender( port=port, @@ -372,6 +377,7 @@ def run_sender( mqtt_username=mqtt_username, mqtt_password=mqtt_password, mqtt_prefix=mqtt_prefix, + mqtt_tls=mqtt_tls, ) # Set up signal handlers