package client import ( "encoding/json" "fmt" "os" "strings" ) // Protocol constants (mirror of the Java Protocol class; see PROTOCOL.md). const ( IntentRedapricot = 17 ProtocolVersion = 767 // arbitrary; the hub ignores it MagicControl = 0x01 MagicWorker = 0x02 CIDLen = 16 CtlSessionReady = 0x00 CtlRegister = 0x01 CtlUnregister = 0x02 CtlRegisterAck = 0x03 CtlControlRequest = 0x04 CtlPing = 0x05 CtlPong = 0x06 MuxSyn = 0x00 MuxData = 0x01 MuxFin = 0x02 MuxRst = 0x03 MuxWnd = 0x04 FrameError = 0x7F SaturationThreshold = 8 // Session-establishment feature flags (trailing VarInt on the Rekey message). FlagStreamFC = 0x01 // Per-stream flow-control window bounds (bytes). The advertised window is the // receiver's promise of how much un-credited DATA it will buffer per stream. DefaultStreamWindow = 256 * 1024 MinStreamWindow = 32 * 1024 MaxStreamWindow = 8 << 20 // DataChunkSize caps a single DATA frame's payload so no stream monopolizes // the shared worker connection for long. DataChunkSize = 32 * 1024 ) // Mapping routes a registered pattern to a real destination. type Mapping struct { Pattern string `json:"pattern"` Destination string `json:"destination"` ProxyProtocol bool `json:"proxyProtocol"` } // Config is the client configuration (PROTOCOL.md ยง9.2). type Config struct { Server string `json:"server"` PSK string `json:"psk"` MaxConn int `json:"maxConn"` PingIntervalMs int `json:"pingIntervalMs"` StreamWindowBytes int `json:"streamWindowBytes"` // per-stream receive window; 0 = default Mappings []Mapping `json:"mappings"` } // LoadConfig reads and validates a JSON config file. func LoadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var c Config if err := json.Unmarshal(data, &c); err != nil { return nil, fmt.Errorf("parse config: %w", err) } if c.Server == "" { return nil, fmt.Errorf("server is required") } if c.PSK == "" { return nil, fmt.Errorf("psk is required") } if c.MaxConn < 1 { c.MaxConn = 1 } if c.MaxConn > 8 { c.MaxConn = 8 } if c.PingIntervalMs <= 0 { c.PingIntervalMs = 20000 } if len(c.Mappings) == 0 { return nil, fmt.Errorf("at least one mapping is required") } return &c, nil } // NormalizeAddress matches the hub's normalization: lower-cased, FML-suffix and // trailing-dot stripped. func NormalizeAddress(addr string) string { if i := strings.IndexByte(addr, 0); i >= 0 { addr = addr[:i] } addr = strings.ToLower(addr) addr = strings.TrimRight(addr, ".") return addr }