package config import ( "crypto/tls" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" "gopkg.in/yaml.v3" ) type Config struct { Server ServerConfig `yaml:"server"` Database DatabaseConfig `yaml:"database"` Databases map[string]DatabaseConfig `yaml:"databases"` TLS TLSConfig `yaml:"tls"` LDAP LDAPConfig `yaml:"ldap"` PowerDNS PowerDNSConfig `yaml:"powerdns"` EJBCA EJBCAConfig `yaml:"ejbca"` Hockeypuck HockeypuckConfig `yaml:"hockeypuck"` Infisical InfisicalConfig `yaml:"infisical"` CardDAV CardDAVConfig `yaml:"carddav"` Asterisk AsteriskConfig `yaml:"asterisk"` Kamailio KamailioConfig `yaml:"kamailio"` Auth AuthConfig `yaml:"auth"` Logging LoggingConfig `yaml:"logging"` } type ServerConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` ReadTimeout time.Duration `yaml:"readTimeout"` WriteTimeout time.Duration `yaml:"writeTimeout"` } type DatabaseConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` Database string `yaml:"database"` Schema string `yaml:"schema"` User string `yaml:"user"` Password string `yaml:"password"` SSLMode string `yaml:"sslMode"` MaxConns int `yaml:"maxConns"` MinConns int `yaml:"minConns"` } type TLSConfig struct { CertFile string `yaml:"certFile"` KeyFile string `yaml:"keyFile"` CAFile string `yaml:"caFile"` } type LDAPConfig struct { Servers []string `yaml:"servers"` BaseDN string `yaml:"baseDn"` BindDN string `yaml:"bindDn"` BindPass string `yaml:"bindPassword"` PoolSize int `yaml:"poolSize"` UseTLS bool `yaml:"useTls"` CAFile string `yaml:"caFile"` } type PowerDNSConfig struct { BaseURL string `yaml:"baseUrl"` APIKey string `yaml:"apiKey"` ServerID string `yaml:"serverId"` } type EJBCAConfig struct { BaseURL string `yaml:"baseUrl"` CertFile string `yaml:"certFile"` KeyFile string `yaml:"keyFile"` CAFile string `yaml:"caFile"` } type HockeypuckConfig struct { Servers []string `yaml:"servers"` } type CardDAVConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` Database string `yaml:"database"` User string `yaml:"user"` Password string `yaml:"password"` SSLMode string `yaml:"sslMode"` } type AsteriskServerConfig struct { Host string `yaml:"host"` AMIPort int `yaml:"amiPort"` } type AsteriskConfig struct { Servers []AsteriskServerConfig `yaml:"servers"` AMIUser string `yaml:"amiUser"` AMISecret string `yaml:"amiSecret"` } type KamailioConfig struct { Servers []string `yaml:"servers"` SSHUser string `yaml:"sshUser"` SSHKey string `yaml:"sshKeyFile"` } type InfisicalConfig struct { Host string `yaml:"host"` ProjectID string `yaml:"projectId"` Environment string `yaml:"environment"` TokenFile string `yaml:"tokenFile"` SecretPath string `yaml:"secretPath"` } type AuthConfig struct { APIKeys []string `yaml:"apiKeys"` } type LoggingConfig struct { Level string `yaml:"level"` Format string `yaml:"format"` } func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } cfg.setDefaults() return &cfg, nil } func (c *Config) setDefaults() { if c.Server.Host == "" { c.Server.Host = "0.0.0.0" } if c.Server.Port == 0 { c.Server.Port = 8443 } if c.Server.ReadTimeout == 0 { c.Server.ReadTimeout = 30 * time.Second } if c.Server.WriteTimeout == 0 { c.Server.WriteTimeout = 30 * time.Second } // Legacy single database config if c.Database.Port == 0 { c.Database.Port = 5432 } if c.Database.SSLMode == "" { c.Database.SSLMode = "require" } if c.Database.MaxConns == 0 { c.Database.MaxConns = 25 } if c.Database.MinConns == 0 { c.Database.MinConns = 5 } // Multi-database defaults if c.Databases == nil { c.Databases = make(map[string]DatabaseConfig) } for name, db := range c.Databases { if db.Port == 0 { db.Port = 5432 } if db.SSLMode == "" { db.SSLMode = c.Database.SSLMode } if db.Host == "" { db.Host = c.Database.Host } if db.MaxConns == 0 { db.MaxConns = 10 } if db.MinConns == 0 { db.MinConns = 2 } c.Databases[name] = db } if c.LDAP.PoolSize == 0 { c.LDAP.PoolSize = 10 } if c.PowerDNS.ServerID == "" { c.PowerDNS.ServerID = "localhost" } if c.CardDAV.Port == 0 { c.CardDAV.Port = 5432 } if c.CardDAV.SSLMode == "" { c.CardDAV.SSLMode = "disable" } if c.Asterisk.AMIUser == "" { c.Asterisk.AMIUser = "gsc-ops-api" } for i := range c.Asterisk.Servers { if c.Asterisk.Servers[i].AMIPort == 0 { c.Asterisk.Servers[i].AMIPort = 5038 } } if c.Kamailio.SSHUser == "" { c.Kamailio.SSHUser = "root" } if c.Logging.Level == "" { c.Logging.Level = "info" } if c.Logging.Format == "" { c.Logging.Format = "json" } if c.Infisical.SecretPath == "" { c.Infisical.SecretPath = "/gsc-ops-api" } } // LoadSecretsFromInfisical fetches secrets from the Infisical API func (c *Config) LoadSecretsFromInfisical() error { if c.Infisical.Host == "" { return nil } tokenFile := c.Infisical.TokenFile if tokenFile == "" { tokenFile = "/etc/gsc-ops-api/.infisical" } tokenData, err := os.ReadFile(tokenFile) if err != nil { return fmt.Errorf("failed to read Infisical token: %w", err) } token := strings.TrimSpace(string(tokenData)) // Legacy single database credentials if v, err := c.fetchSecret(token, "GSC_OPS_DB_USER"); err == nil && v != "" { c.Database.User = v } if v, err := c.fetchSecret(token, "GSC_OPS_DB_PASSWORD"); err == nil && v != "" { c.Database.Password = v } // Per-database credentials: inherit from primary DB user/password for name, db := range c.Databases { if db.User == "" { db.User = c.Database.User } if db.Password == "" { db.Password = c.Database.Password } c.Databases[name] = db } // LDAP credentials if v, err := c.fetchSecret(token, "GSC_OPS_LDAP_BIND_DN"); err == nil && v != "" { c.LDAP.BindDN = v } if v, err := c.fetchSecret(token, "GSC_OPS_LDAP_BIND_PASSWORD"); err == nil && v != "" { c.LDAP.BindPass = v } // PowerDNS API key if v, err := c.fetchSecret(token, "GSC_OPS_PDNS_API_KEY"); err == nil && v != "" { c.PowerDNS.APIKey = v } // CardDAV DB password if v, err := c.fetchSecret(token, "GSC_OPS_CARDDAV_DB_PASSWORD"); err == nil && v != "" { c.CardDAV.Password = v } // API keys for authorized clients if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_SKILL_SERVER"); err == nil && v != "" { c.Auth.APIKeys = append(c.Auth.APIKeys, v) } if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_VOICE_AGENT"); err == nil && v != "" { c.Auth.APIKeys = append(c.Auth.APIKeys, v) } if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_GSC_MY"); err == nil && v != "" { c.Auth.APIKeys = append(c.Auth.APIKeys, v) } if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_SYNAPSE_HUB"); err == nil && v != "" { c.Auth.APIKeys = append(c.Auth.APIKeys, v) } // Asterisk AMI secret if v, err := c.fetchSecret(token, "GSC_OPS_ASTERISK_AMI_SECRET"); err == nil && v != "" { c.Asterisk.AMISecret = v } return nil } func (c *Config) fetchSecret(token, secretName string) (string, error) { url := fmt.Sprintf("%s/api/v3/secrets/raw/%s?workspaceId=%s&environment=%s&secretPath=%s", c.Infisical.Host, secretName, c.Infisical.ProjectID, c.Infisical.Environment, c.Infisical.SecretPath) req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("Infisical returned status %d for %s", resp.StatusCode, secretName) } body, err := io.ReadAll(resp.Body) if err != nil { return "", err } var result struct { Secret struct { SecretValue string `json:"secretValue"` } `json:"secret"` } if err := json.Unmarshal(body, &result); err != nil { return "", err } return result.Secret.SecretValue, nil } // DatabaseDSN returns the legacy database connection string func (c *Config) DatabaseDSN() string { return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s", c.Database.Host, c.Database.Port, c.Database.Database, c.Database.User, c.Database.Password, c.Database.SSLMode) } // NamedDatabaseDSN returns the connection string for a named database func (c *Config) NamedDatabaseDSN(name string) string { db, ok := c.Databases[name] if !ok { return c.DatabaseDSN() } dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s", db.Host, db.Port, db.Database, db.User, db.Password, db.SSLMode) if db.Schema != "" { dsn += fmt.Sprintf(" search_path=%s,public", db.Schema) } return dsn } // HasDatabase returns true if a named database is configured func (c *Config) HasDatabase(name string) bool { _, ok := c.Databases[name] return ok } // CardDAVDSN returns the CardDAV database connection string func (c *Config) CardDAVDSN() string { return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s", c.CardDAV.Host, c.CardDAV.Port, c.CardDAV.Database, c.CardDAV.User, c.CardDAV.Password, c.CardDAV.SSLMode) }