mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
feat(xray): support the XDRIVE s3 storage service
xray-core 26.10.31 (#7231) added an "s3" XDRIVE service that carries the stream through S3-compatible storage: secrets are AccessKeyID, SecretAccessKey and an optional SessionToken, an "s3" object names the endpoint, region, bucket and pathStyle, and remoteFolder becomes the object prefix. The schema and the shared XDRIVE form offer the service with a field for every S3 key, so an outbound or inbound keeps its storage on save. The save path refused "s3" as an unknown service; it now runs the vendored loader, whose ValidateS3Config would otherwise only fire when the listener starts and stop every inbound. A node refuses a pushed S3 storage like a pushed template one, since its endpoint is any URL the node would dial, including its own panel. Labels in all 14 locales; docs updated in en/fa/ru/zh.
This commit is contained in:
@@ -1028,18 +1028,21 @@ const (
|
||||
// listening (stopping every inbound) or an outbound dials.
|
||||
func validateXDriveStorage(streamSettings string) error {
|
||||
var stream struct {
|
||||
Network string `json:"network"`
|
||||
XDriveSettings struct {
|
||||
Service string `json:"service"`
|
||||
RemoteFolder string `json:"remoteFolder"`
|
||||
Secrets []string `json:"secrets"`
|
||||
Template json.RawMessage `json:"template"`
|
||||
} `json:"xdriveSettings"`
|
||||
Network string `json:"network"`
|
||||
XDriveSettings json.RawMessage `json:"xdriveSettings"`
|
||||
}
|
||||
if json.Unmarshal([]byte(streamSettings), &stream) != nil || stream.Network != "xdrive" {
|
||||
return nil
|
||||
}
|
||||
xd := stream.XDriveSettings
|
||||
var xd struct {
|
||||
Service string `json:"service"`
|
||||
RemoteFolder string `json:"remoteFolder"`
|
||||
Secrets []string `json:"secrets"`
|
||||
Template json.RawMessage `json:"template"`
|
||||
}
|
||||
if len(stream.XDriveSettings) > 0 && json.Unmarshal(stream.XDriveSettings, &xd) != nil {
|
||||
return common.NewError("XDRIVE settings are not a valid object.")
|
||||
}
|
||||
hasFolder := strings.TrimSpace(xd.RemoteFolder) != ""
|
||||
switch xd.Service {
|
||||
case "local":
|
||||
@@ -1058,8 +1061,12 @@ func validateXDriveStorage(streamSettings string) error {
|
||||
return common.NewError(`XDRIVE service "template" needs a "template" object describing the storage API.`)
|
||||
}
|
||||
return validateXDriveTemplate(xd.Template)
|
||||
case "s3":
|
||||
if err := xray.ValidateXDriveS3(stream.XDriveSettings); err != nil {
|
||||
return common.NewError("XDRIVE ", err)
|
||||
}
|
||||
default:
|
||||
return common.NewError(fmt.Sprintf("unsupported XDRIVE service %q: use local, google-drive or template.", xd.Service))
|
||||
return common.NewError(fmt.Sprintf("unsupported XDRIVE service %q: use local, google-drive, template or s3.", xd.Service))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ func hostReaches(inbound *model.Inbound) ([]pushReach, error) {
|
||||
object("hysteria masquerade", "hysteriasettings", "masquerade")
|
||||
case leaf.endsWith("masterkeylog") && lower != "" && lower != "none":
|
||||
add("masterKeyLog", leaf)
|
||||
case leaf.endsWith("xdrivesettings", "service") && (lower == "local" || lower == "template"):
|
||||
case leaf.endsWith("xdrivesettings", "service") && (lower == "local" || lower == "template" || lower == "s3"):
|
||||
object("xdrive service", "xdrivesettings")
|
||||
case leaf.endsWith("echconfiglist") && strings.Contains(lower, "://"):
|
||||
add("echConfigList", leaf)
|
||||
|
||||
@@ -252,6 +252,10 @@ func TestAddInboundRefusesPushedFieldsThatReachThisHost(t *testing.T) {
|
||||
t.Errorf("%s: refused: %v", name, err)
|
||||
}
|
||||
}
|
||||
s3 := xdriveInbound(44404, `{"service":"s3","remoteFolder":"x","secrets":["AKID","SECRET"],"s3":{"endpoint":"http://127.0.0.1:2053","region":"r","bucket":"b"}}`)
|
||||
if _, _, err := push.AddInbound(s3); err == nil || !strings.Contains(err.Error(), "may not set xdrive service") {
|
||||
t.Fatalf("pushed S3 storage on this host's own port = %v, want refused by the push guard", err)
|
||||
}
|
||||
if _, _, err := (&InboundService{}).AddInbound(&model.Inbound{Enable: true, Listen: "env:XUI_DB_DSN", Port: 44403, Protocol: model.VLESS, Settings: `{"clients":[]}`}); err == nil || !strings.Contains(err.Error(), "env:") {
|
||||
t.Fatalf("env: listen = %v, want refused for every caller", err)
|
||||
}
|
||||
|
||||
@@ -140,6 +140,10 @@ func TestAddInboundRefusesXDriveStorageTheCoreCannotOpen(t *testing.T) {
|
||||
{"template without a delete op", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u","namesRegex":"(.+)"}}}`, "XDRIVE template needs put, get, list and delete operations"},
|
||||
{"template without namesRegex", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u"},"delete":{"url":"u"}}}`, "XDRIVE template list needs a namesRegex"},
|
||||
{"template namesRegex without a group", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u","namesRegex":"name"},"delete":{"url":"u"}}}`, "XDRIVE template namesRegex needs one capture group"},
|
||||
{"s3 without an s3 object", `{"service":"s3","remoteFolder":"f","secrets":["key","secret"]}`, `service "s3" needs an "s3" object`},
|
||||
{"s3 endpoint that is not http", `{"service":"s3","remoteFolder":"f","secrets":["key","secret"],"s3":{"endpoint":"ftp://s3.example","region":"r","bucket":"b"}}`, "S3 endpoint must be an http(s) URL"},
|
||||
{"s3 with one secret", `{"service":"s3","remoteFolder":"f","secrets":["key"],"s3":{"endpoint":"https://s3.example","region":"r","bucket":"b"}}`, "S3 needs AccessKeyID, SecretAccessKey"},
|
||||
{"s3 virtual-hosted on an IP endpoint", `{"service":"s3","remoteFolder":"f","secrets":["key","secret"],"s3":{"endpoint":"https://10.0.0.1","region":"r","bucket":"b","pathStyle":false}}`, "requires a DNS endpoint"},
|
||||
{"unknown service", `{"service":"Dropbox","remoteFolder":"f"}`, `unsupported XDRIVE service "Dropbox"`},
|
||||
}
|
||||
for i, tc := range tests {
|
||||
@@ -161,6 +165,14 @@ func TestAddInboundAcceptsACompleteXDriveTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddInboundAcceptsACompleteXDriveS3Storage(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
s3 := `{"service":"s3","remoteFolder":"xdrive","secrets":["AKID","SECRET"],"s3":{"endpoint":"https://storage.yandexcloud.net","region":"ru-central1","bucket":"example-bucket"}}`
|
||||
if _, _, err := (&InboundService{}).AddInbound(xdriveInbound(18481, s3)); err != nil {
|
||||
t.Fatalf("AddInbound of an S3 storage xray-core can open: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavePathsRefuseXDriveWithoutStorage(t *testing.T) {
|
||||
const want = `XDRIVE needs a "remoteFolder"`
|
||||
const empty = `{"service":"local","remoteFolder":""}`
|
||||
|
||||
Reference in New Issue
Block a user