refactor: improve config parsing robustness
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user