Files
ntfy-bridge/docs/ARCHITECTURE.md
T
bruno a3e10a68d3
CI / build (push) Successful in 6m8s
chore-sync-version-0.7.0
2026-08-12 08:23:30 -04:00

16 KiB

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 /<topic>           │                   │
│              │  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 <url> 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

// 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/ :

// 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.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

# /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

# Quick start avec docker compose
cp ntfy-bridge.example.yaml ntfy-bridge.yaml
vim ntfy-bridge.yaml
docker compose up -d
# 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

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)