Files
rvbox/internal/config/validate.go
T

555 lines
25 KiB
Go

package config
import (
"fmt"
"net"
"net/url"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
)
func validateServer(raw serverFile) (*Server, error) {
dataDir, err := validateAbsolutePath("server.data_dir", raw.Server.DataDir, PlatformUnix, false)
if err != nil {
return nil, err
}
controlSocket, err := validateAbsolutePath("server.control_socket", raw.Server.ControlSocket, PlatformUnix, false)
if err != nil {
return nil, err
}
if dataDir == "/" {
return nil, fmt.Errorf("server.data_dir may not be the filesystem root")
}
if !pathWithin(controlSocket, "/run") && !pathWithin(controlSocket, "/var/run") {
return nil, fmt.Errorf("server.control_socket must be below /run or /var/run")
}
shutdownGrace, err := parseDuration("server.shutdown_grace", raw.Server.ShutdownGrace, false)
if err != nil {
return nil, err
}
defaultTTL, err := parseDuration("queue.default_ttl", raw.Queue.DefaultTTL, true)
if err != nil {
return nil, err
}
retryInitial, err := parseDuration("queue.retry_initial", raw.Queue.RetryInitial, false)
if err != nil {
return nil, err
}
retryMax, err := parseDuration("queue.retry_max", raw.Queue.RetryMax, false)
if err != nil {
return nil, err
}
terminalRetention, err := parseDuration("storage.terminal_retention", raw.Storage.TerminalRetention, true)
if err != nil {
return nil, err
}
auditRetention, err := parseDuration("storage.audit_retention", raw.Storage.AuditRetention, true)
if err != nil {
return nil, err
}
durabilityInterval, err := parseDuration("storage.durability_interval", raw.Storage.DurabilityInterval, false)
if err != nil {
return nil, err
}
sqliteBusyTimeout, err := parseDuration("storage.sqlite_busy_timeout", raw.Storage.SQLiteBusyTimeout, false)
if err != nil {
return nil, err
}
writeDeadline, err := parseDuration("flow.write_deadline", raw.Flow.WriteDeadline, false)
if err != nil {
return nil, err
}
heartbeatIdle, err := parseDuration("protocol.heartbeat_idle", raw.Protocol.HeartbeatIdle, false)
if err != nil {
return nil, err
}
livenessTimeout, err := parseDuration("protocol.liveness_timeout", raw.Protocol.LivenessTimeout, false)
if err != nil {
return nil, err
}
takeoverTTL, err := parseDuration("protocol.takeover_ttl", raw.Protocol.TakeoverTTL, false)
if err != nil {
return nil, err
}
if err := validateListener("server.agent_listen", raw.Server.AgentListen); err != nil {
return nil, err
}
if err := validateHTTPPath("server.agent_path", raw.Server.AgentPath); err != nil {
return nil, err
}
if err := validateListener("json_rpc.listen", raw.JSONRPC.Listen); err != nil {
return nil, err
}
if raw.Queue.MaxPerClient == 0 || raw.Queue.MaxServer == 0 || raw.Queue.MaxPerClient > raw.Queue.MaxServer {
return nil, fmt.Errorf("queue limits must be positive and max_per_client <= max_server")
}
if retryInitial > retryMax {
return nil, fmt.Errorf("queue.retry_initial must be <= queue.retry_max")
}
if err := increasing("server storage tiers", raw.Storage.CommandOutputLimitBytes, raw.Storage.CommandTotalLimitBytes, raw.Storage.ClientTotalLimitBytes, raw.Storage.ServerTotalLimitBytes); err != nil {
return nil, err
}
if raw.Storage.CommandCloseoutReserveBytes == 0 || raw.Storage.CommandCloseoutReserveBytes >= raw.Storage.CommandTotalLimitBytes {
return nil, fmt.Errorf("storage.command_closeout_reserve_bytes must fit below command_total_limit_bytes")
}
for name, value := range map[string]uint64{
"storage.audit_limit_bytes": raw.Storage.AuditLimitBytes, "storage.tombstone_max_entries": raw.Storage.TombstoneMaxEntries,
"storage.free_space_floor_bytes": raw.Storage.FreeSpaceFloorBytes, "storage.segment_target_bytes": raw.Storage.SegmentTargetBytes,
} {
if value == 0 {
return nil, fmt.Errorf("%s must be positive", name)
}
}
if raw.Storage.IncidentNoteMaxBytes == 0 || raw.Storage.IncidentNoteMaxBytes > HardMaxProtocolDetailBytes || raw.Storage.ProtocolDetailMaxBytes == 0 || raw.Storage.ProtocolDetailMaxBytes > HardMaxProtocolDetailBytes {
return nil, fmt.Errorf("incident and protocol detail limits must be in 1..%d", HardMaxProtocolDetailBytes)
}
if err := watermarks("flow.raw_output", raw.Flow.RawOutputLowBytes, raw.Flow.RawOutputHighBytes); err != nil {
return nil, err
}
if raw.Flow.UnacknowledgedPerCommandBytes == 0 || raw.Flow.UnacknowledgedPerCommandBytes >= raw.Storage.CommandTotalLimitBytes || raw.Flow.UnacknowledgedPerSessionBytes == 0 || raw.Flow.UnacknowledgedPerSessionBytes >= raw.Storage.ClientTotalLimitBytes || raw.Flow.UnacknowledgedPerCommandBytes > raw.Flow.UnacknowledgedPerSessionBytes {
return nil, fmt.Errorf("flow send windows must be positive, ordered, and below their durable tiers")
}
if heartbeatIdle >= livenessTimeout {
return nil, fmt.Errorf("protocol.heartbeat_idle must be below liveness_timeout")
}
if err := protocolLimits(raw.Protocol.MaxAgentEnvelopeBytes, raw.Protocol.MaxExecutionSpecBytes, raw.Protocol.MaxRawChunkBytes, raw.Protocol.MaxScriptBytes, raw.Protocol.MaxControlRequestBytes, raw.Protocol.MaxJSONRPCBodyBytes); err != nil {
return nil, err
}
observability, err := validateObservability(raw.Observability, PlatformUnix, false)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(raw.JSONRPC.Listen)
return &Server{
Server: ServerCore{DataDir: dataDir, AgentListen: raw.Server.AgentListen, AgentPath: raw.Server.AgentPath, ControlSocket: controlSocket, ShutdownGrace: shutdownGrace},
JSONRPC: JSONRPC{Enabled: raw.JSONRPC.Enabled, Listen: raw.JSONRPC.Listen, NonLoopbackBind: raw.JSONRPC.Enabled && !isLoopbackHost(host)},
Queue: ServerQueue{DefaultTTL: defaultTTL, MaxPerClient: raw.Queue.MaxPerClient, MaxServer: raw.Queue.MaxServer, RetryInitial: retryInitial, RetryMax: retryMax},
Storage: ServerStorage{
CommandOutputLimitBytes: raw.Storage.CommandOutputLimitBytes, CommandTotalLimitBytes: raw.Storage.CommandTotalLimitBytes,
ClientTotalLimitBytes: raw.Storage.ClientTotalLimitBytes, ServerTotalLimitBytes: raw.Storage.ServerTotalLimitBytes,
TerminalRetention: terminalRetention, AuditLimitBytes: raw.Storage.AuditLimitBytes, AuditRetention: auditRetention,
TombstoneMaxEntries: raw.Storage.TombstoneMaxEntries, CommandCloseoutReserveBytes: raw.Storage.CommandCloseoutReserveBytes,
FreeSpaceFloorBytes: raw.Storage.FreeSpaceFloorBytes, SegmentTargetBytes: raw.Storage.SegmentTargetBytes,
DurabilityInterval: durabilityInterval, SQLiteBusyTimeout: sqliteBusyTimeout,
IncidentNoteMaxBytes: raw.Storage.IncidentNoteMaxBytes, ProtocolDetailMaxBytes: raw.Storage.ProtocolDetailMaxBytes,
},
Flow: ServerFlow{RawOutputHighBytes: raw.Flow.RawOutputHighBytes, RawOutputLowBytes: raw.Flow.RawOutputLowBytes, UnacknowledgedPerCommandBytes: raw.Flow.UnacknowledgedPerCommandBytes, UnacknowledgedPerSessionBytes: raw.Flow.UnacknowledgedPerSessionBytes, WriteDeadline: writeDeadline},
Protocol: ServerProtocol{HeartbeatIdle: heartbeatIdle, LivenessTimeout: livenessTimeout, TakeoverTTL: takeoverTTL, MaxAgentEnvelopeBytes: raw.Protocol.MaxAgentEnvelopeBytes, MaxExecutionSpecBytes: raw.Protocol.MaxExecutionSpecBytes, MaxRawChunkBytes: raw.Protocol.MaxRawChunkBytes, MaxScriptBytes: raw.Protocol.MaxScriptBytes, MaxControlRequestBytes: raw.Protocol.MaxControlRequestBytes, MaxJSONRPCBodyBytes: raw.Protocol.MaxJSONRPCBodyBytes},
Observability: observability,
}, nil
}
func validateClient(raw clientFile, options ClientOptions) (*Client, error) {
if options.Platform != PlatformUnix && options.Platform != PlatformWindows {
return nil, fmt.Errorf("client target platform is required")
}
parsedURL, err := url.Parse(raw.Client.ServerURL)
if err != nil || parsedURL.Scheme != "wss" || parsedURL.Host == "" || parsedURL.User != nil {
return nil, fmt.Errorf("client.server_url must be an absolute wss URL without userinfo")
}
stateDir, err := validateAbsolutePath("client.state_dir", raw.Client.StateDir, options.Platform, false)
if err != nil {
return nil, err
}
daemonCWD, err := validateAbsolutePath("client.daemon_cwd", raw.Client.DaemonCWD, options.Platform, false)
if err != nil {
return nil, err
}
if options.CheckFilesystem {
daemonCWD, err = checkNativeDirectory("client.daemon_cwd", daemonCWD, options.Platform)
if err != nil {
return nil, err
}
}
if err := validateClientID(raw.Client.ClientID); err != nil {
return nil, err
}
if raw.Client.MaxRunningCommands == 0 || raw.Client.MaxQueuedCommands == 0 {
return nil, fmt.Errorf("client command limits must be positive")
}
shutdownGrace, err := parseDuration("client.shutdown_grace", raw.Client.ShutdownGrace, false)
if err != nil {
return nil, err
}
caFile, err := validateAbsolutePath("tls.ca_file", raw.TLS.CAFile, options.Platform, true)
if err != nil {
return nil, err
}
if options.CheckFilesystem && caFile != "" {
caFile, err = checkNativeRegularFile("tls.ca_file", caFile, options.Platform)
if err != nil {
return nil, err
}
}
shells, err := validateShells(raw.Shells, options)
if err != nil {
return nil, err
}
network, err := validateClientNetwork(raw.Network)
if err != nil {
return nil, err
}
durability, err := parseDuration("storage.durability_interval", raw.Storage.DurabilityInterval, false)
if err != nil {
return nil, err
}
if err := increasing("client storage tiers", raw.Storage.CommandOutputLimitBytes, raw.Storage.CommandTotalLimitBytes, raw.Storage.ClientTotalLimitBytes); err != nil {
return nil, err
}
if raw.Storage.TombstoneMaxEntries == 0 || raw.Storage.FreeSpaceFloorBytes == 0 || raw.Storage.SegmentTargetBytes == 0 || raw.Storage.CommandCloseoutReserveBytes == 0 || raw.Storage.CommandCloseoutReserveBytes >= raw.Storage.CommandTotalLimitBytes {
return nil, fmt.Errorf("client storage counts must be positive and closeout reserve must fit within command quota")
}
if err := watermarks("flow.raw_output_command", raw.Flow.RawOutputCommandLowBytes, raw.Flow.RawOutputCommandHighBytes); err != nil {
return nil, err
}
if err := watermarks("flow.raw_output_client", raw.Flow.RawOutputClientLowBytes, raw.Flow.RawOutputClientHighBytes); err != nil {
return nil, err
}
if raw.Flow.RawOutputCommandHighBytes >= raw.Flow.RawOutputClientHighBytes {
return nil, fmt.Errorf("command raw-output high watermark must be below client high watermark")
}
if raw.Flow.UnacknowledgedPerCommandBytes == 0 || raw.Flow.UnacknowledgedPerCommandBytes >= raw.Storage.CommandTotalLimitBytes || raw.Flow.UnacknowledgedPerSessionBytes == 0 || raw.Flow.UnacknowledgedPerSessionBytes >= raw.Storage.ClientTotalLimitBytes || raw.Flow.UnacknowledgedPerCommandBytes > raw.Flow.UnacknowledgedPerSessionBytes {
return nil, fmt.Errorf("client send windows must be positive, ordered, and below durable tiers")
}
execution, err := validateExecution(raw.Execution)
if err != nil {
return nil, err
}
observability, err := validateObservability(raw.Observability, options.Platform, true)
if err != nil {
return nil, err
}
profiles, err := validateProfiles(raw.Profiles, options.Platform)
if err != nil {
return nil, err
}
return &Client{
Client: ClientCore{ServerURL: raw.Client.ServerURL, StateDir: stateDir, ClientID: raw.Client.ClientID, DaemonCWD: daemonCWD, MaxRunningCommands: raw.Client.MaxRunningCommands, MaxQueuedCommands: raw.Client.MaxQueuedCommands, ShutdownGrace: shutdownGrace},
TLS: TLS{CAFile: caFile, ServerName: raw.TLS.ServerName}, Shells: shells, Network: network,
Storage: ClientStorage{CommandOutputLimitBytes: raw.Storage.CommandOutputLimitBytes, CommandTotalLimitBytes: raw.Storage.CommandTotalLimitBytes, ClientTotalLimitBytes: raw.Storage.ClientTotalLimitBytes, TombstoneMaxEntries: raw.Storage.TombstoneMaxEntries, CommandCloseoutReserveBytes: raw.Storage.CommandCloseoutReserveBytes, FreeSpaceFloorBytes: raw.Storage.FreeSpaceFloorBytes, SegmentTargetBytes: raw.Storage.SegmentTargetBytes, DurabilityInterval: durability},
Flow: ClientFlow{RawOutputCommandHighBytes: raw.Flow.RawOutputCommandHighBytes, RawOutputCommandLowBytes: raw.Flow.RawOutputCommandLowBytes, RawOutputClientHighBytes: raw.Flow.RawOutputClientHighBytes, RawOutputClientLowBytes: raw.Flow.RawOutputClientLowBytes, UnacknowledgedPerCommandBytes: raw.Flow.UnacknowledgedPerCommandBytes, UnacknowledgedPerSessionBytes: raw.Flow.UnacknowledgedPerSessionBytes},
Execution: execution, Observability: observability, Profiles: profiles,
}, nil
}
func validateClientNetwork(raw clientNetworkFile) (ClientNetwork, error) {
values := make([]timePair, 0, 7)
for _, item := range []struct{ name, value string }{
{"network.heartbeat_idle", raw.HeartbeatIdle}, {"network.liveness_timeout", raw.LivenessTimeout},
{"network.reconnect_initial", raw.ReconnectInitial}, {"network.reconnect_max", raw.ReconnectMax},
{"network.stable_session_reset", raw.StableSessionReset}, {"network.connect_timeout", raw.ConnectTimeout},
{"network.write_deadline", raw.WriteDeadline},
} {
value, err := parseDuration(item.name, item.value, false)
if err != nil {
return ClientNetwork{}, err
}
values = append(values, timePair{item.name, value})
}
if values[0].value >= values[1].value {
return ClientNetwork{}, fmt.Errorf("network.heartbeat_idle must be below liveness_timeout")
}
if values[2].value > values[3].value {
return ClientNetwork{}, fmt.Errorf("network.reconnect_initial must be <= reconnect_max")
}
return ClientNetwork{HeartbeatIdle: values[0].value, LivenessTimeout: values[1].value, ReconnectInitial: values[2].value, ReconnectMax: values[3].value, StableSessionReset: values[4].value, ConnectTimeout: values[5].value, WriteDeadline: values[6].value}, nil
}
type timePair struct {
name string
value time.Duration
}
func validateExecution(raw executionFile) (Execution, error) {
durations := make([]time.Duration, 4)
for index, item := range []struct{ name, value string }{{"execution.descendant_drain_grace", raw.DescendantDrainGrace}, {"execution.windows_term_grace", raw.WindowsTermGrace}, {"execution.hung_threshold", raw.HungThreshold}, {"execution.diagnostic_interval", raw.DiagnosticInterval}} {
value, err := parseDuration(item.name, item.value, false)
if err != nil {
return Execution{}, err
}
durations[index] = value
}
if err := protocolLimits(raw.MaxAgentEnvelopeBytes, raw.MaxExecutionSpecBytes, raw.MaxRawChunkBytes, raw.MaxScriptBytes, HardMaxControlRequestBytes, HardMaxJSONRPCBodyBytes); err != nil {
return Execution{}, err
}
if raw.ProtocolDetailMaxBytes == 0 || raw.ProtocolDetailMaxBytes > HardMaxProtocolDetailBytes {
return Execution{}, fmt.Errorf("execution.protocol_detail_max_bytes exceeds hard ceiling")
}
return Execution{DescendantDrainGrace: durations[0], WindowsTermGrace: durations[1], HungThreshold: durations[2], DiagnosticInterval: durations[3], MaxScriptBytes: raw.MaxScriptBytes, MaxExecutionSpecBytes: raw.MaxExecutionSpecBytes, MaxAgentEnvelopeBytes: raw.MaxAgentEnvelopeBytes, MaxRawChunkBytes: raw.MaxRawChunkBytes, ProtocolDetailMaxBytes: raw.ProtocolDetailMaxBytes}, nil
}
func validateShells(raw shellsFile, options ClientOptions) (Shells, error) {
if !slices.Contains([]string{"sh", "bash"}, raw.DefaultUnix) {
return Shells{}, fmt.Errorf("shells.default_unix must be sh or bash")
}
if !slices.Contains([]string{"cmd", "powershell"}, raw.DefaultWindows) {
return Shells{}, fmt.Errorf("shells.default_windows must be cmd or powershell")
}
type shellPath struct {
name, value string
platform Platform
}
paths := []shellPath{{"sh", raw.SH, PlatformUnix}, {"bash", raw.Bash, PlatformUnix}, {"cmd", raw.CMD, PlatformWindows}, {"powershell", raw.PowerShell, PlatformWindows}}
advertised := make(map[string]string)
resolved := make(map[string]string)
for _, shell := range paths {
canonical, err := validateAbsolutePath("shells."+shell.name, shell.value, shell.platform, true)
if err != nil {
return Shells{}, err
}
resolved[shell.name] = canonical
if shell.platform == options.Platform && canonical != "" {
if options.CheckFilesystem {
canonical, err = checkNativeExecutable("shells."+shell.name, canonical, options.Platform)
if err != nil {
return Shells{}, err
}
}
advertised[shell.name] = canonical
resolved[shell.name] = canonical
}
}
defaultShell := raw.DefaultUnix
if options.Platform == PlatformWindows {
defaultShell = raw.DefaultWindows
}
if _, exists := advertised[defaultShell]; !exists {
return Shells{}, fmt.Errorf("current-platform default shell %q is not validated and advertised", defaultShell)
}
roots := make([]string, len(raw.AllowedCWDRoots))
for index, root := range raw.AllowedCWDRoots {
canonical, err := validateAbsolutePath(fmt.Sprintf("shells.allowed_cwd_roots[%d]", index), root, options.Platform, false)
if err != nil {
return Shells{}, err
}
if options.CheckFilesystem {
canonical, err = checkNativeDirectory(fmt.Sprintf("shells.allowed_cwd_roots[%d]", index), canonical, options.Platform)
if err != nil {
return Shells{}, err
}
}
roots[index] = canonical
}
return Shells{DefaultUnix: raw.DefaultUnix, DefaultWindows: raw.DefaultWindows, SH: resolved["sh"], Bash: resolved["bash"], CMD: resolved["cmd"], PowerShell: resolved["powershell"], AllowedCWDRoots: roots, Advertised: advertised}, nil
}
func validateProfiles(raw profilesFile, platform Platform) (Profiles, error) {
convert := func(name string, source profileFile) (Profile, error) {
seen := map[string]bool{}
for _, control := range source.RequiredControls {
if seen[control] || !slices.Contains([]string{"cpu", "memory", "pids", "io"}, control) {
return Profile{}, fmt.Errorf("profiles.%s has duplicate or unknown required control %q", name, control)
}
seen[control] = true
}
for key, value := range source.LinuxIOReadBPS {
if err := validateDeviceRate(key, value); err != nil {
return Profile{}, fmt.Errorf("profiles.%s.linux_io_read_bps: %w", name, err)
}
}
for key, value := range source.LinuxIOWriteBPS {
if err := validateDeviceRate(key, value); err != nil {
return Profile{}, fmt.Errorf("profiles.%s.linux_io_write_bps: %w", name, err)
}
}
if source.Enabled {
if len(source.RequiredControls) == 0 {
return Profile{}, fmt.Errorf("profiles.%s is enabled without required_controls", name)
}
if seen["cpu"] && source.CPUPercent == 0 {
return Profile{}, fmt.Errorf("profiles.%s requires nonzero cpu_percent", name)
}
if seen["memory"] && source.MemoryMaxBytes == 0 {
return Profile{}, fmt.Errorf("profiles.%s requires nonzero memory_max_bytes", name)
}
if seen["pids"] && source.PIDsMax == 0 {
return Profile{}, fmt.Errorf("profiles.%s requires nonzero pids_max", name)
}
if seen["io"] {
if platform == PlatformWindows && source.WindowsIOReadBPS == 0 && source.WindowsIOWriteBPS == 0 {
return Profile{}, fmt.Errorf("profiles.%s requires a nonzero Windows IO rate", name)
}
if platform == PlatformUnix && len(source.LinuxIOReadBPS) == 0 && len(source.LinuxIOWriteBPS) == 0 {
return Profile{}, fmt.Errorf("profiles.%s requires a nonzero Linux IO rate", name)
}
}
}
return Profile{Enabled: source.Enabled, RequiredControls: slices.Clone(source.RequiredControls), CPUPercent: source.CPUPercent, MemoryMaxBytes: source.MemoryMaxBytes, PIDsMax: source.PIDsMax, WindowsIOReadBPS: source.WindowsIOReadBPS, WindowsIOWriteBPS: source.WindowsIOWriteBPS, LinuxIOReadBPS: cloneMap(source.LinuxIOReadBPS), LinuxIOWriteBPS: cloneMap(source.LinuxIOWriteBPS)}, nil
}
var result Profiles
for _, item := range []struct {
name string
source profileFile
target *Profile
}{{"light", raw.Light, &result.Light}, {"cpu_medium", raw.CPUMedium, &result.CPUMedium}, {"cpu_heavy", raw.CPUHeavy, &result.CPUHeavy}, {"mem_medium", raw.MemMedium, &result.MemMedium}, {"mem_heavy", raw.MemHeavy, &result.MemHeavy}, {"disk_medium", raw.DiskMedium, &result.DiskMedium}, {"disk_heavy", raw.DiskHeavy, &result.DiskHeavy}} {
profile, err := convert(item.name, item.source)
if err != nil {
return Profiles{}, err
}
*item.target = profile
}
return result, nil
}
func ValidateProfileCombination(names []string) error {
seen := map[string]bool{}
dimensions := map[string]string{"cpu_medium": "cpu", "cpu_heavy": "cpu", "mem_medium": "memory", "mem_heavy": "memory", "disk_medium": "disk", "disk_heavy": "disk"}
for _, name := range names {
name = strings.ToLower(name)
if seen[name] {
return fmt.Errorf("duplicate execution profile %q", name)
}
seen[name] = true
if name == "light" {
if len(names) != 1 {
return fmt.Errorf("light execution profile is exclusive")
}
continue
}
dimension, exists := dimensions[name]
if !exists {
return fmt.Errorf("unknown execution profile %q", name)
}
for prior := range seen {
if prior != name && dimensions[prior] == dimension {
return fmt.Errorf("multiple %s execution profiles", dimension)
}
}
}
return nil
}
func validateDeviceRate(key string, value uint64) error {
parts := strings.Split(key, ":")
if len(parts) != 2 || value == 0 {
return fmt.Errorf("device %q must be canonical major:minor with nonzero rate", key)
}
for _, part := range parts {
parsed, err := strconv.ParseUint(part, 10, 32)
if err != nil || strconv.FormatUint(parsed, 10) != part {
return fmt.Errorf("device %q is not canonical major:minor", key)
}
}
return nil
}
func validateObservability(raw observabilityFile, platform Platform, allowDisabled bool) (Observability, error) {
if raw.Listen == "" && !allowDisabled {
return Observability{}, fmt.Errorf("observability.listen must be a host:port listener")
}
if raw.Listen != "" {
if err := validateListener("observability.listen", raw.Listen); err != nil {
return Observability{}, err
}
}
for name, value := range map[string]string{"liveness_path": raw.LivenessPath, "readiness_path": raw.ReadinessPath, "metrics_path": raw.MetricsPath} {
if err := validateHTTPPath("observability."+name, value); err != nil {
return Observability{}, err
}
}
if raw.LivenessPath == raw.ReadinessPath || raw.LivenessPath == raw.MetricsPath || raw.ReadinessPath == raw.MetricsPath {
return Observability{}, fmt.Errorf("observability paths must be distinct")
}
if !slices.Contains([]string{"debug", "info", "warn", "error"}, raw.LogLevel) {
return Observability{}, fmt.Errorf("observability.log_level is invalid")
}
if !slices.Contains([]string{"json", "text"}, raw.LogFormat) {
return Observability{}, fmt.Errorf("observability.log_format is invalid")
}
logFile, err := validateAbsolutePath("observability.log_file", raw.LogFile, platform, true)
if err != nil {
return Observability{}, err
}
if (raw.LogMaxBytes == 0) != (raw.LogMaxFiles == 0) {
return Observability{}, fmt.Errorf("observability log rotation byte/file limits must both be zero or positive")
}
return Observability{Listen: raw.Listen, LivenessPath: raw.LivenessPath, ReadinessPath: raw.ReadinessPath, MetricsPath: raw.MetricsPath, LogLevel: raw.LogLevel, LogFormat: raw.LogFormat, LogFile: logFile, LogMaxBytes: raw.LogMaxBytes, LogMaxFiles: raw.LogMaxFiles}, nil
}
func increasing(name string, values ...uint64) error {
for index, value := range values {
if value == 0 {
return fmt.Errorf("%s values must be positive", name)
}
if index > 0 && value <= values[index-1] {
return fmt.Errorf("%s must be strictly increasing", name)
}
}
return nil
}
func watermarks(name string, low, high uint64) error {
if low == 0 || low >= high {
return fmt.Errorf("%s requires 0 < low < high", name)
}
return nil
}
func protocolLimits(agent, spec, chunk, script, control, jsonRPC uint64) error {
values := []struct {
name string
value, max uint64
}{{"max_agent_envelope_bytes", agent, HardMaxAgentEnvelopeBytes}, {"max_execution_spec_bytes", spec, HardMaxExecutionSpecBytes}, {"max_raw_chunk_bytes", chunk, HardMaxRawChunkBytes}, {"max_script_bytes", script, HardMaxScriptBytes}, {"max_control_request_bytes", control, HardMaxControlRequestBytes}, {"max_json_rpc_body_bytes", jsonRPC, HardMaxJSONRPCBodyBytes}}
for _, item := range values {
if item.value == 0 || item.value > item.max {
return fmt.Errorf("protocol.%s must be in 1..%d", item.name, item.max)
}
}
return nil
}
func validateListener(name, value string) error {
host, port, err := net.SplitHostPort(value)
if err != nil || host == "" || port == "" {
return fmt.Errorf("%s must be a host:port listener", name)
}
if _, err := strconv.ParseUint(port, 10, 16); err != nil || port == "0" {
return fmt.Errorf("%s has invalid port", name)
}
return nil
}
func validateHTTPPath(name, value string) error {
if value == "" || !strings.HasPrefix(value, "/") || strings.HasPrefix(value, "//") {
return fmt.Errorf("%s must be an absolute HTTP path", name)
}
return nil
}
func isLoopbackHost(host string) bool {
if strings.EqualFold(host, "localhost") {
return true
}
ip := net.ParseIP(strings.Trim(host, "[]"))
return ip != nil && ip.IsLoopback()
}
func validateClientID(value string) error {
if value == "" {
return nil
}
if len(value) > 128 {
return fmt.Errorf("client.client_id exceeds 128 bytes")
}
for _, current := range []byte(value) {
if current < 0x21 || current > 0x7e {
return fmt.Errorf("client.client_id must contain printable ASCII without spaces")
}
}
return nil
}
func pathWithin(value, root string) bool {
relative, err := filepath.Rel(root, value)
return err == nil && relative != "." && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
}
func cloneMap(source map[string]uint64) map[string]uint64 {
if source == nil {
return nil
}
result := make(map[string]uint64, len(source))
for key, value := range source {
result[key] = value
}
return result
}