From 68fc27c7a43e3490178520134d6b043ab13bd8a9 Mon Sep 17 00:00:00 2001 From: kastov Date: Mon, 11 May 2026 01:36:34 +0300 Subject: [PATCH] Refactor webhook and external config handling to utilize utility functions for HTTP/Unix socket URL parsing - Replaced custom URL parsing and socket resolution logic in `webhook.go` with utility functions from `utils`. - Updated `ConfigLoader` in `external.go` to handle deprecated `http+unix://` prefix and streamline HTTP content fetching. --- app/router/webhook.go | 50 +----------- common/utils/unixsocket.go | 55 +++++++++++++ main/confloader/external/external.go | 117 ++++++++++++++------------- 3 files changed, 121 insertions(+), 101 deletions(-) create mode 100644 common/utils/unixsocket.go diff --git a/app/router/webhook.go b/app/router/webhook.go index 32ae28872f77..535ed294f016 100644 --- a/app/router/webhook.go +++ b/app/router/webhook.go @@ -7,60 +7,16 @@ import ( "io" "net" "net/http" - "path/filepath" - "runtime" - "strings" "sync" - "syscall" "time" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/features/routing" routing_session "github.com/xtls/xray-core/features/routing/session" ) -// parseURL splits a webhook URL into an HTTP URL and an optional Unix socket -// path. For regular http/https URLs the input is returned unchanged with an -// empty socketPath. For Unix sockets the format is: -// -// /path/to/socket.sock:/http/path -// @abstract:/http/path -// @@padded:/http/path -// -// The :/ separator after the socket path delimits the HTTP request path. -// If omitted, "/" is used. -func parseURL(raw string) (httpURL, socketPath string) { - if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') { - return raw, "" - } - if idx := strings.Index(raw, ":/"); idx >= 0 { - return "http://localhost" + raw[idx+1:], raw[:idx] - } - return "http://localhost/", raw -} -// resolveSocketPath applies platform-specific transformations to a Unix -// socket path, matching the behaviour of the listen side in -// transport/internet/system_listener.go. -// -// For abstract sockets (prefix @) on Linux/Android: -// - single @ — used as-is (lock-free abstract socket) -// - double @@ — stripped to single @ and padded to -// syscall.RawSockaddrUnix{}.Path length (HAProxy compat) -func resolveSocketPath(path string) string { - if len(path) == 0 || path[0] != '@' { - return path - } - if runtime.GOOS != "linux" && runtime.GOOS != "android" { - return path - } - if len(path) > 1 && path[1] == '@' { - fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) - copy(fullAddr, path[1:]) - return string(fullAddr) - } - return path -} func ptr[T any](v T) *T { return &v } @@ -96,7 +52,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) { return nil, nil } - httpURL, socketPath := parseURL(cfg.Url) + httpURL, socketPath := utils.SplitHTTPUnixURL(cfg.Url) h := &WebhookNotifier{ url: httpURL, deduplication: cfg.Deduplication, @@ -107,7 +63,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) { } if socketPath != "" { - dialAddr := resolveSocketPath(socketPath) + dialAddr := utils.ResolveSocketPath(socketPath) h.client.Transport = &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { var d net.Dialer diff --git a/common/utils/unixsocket.go b/common/utils/unixsocket.go new file mode 100644 index 000000000000..52d36256e8c1 --- /dev/null +++ b/common/utils/unixsocket.go @@ -0,0 +1,55 @@ +package utils + +import ( + "path/filepath" + "runtime" + "strings" + "syscall" +) + +// ResolveSocketPath applies platform-specific transformations to a Unix +// socket path, matching the listen-side behaviour in +// transport/internet/system_listener.go. +// +// For abstract sockets (prefix @) on Linux/Android: +// - single @ — used as-is (lock-free abstract socket) +// - double @@ — stripped to single @ and padded to +// syscall.RawSockaddrUnix{}.Path length (HAProxy compat) +// +// Filesystem paths and abstract sockets on other platforms are returned +// unchanged. +func ResolveSocketPath(path string) string { + if len(path) == 0 || path[0] != '@' { + return path + } + if runtime.GOOS != "linux" && runtime.GOOS != "android" { + return path + } + if len(path) > 1 && path[1] == '@' { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) + copy(fullAddr, path[1:]) + return string(fullAddr) + } + return path +} + +// SplitHTTPUnixURL splits a target into an HTTP URL and an optional Unix +// socket path. For regular http(s) URLs the input is returned unchanged +// with an empty socketPath. For Unix sockets the format is: +// +// /path/to/socket.sock[:/http/path] +// @abstract[:/http/path] +// @@padded[:/http/path] +// +// The :/ separator delimits the socket path from the HTTP request path. +// If omitted, "/" is used. +func SplitHTTPUnixURL(raw string) (httpURL, socketPath string) { + if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') { + return raw, "" + } + if idx := strings.Index(raw, ":/"); idx >= 0 { + return "http://localhost" + raw[idx+1:], raw[:idx] + } + return "http://localhost/", raw +} + diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index 110b483da95e..f9ea874c8d9c 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -3,8 +3,8 @@ package external import ( "bytes" "context" - "net" "io" + "net" "net/http" "net/url" "os" @@ -13,6 +13,7 @@ import ( "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/main/confloader" ) @@ -20,9 +21,10 @@ func ConfigLoader(arg string) (out io.Reader, err error) { var data []byte switch { case strings.HasPrefix(arg, "http+unix://"): - data, err = FetchUnixSocketHTTPContent(arg) + errors.PrintDeprecatedFeatureWarning(`"http+unix://" prefix`, `direct Unix socket path (e.g. /path/socket.sock:/api or @abstract:/api)`) + data, err = FetchHTTPContent(httpUnixToCanonical(arg)) - case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): + case isRemoteSource(arg): data, err = FetchHTTPContent(arg) case arg == "stdin:": @@ -39,19 +41,38 @@ func ConfigLoader(arg string) (out io.Reader, err error) { return } +// FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL +// or a Unix socket HTTP endpoint. +// +// http(s)://host/api regular HTTP(S) +// /path/to/socket.sock[:/api] filesystem socket +// @abstract[:/api] abstract socket (Linux/Android) +// @@padded[:/api] padded abstract socket (HAProxy compat) +// +// When the ":/" separator is omitted on a socket target, the request is +// made to "/". func FetchHTTPContent(target string) ([]byte, error) { - parsedTarget, err := url.Parse(target) + httpURL, socketPath := utils.SplitHTTPUnixURL(target) + + parsedTarget, err := url.Parse(httpURL) if err != nil { return nil, errors.New("invalid URL: ", target).Base(err) } - if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" { - return nil, errors.New("invalid scheme: ", parsedTarget.Scheme) - } - client := &http.Client{ Timeout: 30 * time.Second, } + + if socketPath != "" { + dialAddr := utils.ResolveSocketPath(socketPath) + client.Transport = &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", dialAddr) + }, + } + } + resp, err := client.Do(&http.Request{ Method: "GET", URL: parsedTarget, @@ -74,58 +95,46 @@ func FetchHTTPContent(target string) ([]byte, error) { return content, nil } -// Format: http+unix:///path/to/socket.sock/api/endpoint -func FetchUnixSocketHTTPContent(target string) ([]byte, error) { - path := strings.TrimPrefix(target, "http+unix://") - - if !strings.HasPrefix(path, "/") { - return nil, errors.New("unix socket path must be absolute") + +// isRemoteSource reports whether arg should be fetched via HTTP (regular +// network or Unix socket) rather than read from the local filesystem. +// Recognized forms: +// +// - http(s)://... regular HTTP(S) +// - @abstract[:/api] abstract socket (Linux/Android) +// - /abs/path:/api filesystem socket, explicit HTTP path +// - /abs/path filesystem socket detected via os.ModeSocket +func isRemoteSource(arg string) bool { + if arg == "" { + return false } - - var socketPath, httpPath string - - sockIdx := strings.Index(path, ".sock") - if sockIdx != -1 { - socketPath = path[:sockIdx+5] - httpPath = path[sockIdx+5:] - if httpPath == "" { - httpPath = "/" - } - } else { - return nil, errors.New("cannot determine socket path, socket file should have .sock extension") + if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { + return true } - - if _, err := os.Stat(socketPath); err != nil { - return nil, errors.New("socket file not found: ", socketPath).Base(err) + if arg[0] == '@' { + return true } - - client := &http.Client{ - Timeout: 30 * time.Second, - Transport: &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, "unix", socketPath) - }, - }, + if arg[0] != '/' { + return false } - defer client.CloseIdleConnections() - - resp, err := client.Get("http://localhost" + httpPath) - if err != nil { - return nil, errors.New("failed to fetch from unix socket: ", socketPath).Base(err) + if strings.Contains(arg, ":/") { + return true } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, errors.New("unexpected HTTP status code: ", resp.StatusCode) - } - - content, err := buf.ReadAllToBytes(resp.Body) - if err != nil { - return nil, errors.New("failed to read response").Base(err) + info, err := os.Stat(arg) + return err == nil && info.Mode()&os.ModeSocket != 0 +} + + +// httpUnixToCanonical converts the deprecated http+unix:///path/to/socket.sock/api +// URL into the canonical /path/to/socket.sock:/api form by inserting ":" +// between the ".sock" extension and the HTTP path. Inputs without a path +// after ".sock" are returned with just the "http+unix://" prefix stripped. +func httpUnixToCanonical(target string) string { + raw := strings.TrimPrefix(target, "http+unix://") + if i := strings.Index(raw, ".sock/"); i >= 0 { + raw = raw[:i+5] + ":" + raw[i+5:] } - - return content, nil + return raw } func init() {