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
106 lines
2.7 KiB
V
106 lines
2.7 KiB
V
module main
|
|
|
|
import net.http
|
|
import sources
|
|
|
|
fn test_ip_matches_exact() {
|
|
assert ip_matches('192.168.30.5', '192.168.30.5')
|
|
assert !ip_matches('192.168.30.6', '192.168.30.5')
|
|
}
|
|
|
|
fn test_ip_matches_cidr() {
|
|
assert ip_matches('192.168.30.5', '192.168.0.0/16')
|
|
assert ip_matches('10.0.0.1', '10.0.0.0/8')
|
|
assert ip_matches('10.255.255.255', '10.0.0.0/8')
|
|
assert !ip_matches('11.0.0.1', '10.0.0.0/8')
|
|
assert ip_matches('192.168.1.100', '192.168.1.0/24')
|
|
assert !ip_matches('192.168.2.1', '192.168.1.0/24')
|
|
}
|
|
|
|
fn test_parse_ip_valid() {
|
|
octets := parse_ip('192.168.1.5')!
|
|
assert octets[0] == 192
|
|
assert octets[1] == 168
|
|
assert octets[2] == 1
|
|
assert octets[3] == 5
|
|
}
|
|
|
|
fn test_parse_ip_invalid() {
|
|
// parse_ip with invalid input should return an error
|
|
result := parse_ip('invalid') or { []u8{} }
|
|
assert result.len == 0
|
|
|
|
result2 := parse_ip('1.2.3') or { []u8{} }
|
|
assert result2.len == 0
|
|
|
|
result3 := parse_ip('999.1.2.3') or { []u8{} }
|
|
assert result3.len == 0
|
|
}
|
|
|
|
fn test_check_acl_no_acl() {
|
|
mut app := new_test_acl_app()
|
|
route := WebhookRoute{kind: .generic, index: 0}
|
|
assert app.check_acl(route, '1.2.3.4', '') == true
|
|
}
|
|
|
|
fn test_check_acl_ip_allow() {
|
|
mut app := new_test_acl_app()
|
|
app.cfg.sources.generic << sources.GenericSource{
|
|
name: 'test-source'
|
|
webhook_path: '/test'
|
|
topic: 'test-topic'
|
|
acl: sources.AclConfig{
|
|
allowed_ips: ['10.0.0.0/8']
|
|
}
|
|
}
|
|
route := WebhookRoute{kind: .generic, index: 0}
|
|
|
|
assert app.check_acl(route, '10.0.0.5', '') == true
|
|
assert app.check_acl(route, '10.255.0.1', '') == true
|
|
assert app.check_acl(route, '192.168.1.1', '') == false
|
|
assert app.check_acl(route, '', '') == false
|
|
}
|
|
|
|
fn test_check_acl_combined_ip_and_token() {
|
|
mut app := new_test_acl_app()
|
|
app.cfg.sources.generic << sources.GenericSource{
|
|
name: 'test-source'
|
|
webhook_path: '/test'
|
|
topic: 'test-topic'
|
|
acl: sources.AclConfig{
|
|
allowed_ips: ['10.0.0.0/8']
|
|
allowed_tokens: ['secret123']
|
|
}
|
|
}
|
|
route := WebhookRoute{kind: .generic, index: 0}
|
|
|
|
// Both pass
|
|
assert app.check_acl(route, '10.0.0.5', 'secret123') == true
|
|
// IP ok but no token
|
|
assert app.check_acl(route, '10.0.0.5', '') == false
|
|
// Token ok but wrong IP
|
|
assert app.check_acl(route, '192.168.1.1', 'secret123') == false
|
|
// Both wrong
|
|
assert app.check_acl(route, '1.1.1.1', 'wrong') == false
|
|
}
|
|
|
|
fn test_extract_bearer_token_empty() {
|
|
req := http.Request{
|
|
header: http.new_header()
|
|
}
|
|
assert extract_bearer_token(req) == ''
|
|
}
|
|
|
|
// Helper: create a minimal App for ACL tests
|
|
fn new_test_acl_app() App {
|
|
return App{
|
|
logger: new_logger(.info, false)
|
|
cfg: Config{
|
|
filters: FilterConfig{}
|
|
outgoing: OutgoingConfig{}
|
|
state: StateConfig{}
|
|
}
|
|
poll_state: map[string]bool{}
|
|
}
|
|
}
|