Files
Xray-core/main/confloader/external/external.go
T

143 lines
3.7 KiB
Go

package external
import (
"bytes"
"context"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"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"
)
func ConfigLoader(arg string) (out io.Reader, err error) {
var data []byte
switch {
case strings.HasPrefix(arg, "http+unix://"):
errors.PrintDeprecatedFeatureWarning(`"http+unix://" prefix`, `direct Unix socket path (e.g. /path/socket.sock:/api or @abstract:/api)`)
data, err = FetchHTTPContent(httpUnixToCanonical(arg))
case isRemoteSource(arg):
data, err = FetchHTTPContent(arg)
case arg == "stdin:":
data, err = io.ReadAll(os.Stdin)
default:
data, err = os.ReadFile(arg)
}
if err != nil {
return
}
out = bytes.NewBuffer(data)
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) {
httpURL, socketPath := utils.SplitHTTPUnixURL(target)
parsedTarget, err := url.Parse(httpURL)
if err != nil {
return nil, errors.New("invalid URL: ", target).Base(err)
}
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,
Close: true,
})
if err != nil {
return nil, errors.New("failed to dial to ", target).Base(err)
}
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 HTTP response").Base(err)
}
return content, nil
}
// 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
}
if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
return true
}
if arg[0] == '@' {
return true
}
if arg[0] != '/' {
return false
}
if strings.Contains(arg, ":/") {
return true
}
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 raw
}
func init() {
confloader.EffectiveConfigFileLoader = ConfigLoader
}