TLS-Unterstuetzung fuer MQTT
MqttBridge(ssl=True) / mqtt_config.SSL: umqtt.simple.MQTTClient wird mit
ssl_params={"server_hostname": ..., "cert_reqs": CERT_NONE} aufgebaut --
SNI fuer virtuelle Hosts hinter einem Reverse-Proxy, aber ohne
Zertifikatspruefung (kein CA-Bundle auf dem Geraet). Verschluesselt die
Zugangsdaten auf der Leitung, schuetzt aber nicht vor einem aktiven
Man-in-the-Middle.
Anlass: Der Broker wechselte auf einen Host, der nur noch TLS anbot --
Klartext gegen den TLS-Port endete in ECONNRESET.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zTSXLK4w32PoCmFgSzp68
This commit is contained in:
@@ -538,6 +538,12 @@ cp mqtt_config_example.py mqtt_config.py # Broker, Port, ggf. Benutzer/Passw
|
|||||||
**Fehlt die Datei, läuft die Anzeige einfach ohne MQTT** — `deploy.sh` weist nur
|
**Fehlt die Datei, läuft die Anzeige einfach ohne MQTT** — `deploy.sh` weist nur
|
||||||
darauf hin und kopiert sie nicht.
|
darauf hin und kopiert sie nicht.
|
||||||
|
|
||||||
|
**TLS:** `mqtt_config.SSL = True` (üblicherweise mit `PORT = 8883`) verschlüsselt
|
||||||
|
die Verbindung, prüft aber **nicht** das Serverzertifikat — auf dem Gerät liegt
|
||||||
|
kein CA-Bundle. Schützt die Zugangsdaten auf der Leitung, nicht vor einem
|
||||||
|
aktiven Man-in-the-Middle. Details in `mqtt_config_example.py` und
|
||||||
|
`mqtt.py` (`_neuer_client`).
|
||||||
|
|
||||||
### Topics
|
### Topics
|
||||||
|
|
||||||
Empfangen (`<PREFIX>/set/<name>`):
|
Empfangen (`<PREFIX>/set/<name>`):
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ kehrt es sofort zurueck.
|
|||||||
|
|
||||||
Ohne mqtt_config.py (oder ohne umqtt) gibt connect_from_config() None zurueck --
|
Ohne mqtt_config.py (oder ohne umqtt) gibt connect_from_config() None zurueck --
|
||||||
die Anzeige laeuft dann ohne MQTT weiter.
|
die Anzeige laeuft dann ohne MQTT weiter.
|
||||||
|
|
||||||
|
TLS (mqtt_config.SSL = True, ueblicherweise mit PORT = 8883): Serverzertifikat
|
||||||
|
wird NICHT geprueft (kein CA-Bundle auf dem Geraet) -- schuetzt die Zugangsdaten
|
||||||
|
auf der Leitung, nicht vor einem aktiven Man-in-the-Middle. Siehe _neuer_client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
@@ -61,7 +65,8 @@ class MqttBridge:
|
|||||||
|
|
||||||
def __init__(self, broker=None, port=1883, user="", password="",
|
def __init__(self, broker=None, port=1883, user="", password="",
|
||||||
prefix="grossanzeige", client_id="grossanzeige",
|
prefix="grossanzeige", client_id="grossanzeige",
|
||||||
client=None, log=None, on_change=None, on_ota=None):
|
client=None, log=None, on_change=None, on_ota=None,
|
||||||
|
ssl=False):
|
||||||
self._broker = broker
|
self._broker = broker
|
||||||
self._port = port
|
self._port = port
|
||||||
self._user = user
|
self._user = user
|
||||||
@@ -72,6 +77,7 @@ class MqttBridge:
|
|||||||
self._log = log or (lambda *a: None)
|
self._log = log or (lambda *a: None)
|
||||||
self._on_change = on_change # wird nach jeder Aenderung gerufen
|
self._on_change = on_change # wird nach jeder Aenderung gerufen
|
||||||
self._on_ota = on_ota # wird bei cmd/ota gerufen (Payload = URL)
|
self._on_ota = on_ota # wird bei cmd/ota gerufen (Payload = URL)
|
||||||
|
self._ssl = ssl
|
||||||
|
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self._retry = RETRY_START
|
self._retry = RETRY_START
|
||||||
@@ -92,10 +98,20 @@ class MqttBridge:
|
|||||||
def _neuer_client(self):
|
def _neuer_client(self):
|
||||||
if MQTTClient is None:
|
if MQTTClient is None:
|
||||||
raise OSError("umqtt.simple nicht verfuegbar")
|
raise OSError("umqtt.simple nicht verfuegbar")
|
||||||
return MQTTClient(self._client_id, self._broker, port=self._port,
|
kwargs = dict(port=self._port, user=self._user or None,
|
||||||
user=self._user or None,
|
password=self._password or None, keepalive=60)
|
||||||
password=self._password or None,
|
if self._ssl:
|
||||||
keepalive=60)
|
# server_hostname fuers SNI -- ohne das weisen manche TLS-Server
|
||||||
|
# (virtuelle Hosts hinter einem Reverse-Proxy) die Verbindung ab.
|
||||||
|
# CERT_NONE, weil auf dem Geraet kein CA-Bundle liegt: die Leitung
|
||||||
|
# ist verschluesselt, aber ein aktiver Man-in-the-Middle waere
|
||||||
|
# nicht nachweisbar. Reicht fuer den hiesigen Zweck (Zugangsdaten
|
||||||
|
# nicht im Klartext), ist aber keine volle Zertifikatspruefung.
|
||||||
|
import ssl as _ssl
|
||||||
|
kwargs["ssl"] = True
|
||||||
|
kwargs["ssl_params"] = {"server_hostname": self._broker,
|
||||||
|
"cert_reqs": _ssl.CERT_NONE}
|
||||||
|
return MQTTClient(self._client_id, self._broker, **kwargs)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Verbinden, Set-Topics abonnieren, online melden.
|
"""Verbinden, Set-Topics abonnieren, online melden.
|
||||||
@@ -288,6 +304,7 @@ def connect_from_config(log=None, on_change=None, on_ota=None):
|
|||||||
log=log,
|
log=log,
|
||||||
on_change=on_change,
|
on_change=on_change,
|
||||||
on_ota=on_ota,
|
on_ota=on_ota,
|
||||||
|
ssl=getattr(mqtt_config, "SSL", False),
|
||||||
)
|
)
|
||||||
bridge.connect() # scheitert es, versucht check() es spaeter erneut
|
bridge.connect() # scheitert es, versucht check() es spaeter erneut
|
||||||
return bridge
|
return bridge
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ Fehlt die Datei, laeuft die Anzeige einfach ohne MQTT weiter.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
BROKER = "192.168.178.30" # IP oder Hostname des Brokers
|
BROKER = "192.168.178.30" # IP oder Hostname des Brokers
|
||||||
PORT = 1883 # 1883 = unverschluesselt, 8883 = TLS (nicht unterstuetzt)
|
PORT = 1883 # 1883 = unverschluesselt, 8883 = ueblicher TLS-Port
|
||||||
|
|
||||||
|
# True fuer TLS (dann meist PORT = 8883). Das Serverzertifikat wird dabei NICHT
|
||||||
|
# geprueft (kein CA-Bundle auf dem Geraet) -- schuetzt die Zugangsdaten auf der
|
||||||
|
# Leitung, aber nicht vor einem aktiven Man-in-the-Middle. Siehe mqtt.py.
|
||||||
|
SSL = False
|
||||||
|
|
||||||
# Leer lassen, wenn der Broker keine Anmeldung verlangt.
|
# Leer lassen, wenn der Broker keine Anmeldung verlangt.
|
||||||
USER = ""
|
USER = ""
|
||||||
|
|||||||
@@ -132,6 +132,46 @@ class TestVerbinden(MqttTest):
|
|||||||
self.assertFalse(self.bridge.connected)
|
self.assertFalse(self.bridge.connected)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeMQTTClient:
|
||||||
|
"""Ersatz fuer umqtt.simple.MQTTClient selbst (nicht fuer eine Instanz) --
|
||||||
|
zeichnet nur auf, mit welchen Argumenten mqtt._neuer_client() ihn baut."""
|
||||||
|
|
||||||
|
aufrufe = []
|
||||||
|
|
||||||
|
def __init__(self, client_id, server, **kwargs):
|
||||||
|
FakeMQTTClient.aufrufe.append((client_id, server, kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
class TestNeuerClient(unittest.TestCase):
|
||||||
|
"""mqtt.MqttBridge._neuer_client(): baut den echten MQTTClient auf (kein
|
||||||
|
FakeClient injiziert). Schwerpunkt TLS -- siehe mqtt_config_example.py."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
FakeMQTTClient.aufrufe = []
|
||||||
|
self._alt = mqtt.MQTTClient
|
||||||
|
mqtt.MQTTClient = FakeMQTTClient
|
||||||
|
self.addCleanup(lambda: setattr(mqtt, "MQTTClient", self._alt))
|
||||||
|
|
||||||
|
def test_ohne_ssl_werden_keine_ssl_argumente_gesetzt(self):
|
||||||
|
bridge = mqtt.MqttBridge(broker="b", port=1883)
|
||||||
|
bridge._neuer_client()
|
||||||
|
_, _, kwargs = FakeMQTTClient.aufrufe[0]
|
||||||
|
self.assertNotIn("ssl", kwargs)
|
||||||
|
self.assertNotIn("ssl_params", kwargs)
|
||||||
|
|
||||||
|
def test_ssl_setzt_server_hostname_und_cert_none(self):
|
||||||
|
import ssl as echtes_ssl
|
||||||
|
bridge = mqtt.MqttBridge(broker="mqtt.example.org", port=8883, ssl=True)
|
||||||
|
bridge._neuer_client()
|
||||||
|
client_id, server, kwargs = FakeMQTTClient.aufrufe[0]
|
||||||
|
self.assertEqual(server, "mqtt.example.org")
|
||||||
|
self.assertTrue(kwargs["ssl"])
|
||||||
|
self.assertEqual(kwargs["ssl_params"]["server_hostname"],
|
||||||
|
"mqtt.example.org")
|
||||||
|
self.assertEqual(kwargs["ssl_params"]["cert_reqs"],
|
||||||
|
echtes_ssl.CERT_NONE)
|
||||||
|
|
||||||
|
|
||||||
class TestEmpfang(MqttTest):
|
class TestEmpfang(MqttTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user