CI / test (push) Has been cancelled
**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
189 lines
5.3 KiB
V
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
|
|
}
|