mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +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":""}`
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "معرّف مفتاح الوصول",
|
||||
"secretAccessKey": "مفتاح الوصول السري",
|
||||
"sessionToken": "رمز الجلسة",
|
||||
"s3Endpoint": "نقطة النهاية",
|
||||
"s3Region": "المنطقة",
|
||||
"s3Bucket": "الحاوية (Bucket)",
|
||||
"s3PathStyle": "عنونة بنمط المسار",
|
||||
"s3PathStyleDesc": "مفعّل: اسم الحاوية بيتحط في مسار الرابط. مقفول: بيتحط في اسم المضيف، وده محتاج endpoint باسم DNS.",
|
||||
"secrets": "الأسرار",
|
||||
"secretsDesc": "قيم يقرؤها القالب باسم {secret0} و{secret1} …",
|
||||
"template": "قالب التخزين",
|
||||
|
||||
@@ -738,6 +738,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client secret",
|
||||
"refreshToken": "Refresh token",
|
||||
"accessKeyId": "Access key ID",
|
||||
"secretAccessKey": "Secret access key",
|
||||
"sessionToken": "Session token",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"s3Region": "Region",
|
||||
"s3Bucket": "Bucket",
|
||||
"s3PathStyle": "Path-style addressing",
|
||||
"s3PathStyleDesc": "On: the bucket goes in the URL path. Off: in the host name, which needs a DNS endpoint.",
|
||||
"secrets": "Secrets",
|
||||
"secretsDesc": "Values the template reads as {secret0}, {secret1}, …",
|
||||
"template": "Storage template",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "ID de clave de acceso",
|
||||
"secretAccessKey": "Clave de acceso secreta",
|
||||
"sessionToken": "Token de sesión",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"s3Region": "Región",
|
||||
"s3Bucket": "Bucket",
|
||||
"s3PathStyle": "Direccionamiento por ruta",
|
||||
"s3PathStyleDesc": "Activado: el bucket va en la ruta de la URL. Desactivado: en el nombre de host, lo que requiere un endpoint DNS.",
|
||||
"secrets": "Secretos",
|
||||
"secretsDesc": "Valores que la plantilla lee como {secret0}, {secret1}, …",
|
||||
"template": "Plantilla de almacenamiento",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "شناسه کلید دسترسی",
|
||||
"secretAccessKey": "کلید دسترسی محرمانه",
|
||||
"sessionToken": "توکن نشست",
|
||||
"s3Endpoint": "نقطه پایانی (Endpoint)",
|
||||
"s3Region": "منطقه (Region)",
|
||||
"s3Bucket": "باکت (Bucket)",
|
||||
"s3PathStyle": "آدرسدهی مسیرمحور",
|
||||
"s3PathStyleDesc": "روشن: نام باکت در مسیر URL قرار میگیرد. خاموش: در نام میزبان، که به یک endpoint دامنهای نیاز دارد.",
|
||||
"secrets": "مقادیر محرمانه",
|
||||
"secretsDesc": "مقادیری که قالب با {secret0}، {secret1}، … میخواند",
|
||||
"template": "قالب ذخیرهسازی",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "ID kunci akses",
|
||||
"secretAccessKey": "Kunci akses rahasia",
|
||||
"sessionToken": "Token sesi",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"s3Region": "Region",
|
||||
"s3Bucket": "Bucket",
|
||||
"s3PathStyle": "Pengalamatan gaya path",
|
||||
"s3PathStyleDesc": "Aktif: bucket ditaruh di path URL. Nonaktif: di nama host, yang membutuhkan endpoint DNS.",
|
||||
"secrets": "Rahasia",
|
||||
"secretsDesc": "Nilai yang dibaca template sebagai {secret0}, {secret1}, …",
|
||||
"template": "Template penyimpanan",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "クライアント ID",
|
||||
"clientSecret": "クライアントシークレット",
|
||||
"refreshToken": "リフレッシュトークン",
|
||||
"accessKeyId": "アクセスキー ID",
|
||||
"secretAccessKey": "シークレットアクセスキー",
|
||||
"sessionToken": "セッショントークン",
|
||||
"s3Endpoint": "エンドポイント",
|
||||
"s3Region": "リージョン",
|
||||
"s3Bucket": "バケット",
|
||||
"s3PathStyle": "パス形式のアドレス指定",
|
||||
"s3PathStyleDesc": "オン:バケットを URL のパスに入れます。オフ:ホスト名に入れます(DNS 名のエンドポイントが必要)。",
|
||||
"secrets": "シークレット",
|
||||
"secretsDesc": "テンプレートで {secret0}、{secret1}、… として参照される値",
|
||||
"template": "ストレージテンプレート",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "ID da chave de acesso",
|
||||
"secretAccessKey": "Chave de acesso secreta",
|
||||
"sessionToken": "Token de sessão",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"s3Region": "Região",
|
||||
"s3Bucket": "Bucket",
|
||||
"s3PathStyle": "Endereçamento por caminho",
|
||||
"s3PathStyleDesc": "Ligado: o bucket vai no caminho da URL. Desligado: no nome do host, o que exige um endpoint DNS.",
|
||||
"secrets": "Segredos",
|
||||
"secretsDesc": "Valores que o template lê como {secret0}, {secret1}, …",
|
||||
"template": "Template de armazenamento",
|
||||
|
||||
@@ -738,6 +738,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "ID ключа доступа",
|
||||
"secretAccessKey": "Секретный ключ доступа",
|
||||
"sessionToken": "Токен сессии",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"s3Region": "Регион",
|
||||
"s3Bucket": "Бакет",
|
||||
"s3PathStyle": "Адресация через путь",
|
||||
"s3PathStyleDesc": "Вкл.: бакет указывается в пути URL. Выкл.: в имени хоста, для чего нужен DNS-endpoint.",
|
||||
"secrets": "Секреты",
|
||||
"secretsDesc": "Значения, которые шаблон читает как {secret0}, {secret1}, …",
|
||||
"template": "Шаблон хранилища",
|
||||
|
||||
@@ -738,6 +738,14 @@
|
||||
"clientId": "ID klienta",
|
||||
"clientSecret": "Tajný kľúč klienta",
|
||||
"refreshToken": "Obnovovací token",
|
||||
"accessKeyId": "ID prístupového kľúča",
|
||||
"secretAccessKey": "Tajný prístupový kľúč",
|
||||
"sessionToken": "Token relácie",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"s3Region": "Región",
|
||||
"s3Bucket": "Bucket",
|
||||
"s3PathStyle": "Adresovanie cez cestu",
|
||||
"s3PathStyleDesc": "Zapnuté: bucket je v ceste URL. Vypnuté: v názve hostiteľa, čo vyžaduje DNS endpoint.",
|
||||
"secrets": "Tajné hodnoty",
|
||||
"secretsDesc": "Hodnoty, ktoré šablóna číta ako {secret0}, {secret1}, …",
|
||||
"template": "Šablóna úložiska",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "Erişim anahtarı kimliği",
|
||||
"secretAccessKey": "Gizli erişim anahtarı",
|
||||
"sessionToken": "Oturum belirteci",
|
||||
"s3Endpoint": "Uç nokta",
|
||||
"s3Region": "Bölge",
|
||||
"s3Bucket": "Bucket",
|
||||
"s3PathStyle": "Yol tarzı adresleme",
|
||||
"s3PathStyleDesc": "Açık: bucket URL yoluna yazılır. Kapalı: ana bilgisayar adına yazılır, bu da DNS uç noktası gerektirir.",
|
||||
"secrets": "Gizli değerler",
|
||||
"secretsDesc": "Şablonun {secret0}, {secret1}, … olarak okuduğu değerler",
|
||||
"template": "Depolama şablonu",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "ID ключа доступу",
|
||||
"secretAccessKey": "Секретний ключ доступу",
|
||||
"sessionToken": "Токен сесії",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"s3Region": "Регіон",
|
||||
"s3Bucket": "Бакет",
|
||||
"s3PathStyle": "Адресація через шлях",
|
||||
"s3PathStyleDesc": "Увімк.: бакет вказується в шляху URL. Вимк.: в імені хоста, для чого потрібен DNS-endpoint.",
|
||||
"secrets": "Секрети",
|
||||
"secretsDesc": "Значення, які шаблон читає як {secret0}, {secret1}, …",
|
||||
"template": "Шаблон сховища",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"refreshToken": "Refresh Token",
|
||||
"accessKeyId": "ID khóa truy cập",
|
||||
"secretAccessKey": "Khóa truy cập bí mật",
|
||||
"sessionToken": "Token phiên",
|
||||
"s3Endpoint": "Endpoint",
|
||||
"s3Region": "Vùng",
|
||||
"s3Bucket": "Bucket",
|
||||
"s3PathStyle": "Định địa chỉ kiểu đường dẫn",
|
||||
"s3PathStyleDesc": "Bật: bucket nằm trong đường dẫn URL. Tắt: nằm trong tên máy chủ, cần endpoint dạng DNS.",
|
||||
"secrets": "Giá trị bí mật",
|
||||
"secretsDesc": "Các giá trị template đọc dưới dạng {secret0}, {secret1}, …",
|
||||
"template": "Template lưu trữ",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "客户端 ID",
|
||||
"clientSecret": "客户端密钥",
|
||||
"refreshToken": "刷新令牌",
|
||||
"accessKeyId": "访问密钥 ID",
|
||||
"secretAccessKey": "秘密访问密钥",
|
||||
"sessionToken": "会话令牌",
|
||||
"s3Endpoint": "端点",
|
||||
"s3Region": "区域",
|
||||
"s3Bucket": "存储桶",
|
||||
"s3PathStyle": "路径风格寻址",
|
||||
"s3PathStyleDesc": "开启:存储桶名放在 URL 路径中。关闭:放在主机名中,需要使用域名端点。",
|
||||
"secrets": "密钥",
|
||||
"secretsDesc": "模板中以 {secret0}、{secret1}… 引用的值",
|
||||
"template": "存储模板",
|
||||
|
||||
@@ -737,6 +737,14 @@
|
||||
"clientId": "用戶端 ID",
|
||||
"clientSecret": "用戶端密鑰",
|
||||
"refreshToken": "重新整理權杖",
|
||||
"accessKeyId": "存取金鑰 ID",
|
||||
"secretAccessKey": "秘密存取金鑰",
|
||||
"sessionToken": "工作階段權杖",
|
||||
"s3Endpoint": "端點",
|
||||
"s3Region": "區域",
|
||||
"s3Bucket": "儲存貯體",
|
||||
"s3PathStyle": "路徑樣式定址",
|
||||
"s3PathStyleDesc": "開啟:儲存貯體名稱放在 URL 路徑中。關閉:放在主機名稱中,需要使用網域端點。",
|
||||
"secrets": "密鑰",
|
||||
"secretsDesc": "範本中以 {secret0}、{secret1}… 引用的值",
|
||||
"template": "儲存範本",
|
||||
|
||||
@@ -237,6 +237,17 @@ func ValidateTunWfpBlockLeak(values []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateXDriveS3 builds an "s3" xdriveSettings object through the vendored loader, which runs
|
||||
// the endpoint, bucket and secret checks xray-core 26.10.31 (#7231) applies to the S3 backend.
|
||||
func ValidateXDriveS3(xdriveSettings []byte) error {
|
||||
c := new(conf.XDriveConfig)
|
||||
if err := json.Unmarshal(xdriveSettings, c); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
// validateXhttpDownloadSettings refuses what conf.Build() lets through but the core's XHTTP
|
||||
// dialer panics on at every dial: a downloadSettings block without an address or not on xhttp.
|
||||
func validateXhttpDownloadSettings(settings *serial.TypedMessage) error {
|
||||
|
||||
Reference in New Issue
Block a user