mattermost-push-proxy-router/config.go
Felipe M. 3c6fea8a46
All checks were successful
Release / release (push) Successful in 2m26s
CI / goreleaser-lint (push) Successful in 6s
CI / format (push) Successful in 28s
CI / test (push) Successful in 1m20s
CI / lint (push) Successful in 2m36s
CI / build (push) Successful in 34s
feat: add Mattermost push proxy prefix router
Implement a stateless Go service that routes /api/v1/send_push and /api/v1/ack
by platform prefix to configured backends, enabling mixed official and custom
mobile clients on a single PushNotificationServer URL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 10:03:43 +02:00

159 lines
3.2 KiB
Go

package main
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/BurntSushi/toml"
)
const defaultPort = "8066"
const defaultRequestTimeoutSec = 60
type Route struct {
Prefixes []string
URL string
}
type Config struct {
Port string
Routes []Route
RequestTimeout time.Duration
}
type fileConfig struct {
Listen string `toml:"listen"`
RequestTimeoutSec int `toml:"request_timeout_sec"`
Routes []routeConfig `toml:"routes"`
}
type routeConfig struct {
Prefixes []string `toml:"prefixes"`
URL string `toml:"url"`
}
func LoadConfig() (Config, error) {
configFile := os.Getenv("CONFIG_FILE")
if configFile == "" {
return Config{}, fmt.Errorf("CONFIG_FILE is required")
}
data, err := os.ReadFile(configFile)
if err != nil {
return Config{}, fmt.Errorf("read config file: %w", err)
}
var fc fileConfig
if err := toml.Unmarshal(data, &fc); err != nil {
return Config{}, fmt.Errorf("invalid config file: %w", err)
}
cfg := Config{
Port: envOr("PORT", listenPort(fc.Listen)),
RequestTimeout: time.Duration(envIntOr("REQUEST_TIMEOUT_SEC", timeoutSec(fc.RequestTimeoutSec))) * time.Second,
}
cfg.Routes, err = normalizeRoutes(fc.Routes)
if err != nil {
return Config{}, err
}
return cfg, nil
}
func listenPort(listen string) string {
listen = strings.TrimSpace(listen)
if listen == "" {
return defaultPort
}
if strings.HasPrefix(listen, ":") {
return strings.TrimPrefix(listen, ":")
}
if idx := strings.LastIndex(listen, ":"); idx >= 0 && idx < len(listen)-1 {
return listen[idx+1:]
}
return listen
}
func timeoutSec(sec int) int {
if sec <= 0 {
return defaultRequestTimeoutSec
}
return sec
}
func normalizeRoutes(raw []routeConfig) ([]Route, error) {
if len(raw) == 0 {
return nil, fmt.Errorf("at least one route is required")
}
routes := make([]Route, 0, len(raw))
seen := make(map[string]string)
for i, r := range raw {
url := strings.TrimRight(strings.TrimSpace(r.URL), "/")
if url == "" {
return nil, fmt.Errorf("route %d: url is required", i)
}
prefixes := make([]string, 0, len(r.Prefixes))
for _, prefix := range r.Prefixes {
prefix = strings.TrimSpace(prefix)
if prefix == "" {
continue
}
if otherURL, ok := seen[prefix]; ok {
return nil, fmt.Errorf("duplicate prefix %q in routes for %q and %q", prefix, otherURL, url)
}
seen[prefix] = url
prefixes = append(prefixes, prefix)
}
if len(prefixes) == 0 {
return nil, fmt.Errorf("route %d: at least one prefix is required", i)
}
routes = append(routes, Route{Prefixes: prefixes, URL: url})
}
return routes, nil
}
func (c Config) BackendForPlatform(platform string) (string, bool) {
platform = stripVersionSuffix(platform)
if platform == "" {
return "", false
}
for _, route := range c.Routes {
for _, prefix := range route.Prefixes {
if platform == prefix {
return route.URL, true
}
}
}
return "", false
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func envIntOr(key string, fallback int) int {
v := os.Getenv(key)
if v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil {
return fallback
}
return n
}