diff --git a/docs/content/docs/en/config/transports.mdx b/docs/content/docs/en/config/transports.mdx index 399bb1044..59c5bc08f 100644 --- a/docs/content/docs/en/config/transports.mdx +++ b/docs/content/docs/en/config/transports.mdx @@ -23,7 +23,7 @@ network writes its own settings key on the wire (`tcpSettings`, `kcpSettings`, | **HTTPUpgrade** | `httpupgradeSettings` | CDN-friendly HTTP/1.1 `Upgrade`; lighter than full WebSocket. | | **XHTTP** | `xhttpSettings` | Modern stream-multiplexed HTTP transport; CDN-friendly and REALITY-capable. | | **Hysteria** | `hysteriaSettings` | QUIC-based transport — only for the **Hysteria2** protocol. | -| **XDRIVE** | `xdriveSettings` | Tunnels the stream through files in shared cloud storage (Google Drive, a local folder or any HTTP storage API). | +| **XDRIVE** | `xdriveSettings` | Tunnels the stream through files in shared cloud storage (Google Drive, S3, a local folder or any HTTP storage API). | | **MASQUE** | `masqueSettings` | CONNECT-IP over HTTP/3 or HTTP/2 — only for the **MASQUE** protocol. See [MASQUE](/docs/config/masque). | @@ -132,10 +132,11 @@ enabled. The panel offers XDRIVE only for VLESS, refuses to save an inbound with | Field | Default | Meaning | | -------------- | -------------- | ----------------------------------------------------------------------- | -| `service` | `google-drive` | `google-drive`, `local` (a folder on disk) or `template` (any HTTP storage API). The old `Google Drive` spelling is still read. | +| `service` | `google-drive` | `google-drive`, `local` (a folder on disk) `template` (any HTTP storage API) or `s3` (S3-compatible storage). The old `Google Drive` spelling is still read. | | `remoteFolder` | — | Folder that holds the session files. | -| `secrets` | — | Google Drive: ClientID, ClientSecret, RefreshToken — exactly three, in that order. Template: values read as `{secret0}`, `{secret1}`, …. | +| `secrets` | — | Google Drive: ClientID, ClientSecret, RefreshToken — exactly three, in that order. Template: values read as `{secret0}`, `{secret1}`, …. S3: AccessKeyID, SecretAccessKey and an optional SessionToken. | | `template` | — | Template service only: the storage API's `auth`, `put`, `get`, `list` and `delete` operations. | +| `s3` | — | S3 service only: `endpoint` (an http(s) URL), `region`, `bucket` and `pathStyle` (default `true`; `false` puts the bucket in the host name). `remoteFolder` is the object prefix. | | tuning | core defaults | `segmentBytes` (512 KiB), `flushIntervalMs` (20), `pollIntervalMs`/`maxPollIntervalMs` (50/500), `eagerWindowMs` (2000), `holeTimeoutMs` (30000), `sessionTtlSeconds` (300), `concurrency` (8). | The optional **Front address/port** (the stream-level `address`/`port`) is a domain diff --git a/docs/content/docs/fa/config/transports.mdx b/docs/content/docs/fa/config/transports.mdx index 8d74b7681..9066c9604 100644 --- a/docs/content/docs/fa/config/transports.mdx +++ b/docs/content/docs/fa/config/transports.mdx @@ -133,10 +133,11 @@ inbound به مقدار `decryption` نیاز دارد و outbound به `encrypt | فیلد | پیش‌فرض | معنا | | -------------- | -------------- | ---- | -| `service` | `google-drive` | `google-drive`، `local` (پوشه‌ای روی دیسک) یا `template` (هر API ذخیره‌سازی HTTP). نگارش قدیمی `Google Drive` همچنان خوانده می‌شود. | +| `service` | `google-drive` | `google-drive`، `local` (پوشه‌ای روی دیسک)، `template` (هر API ذخیره‌سازی HTTP) یا `s3` (ذخیره‌ساز سازگار با S3). نگارش قدیمی `Google Drive` همچنان خوانده می‌شود. | | `remoteFolder` | — | پوشه‌ای که فایل‌های نشست در آن قرار می‌گیرند. | -| `secrets` | — | Google Drive: دقیقاً سه مقدار ClientID، ClientSecret و RefreshToken به همین ترتیب. Template: مقادیری که با `{secret0}`، `{secret1}`، … خوانده می‌شوند. | +| `secrets` | — | Google Drive: دقیقاً سه مقدار ClientID، ClientSecret و RefreshToken به همین ترتیب. Template: مقادیری که با `{secret0}`، `{secret1}`، … خوانده می‌شوند. S3: ‏AccessKeyID، SecretAccessKey و در صورت نیاز SessionToken. | | `template` | — | فقط برای سرویس template: عملیات `auth`، `put`، `get`، `list` و `delete` در API ذخیره‌ساز. | +| `s3` | — | فقط برای سرویس s3: `endpoint` (یک URL از نوع http(s))، `region`، `bucket` و `pathStyle` (پیش‌فرض `true`؛ با `false` نام باکت در نام میزبان می‌آید). `remoteFolder` پیشوند اشیاء است. | | تنظیمات دقیق | پیش‌فرض هسته | `segmentBytes` (512 KiB)، `flushIntervalMs` (20)، `pollIntervalMs`/`maxPollIntervalMs` (50/500)، `eagerWindowMs` (2000)، `holeTimeoutMs` (30000)، `sessionTtlSeconds` (300)، `concurrency` (8). | **آدرس/پورت Front** اختیاری (کلیدهای `address`/`port` در سطح stream) یک domain front diff --git a/docs/content/docs/ru/config/transports.mdx b/docs/content/docs/ru/config/transports.mdx index ee44e7df3..04b13642d 100644 --- a/docs/content/docs/ru/config/transports.mdx +++ b/docs/content/docs/ru/config/transports.mdx @@ -135,10 +135,11 @@ Mux. Панель предлагает XDRIVE только для VLESS, не с | Поле | По умолчанию | Значение | | -------------- | -------------- | -------- | -| `service` | `google-drive` | `google-drive`, `local` (папка на диске) или `template` (любой HTTP API хранилища). Старое написание `Google Drive` по-прежнему читается. | +| `service` | `google-drive` | `google-drive`, `local` (папка на диске), `template` (любой HTTP API хранилища) или `s3` (S3-совместимое хранилище). Старое написание `Google Drive` по-прежнему читается. | | `remoteFolder` | — | Папка с файлами сессий. | -| `secrets` | — | Google Drive: ровно три значения — ClientID, ClientSecret, RefreshToken, в этом порядке. Template: значения, доступные как `{secret0}`, `{secret1}`, …. | +| `secrets` | — | Google Drive: ровно три значения — ClientID, ClientSecret, RefreshToken, в этом порядке. Template: значения, доступные как `{secret0}`, `{secret1}`, …. S3: AccessKeyID, SecretAccessKey и при необходимости SessionToken. | | `template` | — | Только для template: операции `auth`, `put`, `get`, `list` и `delete` API хранилища. | +| `s3` | — | Только для s3: `endpoint` (URL http(s)), `region`, `bucket` и `pathStyle` (по умолчанию `true`; `false` переносит бакет в имя хоста). `remoteFolder` — префикс объектов. | | тонкая настройка | значения ядра | `segmentBytes` (512 KiB), `flushIntervalMs` (20), `pollIntervalMs`/`maxPollIntervalMs` (50/500), `eagerWindowMs` (2000), `holeTimeoutMs` (30000), `sessionTtlSeconds` (300), `concurrency` (8). | Необязательные **адрес/порт фронтинга** (`address`/`port` на уровне stream) — домен-фронт, diff --git a/docs/content/docs/zh/config/transports.mdx b/docs/content/docs/zh/config/transports.mdx index e6e6562a2..b9634185a 100644 --- a/docs/content/docs/zh/config/transports.mdx +++ b/docs/content/docs/zh/config/transports.mdx @@ -124,10 +124,11 @@ XDRIVE 把数据流作为文件存放在双方都能访问的文件夹中:服 | 字段 | 默认值 | 含义 | | -------------- | -------------- | ---- | -| `service` | `google-drive` | `google-drive`、`local`(磁盘上的文件夹)或 `template`(任意 HTTP 存储 API)。旧写法 `Google Drive` 仍可读取。 | +| `service` | `google-drive` | `google-drive`、`local`(磁盘上的文件夹)、`template`(任意 HTTP 存储 API)或 `s3`(S3 兼容存储)。旧写法 `Google Drive` 仍可读取。 | | `remoteFolder` | — | 存放会话文件的文件夹。 | -| `secrets` | — | Google Drive:恰好三项,依次为 ClientID、ClientSecret、RefreshToken。Template:以 `{secret0}`、`{secret1}`… 引用的值。 | +| `secrets` | — | Google Drive:恰好三项,依次为 ClientID、ClientSecret、RefreshToken。Template:以 `{secret0}`、`{secret1}`… 引用的值。S3:AccessKeyID、SecretAccessKey,以及可选的 SessionToken。 | | `template` | — | 仅用于 template 服务:存储 API 的 `auth`、`put`、`get`、`list` 和 `delete` 操作。 | +| `s3` | — | 仅用于 s3 服务:`endpoint`(http(s) URL)、`region`、`bucket` 和 `pathStyle`(默认 `true`;为 `false` 时存储桶名放在主机名中)。`remoteFolder` 为对象前缀。 | | 调优参数 | 内核默认值 | `segmentBytes`(512 KiB)、`flushIntervalMs`(20)、`pollIntervalMs`/`maxPollIntervalMs`(50/500)、`eagerWindowMs`(2000)、`holeTimeoutMs`(30000)、`sessionTtlSeconds`(300)、`concurrency`(8)。 | 可选的**前置地址/端口**(stream 级的 `address`/`port`)是连接存储 API 时使用的域前置;TLS 中仍使用 API 主机名。 diff --git a/frontend/src/lib/xray/forms/transport/XDriveForm.tsx b/frontend/src/lib/xray/forms/transport/XDriveForm.tsx index 20b84cdab..dc4020543 100644 --- a/frontend/src/lib/xray/forms/transport/XDriveForm.tsx +++ b/frontend/src/lib/xray/forms/transport/XDriveForm.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Input, InputNumber, Select, Typography } from 'antd'; +import { Input, InputNumber, Select, Switch, Typography } from 'antd'; import { useFormContext, useWatch } from 'react-hook-form'; import { JsonEditor } from '@/components/form'; @@ -13,7 +13,7 @@ const SERVICE_LABEL_KEYS: Partial> = { local: 'pages.inbounds.form.xdrive.serviceLocal', template: 'pages.inbounds.form.xdrive.serviceTemplate', }; -const SERVICE_NAMES: Partial> = { 'google-drive': 'Google Drive' }; +const SERVICE_NAMES: Partial> = { 'google-drive': 'Google Drive', s3: 'S3' }; // Core defaults (transport/internet/xdrive/params.go), shown as placeholders so an // empty field keeps meaning "use the core's value". @@ -29,6 +29,11 @@ const TUNING_FIELDS = [ ] as const; const GOOGLE_DRIVE_SECRETS = ['clientId', 'clientSecret', 'refreshToken'] as const; +const S3_SECRETS = [ + ['accessKeyId', true], + ['secretAccessKey', true], + ['sessionToken', false], +] as const; function TemplateEditor({ value, @@ -90,7 +95,11 @@ export default function XDriveForm() { name={[...BASE, 'remoteFolder']} required > - + {service === 'google-drive' && GOOGLE_DRIVE_SECRETS.map((key, index) => ( @@ -103,6 +112,48 @@ export default function XDriveForm() { {key === 'clientId' ? : } ))} + {service === 's3' && ( + <> + {S3_SECRETS.map(([key, required], index) => ( + + {key === 'accessKeyId' ? : } + + ))} + + + + + + + + + + value !== false }} + > + + + + )} {service === 'template' && ( <> ; // xray-core 26.10.17 (#7182) renamed "Google Drive" and reads the old name only as a deprecated alias. @@ -22,10 +22,19 @@ export function liftLegacyXDriveService(stream: Record): void { const MillisSchema = z.number().int().min(0); +// S3-compatible storage (xray-core 26.10.31, #7231); an unset pathStyle means path-style addressing. +export const XDriveS3Schema = z.object({ + endpoint: z.string().optional(), + region: z.string().optional(), + bucket: z.string().optional(), + pathStyle: z.boolean().optional(), +}); + export const XDriveStreamSettingsSchema = z.object({ service: XDriveServiceInputSchema.default('google-drive'), remoteFolder: z.string().default(''), - // Google Drive takes exactly three, in order: ClientID, ClientSecret, RefreshToken. + // Google Drive takes exactly three, in order: ClientID, ClientSecret, RefreshToken; S3 takes + // AccessKeyID, SecretAccessKey and an optional SessionToken. secrets: z.array(z.string()).default([]), segmentBytes: z.number().int().min(0).optional(), flushIntervalMs: MillisSchema.optional(), @@ -36,6 +45,7 @@ export const XDriveStreamSettingsSchema = z.object({ eagerWindowMs: MillisSchema.optional(), holeTimeoutMs: MillisSchema.optional(), template: z.record(z.string(), z.unknown()).optional(), + s3: XDriveS3Schema.optional(), // Client XMUX (xray-core 26.10.21); left unset, the core sizes it per service, so no // defaults are filled in here. A JSON subscription hands an inbound's value to clients. xmux: z.record(z.string(), z.unknown()).optional(), diff --git a/frontend/src/test/outbound-form-modal.test.tsx b/frontend/src/test/outbound-form-modal.test.tsx index 29e19fed6..cb8d4bbd1 100644 --- a/frontend/src/test/outbound-form-modal.test.tsx +++ b/frontend/src/test/outbound-form-modal.test.tsx @@ -563,6 +563,69 @@ describe('OutboundFormModal XDRIVE service name', () => { }); }); +// xray-core 26.10.31 (#7231): every S3 key needs a registered field or the editor drops it on save. +describe('OutboundFormModal XDRIVE S3 storage', () => { + it('opens and saves a stored S3 outbound unchanged', async () => { + const onConfirm = vi.fn(); + const queryClient = makeTestQueryClient(); + const xdriveSettings = { + service: 's3', + remoteFolder: 'xdrive', + secrets: ['AKID', 'SECRET', 'TOKEN'], + s3: { + endpoint: 'https://storage.yandexcloud.net', + region: 'ru-central1', + bucket: 'example-bucket', + pathStyle: false, + }, + }; + const outbound = { + protocol: 'vless', + tag: 's3-out', + settings: { + address: 'example.com', + port: 443, + id: 'c9f0c2d0-0000-4000-8000-000000000000', + encryption: 'mlkem768x25519plus.native.0rtt.key', + }, + mux: { enabled: true, concurrency: 8 }, + streamSettings: { network: 'xdrive', security: 'none', xdriveSettings }, + }; + const tree = (open: boolean) => ( + + + {}} + onConfirm={onConfirm} + /> + + + ); + const { rerender } = render(tree(false)); + rerender(tree(true)); + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); + + expect((screen.getByLabelText('Bucket') as HTMLInputElement).value).toBe('example-bucket'); + expect((screen.getByLabelText('Access key ID') as HTMLInputElement).value).toBe('AKID'); + const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement; + await act(async () => { + fireEvent.click(ok); + }); + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); + + expect(onConfirm).toHaveBeenCalledTimes(1); + const payload = onConfirm.mock.calls[0][0] as { streamSettings: Record }; + expect(payload.streamSettings.xdriveSettings).toMatchObject(xdriveSettings); + }); +}); + // xray-core 26.10.10 dials the XDRIVE storage API with the outbound's tlsSettings (SNI, // uTLS fingerprint), so switching a TLS outbound to XDRIVE must keep them. describe('OutboundFormModal XDRIVE TLS', () => { diff --git a/frontend/src/test/stream.test.ts b/frontend/src/test/stream.test.ts index 5be9db518..c61b4efd0 100644 --- a/frontend/src/test/stream.test.ts +++ b/frontend/src/test/stream.test.ts @@ -56,6 +56,17 @@ describe('NetworkSettingsSchema xray-core 26.10.21 keys', () => { expect(parsed.xdriveSettings.service).toBe('google-drive'); }); + // xray-core 26.10.31 (#7231): the "s3" service reads its storage from an "s3" object. + it('keeps the XDRIVE s3 service and its storage', () => { + const s3 = { endpoint: 'https://s3.example', region: 'r', bucket: 'b', pathStyle: false }; + const parsed = NetworkSettingsSchema.parse({ + network: 'xdrive', + xdriveSettings: { service: 's3', remoteFolder: 'x', secrets: ['AKID', 'SECRET'], s3 }, + }) as { xdriveSettings: { service: string; s3?: unknown } }; + expect(parsed.xdriveSettings.service).toBe('s3'); + expect(parsed.xdriveSettings.s3).toEqual(s3); + }); + it('keeps the XDRIVE and MASQUE xmux', () => { const xmux = { maxConnections: 2, hMaxReusableSecs: '1800-3000' }; const xdrive = NetworkSettingsSchema.parse({ diff --git a/internal/web/service/inbound.go b/internal/web/service/inbound.go index 447bee557..faf799701 100644 --- a/internal/web/service/inbound.go +++ b/internal/web/service/inbound.go @@ -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 } diff --git a/internal/web/service/inbound_push_guard.go b/internal/web/service/inbound_push_guard.go index 2e6f898f7..16dc185ab 100644 --- a/internal/web/service/inbound_push_guard.go +++ b/internal/web/service/inbound_push_guard.go @@ -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) diff --git a/internal/web/service/inbound_tls_test.go b/internal/web/service/inbound_tls_test.go index 978a73d34..6dc8f9300 100644 --- a/internal/web/service/inbound_tls_test.go +++ b/internal/web/service/inbound_tls_test.go @@ -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) } diff --git a/internal/web/service/inbound_xdrive_test.go b/internal/web/service/inbound_xdrive_test.go index a580315ca..60f7116c8 100644 --- a/internal/web/service/inbound_xdrive_test.go +++ b/internal/web/service/inbound_xdrive_test.go @@ -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":""}` diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index c89343738..47f90a788 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -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": "قالب التخزين", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 6578a9292..27f14cf38 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -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", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index 6fb602d31..a933ce3ae 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -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", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index f01c53540..a04d621d7 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -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": "قالب ذخیره‌سازی", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index f0b8ac109..45d730e1d 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -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", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 0bfeeb5be..28f018b7e 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -737,6 +737,14 @@ "clientId": "クライアント ID", "clientSecret": "クライアントシークレット", "refreshToken": "リフレッシュトークン", + "accessKeyId": "アクセスキー ID", + "secretAccessKey": "シークレットアクセスキー", + "sessionToken": "セッショントークン", + "s3Endpoint": "エンドポイント", + "s3Region": "リージョン", + "s3Bucket": "バケット", + "s3PathStyle": "パス形式のアドレス指定", + "s3PathStyleDesc": "オン:バケットを URL のパスに入れます。オフ:ホスト名に入れます(DNS 名のエンドポイントが必要)。", "secrets": "シークレット", "secretsDesc": "テンプレートで {secret0}、{secret1}、… として参照される値", "template": "ストレージテンプレート", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 01ff63735..58296ab40 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -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", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index eba1e20e4..c0ff714ca 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -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": "Шаблон хранилища", diff --git a/internal/web/translation/sk-SK.json b/internal/web/translation/sk-SK.json index 95449f105..19b9a472a 100644 --- a/internal/web/translation/sk-SK.json +++ b/internal/web/translation/sk-SK.json @@ -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", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index adc69e513..56500bc01 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -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", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index cc824b81d..15c1e011b 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -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": "Шаблон сховища", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 25e17db14..18aa3db53 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -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ữ", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 2684cda56..9d760b324 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -737,6 +737,14 @@ "clientId": "客户端 ID", "clientSecret": "客户端密钥", "refreshToken": "刷新令牌", + "accessKeyId": "访问密钥 ID", + "secretAccessKey": "秘密访问密钥", + "sessionToken": "会话令牌", + "s3Endpoint": "端点", + "s3Region": "区域", + "s3Bucket": "存储桶", + "s3PathStyle": "路径风格寻址", + "s3PathStyleDesc": "开启:存储桶名放在 URL 路径中。关闭:放在主机名中,需要使用域名端点。", "secrets": "密钥", "secretsDesc": "模板中以 {secret0}、{secret1}… 引用的值", "template": "存储模板", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 0bcc9bea3..71e993a22 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -737,6 +737,14 @@ "clientId": "用戶端 ID", "clientSecret": "用戶端密鑰", "refreshToken": "重新整理權杖", + "accessKeyId": "存取金鑰 ID", + "secretAccessKey": "秘密存取金鑰", + "sessionToken": "工作階段權杖", + "s3Endpoint": "端點", + "s3Region": "區域", + "s3Bucket": "儲存貯體", + "s3PathStyle": "路徑樣式定址", + "s3PathStyleDesc": "開啟:儲存貯體名稱放在 URL 路徑中。關閉:放在主機名稱中,需要使用網域端點。", "secrets": "密鑰", "secretsDesc": "範本中以 {secret0}、{secret1}… 引用的值", "template": "儲存範本", diff --git a/internal/xray/api.go b/internal/xray/api.go index a03aac962..81555fdc7 100644 --- a/internal/xray/api.go +++ b/internal/xray/api.go @@ -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 {