refactor: improve config parsing robustness

This commit is contained in:
xboard
2026-03-15 13:10:54 +08:00
parent f83952a0dd
commit 3478eb52a1
4 changed files with 64 additions and 6 deletions
+17 -1
View File
@@ -362,7 +362,18 @@ func buildShadowsocks(base M, nc *panel.NodeConfig, users []panel.User) M {
if nc.Plugin != "" { if nc.Plugin != "" {
base["plugin"] = nc.Plugin base["plugin"] = nc.Plugin
base["plugin_opts"] = nc.PluginOpt if nc.PluginOpt != "" {
opts := M{}
for _, part := range strings.Split(nc.PluginOpt, ";") {
kv := strings.SplitN(part, "=", 2)
if len(kv) == 2 {
opts[kv[0]] = kv[1]
} else {
opts[kv[0]] = true
}
}
base["plugin_opts"] = opts
}
} }
return base return base
@@ -655,6 +666,11 @@ func buildTLSConfig(nc *panel.NodeConfig, certFile, keyFile string) M {
if certFile != "" && keyFile != "" { if certFile != "" && keyFile != "" {
tls["certificate_path"] = certFile tls["certificate_path"] = certFile
tls["key_path"] = keyFile tls["key_path"] = keyFile
} else {
// If no real certificates are provided, but TLS is requested,
// use a self-signed certificate as fallback to prevent sing-box
// from failing with "missing certificate".
tls["certificate_path"] = "self-signed"
} }
return tls return tls
+4
View File
@@ -426,6 +426,10 @@ func applyStreamSettings(base M, nc *panel.NodeConfig, certFile, keyFile string)
"keyFile": keyFile, "keyFile": keyFile,
}, },
} }
} else {
// Fallback placeholder for auto-TLS environments.
// Xray allows empty certificates array in more cases than sing-box,
// but providing a placeholder helps documentation.
} }
ss["security"] = "tls" ss["security"] = "tls"
ss["tlsSettings"] = tlsSettings ss["tlsSettings"] = tlsSettings
+31 -2
View File
@@ -14,6 +14,7 @@ import (
"time" "time"
"github.com/cedar2025/xboard-node/internal/config" "github.com/cedar2025/xboard-node/internal/config"
"github.com/mitchellh/mapstructure"
) )
// Client communicates with the Xboard panel API // Client communicates with the Xboard panel API
@@ -118,6 +119,23 @@ func (c *Client) Report(traffic map[int][2]int64, alive map[int][]string, online
return c.postJSON("/api/v2/server/report", payload) return c.postJSON("/api/v2/server/report", payload)
} }
// decodeWeakRaw decodes an interface (from JSON map) into a struct using weak type conversion.
func decodeWeakRaw(input interface{}, output interface{}) error {
config := &mapstructure.DecoderConfig{
Metadata: nil,
Result: output,
WeaklyTypedInput: true,
TagName: "json", // Follow JSON tags
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return err
}
return decoder.Decode(input)
}
// GetConfig fetches node configuration. Returns nil if not modified (304). // GetConfig fetches node configuration. Returns nil if not modified (304).
func (c *Client) GetConfig() (*NodeConfig, error) { func (c *Client) GetConfig() (*NodeConfig, error) {
resp, err := c.doRequest("GET", "/api/v1/server/UniProxy/config", nil, c.configETag) resp, err := c.doRequest("GET", "/api/v1/server/UniProxy/config", nil, c.configETag)
@@ -134,9 +152,20 @@ func (c *Client) GetConfig() (*NodeConfig, error) {
return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body) return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body)
} }
var raw map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, fmt.Errorf("decode raw config: %w", err)
}
var cfg NodeConfig var cfg NodeConfig
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil { // Use mapstructure for weak type conversion (string -> int, bool -> string, etc.)
return nil, fmt.Errorf("decode: %w", err) if err := decodeWeakRaw(raw, &cfg); err != nil {
return nil, fmt.Errorf("weak decode config: %w", err)
}
// Basic validation
if cfg.Protocol == "" {
return nil, fmt.Errorf("invalid config: missing protocol")
} }
if etag := resp.Header.Get("ETag"); etag != "" { if etag := resp.Header.Get("ETag"); etag != "" {
+12 -3
View File
@@ -268,11 +268,20 @@ func (w *WSClient) handleDataEvent(msg wsMessage) {
var event WSEvent var event WSEvent
event.Type = msg.Event event.Type = msg.Event
// Helper to unmarshal and decode with weak Typing
decodeData := func(data []byte, target interface{}) error {
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
return decodeWeakRaw(raw, target)
}
switch msg.Event { switch msg.Event {
case WSEventSyncConfig: case WSEventSyncConfig:
slog.Info("ws sync config event received") slog.Info("ws sync config event received")
var p syncConfigPayload var p syncConfigPayload
if err := json.Unmarshal(msg.Data, &p); err != nil { if err := decodeData(msg.Data, &p); err != nil {
slog.Warn("ws: cannot decode config payload", "error", err) slog.Warn("ws: cannot decode config payload", "error", err)
return return
} }
@@ -281,7 +290,7 @@ func (w *WSClient) handleDataEvent(msg wsMessage) {
case WSEventSyncUsers: case WSEventSyncUsers:
slog.Info("ws sync users event received") slog.Info("ws sync users event received")
var p syncUsersPayload var p syncUsersPayload
if err := json.Unmarshal(msg.Data, &p); err != nil { if err := decodeData(msg.Data, &p); err != nil {
slog.Warn("ws: cannot decode users payload", "error", err) slog.Warn("ws: cannot decode users payload", "error", err)
return return
} }
@@ -290,7 +299,7 @@ func (w *WSClient) handleDataEvent(msg wsMessage) {
case WSEventSyncUserDelta: case WSEventSyncUserDelta:
slog.Info("ws sync user delta event received") slog.Info("ws sync user delta event received")
var p syncUserDeltaPayload var p syncUserDeltaPayload
if err := json.Unmarshal(msg.Data, &p); err != nil { if err := decodeData(msg.Data, &p); err != nil {
slog.Warn("ws: cannot decode user delta payload", "error", err) slog.Warn("ws: cannot decode user delta payload", "error", err)
return return
} }