van-router-mqtt: broker credentials in a separate 0600 file; log refused connects
config.json is re-rendered every deploy, so credentials go in /etc/van-router-mqtt/credentials.json (seeded empty if absent, never overwritten). The service logs MQTT connect/refuse/disconnect state changes, and --post-check warns when the broker refuses the service. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
This commit is contained in:
@@ -29,8 +29,47 @@ import time
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
CONFIG = "/etc/van-router-mqtt/config.json"
|
||||
with open(sys.argv[1] if len(sys.argv) > 1 else CONFIG) as f:
|
||||
cfg = json.load(f)
|
||||
CONFIG_PATH = sys.argv[1] if len(sys.argv) > 1 else CONFIG
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(msg, flush=True)
|
||||
|
||||
|
||||
def credentials_path(config_path):
|
||||
return os.path.join(os.path.dirname(config_path), "credentials.json")
|
||||
|
||||
|
||||
def load_credentials(config_path, cfg):
|
||||
"""Merge username/password from credentials.json (next to the config file)
|
||||
into cfg. The config is re-rendered on every deploy, so secrets live in
|
||||
this separate 0600 file. Never raises; never logs the values."""
|
||||
path = credentials_path(config_path)
|
||||
if not os.path.exists(path):
|
||||
return cfg
|
||||
try:
|
||||
with open(path) as f:
|
||||
cred = json.load(f)
|
||||
if not isinstance(cred, dict):
|
||||
raise ValueError("not a JSON object")
|
||||
except (OSError, ValueError) as e:
|
||||
log(f"credentials file {path} ignored ({type(e).__name__}); connecting anonymously")
|
||||
return cfg
|
||||
if not cred.get("username"):
|
||||
log(f"credentials file {path} has no username; connecting anonymously")
|
||||
return cfg
|
||||
cfg["username"] = cred["username"]
|
||||
cfg["password"] = cred.get("password", "")
|
||||
return cfg
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open(CONFIG_PATH) as f:
|
||||
cfg = json.load(f)
|
||||
cfg = load_credentials(CONFIG_PATH, cfg)
|
||||
else: # imported (tests): no config file, no side effects
|
||||
import collections
|
||||
cfg = collections.defaultdict(str)
|
||||
|
||||
APS = [
|
||||
{"id": "5g", "iface": cfg["wifi_5g_iface"], "unit": "hostapd", "band": "5GHz"},
|
||||
@@ -60,10 +99,6 @@ DEVICE_INFO = {"identifiers": [device_id], "name": "Van Router", "manufacturer":
|
||||
WAN_NAMES = ("wifi", "starlink", "cellular")
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(msg, flush=True)
|
||||
|
||||
|
||||
def sh(args, timeout=10):
|
||||
try:
|
||||
return subprocess.run(args, capture_output=True, text=True, timeout=timeout)
|
||||
@@ -523,11 +558,58 @@ def on_message(mqc, userdata, msg):
|
||||
publish_state(mqc) # reflect the result immediately, don't wait for next poll
|
||||
|
||||
|
||||
REFUSAL_REASONS = {
|
||||
1: "unacceptable protocol version", 2: "client identifier rejected",
|
||||
3: "server unavailable", 4: "bad username or password", 5: "not authorised",
|
||||
134: "bad username or password", 135: "not authorised",
|
||||
}
|
||||
_mqtt_state = None # None | "connected" | "refused" | "down": log changes only
|
||||
|
||||
|
||||
def refusal_reason(rc):
|
||||
n = getattr(rc, "value", rc)
|
||||
if n in REFUSAL_REASONS:
|
||||
return REFUSAL_REASONS[n]
|
||||
return f"unknown reason, rc={n}"
|
||||
|
||||
|
||||
def note_connect(rc):
|
||||
"""rc: paho reason code (int-like) of a CONNACK. Logs on state change only."""
|
||||
global _mqtt_state
|
||||
if getattr(rc, "value", rc) != 0:
|
||||
if _mqtt_state != "refused":
|
||||
log(f"MQTT connect refused by broker ({refusal_reason(rc)}) — if the broker "
|
||||
f"requires authentication, set username/password in {credentials_path(CONFIG_PATH)}")
|
||||
_mqtt_state = "refused"
|
||||
else:
|
||||
if _mqtt_state != "connected":
|
||||
log(f"MQTT connected to {broker}:{mqtt_port}")
|
||||
_mqtt_state = "connected"
|
||||
|
||||
|
||||
def note_disconnect(rc):
|
||||
global _mqtt_state
|
||||
if _mqtt_state == "connected":
|
||||
log(f"MQTT disconnected (rc={getattr(rc, 'value', rc)})")
|
||||
if _mqtt_state != "refused":
|
||||
_mqtt_state = "down"
|
||||
|
||||
|
||||
def on_connect(mqc, userdata, flags, reason_code, properties=None):
|
||||
note_connect(reason_code)
|
||||
|
||||
|
||||
def on_disconnect(mqc, userdata, disconnect_flags, reason_code, properties=None):
|
||||
note_disconnect(reason_code)
|
||||
|
||||
|
||||
def main():
|
||||
mqc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=cfg.get("client_id", "van-router-mqtt"))
|
||||
if cfg.get("username"):
|
||||
mqc.username_pw_set(cfg["username"], cfg.get("password", ""))
|
||||
mqc.on_message = on_message
|
||||
mqc.on_connect = on_connect
|
||||
mqc.on_disconnect = on_disconnect
|
||||
mqc.connect(broker, mqtt_port, keepalive=60)
|
||||
for topic in COMMANDS:
|
||||
mqc.subscribe(topic)
|
||||
|
||||
Reference in New Issue
Block a user