147 lines
4.8 KiB
Go
147 lines
4.8 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/goccy/go-yaml"
|
|
)
|
|
|
|
const maxSettingsBytes = 1 << 20
|
|
|
|
type runtimeSettings struct {
|
|
SchemaVersion int `yaml:"schema_version"`
|
|
Server serverSettings `yaml:"server"`
|
|
Registration registrationSettings `yaml:"registration"`
|
|
Provider providerSettings `yaml:"provider"`
|
|
Worker workerSettings `yaml:"worker"`
|
|
}
|
|
|
|
type serverSettings struct {
|
|
ListenAddress *string `yaml:"listen_address"`
|
|
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs"`
|
|
}
|
|
|
|
type registrationSettings struct {
|
|
Attempts *int `yaml:"attempts"`
|
|
WindowSeconds *int `yaml:"window_seconds"`
|
|
}
|
|
|
|
type providerSettings struct {
|
|
HTTPTimeoutSeconds int `yaml:"http_timeout_seconds"`
|
|
MaxResponseBytes int `yaml:"max_response_bytes"`
|
|
AllowedPorts []uint16 `yaml:"allowed_ports"`
|
|
}
|
|
|
|
type workerSettings struct {
|
|
LeaseSeconds int `yaml:"lease_seconds"`
|
|
PollMilliseconds int `yaml:"poll_milliseconds"`
|
|
}
|
|
|
|
func defaultRuntimeSettings() runtimeSettings {
|
|
listenAddress := "127.0.0.1:8080"
|
|
return runtimeSettings{
|
|
SchemaVersion: 1,
|
|
Server: serverSettings{ListenAddress: &listenAddress},
|
|
Provider: providerSettings{
|
|
HTTPTimeoutSeconds: 45,
|
|
MaxResponseBytes: 32 << 20,
|
|
AllowedPorts: []uint16{80, 443},
|
|
},
|
|
Worker: workerSettings{LeaseSeconds: 60, PollMilliseconds: 250},
|
|
}
|
|
}
|
|
|
|
func loadRuntimeSettings(path string) (runtimeSettings, error) {
|
|
if strings.TrimSpace(path) == "" {
|
|
return runtimeSettings{}, errors.New("portal --config is required")
|
|
}
|
|
extension := strings.ToLower(filepath.Ext(path))
|
|
if extension != ".yml" && extension != ".yaml" {
|
|
return runtimeSettings{}, errors.New("portal settings file must use .yml or .yaml")
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil || !info.Mode().IsRegular() {
|
|
return runtimeSettings{}, errors.New("portal settings file does not exist")
|
|
}
|
|
if info.Size() > maxSettingsBytes {
|
|
return runtimeSettings{}, errors.New("portal settings file exceeds 1 MiB")
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return runtimeSettings{}, errors.New("read portal settings file")
|
|
}
|
|
var settings runtimeSettings
|
|
decoder := yaml.NewDecoder(bytes.NewReader(body), yaml.Strict())
|
|
if err := decoder.Decode(&settings); err != nil {
|
|
return runtimeSettings{}, fmt.Errorf("portal settings file is invalid: %w", err)
|
|
}
|
|
if err := rejectAdditionalDocument(decoder); err != nil {
|
|
return runtimeSettings{}, err
|
|
}
|
|
if settings.SchemaVersion != 1 {
|
|
return runtimeSettings{}, errors.New("portal settings schema_version must be 1")
|
|
}
|
|
if settings.Server.ListenAddress != nil {
|
|
listenAddress := strings.TrimSpace(*settings.Server.ListenAddress)
|
|
if listenAddress != *settings.Server.ListenAddress {
|
|
return runtimeSettings{}, errors.New("portal settings server.listen_address must not contain surrounding whitespace")
|
|
}
|
|
if err := validateListenAddress(listenAddress); err != nil {
|
|
return runtimeSettings{}, err
|
|
}
|
|
}
|
|
if (settings.Registration.Attempts == nil) != (settings.Registration.WindowSeconds == nil) {
|
|
return runtimeSettings{}, errors.New("portal settings registration attempts and window_seconds must be configured together")
|
|
}
|
|
if settings.Registration.Attempts != nil && (*settings.Registration.Attempts <= 0 || *settings.Registration.WindowSeconds <= 0) {
|
|
return runtimeSettings{}, errors.New("portal settings registration values must be positive")
|
|
}
|
|
if settings.Provider.HTTPTimeoutSeconds <= 0 || settings.Provider.MaxResponseBytes <= 0 || settings.Worker.LeaseSeconds <= 0 || settings.Worker.PollMilliseconds <= 0 {
|
|
return runtimeSettings{}, errors.New("portal settings values must be positive")
|
|
}
|
|
ports, err := normalizePorts(settings.Provider.AllowedPorts)
|
|
if err != nil {
|
|
return runtimeSettings{}, err
|
|
}
|
|
settings.Provider.AllowedPorts = ports
|
|
if settings.Worker.LeaseSeconds <= settings.Provider.HTTPTimeoutSeconds+5 {
|
|
return runtimeSettings{}, errors.New("worker lease must exceed provider HTTP timeout by more than 5 seconds")
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
func rejectAdditionalDocument(decoder *yaml.Decoder) error {
|
|
var extra any
|
|
if err := decoder.Decode(&extra); err == io.EOF {
|
|
return nil
|
|
} else if err != nil {
|
|
return fmt.Errorf("portal settings file is invalid: %w", err)
|
|
}
|
|
return errors.New("portal settings file must contain one YAML document")
|
|
}
|
|
|
|
func normalizePorts(values []uint16) ([]uint16, error) {
|
|
if len(values) == 0 {
|
|
return nil, errors.New("provider allowed_ports must not be empty")
|
|
}
|
|
ports := make([]uint16, 0, len(values))
|
|
seen := make(map[uint16]struct{}, len(values))
|
|
for _, port := range values {
|
|
if port == 0 {
|
|
return nil, errors.New("provider allowed_ports contains an invalid port")
|
|
}
|
|
if _, exists := seen[port]; exists {
|
|
continue
|
|
}
|
|
seen[port] = struct{}{}
|
|
ports = append(ports, port)
|
|
}
|
|
return ports, nil
|
|
}
|