Files
bruno 1a4808f5f7
CI / test (push) Has been cancelled
feat: v0.6.0 — ACLs multi-utilisateurs + Plugin system
**ACLs (Access Control Lists):**
- Ajout du type AclConfig dans sources/event.v (allowed_ips CIDR + allowed_tokens)
- Chaque source webhook (Gitea, Uptime Kuma, Cron, Generic) supporte les ACLs
- Validation IP via X-Forwarded-For / X-Real-IP avant HMAC
- Validation Bearer token via Authorization header
- Tests: 7 tests ACL (IP exact, CIDR, parse IPv4, ACL vide, IP+token combiné)

**Plugin system:**
- sources/plugin.v: runner exécutable externe, stdout JSON → Event
- Exit 0 = publish, exit ≠ 0 = skip. Timeout configurable
- Plugin loop dans server.v (goroutine, toutes les 60s)
- Example: scripts/example-plugin-disk.sh (vérifie espace disque)

**Docs:**
- README.md: ajout source Plugin + section Features complète
- ARCHITECTURE.md: flux Plugin, flux ACL, endpoints /metrics /api/silence
- ROADMAP.md: Phase 5 → 8/8 complet, ajout v0.6.0
- ntfy-bridge.example.yaml: sections ACLs et Plugins commentées
- Version bump: 0.5.0 → 0.6.0
2026-08-04 22:25:48 -04:00

189 lines
5.3 KiB
V

module main
import os
import yaml
import sources
pub struct ServerConfig {
pub mut:
url string @[json: 'url']
auth_token string @[json: 'auth_token']
listen string @[json: 'listen']
hmac_secret string @[json: 'hmac_secret'] // shared secret for webhook HMAC-SHA256 validation
}
pub struct DefaultConfig {
pub mut:
priority int @[json: 'priority']
tags []string @[json: 'tags']
}
pub struct RateLimitConfig {
pub mut:
max_per_minute int @[json: 'max_per_minute']
max_per_source int @[json: 'max_per_source']
}
pub struct DedupConfig {
pub mut:
enabled bool @[json: 'enabled']
ttl_seconds int @[json: 'ttl_seconds']
rate_limit RateLimitConfig @[json: 'rate_limit']
}
// v0.5 — Advanced filters
pub struct FilterRule {
pub mut:
match_source string @[json: 'match_source'] // source type: gitea, docker, etc.
match_name string @[json: 'match_name'] // source name (supports * glob)
match_topic string @[json: 'match_topic'] // topic name
action string @[json: 'action'] // drop, set_priority:N, add_tag:X
value string @[json: 'value'] // value for set_priority / add_tag
}
// v0.5 — Outgoing webhook (Slack, Discord, etc.)
pub struct OutgoingWebhook {
pub mut:
url string @[json: 'url']
format string @[json: 'format'] // slack, discord, json
}
pub struct FilterConfig {
pub mut:
rules []FilterRule @[json: 'rules']
}
pub struct OutgoingConfig {
pub mut:
webhooks []OutgoingWebhook @[json: 'webhooks']
}
pub struct SourcesConfig {
pub mut:
gitea []sources.GiteaSource @[json: 'gitea']
uptime_kuma []sources.UptimeKumaSource @[json: 'uptime_kuma']
docker []sources.DockerSource @[json: 'docker']
http_poll []sources.HttpPollSource @[json: 'http_poll']
cron []sources.CronSource @[json: 'cron']
generic []sources.GenericSource @[json: 'generic']
plugin []sources.PluginSource @[json: 'plugin']
}
pub struct Config {
pub mut:
server ServerConfig @[json: 'server']
defaults DefaultConfig @[json: 'defaults']
sources SourcesConfig @[json: 'sources']
dedup DedupConfig @[json: 'dedup']
filters FilterConfig @[json: 'filters']
outgoing OutgoingConfig @[json: 'outgoing']
state StateConfig @[json: 'state']
}
pub struct StateConfig {
pub mut:
file string @[json: 'file'] // path to state file for persisting poll_state
}
pub fn load_config(path string) !Config {
if !os.exists(path) {
return error('config file not found: ${path}')
}
mut cfg := yaml.decode_file[Config](path)!
// Env override for auth token
env_token := os.getenv('NTFY_TOKEN')
if env_token != '' {
cfg.server.auth_token = env_token
}
// Env override for HMAC secret
env_hmac := os.getenv('NTFY_HMAC_SECRET')
if env_hmac != '' {
cfg.server.hmac_secret = env_hmac
}
validate_config(cfg)!
return cfg
}
pub fn validate_config(cfg Config) ! {
if cfg.server.url == '' {
return error('server.url is required')
}
if cfg.server.listen == '' {
return error('server.listen is required')
}
if cfg.defaults.priority < 1 || cfg.defaults.priority > 5 {
return error('defaults.priority must be between 1 and 5')
}
// Collect all webhook paths and detect duplicates
mut seen_paths := map[string]string{} // path -> source description
for src in cfg.sources.gitea {
if src.webhook_path == '' {
return error('gitea source "${src.name}" missing webhook_path')
}
if src.topic == '' {
return error('gitea source "${src.name}" missing topic')
}
check_duplicate_path(mut seen_paths, src.webhook_path, 'gitea:${src.name}')!
}
for src in cfg.sources.uptime_kuma {
if src.webhook_path == '' {
return error('uptime_kuma source "${src.name}" missing webhook_path')
}
if src.topic == '' {
return error('uptime_kuma source "${src.name}" missing topic')
}
check_duplicate_path(mut seen_paths, src.webhook_path, 'uptime_kuma:${src.name}')!
}
for src in cfg.sources.docker {
if src.topic == '' {
return error('docker source "${src.name}" missing topic')
}
if src.hosts.len == 0 {
return error('docker source "${src.name}" requires at least one host')
}
}
for src in cfg.sources.http_poll {
if src.interval <= 0 {
return error('http_poll source "${src.name}" interval must be > 0')
}
for chk in src.checks {
if chk.url == '' {
return error('http_poll check missing url')
}
if chk.topic == '' {
return error('http_poll check for "${chk.url}" missing topic')
}
}
}
for src in cfg.sources.cron {
if src.webhook_path == '' {
return error('cron source "${src.name}" missing webhook_path')
}
if src.topic == '' {
return error('cron source "${src.name}" missing topic')
}
check_duplicate_path(mut seen_paths, src.webhook_path, 'cron:${src.name}')!
}
for src in cfg.sources.generic {
if src.webhook_path == '' {
return error('generic source "${src.name}" missing webhook_path')
}
if src.topic == '' {
return error('generic source "${src.name}" missing topic')
}
check_duplicate_path(mut seen_paths, src.webhook_path, 'generic:${src.name}')!
}
}
fn check_duplicate_path(mut seen map[string]string, path string, source_name string) ! {
if existing := seen[path] {
return error('duplicate webhook path "${path}" used by ${existing} and ${source_name}')
}
seen[path] = source_name
}