init
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const CurrentVersion = 1
|
||||
|
||||
type Config struct {
|
||||
Version int `json:"version"`
|
||||
Server ServerConfig `json:"server"`
|
||||
Divera DiveraConfig `json:"divera"`
|
||||
Ingress IngressConfig `json:"ingress"`
|
||||
Mappings []Mapping `json:"mappings"`
|
||||
Outbounds []OutboundConfig `json:"outbounds,omitempty"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Listen string `json:"listen"`
|
||||
AdminUsername string `json:"admin_username"`
|
||||
AdminPasswordHash string `json:"admin_password_hash"`
|
||||
SessionSecret string `json:"session_secret"`
|
||||
}
|
||||
|
||||
type DiveraConfig struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
AccessKey string `json:"access_key"`
|
||||
UCR int64 `json:"ucr,omitempty"`
|
||||
TimeoutS int `json:"timeout_seconds"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
}
|
||||
|
||||
type IngressConfig struct {
|
||||
Mail []MailIngress `json:"mail,omitempty"`
|
||||
Discord DiscordIngress `json:"discord"`
|
||||
AllowUnauthenticated bool `json:"allow_unauthenticated"`
|
||||
NtfyTokens map[string]string `json:"ntfy_tokens"`
|
||||
WebhookTokens map[string]string `json:"webhook_tokens"`
|
||||
GotifyTokens []string `json:"gotify_tokens"`
|
||||
}
|
||||
|
||||
type Mapping struct {
|
||||
OutboundID string `json:"outbound_id,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Source string `json:"source"`
|
||||
ChannelRegex string `json:"channel_regex,omitempty"`
|
||||
TitleRegex string `json:"title_regex,omitempty"`
|
||||
MessageRegex string `json:"message_regex,omitempty"`
|
||||
MinPriority int `json:"min_priority,omitempty"`
|
||||
Target string `json:"target"`
|
||||
TitleTemplate string `json:"title_template"`
|
||||
TextTemplate string `json:"text_template"`
|
||||
AddressTemplate string `json:"address_template,omitempty"`
|
||||
NotificationType int `json:"notification_type"`
|
||||
Clusters []int64 `json:"clusters,omitempty"`
|
||||
ClusterRoutes map[string]int `json:"cluster_routes,omitempty"`
|
||||
Groups []int64 `json:"groups,omitempty"`
|
||||
Users []int64 `json:"users,omitempty"`
|
||||
Vehicles []int64 `json:"vehicles,omitempty"`
|
||||
SendPush bool `json:"send_push"`
|
||||
SendSMS bool `json:"send_sms"`
|
||||
SendCall bool `json:"send_call"`
|
||||
SendMail bool `json:"send_mail"`
|
||||
SendPager bool `json:"send_pager"`
|
||||
PrivateMode bool `json:"private_mode"`
|
||||
Extra map[string]any `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
func Default() Config {
|
||||
return Config{
|
||||
Version: CurrentVersion,
|
||||
Server: ServerConfig{Listen: ":8080", AdminUsername: "admin", SessionSecret: randomSecret()},
|
||||
Divera: DiveraConfig{BaseURL: "https://app.divera247.com", TimeoutS: 15, DryRun: true},
|
||||
Ingress: IngressConfig{
|
||||
NtfyTokens: make(map[string]string), WebhookTokens: make(map[string]string), GotifyTokens: []string{},
|
||||
},
|
||||
Mappings: []Mapping{
|
||||
{
|
||||
ID: "example-ntfy-alarm", Name: "Beispiel ntfy -> Alarm", Enabled: false,
|
||||
Source: "ntfy", ChannelRegex: "^alarm$", Target: "alarm",
|
||||
TitleTemplate: "{{if .Title}}{{.Title}}{{else}}ALARM{{end}}", TextTemplate: "{{.Message}}",
|
||||
NotificationType: 2, SendPush: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func randomSecret() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
path string
|
||||
mu sync.RWMutex
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func Open(path string) (*Store, error) {
|
||||
s := &Store{path: path}
|
||||
b, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
s.cfg = Default()
|
||||
if err := s.saveLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &s.cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config: %w", err)
|
||||
}
|
||||
normalize(&s.cfg)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func normalize(c *Config) {
|
||||
if c.Version == 0 {
|
||||
c.Version = CurrentVersion
|
||||
}
|
||||
if c.Server.Listen == "" {
|
||||
c.Server.Listen = ":8080"
|
||||
}
|
||||
if c.Server.AdminUsername == "" {
|
||||
c.Server.AdminUsername = "admin"
|
||||
}
|
||||
if c.Server.SessionSecret == "" {
|
||||
c.Server.SessionSecret = randomSecret()
|
||||
}
|
||||
if c.Divera.BaseURL == "" {
|
||||
c.Divera.BaseURL = "https://app.divera247.com"
|
||||
}
|
||||
if c.Divera.TimeoutS <= 0 {
|
||||
c.Divera.TimeoutS = 15
|
||||
}
|
||||
if c.Ingress.NtfyTokens == nil {
|
||||
c.Ingress.NtfyTokens = map[string]string{}
|
||||
}
|
||||
if c.Ingress.WebhookTokens == nil {
|
||||
c.Ingress.WebhookTokens = map[string]string{}
|
||||
}
|
||||
if c.Ingress.GotifyTokens == nil {
|
||||
c.Ingress.GotifyTokens = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) Get() Config {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
b, _ := json.Marshal(s.cfg)
|
||||
var out Config
|
||||
_ = json.Unmarshal(b, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Store) Replace(c Config) error {
|
||||
normalize(&c)
|
||||
if err := Validate(c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Version != CurrentVersion {
|
||||
return fmt.Errorf("unsupported config version %d", c.Version)
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old := s.cfg
|
||||
s.cfg = c
|
||||
if err := s.saveLocked(); err != nil {
|
||||
s.cfg = old
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Update(fn func(*Config) error) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
copyBytes, _ := json.Marshal(s.cfg)
|
||||
var next Config
|
||||
_ = json.Unmarshal(copyBytes, &next)
|
||||
if err := fn(&next); err != nil {
|
||||
return err
|
||||
}
|
||||
normalize(&next)
|
||||
old := s.cfg
|
||||
s.cfg = next
|
||||
if err := s.saveLocked(); err != nil {
|
||||
s.cfg = old
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) saveLocked() error {
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.MarshalIndent(s.cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := s.path + ".tmp"
|
||||
if err := os.WriteFile(tmp, append(b, '\n'), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(tmp, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, s.path)
|
||||
}
|
||||
|
||||
// Validate checks user-editable configuration before it is persisted.
|
||||
func Validate(c Config) error {
|
||||
if err := validateIngress(c.Ingress); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(c.Server.Listen) == "" {
|
||||
return fmt.Errorf("server.listen darf nicht leer sein")
|
||||
}
|
||||
if strings.TrimSpace(c.Server.AdminUsername) == "" {
|
||||
return fmt.Errorf("server.admin_username darf nicht leer sein")
|
||||
}
|
||||
if c.Divera.TimeoutS < 1 || c.Divera.TimeoutS > 300 {
|
||||
return fmt.Errorf("divera.timeout_seconds muss zwischen 1 und 300 liegen")
|
||||
}
|
||||
if err := validateOutbounds(c); err != nil {
|
||||
return err
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, m := range c.Mappings {
|
||||
if strings.TrimSpace(m.ID) == "" {
|
||||
return fmt.Errorf("mapping %d: id fehlt", i+1)
|
||||
}
|
||||
if seen[m.ID] {
|
||||
return fmt.Errorf("mapping id %q ist doppelt", m.ID)
|
||||
}
|
||||
seen[m.ID] = true
|
||||
switch m.Source {
|
||||
case "", "any", "ntfy", "gotify", "webhook", "mail", "discord":
|
||||
default:
|
||||
return fmt.Errorf("mapping %q: unbekannte quelle %q", m.ID, m.Source)
|
||||
}
|
||||
switch strings.ToLower(m.Target) {
|
||||
case "alarm", "alarms", "news", "message", "mitteilung", "event", "termin", "discord", "webhook", "smtp", "ntfy", "gotify":
|
||||
default:
|
||||
return fmt.Errorf("mapping %q: unbekanntes ziel %q", m.ID, m.Target)
|
||||
}
|
||||
for field, pattern := range map[string]string{"channel_regex": m.ChannelRegex, "title_regex": m.TitleRegex, "message_regex": m.MessageRegex} {
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := regexp.Compile(pattern); err != nil {
|
||||
return fmt.Errorf("mapping %q: %s ist kein gültiger regulärer Ausdruck: %v", m.ID, field, err)
|
||||
}
|
||||
}
|
||||
if m.NotificationType < 0 || m.NotificationType > 4 {
|
||||
return fmt.Errorf("mapping %q: notification_type muss 0..4 sein", m.ID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MailIngress struct {
|
||||
ID string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Address string `json:"address"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Folder string `json:"folder"`
|
||||
Channel string `json:"channel"`
|
||||
PollSeconds int `json:"poll_seconds"`
|
||||
From []string `json:"from,omitempty"`
|
||||
To []string `json:"to,omitempty"`
|
||||
ImportExisting bool `json:"import_existing"`
|
||||
}
|
||||
type DiscordIngress struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PublicKey string `json:"public_key"`
|
||||
ApplicationID string `json:"application_id"`
|
||||
GuildIDs []string `json:"guild_ids"`
|
||||
ChannelIDs []string `json:"channel_ids"`
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
func validateIngress(c IngressConfig) error {
|
||||
seen := map[string]bool{}
|
||||
for _, m := range c.Mail {
|
||||
if m.ID == "" || seen[m.ID] {
|
||||
return fmt.Errorf("Mail-Eingang benötigt eindeutige ID")
|
||||
}
|
||||
seen[m.ID] = true
|
||||
if !m.Enabled {
|
||||
continue
|
||||
}
|
||||
host, port, err := net.SplitHostPort(m.Address)
|
||||
p, _ := strconv.Atoi(port)
|
||||
if err != nil || host == "" || p < 1 || p > 65535 || m.Username == "" || m.Password == "" || m.Channel == "" {
|
||||
return fmt.Errorf("Mail-Eingang %q: Adresse (Host:Port), Benutzer, Passwort und Kanal erforderlich", m.ID)
|
||||
}
|
||||
if m.PollSeconds < 10 || m.PollSeconds > 86400 {
|
||||
return fmt.Errorf("Mail-Abfrageintervall muss 10..86400 Sekunden sein")
|
||||
}
|
||||
}
|
||||
d := c.Discord
|
||||
if d.Enabled {
|
||||
for _, id := range append(append([]string{}, d.GuildIDs...), d.ChannelIDs...) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return fmt.Errorf("Discord-Allowlists dürfen keine leeren IDs enthalten")
|
||||
}
|
||||
}
|
||||
key, err := hex.DecodeString(d.PublicKey)
|
||||
if err != nil || len(key) != ed25519.PublicKeySize || d.ApplicationID == "" || d.Command == "" || len(d.GuildIDs) == 0 || len(d.ChannelIDs) == 0 {
|
||||
return fmt.Errorf("Discord benötigt Public Key, Application-ID, Command und Guild-/Channel-Allowlist")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var topicPattern = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)
|
||||
|
||||
func Secret(value string) (string, error) {
|
||||
if strings.HasPrefix(value, "env:") {
|
||||
v := os.Getenv(strings.TrimPrefix(value, "env:"))
|
||||
if v == "" {
|
||||
return "", fmt.Errorf("required secret environment variable is missing")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Live is opt-in: omitted fields in new or existing configurations never send.
|
||||
type OutboundConfig struct {
|
||||
Topic string `json:"topic,omitempty"`
|
||||
SMTPHost string `json:"smtp_host,omitempty"`
|
||||
SMTPPort int `json:"smtp_port,omitempty"`
|
||||
TLSMode string `json:"tls_mode,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
To []string `json:"to,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
URL string `json:"url"`
|
||||
BearerToken string `json:"bearer_token,omitempty"`
|
||||
TimeoutS int `json:"timeout_seconds"`
|
||||
Live bool `json:"live"`
|
||||
}
|
||||
|
||||
func IsOutbound(kind string) bool {
|
||||
switch kind {
|
||||
case "discord", "webhook", "smtp", "ntfy", "gotify":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ValidateOutbound(o OutboundConfig) error {
|
||||
if strings.TrimSpace(o.ID) == "" {
|
||||
return fmt.Errorf("Ausgang: id fehlt")
|
||||
}
|
||||
if !IsOutbound(o.Provider) {
|
||||
return fmt.Errorf("Ausgang %q: unbekannter Provider", o.ID)
|
||||
}
|
||||
if o.TimeoutS < 0 || o.TimeoutS > 300 {
|
||||
return fmt.Errorf("Ausgang %q: Timeout muss 0..300 sein", o.ID)
|
||||
}
|
||||
if o.Provider == "smtp" {
|
||||
if o.SMTPHost == "" || strings.ContainsAny(o.SMTPHost, "\r\n /:") && net.ParseIP(o.SMTPHost) == nil || o.SMTPPort < 1 || o.SMTPPort > 65535 {
|
||||
return fmt.Errorf("Ausgang %q: SMTP-Host/Port ungültig", o.ID)
|
||||
}
|
||||
if o.TLSMode != "tls" && o.TLSMode != "starttls" {
|
||||
return fmt.Errorf("SMTP benötigt tls oder starttls")
|
||||
}
|
||||
if len(o.To) == 0 {
|
||||
return fmt.Errorf("SMTP benötigt Empfänger")
|
||||
}
|
||||
for _, address := range append([]string{o.From}, o.To...) {
|
||||
if strings.ContainsAny(address, "\r\n") {
|
||||
return fmt.Errorf("ungültige Mailadresse")
|
||||
}
|
||||
a, err := mail.ParseAddress(address)
|
||||
if err != nil || a.Address != address {
|
||||
return fmt.Errorf("SMTP benötigt reine Mailadressen ohne Anzeigenamen")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if o.Provider == "ntfy" && !topicPattern.MatchString(o.Topic) {
|
||||
return fmt.Errorf("ntfy benötigt Topic mit 1..64 Buchstaben, Ziffern, _ oder -")
|
||||
}
|
||||
u, err := url.Parse(o.URL)
|
||||
if err != nil || u.Hostname() == "" || (u.Scheme != "http" && u.Scheme != "https") || u.User != nil || u.Fragment != "" {
|
||||
return fmt.Errorf("Ausgang %q: gültige HTTP(S)-URL ohne Benutzerinfo oder Fragment erforderlich", o.ID)
|
||||
}
|
||||
if o.Provider == "discord" {
|
||||
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if u.Scheme != "https" || u.Host != "discord.com" || len(parts) != 4 || parts[0] != "api" || parts[1] != "webhooks" || parts[2] == "" || parts[3] == "" {
|
||||
return fmt.Errorf("Ausgang %q: Discord-URL muss https://discord.com/api/webhooks/ID/TOKEN entsprechen", o.ID)
|
||||
}
|
||||
if o.BearerToken != "" {
|
||||
return fmt.Errorf("Ausgang %q: Discord benötigt keinen Bearer-Token", o.ID)
|
||||
}
|
||||
}
|
||||
if strings.ContainsAny(o.BearerToken, "\r\n") {
|
||||
return fmt.Errorf("Ausgang %q: ungültiger Bearer-Token", o.ID)
|
||||
}
|
||||
if o.TimeoutS < 0 || o.TimeoutS > 300 {
|
||||
return fmt.Errorf("Ausgang %q: timeout_seconds muss 0..300 sein (0 = 15 Sekunden)", o.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOutbounds(c Config) error {
|
||||
outputs := map[string]OutboundConfig{}
|
||||
for _, o := range c.Outbounds {
|
||||
if err := ValidateOutbound(o); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := outputs[o.ID]; exists {
|
||||
return fmt.Errorf("Ausgang id %q ist doppelt", o.ID)
|
||||
}
|
||||
outputs[o.ID] = o
|
||||
}
|
||||
for _, m := range c.Mappings {
|
||||
target := strings.ToLower(m.Target)
|
||||
if IsOutbound(target) {
|
||||
o, ok := outputs[m.OutboundID]
|
||||
if !ok || o.Provider != target {
|
||||
return fmt.Errorf("mapping %q: passender Ausgang für %s erforderlich", m.ID, target)
|
||||
}
|
||||
} else if m.OutboundID != "" {
|
||||
return fmt.Errorf("mapping %q: Divera247 verwendet keinen outbound_id", m.ID)
|
||||
}
|
||||
for field, value := range map[string]string{"title_template": m.TitleTemplate, "text_template": m.TextTemplate, "address_template": m.AddressTemplate} {
|
||||
if _, err := template.New(field).Parse(value); err != nil {
|
||||
return fmt.Errorf("mapping %q: ungültiges %s: %w", m.ID, field, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateOutbounds(t *testing.T) {
|
||||
valid := OutboundConfig{ID: "ops", Provider: "discord", URL: "https://discord.com/api/webhooks/123/secret"}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
change func(*Config)
|
||||
}{
|
||||
{"duplicate", func(c *Config) { c.Outbounds = append(c.Outbounds, valid) }},
|
||||
{"missing destination", func(c *Config) { c.Mappings[0].OutboundID = "missing" }},
|
||||
{"mismatched provider", func(c *Config) { c.Mappings[0].Target = "webhook" }},
|
||||
{"unsafe scheme", func(c *Config) { c.Outbounds[0].URL = "file:///secret" }},
|
||||
{"discord host", func(c *Config) { c.Outbounds[0].URL = "https://evil.example/api/webhooks/123/secret" }},
|
||||
{"discord credentials", func(c *Config) { c.Outbounds[0].BearerToken = "secret" }},
|
||||
{"timeout", func(c *Config) { c.Outbounds[0].TimeoutS = -1 }},
|
||||
{"template", func(c *Config) { c.Mappings[0].TextTemplate = "{{invalid" }},
|
||||
{"divera reference", func(c *Config) { c.Mappings[0].Target = "alarm" }},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := Default()
|
||||
c.Outbounds = []OutboundConfig{valid}
|
||||
c.Mappings[0].Target = "discord"
|
||||
c.Mappings[0].OutboundID = "ops"
|
||||
if err := Validate(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tc.change(&c)
|
||||
if Validate(c) == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingConfigurationCompatibility(t *testing.T) {
|
||||
c := Default()
|
||||
if err := Validate(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var o OutboundConfig
|
||||
if err := json.Unmarshal([]byte(`{"id":"test","provider":"webhook","url":"https://example.invalid"}`), &o); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if o.Live {
|
||||
t.Fatal("omitted live flag must be dry-run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExampleConfig(t *testing.T) {
|
||||
b, err := os.ReadFile("../../config.example.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var c Config
|
||||
if err := json.Unmarshal(b, &c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Validate(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, o := range c.Outbounds {
|
||||
if o.Live {
|
||||
t.Fatal("example must use dry-run")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailAndPushConfigurationValidation(t *testing.T) {
|
||||
for _, o := range []OutboundConfig{
|
||||
{ID: "smtp", Provider: "smtp", SMTPHost: "smtp.example.org", SMTPPort: 587, TLSMode: "plain", From: "sender@example.org", To: []string{"to@example.org"}},
|
||||
{ID: "smtp", Provider: "smtp", SMTPHost: "smtp.example.org", SMTPPort: 587, TLSMode: "starttls", From: "sender@example.org\r\nBcc: bad@example.org", To: []string{"to@example.org"}},
|
||||
{ID: "ntfy", Provider: "ntfy", URL: "https://ntfy.sh", Topic: "bad/topic"},
|
||||
} {
|
||||
if ValidateOutbound(o) == nil {
|
||||
t.Fatalf("invalid destination accepted: %s", o.ID)
|
||||
}
|
||||
}
|
||||
c := Default()
|
||||
c.Ingress.Mail = []MailIngress{{ID: "mail", Enabled: true, Address: "host:993", Username: "user", Password: "env:PASS", Channel: "ops", PollSeconds: 1}}
|
||||
if Validate(c) == nil {
|
||||
t.Fatal("invalid polling interval")
|
||||
}
|
||||
c.Ingress.Mail = nil
|
||||
c.Ingress.Discord = DiscordIngress{Enabled: true}
|
||||
if Validate(c) == nil {
|
||||
t.Fatal("incomplete Discord credentials")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user