# ntfy-bridge — Architecture ## Philosophie ntfy-bridge est un **daemon HTTP léger** écrit en V (~2000 lignes) qui compile en un seul binaire natif statique. Il écoute des webhooks, se connecte au socket Docker, exécute des plugins externes, et poll des endpoints HTTP — puis transforme chaque événement en notification Ntfy formatée. **Principes :** - **Un binaire, zéro runtime** — pas de Node, Python, JVM ou conteneur obligatoire - **Config-driven** — tout passe par le fichier YAML, pas de recompilation - **Fail-safe** — un échec réseau vers Ntfy ne crashe pas le daemon, retry avec backoff - **Silencieux par défaut** — ne spamme pas, utilise les bons niveaux de priorité Ntfy (1-5) - **Extensible** — ajouter une source = ajouter un fichier dans `sources/` + une route webhook - **Zéro dépendance externe** — 100% stdlib V (`net.http`, `json`, `yaml`, `time`, `os`, `flag`) ## Architecture globale ``` ┌─────────────────────────────────────────────────────────────┐ │ ntfy-bridge │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ │ │ │ HTTP │ │ Docker │ │ HTTP │ │ Cron/ │ │ Plugin │ │ │ │ Server │ │ Watcher │ │ Poller │ │ Generic │ │ Runner │ │ │ │ :9090 │ │ goroutine│ │ goroutine│ │ Receiver │ │ goroutine│ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ └────┬─────┘ │ │ │ │ │ │ │ │ │ └──────────────┼──────────────┼──────────────┼──────────────┘ │ │ ▼ ▼ │ │ ┌──────────────────────────┐ │ │ │ Event Pipeline │ │ │ │ ┌────────┐ ┌────────┐ │ │ │ │ │Parse & │ │Dedup & │ │ │ │ │ │Format │──│RateLim │ │ │ │ │ └────────┘ └────────┘ │ │ │ └────────────┬─────────────┘ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ Ntfy Publisher (retry) │ │ │ │ POST / │ │ │ │ 3 tentatives, backoff │ │ │ └──────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ## Flux de données ### 1. Webhook entrant (Gitea, Uptime Kuma, Cron, Generic) ``` POST /webhooks/gitea-flowdeck │ ▼ 1. O(1) lookup dans webhook_routes (map[string]WebhookRoute) 2. Verify HMAC-SHA256 signature (si configuré, global) 3. Dispatcher vers le transformer approprié (sources/transform_*.v) 4. Parse JSON → Event struct 5. Appliquer priority_map 6. Appliquer template de message (via render_source_template) 7. Pipeline publish: apply_defaults → dedup.allow → ntfy.publish 8. POST vers Ntfy avec retry (3 tentatives, backoff 1s/2s/4s) ``` ### 2. Docker watcher (socket) ``` goroutine docker_watch_loop (via start_background_tasks) │ ▼ 1. Connexion au socket Docker Unix (/var/run/docker.sock) 2. Stream events via GET /events?filters={"die":true,"oom":true,...} 3. Filtrer par événements configurés (die, oom, health_status) 4. Extraire container_name, image, exit_code via transform_docker_event 5. Appliquer template 6. Pipeline publish → Ntfy ``` ### 3. HTTP Poller ``` goroutine http_poll_loop (via start_background_tasks, toutes les N secondes) │ ▼ 1. GET avec timeout configuré 2. Si status ≠ expected → Event(priority=configurée, tags=["x"]) 3. Si OK et précédemment DOWN → Event(priority=1, tags=["white_check_mark"]) 4. Sinon → rien (pas de notification si tout va bien) 5. State machine: poll_state[url] = is_up → notification uniquement au changement ``` ### 4. Cron receiver ``` POST /webhooks/cron-disk │ ▼ 1. Body = texte brut du script cron 2. Pass-through vers Ntfy (transform_cron → Event) 3. Le script cron contrôle son propre format ``` ### 5. Plugin runner (v0.6) ``` goroutine plugin_loop (via start_background_tasks, toutes les 60s) │ ▼ 1. Exécute le binaire configuré (os.execute) 2. Lit stdout → parse JSON → Event struct 3. Exit 0 = publier, exit ≠ 0 = skip (pas de notif) 4. Supporte priority, tags, message, click_url, actions 5. Pipeline publish → Ntfy ``` ### 6. ACL pipeline (v0.6) ``` POST /webhooks/* │ ▼ 1. Extraire IP client (X-Forwarded-For > X-Real-IP) 2. Extraire Bearer token (Authorization header) 3. Si ACL configurée sur le webhook → valider IP et/ou token 4. Si échec → 403 Forbidden 5. Sinon → continuer le pipeline normal ``` ## Modèle de données ```v // config.v struct ServerConfig { url string // URL du serveur Ntfy auth_token string // Token d'auth (optionnel, override via NTFY_TOKEN env) listen string // Adresse d'écoute HTTP (:9090) hmac_secret string // Secret partagé HMAC-SHA256 (override via NTFY_HMAC_SECRET env) } struct DedupConfig { enabled bool ttl_seconds int // TTL du cache de déduplication rate_limit RateLimitConfig // max_per_minute, max_per_source } // sources/event.v struct Event { source string // "gitea", "docker", "uptime_kuma", "http_poll", "cron", "generic", "plugin" name string // nom de la source configurée topic string // topic Ntfy cible priority int // 1-5 tags []string message string // message formaté final raw string // payload brut (pour debug) click_url string // URL ouverte au clic sur la notif (v0.5) actions []Action // boutons d'action Ntfy (v0.5) } ``` Les types de sources (GiteaSource, UptimeKumaSource, DockerSource, HttpPollSource/HttpCheck, CronSource, GenericSource, PluginSource) et AclConfig sont définis dans `sources/event.v`. ## Pipeline de transformation Chaque source a une **free function** dans `sources/` : ```v // Exemple : sources/gitea.v pub fn transform_gitea(raw string, source GiteaSource) ?Event { ... } ``` Pas d'interface — les fonctions sont dispatchées via un `match route.kind` dans `server.v`. L'ajout d'une nouvelle source nécessite : 1. Un fichier `sources/nouveau.v` avec la `transform_*` function 2. Un type source dans `sources/event.v` 3. Une entrée dans `SourcesConfig` (dans `config.v`) 4. Une branche dans `build_webhook_routes` et `handle_webhook` ### Transformers inclus | Transformer | Entrée | Sortie | |---|---|---| | `transform_gitea` | JSON webhook Gitea | `🔨 [FlowDeck] bruno pushed: "Fix bug" (a3f2c1d)` | | `transform_uptime_kuma` | JSON webhook Kuma | `🚨 Gitea is DOWN — 503 — since 14:32` | | `transform_docker_event` | Docker event JSON | `🐳 flowdeck exited on docker-prod-1` | | `transform_http_down/up` | HTTP response | `❌ og.dracodev.net/health → timeout` | | `transform_cron` | Raw body | Pass-through, pas de transformation | | `transform_generic` | Raw body | Pass-through, pas de transformation | | `transform_plugin` | stdout d'un exécutable externe | JSON → Event (exit 0 = publish, ≠ 0 = skip) | ### Template engine `render_source_template(template, vars)` dans `sources/event.v` — remplacement simple `{variable}` → valeur. Supporté par Gitea, Uptime Kuma, Docker, HTTP poll. ``` template: "🐳 {container_name} → {status}\nImage: {image}\nHost: {host}" ``` ### Priorités Ntfy | Niveau | Usage | Exemple | |---|---|---| | 5 (urgent) | Service critique down, container OOM | Uptime Kuma DOWN | | 4 (high) | PR ouverte, container crash | Gitea pull_request, Docker die | | 3 (default) | Push, issue, activité normale | Gitea push | | 2 (low) | Info, succès | Service back UP | | 1 (min) | Debug, heartbeat | Health check OK | ## Déduplication et rate limiting - **Déduplication** : hash du message (FNV-1a-like) → cache avec TTL configurable (défaut: 5 min). Clé = `source:name:topic:hash(message)`. Un message identique dans la fenêtre TTL est ignoré. - **Rate limiting par topic** : `max_per_minute` notifications max par topic (défaut: 10). Fenêtre glissante de 60s. - **Rate limiting par source** : `max_per_source` notifications max par source configurée (défaut: 30). Fenêtre glissante de 60s. - **Nettoyage automatique** : les entrées expirées sont nettoyées à chaque appel `allow()`. ## Structure du projet ``` ntfy-bridge/ ├── README.md # Vue d'ensemble, quickstart ├── docs/ │ ├── ARCHITECTURE.md # Ce document │ ├── ROADMAP.md # Phases de développement │ ├── CONTRIBUTING.md # Guide de contribution │ └── WEBHOOK_GITEA.md # Guide config webhook Gitea ├── ntfy-bridge.example.yaml # Exemple de configuration complet ├── main.v # Entrypoint, parsing CLI, header ASCII ├── config.v # Chargement + validation YAML, env overrides ├── server.v # HTTP server, routing webhooks O(1), dashboard, API ├── ntfy.v # Client HTTP Ntfy (POST + retry backoff) ├── dedup.v # Déduplication + rate limiting (topic + source) ├── template.v # Application des defaults (priority, tags) ├── log.v # Logging structuré (human + JSON) ├── webhook.v # O(1) route dispatch (WebhookRoute map) ├── docker_watcher.v # Watcher socket Docker (Unix seulement) ├── filter_outgoing.v # Filtres avancés + outgoing webhooks + state persistence ├── metrics.v # Endpoint Prometheus /metrics ├── service.v # Installation/désinstallation de service (systemd, openrc, nssm) ├── docker_install.v # Génération de stack docker-compose ├── dashboard.html # Interface web : stats, status, historique ├── sources/ # Types Event + transformers │ ├── event.v # Types Event, Source, render_source_template │ ├── gitea.v # Transformer Gitea (push, PR, issue, release) │ ├── uptime_kuma.v # Transformer Uptime Kuma (state_map) │ ├── docker.v # Transformer Docker events │ ├── http_poll.v # Transformer HTTP poll (up/down) │ ├── cron.v # Transformer Cron (pass-through) │ ├── generic.v # Transformer Generic (pass-through) │ └── plugin.v # Transformer Plugin (exec stdout JSON) ├── *_test.v # Tests unitaires (config, gitea, uptime_kuma, dedup, filter_outgoing, metrics, acl) ├── scripts/ │ ├── example-cron-disk.sh # Exemple script cron → Ntfy │ ├── example-plugin-disk.sh # Exemple plugin disk check │ ├── install.sh # Script d'installation Linux one-liner │ └── install.ps1 # Script d'installation Windows PowerShell └── v.mod # Dépendances V (aucune dépendance externe) ``` ## Dépendances ```v // v.mod Module { name: 'ntfy-bridge' version: '0.7.0' dependencies: ['https://github.com/vlang/yaml'] // parsing YAML } ``` Le projet utilise la stdlib V + le module `yaml` : - `net.http` — serveur HTTP + client Ntfy - `x.json2` — parsing JSON - `yaml` — parsing YAML (module externe vlang/yaml) - `crypto.hmac`, `crypto.sha256`, `encoding.hex` — validation HMAC - `time`, `os`, `flag` — utilitaires - `net.unix` — socket Docker (Linux/macOS seulement, via `$if`) ## Endpoints HTTP | Méthode | Path | Description | |---------|------|-------------| | GET | `/` ou `/dashboard` | Dashboard HTML | | GET | `/health` | Health check → `{"status":"ok","uptime_seconds":N,"total_notifications":N}` | | GET | `/stats` | Stats par source → `{"total":N,"errors":N,"by_source":{...}}` | | GET | `/api/config` | Config résumée (safe, sans secrets) | | GET | `/api/status` | État des health checks HTTP poll | | GET | `/api/history` | 100 derniers événements | | GET | `/api/silence` | État du silence (actif/restant) | | POST | `/api/silence?duration=30m` | Activer le silence | | DELETE | `/api/silence` | Désactiver le silence | | GET | `/metrics` | Métriques Prometheus / OpenMetrics | | POST | `/webhooks/*` | Webhooks (Gitea, Uptime Kuma, Cron, Generic) | ## Déploiement ### Option A : Binaire nu + systemd ```ini # /etc/systemd/system/ntfy-bridge.service [Unit] Description=ntfy-bridge notification hub After=network.target [Service] ExecStart=/usr/local/bin/ntfy-bridge --quiet --config /etc/ntfy-bridge.yaml Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ``` ### Option B : Conteneur Docker ```bash # Quick start avec docker compose cp ntfy-bridge.example.yaml ntfy-bridge.yaml vim ntfy-bridge.yaml docker compose up -d ``` ```dockerfile # Dockerfile (build hôte + runtime Alpine minimal) FROM alpine:3.20 RUN apk add --no-cache ca-certificates tzdata curl COPY ntfy-bridge /usr/local/bin/ntfy-bridge RUN chmod +x /usr/local/bin/ntfy-bridge WORKDIR /etc/ntfy-bridge EXPOSE 9090 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:9090/health || exit 1 ENTRYPOINT ["/usr/local/bin/ntfy-bridge"] CMD ["--quiet", "--config", "/etc/ntfy-bridge/ntfy-bridge.yaml"] ``` ### Option C : Compilation croisée ```bash v -prod -os windows . -o ntfy-bridge.exe v -prod -os linux . -o ntfy-bridge ``` ## Sécurité - **ACLs par webhook** (v0.6) : restriction par IP (CIDR ou exacte) et/ou Bearer token. Validé avant HMAC. - **Signature webhooks** : vérification HMAC-SHA256 pour **tous** les webhooks (header `X-Hub-Signature-256: sha256=...`). Activé dès que `server.hmac_secret` est défini. - **Auth token** : token Bearer optionnel pour le serveur Ntfy (si auth activée sur le serveur Ntfy) - **Pas d'exposition externe** : le serveur HTTP écoute sur localhost par défaut (`127.0.0.1:9090`) - **Rate limiting interne** : par topic ET par source (configurable dans `dedup.rate_limit`) - **Input validation** : tout JSON entrant est validé avant processing, chemins webhook dupliqués détectés au démarrage - **No secrets in config** : `NTFY_TOKEN` et `NTFY_HMAC_SECRET` peuvent être passés via variables d'environnement ## Métriques et observabilité - **Health endpoint** : `GET /health` → 200 OK + uptime + compteur de notifs - **Stats endpoint** : `GET /stats` → compteurs par source + erreurs - **API status** : `GET /api/status` → état des health checks HTTP poll - **API history** : `GET /api/history` → 100 derniers événements avec timestamps - **Dashboard** : interface HTML avec stats temps réel, statut des services, historique récent - **Logging** : stdout en mode human-readable (console) ou JSON structuré (`--quiet`, pour systemd)