Ver código fonte

fix(links): include TCP HTTP host header in share links

The inbound form intentionally only exposes the response side of the
TCP HTTP header object (xray-core's inbound listener reads the
response object, not request — see the existing comment in
InboundFormModal). But the share-link generators were still reading
the Host header from request.headers, so the configured value ended
up in tcpSettings.header.response.headers while the link query
emitted host= (empty).

Fix the host lookup in both code paths:
- sub/subService.go: applyShareNetworkParams (VLESS / Trojan /
  Shadowsocks share URLs) and applyVmessNetworkParams (the VMess
  base64 JSON link) now try header.response.headers first and fall
  back to request.headers for legacy / hand-edited configs.
- frontend/src/lib/xray/inbound-link.ts mirrors the same fallback in
  the three TCP HTTP branches (VMess obj, VLESS params, the shared
  Trojan+Shadowsocks writer) so the JS-side generator used by the
  API docs preview stays in sync with the Go output.

Also restore the request-side inputs (version / method / path /
headers) under the TCP HTTP toggle in InboundFormModal. They were
previously removed because xray-core ignores them on the inbound
side, but they're still useful when copying the same config out to
an outbound or hand-tuning the share link, and they no longer
mislead users about Host — the link now derives Host from
response.headers.host where the response-only form writes it.
MHSanaei 11 horas atrás
pai
commit
8046d1519d

+ 5 - 5
frontend/package-lock.json

@@ -8,7 +8,7 @@
       "name": "3x-ui-frontend",
       "version": "0.1.0",
       "dependencies": {
-        "@ant-design/icons": "^6.2.3",
+        "@ant-design/icons": "^6.2.5",
         "@codemirror/lang-json": "^6.0.2",
         "@codemirror/theme-one-dark": "^6.1.3",
         "@tanstack/react-query": "^5.100.14",
@@ -101,14 +101,14 @@
       }
     },
     "node_modules/@ant-design/icons": {
-      "version": "6.2.3",
-      "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.3.tgz",
-      "integrity": "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==",
+      "version": "6.2.5",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.5.tgz",
+      "integrity": "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==",
       "license": "MIT",
       "dependencies": {
         "@ant-design/colors": "^8.0.1",
         "@ant-design/icons-svg": "^4.4.2",
-        "@rc-component/util": "^1.10.1",
+        "@rc-component/util": "^1.11.0",
         "clsx": "^2.1.1"
       },
       "engines": {

+ 1 - 1
frontend/package.json

@@ -20,7 +20,7 @@
     "gen:zod": "cd .. && go run ./tools/openapigen"
   },
   "dependencies": {
-    "@ant-design/icons": "^6.2.3",
+    "@ant-design/icons": "^6.2.5",
     "@codemirror/lang-json": "^6.0.2",
     "@codemirror/theme-one-dark": "^6.1.3",
     "@tanstack/react-query": "^5.100.14",

+ 9 - 3
frontend/src/lib/xray/inbound-link.ts

@@ -184,7 +184,9 @@ export function genVmessLink(input: GenVmessLinkInput): string {
         const request = header.request;
         if (request) {
           obj.path = request.path.join(',');
-          const host = getHeaderValue(request.headers, 'host');
+          const host =
+            getHeaderValue(header.response?.headers, 'host')
+            || getHeaderValue(request.headers, 'host');
           if (host) obj.host = host;
         }
       }
@@ -309,7 +311,9 @@ export function genVlessLink(input: GenVlessLinkInput): string {
       const request = tcp.header.request;
       if (request) {
         params.set('path', request.path.join(','));
-        const host = getHeaderValue(request.headers, 'host');
+        const host =
+          getHeaderValue(tcp.header.response?.headers, 'host')
+          || getHeaderValue(request.headers, 'host');
         if (host) params.set('host', host);
         params.set('headerType', 'http');
       }
@@ -387,7 +391,9 @@ function writeNetworkParams(stream: NonNullable<Inbound['streamSettings']>, para
       const request = tcp.header.request;
       if (request) {
         params.set('path', request.path.join(','));
-        const host = getHeaderValue(request.headers, 'host');
+        const host =
+          getHeaderValue(tcp.header.response?.headers, 'host')
+          || getHeaderValue(request.headers, 'host');
         if (host) params.set('host', host);
         params.set('headerType', 'http');
       }

+ 42 - 0
frontend/src/pages/inbounds/InboundFormModal.tsx

@@ -1758,6 +1758,48 @@ export default function InboundFormModal({
               if (headerType !== 'http') return null;
               return (
                 <>
+                  <Form.Item
+                    label="Request version"
+                    name={[
+                      'streamSettings', 'tcpSettings', 'header',
+                      'request', 'version',
+                    ]}
+                  >
+                    <Input placeholder="1.1" />
+                  </Form.Item>
+                  <Form.Item
+                    label="Request method"
+                    name={[
+                      'streamSettings', 'tcpSettings', 'header',
+                      'request', 'method',
+                    ]}
+                  >
+                    <Input placeholder="GET" />
+                  </Form.Item>
+                  <Form.Item
+                    label="Request path"
+                    name={[
+                      'streamSettings', 'tcpSettings', 'header',
+                      'request', 'path',
+                    ]}
+                    getValueProps={(v) => ({ value: Array.isArray(v) ? v.join(',') : v })}
+                    getValueFromEvent={(e) => {
+                      const raw = (e?.target?.value ?? '') as string;
+                      const parts = raw.split(',').map((s) => s.trim()).filter(Boolean);
+                      return parts.length > 0 ? parts : ['/'];
+                    }}
+                  >
+                    <Input placeholder="/" />
+                  </Form.Item>
+                  <Form.Item
+                    label="Request headers"
+                    name={[
+                      'streamSettings', 'tcpSettings', 'header',
+                      'request', 'headers',
+                    ]}
+                  >
+                    <HeaderMapEditor mode="v2" />
+                  </Form.Item>
                   <Form.Item
                     label="Response version"
                     name={[

+ 22 - 4
sub/subService.go

@@ -696,8 +696,17 @@ func applyShareNetworkParams(stream map[string]any, streamNetwork string, params
 			request := header["request"].(map[string]any)
 			requestPath, _ := request["path"].([]any)
 			params["path"] = requestPath[0].(string)
-			headers, _ := request["headers"].(map[string]any)
-			params["host"] = searchHost(headers)
+			host := ""
+			if response, ok := header["response"].(map[string]any); ok {
+				if respHeaders, ok := response["headers"].(map[string]any); ok {
+					host = searchHost(respHeaders)
+				}
+			}
+			if host == "" {
+				headers, _ := request["headers"].(map[string]any)
+				host = searchHost(headers)
+			}
+			params["host"] = host
 			params["headerType"] = "http"
 		}
 	case "kcp":
@@ -743,8 +752,17 @@ func applyVmessNetworkParams(stream map[string]any, network string, obj map[stri
 			request := header["request"].(map[string]any)
 			requestPath, _ := request["path"].([]any)
 			obj["path"] = requestPath[0].(string)
-			headers, _ := request["headers"].(map[string]any)
-			obj["host"] = searchHost(headers)
+			host := ""
+			if response, ok := header["response"].(map[string]any); ok {
+				if respHeaders, ok := response["headers"].(map[string]any); ok {
+					host = searchHost(respHeaders)
+				}
+			}
+			if host == "" {
+				headers, _ := request["headers"].(map[string]any)
+				host = searchHost(headers)
+			}
+			obj["host"] = host
 		}
 	case "kcp":
 		applyKcpShareObj(stream, obj)