doorbell: add ntfy token auth to listener poll + authenticated kuma canary
- doorbell-listener.py: poll private ntfy topics with auth= (Bearer token) to stop 403/429; add probe-tag routing to DoorbellProbe - scripts/kuma-heartbeat.sh: publish health canary to ntfy.sh with Authorization: Bearer (was anonymous -> rate limited -> false down in Kuma); read token from .secrets/ntfy-token
This commit is contained in:
@@ -26,13 +26,18 @@ Env vars (all optional):
|
|||||||
POLL_INTERVAL seconds between polls, default 30
|
POLL_INTERVAL seconds between polls, default 30
|
||||||
STATE_DIR default "/data/doorbell-listener"
|
STATE_DIR default "/data/doorbell-listener"
|
||||||
NTFY_TOPICS comma-separated override, default "ALERT_klubhaus_topic_test,ALERT_klubhaus_topic"
|
NTFY_TOPICS comma-separated override, default "ALERT_klubhaus_topic_test,ALERT_klubhaus_topic"
|
||||||
|
NTFY_TOKEN ntfy access token (tk_...). Adds the auth= query param the
|
||||||
|
ntfy.sh topics require; without it, polling the private
|
||||||
|
topics gets 403/429 rate-limited by ntfy.sh.
|
||||||
"""
|
"""
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import urllib.parse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
@@ -40,6 +45,7 @@ import requests
|
|||||||
|
|
||||||
DEFAULT_TOPICS = "ALERT_klubhaus_topic_test,ALERT_klubhaus_topic"
|
DEFAULT_TOPICS = "ALERT_klubhaus_topic_test,ALERT_klubhaus_topic"
|
||||||
NTFY_TOPICS = [t.strip() for t in os.environ.get("NTFY_TOPICS", DEFAULT_TOPICS).split(",") if t.strip()]
|
NTFY_TOPICS = [t.strip() for t in os.environ.get("NTFY_TOPICS", DEFAULT_TOPICS).split(",") if t.strip()]
|
||||||
|
NTFY_TOKEN = os.environ.get("NTFY_TOKEN", "")
|
||||||
|
|
||||||
MQTT_HOST = os.environ.get("MQTT_HOST", "mosquitto")
|
MQTT_HOST = os.environ.get("MQTT_HOST", "mosquitto")
|
||||||
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
|
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
|
||||||
@@ -72,6 +78,14 @@ def log(msg: str) -> None:
|
|||||||
print(msg, flush=True)
|
print(msg, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def auth_param() -> str:
|
||||||
|
"""Raw base64 of 'Bearer <token>' with no trailing '=' — mirrors the
|
||||||
|
doorbell frontend's buildAuthParam(). Empty string when no token set."""
|
||||||
|
if not NTFY_TOKEN:
|
||||||
|
return ""
|
||||||
|
return base64.b64encode(f"Bearer {NTFY_TOKEN}".encode()).decode().rstrip("=")
|
||||||
|
|
||||||
|
|
||||||
def last_id_path(topic: str) -> Path:
|
def last_id_path(topic: str) -> Path:
|
||||||
safe = topic.replace("/", "_").replace(" ", "_")
|
safe = topic.replace("/", "_").replace(" ", "_")
|
||||||
return STATE_DIR / f"last_id_{safe}"
|
return STATE_DIR / f"last_id_{safe}"
|
||||||
@@ -112,6 +126,8 @@ def start_flash(client: mqtt.Client, count: int, interval: float, topic: str) ->
|
|||||||
def poll_topic(client: mqtt.Client, topic: str, last_id: str) -> str:
|
def poll_topic(client: mqtt.Client, topic: str, last_id: str) -> str:
|
||||||
"""Poll one topic. Returns the latest message id seen (or last_id)."""
|
"""Poll one topic. Returns the latest message id seen (or last_id)."""
|
||||||
url = f"https://ntfy.sh/{topic}/json?poll=1"
|
url = f"https://ntfy.sh/{topic}/json?poll=1"
|
||||||
|
if auth_param():
|
||||||
|
url += f"&auth={urllib.parse.quote(auth_param())}"
|
||||||
if last_id:
|
if last_id:
|
||||||
url += f"&since={last_id}"
|
url += f"&since={last_id}"
|
||||||
log(f"[{topic}] polling {url}")
|
log(f"[{topic}] polling {url}")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ CONTAINER="${DB_CONTAINER:-doorbell-listener}"
|
|||||||
|
|
||||||
[ -s "$TOKEN_FILE" ] || exit 1
|
[ -s "$TOKEN_FILE" ] || exit 1
|
||||||
TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
|
TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
|
||||||
|
NTFY_TOKEN="${NTFY_TOKEN:-$(tr -d '[:space:]' < /root/nr-flow-validator/infra/.secrets/ntfy-token 2>/dev/null || true)}"
|
||||||
|
|
||||||
reason=""
|
reason=""
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ else
|
|||||||
# caused false "down" alerts. The subscribe/deliver leg is continuously
|
# caused false "down" alerts. The subscribe/deliver leg is continuously
|
||||||
# exercised by the production listener consuming real alerts.
|
# exercised by the production listener consuming real alerts.
|
||||||
url="https://ntfy.sh/${CANARY_TOPIC}"
|
url="https://ntfy.sh/${CANARY_TOPIC}"
|
||||||
if ! curl -fsS -m 10 -o /dev/null -X POST "$url" -d "canary-$(date +%s)" 2>/dev/null; then
|
if ! curl -fsS -m 10 -o /dev/null -X POST "$url" -H "Authorization: Bearer ${NTFY_TOKEN}" -d "canary-$(date +%s)" 2>/dev/null; then
|
||||||
reason="${reason}ntfy_publish_failed"
|
reason="${reason}ntfy_publish_failed"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user