LoadServer reads and validates a server config file.
(path string)
| 64 | |
| 65 | // LoadServer reads and validates a server config file. |
| 66 | func LoadServer(path string) (*Server, error) { |
| 67 | b, err := os.ReadFile(path) |
| 68 | if err != nil { |
| 69 | if errors.Is(err, fs.ErrNotExist) { |
| 70 | return nil, fmt.Errorf("config file %q not found.\n Fix: copy the example and edit it:\n cp server_config.example.json %s", path, path) |
| 71 | } |
| 72 | return nil, fmt.Errorf("cannot read config %q: %w", path, err) |
| 73 | } |
| 74 | var f serverFile |
| 75 | if err := json.Unmarshal(b, &f); err != nil { |
| 76 | return nil, fmt.Errorf("config %q is not valid JSON: %v\n Common causes: missing comma between fields, trailing comma after the last field, unclosed quote, or a typo in a field name", path, err) |
| 77 | } |
| 78 | |
| 79 | legacyHost, legacyPort := parseLegacyListenAddr(f.ListenAddr) |
| 80 | listenHost := firstNonEmpty(f.ServerHost, legacyHost, "0.0.0.0") |
| 81 | listenPort := firstPositive(f.ServerPort, legacyPort) |
| 82 | if listenPort == 0 { |
| 83 | listenPort = 8443 |
| 84 | } |
| 85 | if listenPort < 1 || listenPort > 65535 { |
| 86 | return nil, fmt.Errorf("server_port %d is out of range (must be 1-65535)", listenPort) |
| 87 | } |
| 88 | |
| 89 | key := strings.TrimSpace(firstNonEmpty(f.TunnelKey, f.AESKeyHex)) |
| 90 | if key == "" || key == "SAME_VALUE_AS_CLIENT_tunnel_key" { |
| 91 | return nil, fmt.Errorf("tunnel_key is empty or still the placeholder text in %s.\n Fix: paste the 64-character key from your client_config.json into the tunnel_key field. Both files must contain the SAME value", path) |
| 92 | } |
| 93 | if len(key) != 64 { |
| 94 | return nil, fmt.Errorf("tunnel_key must be exactly 64 hex characters (got %d) in %s.\n Fix: paste the SAME tunnel_key from client_config.json — both files must contain byte-identical values", len(key), path) |
| 95 | } |
| 96 | raw, err := hex.DecodeString(key) |
| 97 | if err != nil || len(raw) != 32 { |
| 98 | return nil, fmt.Errorf("tunnel_key in %s contains non-hex characters.\n Valid characters are 0-9 and a-f. Copy the value from client_config.json carefully — no spaces, quotes, or extra newlines", path) |
| 99 | } |
| 100 | |
| 101 | var upstreamProxy string |
| 102 | if raw := strings.TrimSpace(f.UpstreamProxy); raw != "" { |
| 103 | u, err := url.Parse(raw) |
| 104 | if err != nil || u.Scheme != "socks5" { |
| 105 | return nil, fmt.Errorf("upstream_proxy must be a socks5:// URL (e.g. socks5://127.0.0.1:40000), got %q", raw) |
| 106 | } |
| 107 | if u.Host == "" { |
| 108 | return nil, fmt.Errorf("upstream_proxy is missing host:port (e.g. socks5://127.0.0.1:40000)") |
| 109 | } |
| 110 | upstreamProxy = u.Host |
| 111 | } |
| 112 | if f.InitialResponseBytesPreEncode < 0 { |
| 113 | return nil, fmt.Errorf("initial_response_bytes_pre_encode must be >= 0") |
| 114 | } |
| 115 | |
| 116 | c := Server{ |
| 117 | ListenAddr: net.JoinHostPort(listenHost, strconv.Itoa(listenPort)), |
| 118 | AESKeyHex: key, |
| 119 | DebugTiming: f.DebugTiming, |
| 120 | UpstreamProxy: upstreamProxy, |
| 121 | InitialResponseBytesPreEncode: f.InitialResponseBytesPreEncode, |
| 122 | } |
| 123 | return &c, nil |