Răsfoiți Sursa

feat(frontend): XHTTP advanced fields on outbound modal

Replace the 'edit via JSON' deferred-features hint with the full XHTTP
sub-form matching the legacy modal's XhttpFields helper.

schemas/protocols/stream/xhttp.ts:
- New XHttpXmuxSchema: 6 connection-multiplexing knobs
  (maxConcurrency, maxConnections, cMaxReuseTimes, hMaxRequestTimes,
  hMaxReusableSecs, hKeepAlivePeriod).
- XHttpStreamSettingsSchema gains 5 outbound-only fields and one
  UI-only toggle: scMinPostsIntervalMs, uplinkChunkSize, noGRPCHeader,
  xmux, enableXmux.

outbound-form-adapter.ts:
- New stripUiOnlyStreamFields() drops xhttpSettings.enableXmux on the
  way to wire so the panel never embeds the UI toggle into the saved
  config. xray-core ignores unknown fields anyway, but the panel reads
  back its own emitted JSON, so a clean wire shape matters.

OutboundFormModal.tsx:
- Headers editor (HeaderMapEditor v1) for xhttpSettings.headers.
- Padding obfs Switch + 4 conditional fields (key/header/placement/
  method) when on.
- Uplink HTTP method Select with GET disabled outside packet-up.
- Session placement + session key (key shown when placement != path).
- Sequence placement + sequence key (same pattern).
- packet-up mode: scMinPostsIntervalMs, scMaxEachPostBytes, uplink
  data placement + key + chunk size (key/chunk-size shown when
  placement != body).
- stream-up / stream-one mode: noGRPCHeader Switch.
- XMUX Switch + 6 nested fields when on.
MHSanaei 11 ore în urmă
părinte
comite
e01acae843

+ 17 - 1
frontend/src/lib/xray/outbound-form-adapter.ts

@@ -554,6 +554,22 @@ function loopbackToWire(s: LoopbackOutboundFormSettings) {
 const MUX_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']);
 const STREAM_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria']);
 
+// Strip UI-only fields the form layered into streamSettings (e.g. the
+// XHTTP modal's enableXmux toggle that controls section visibility but
+// has no meaning on the wire). xray-core would ignore unknown fields
+// anyway but the panel reads back its own emitted JSON, so we keep
+// the wire shape clean.
+function stripUiOnlyStreamFields(stream: unknown): Raw {
+  const next = { ...(stream as Raw) };
+  const xh = next.xhttpSettings;
+  if (xh && typeof xh === 'object') {
+    const cleaned = { ...(xh as Raw) };
+    delete cleaned.enableXmux;
+    next.xhttpSettings = cleaned;
+  }
+  return next;
+}
+
 function muxAllowed(values: OutboundFormValues): boolean {
   if (!MUX_PROTOCOLS.has(values.protocol)) return false;
   const flow = values.protocol === 'vless'
@@ -596,7 +612,7 @@ export function formValuesToWirePayload(values: OutboundFormValues): WireOutboun
   // still emit just `sockopt` if that key is present (legacy behavior).
   if (values.streamSettings) {
     if (STREAM_PROTOCOLS.has(values.protocol)) {
-      result.streamSettings = values.streamSettings;
+      result.streamSettings = stripUiOnlyStreamFields(values.streamSettings);
     } else {
       const sockopt = (values.streamSettings as { sockopt?: unknown }).sockopt;
       if (sockopt) result.streamSettings = { sockopt };

+ 302 - 5
frontend/src/pages/xray/OutboundFormModal.tsx

@@ -1306,11 +1306,308 @@ export default function OutboundFormModal({
                             >
                               <Input />
                             </Form.Item>
-                            <div style={{ marginTop: 4, opacity: 0.6, fontStyle: 'italic' }}>
-                              XHTTP advanced fields (XMUX, sequence/session placement,
-                              padding obfs) are still being migrated — edit them via
-                              the JSON tab.
-                            </div>
+                            <Form.Item
+                              label="Headers"
+                              name={['streamSettings', 'xhttpSettings', 'headers']}
+                            >
+                              <HeaderMapEditor mode="v1" />
+                            </Form.Item>
+
+                            {/* Padding obfs sub-section: gated by a Switch.
+                                When on, four extra knobs (key/header/placement/
+                                method) tune how Xray injects random padding to
+                                disguise the post body shape. */}
+                            <Form.Item
+                              label="Padding obfs mode"
+                              name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
+                              valuePropName="checked"
+                            >
+                              <Switch />
+                            </Form.Item>
+                            <Form.Item shouldUpdate noStyle>
+                              {() => {
+                                const obfs = !!form.getFieldValue([
+                                  'streamSettings', 'xhttpSettings', 'xPaddingObfsMode',
+                                ]);
+                                if (!obfs) return null;
+                                return (
+                                  <>
+                                    <Form.Item
+                                      label="Padding key"
+                                      name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
+                                    >
+                                      <Input placeholder="x_padding" />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Padding header"
+                                      name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
+                                    >
+                                      <Input placeholder="X-Padding" />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Padding placement"
+                                      name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
+                                    >
+                                      <Select
+                                        options={[
+                                          { value: '', label: 'Default (queryInHeader)' },
+                                          { value: 'queryInHeader', label: 'queryInHeader' },
+                                          { value: 'header', label: 'header' },
+                                          { value: 'cookie', label: 'cookie' },
+                                          { value: 'query', label: 'query' },
+                                        ]}
+                                      />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Padding method"
+                                      name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
+                                    >
+                                      <Select
+                                        options={[
+                                          { value: '', label: 'Default (repeat-x)' },
+                                          { value: 'repeat-x', label: 'repeat-x' },
+                                          { value: 'tokenish', label: 'tokenish' },
+                                        ]}
+                                      />
+                                    </Form.Item>
+                                  </>
+                                );
+                              }}
+                            </Form.Item>
+
+                            <Form.Item
+                              label="Uplink HTTP method"
+                              name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
+                            >
+                              <Form.Item shouldUpdate noStyle>
+                                {() => {
+                                  const mode = form.getFieldValue([
+                                    'streamSettings', 'xhttpSettings', 'mode',
+                                  ]);
+                                  return (
+                                    <Select
+                                      options={[
+                                        { value: '', label: 'Default (POST)' },
+                                        { value: 'POST', label: 'POST' },
+                                        { value: 'PUT', label: 'PUT' },
+                                        { value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
+                                      ]}
+                                    />
+                                  );
+                                }}
+                              </Form.Item>
+                            </Form.Item>
+
+                            {/* Session + sequence + uplinkData placements:
+                                three orthogonal slots Xray uses to thread
+                                request metadata through the transport
+                                (path / header / cookie / query). Key field
+                                only matters when placement is not 'path'. */}
+                            <Form.Item
+                              label="Session placement"
+                              name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
+                            >
+                              <Select
+                                options={[
+                                  { value: '', label: 'Default (path)' },
+                                  { value: 'path', label: 'path' },
+                                  { value: 'header', label: 'header' },
+                                  { value: 'cookie', label: 'cookie' },
+                                  { value: 'query', label: 'query' },
+                                ]}
+                              />
+                            </Form.Item>
+                            <Form.Item shouldUpdate noStyle>
+                              {() => {
+                                const placement = form.getFieldValue([
+                                  'streamSettings', 'xhttpSettings', 'sessionPlacement',
+                                ]);
+                                if (!placement || placement === 'path') return null;
+                                return (
+                                  <Form.Item
+                                    label="Session key"
+                                    name={['streamSettings', 'xhttpSettings', 'sessionKey']}
+                                  >
+                                    <Input placeholder="x_session" />
+                                  </Form.Item>
+                                );
+                              }}
+                            </Form.Item>
+                            <Form.Item
+                              label="Sequence placement"
+                              name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
+                            >
+                              <Select
+                                options={[
+                                  { value: '', label: 'Default (path)' },
+                                  { value: 'path', label: 'path' },
+                                  { value: 'header', label: 'header' },
+                                  { value: 'cookie', label: 'cookie' },
+                                  { value: 'query', label: 'query' },
+                                ]}
+                              />
+                            </Form.Item>
+                            <Form.Item shouldUpdate noStyle>
+                              {() => {
+                                const placement = form.getFieldValue([
+                                  'streamSettings', 'xhttpSettings', 'seqPlacement',
+                                ]);
+                                if (!placement || placement === 'path') return null;
+                                return (
+                                  <Form.Item
+                                    label="Sequence key"
+                                    name={['streamSettings', 'xhttpSettings', 'seqKey']}
+                                  >
+                                    <Input placeholder="x_seq" />
+                                  </Form.Item>
+                                );
+                              }}
+                            </Form.Item>
+
+                            {/* Mode-conditional sub-sections. */}
+                            <Form.Item shouldUpdate noStyle>
+                              {() => {
+                                const mode = form.getFieldValue([
+                                  'streamSettings', 'xhttpSettings', 'mode',
+                                ]);
+                                if (mode !== 'packet-up') return null;
+                                return (
+                                  <>
+                                    <Form.Item
+                                      label="Min upload interval (ms)"
+                                      name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
+                                    >
+                                      <Input placeholder="30" />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Max upload size (bytes)"
+                                      name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
+                                    >
+                                      <Input placeholder="1000000" />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Uplink data placement"
+                                      name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
+                                    >
+                                      <Select
+                                        options={[
+                                          { value: '', label: 'Default (body)' },
+                                          { value: 'body', label: 'body' },
+                                          { value: 'header', label: 'header' },
+                                          { value: 'cookie', label: 'cookie' },
+                                          { value: 'query', label: 'query' },
+                                        ]}
+                                      />
+                                    </Form.Item>
+                                    <Form.Item shouldUpdate noStyle>
+                                      {() => {
+                                        const place = form.getFieldValue([
+                                          'streamSettings', 'xhttpSettings', 'uplinkDataPlacement',
+                                        ]);
+                                        if (!place || place === 'body') return null;
+                                        return (
+                                          <>
+                                            <Form.Item
+                                              label="Uplink data key"
+                                              name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
+                                            >
+                                              <Input placeholder="x_data" />
+                                            </Form.Item>
+                                            <Form.Item
+                                              label="Uplink chunk size"
+                                              name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
+                                            >
+                                              <InputNumber
+                                                min={0}
+                                                placeholder="0 (unlimited)"
+                                                style={{ width: '100%' }}
+                                              />
+                                            </Form.Item>
+                                          </>
+                                        );
+                                      }}
+                                    </Form.Item>
+                                  </>
+                                );
+                              }}
+                            </Form.Item>
+                            <Form.Item shouldUpdate noStyle>
+                              {() => {
+                                const mode = form.getFieldValue([
+                                  'streamSettings', 'xhttpSettings', 'mode',
+                                ]);
+                                if (mode !== 'stream-up' && mode !== 'stream-one') return null;
+                                return (
+                                  <Form.Item
+                                    label="No gRPC header"
+                                    name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
+                                    valuePropName="checked"
+                                  >
+                                    <Switch />
+                                  </Form.Item>
+                                );
+                              }}
+                            </Form.Item>
+
+                            {/* XMUX is the connection-multiplexing layer
+                                xHTTP uses to fan out parallel requests over
+                                a small pool of upstream connections. UI-only
+                                toggle (enableXmux) hides the 6 nested knobs
+                                when off. */}
+                            <Form.Item
+                              label="XMUX"
+                              name={['streamSettings', 'xhttpSettings', 'enableXmux']}
+                              valuePropName="checked"
+                            >
+                              <Switch />
+                            </Form.Item>
+                            <Form.Item shouldUpdate noStyle>
+                              {() => {
+                                if (!form.getFieldValue([
+                                  'streamSettings', 'xhttpSettings', 'enableXmux',
+                                ])) return null;
+                                return (
+                                  <>
+                                    <Form.Item
+                                      label="Max concurrency"
+                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
+                                    >
+                                      <Input placeholder="16-32" />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Max connections"
+                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
+                                    >
+                                      <Input placeholder="0" />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Max reuse times"
+                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
+                                    >
+                                      <Input />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Max request times"
+                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
+                                    >
+                                      <Input placeholder="600-900" />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Max reusable secs"
+                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
+                                    >
+                                      <Input placeholder="1800-3000" />
+                                    </Form.Item>
+                                    <Form.Item
+                                      label="Keep alive period"
+                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
+                                    >
+                                      <InputNumber min={0} style={{ width: '100%' }} />
+                                    </Form.Item>
+                                  </>
+                                );
+                              }}
+                            </Form.Item>
                           </>
                         )}
 

+ 24 - 0
frontend/src/schemas/protocols/stream/xhttp.ts

@@ -12,6 +12,19 @@ export type XHttpMode = z.infer<typeof XHttpModeSchema>;
 // server ignores them at runtime. Outbound has additional fields (uplinkChunk
 // sizes, noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) which
 // belong on the outbound class instead, not modeled here.
+// XMUX is the connection-multiplexing layer xHTTP uses to fan out
+// parallel requests over a small pool of upstream connections. Fields
+// are strings because they accept dash-range values like '16-32'.
+export const XHttpXmuxSchema = z.object({
+  maxConcurrency: z.string().default('16-32'),
+  maxConnections: z.union([z.string(), z.number()]).default(0),
+  cMaxReuseTimes: z.union([z.string(), z.number()]).default(0),
+  hMaxRequestTimes: z.string().default('600-900'),
+  hMaxReusableSecs: z.string().default('1800-3000'),
+  hKeepAlivePeriod: z.number().int().min(0).default(0),
+});
+export type XHttpXmux = z.infer<typeof XHttpXmuxSchema>;
+
 export const XHttpStreamSettingsSchema = z.object({
   path: z.string().default('/'),
   host: z.string().default(''),
@@ -35,5 +48,16 @@ export const XHttpStreamSettingsSchema = z.object({
   serverMaxHeaderBytes: z.number().int().min(0).default(0),
   uplinkHTTPMethod: z.string().default(''),
   headers: WsHeaderMapSchema.default({}),
+  // Outbound-only fields. Server (inbound) listener ignores these. The
+  // panel embeds them in share-link `extra` blobs so the same xhttp
+  // config can roundtrip on both sides.
+  scMinPostsIntervalMs: z.string().default('30'),
+  uplinkChunkSize: z.number().int().min(0).default(0),
+  noGRPCHeader: z.boolean().default(false),
+  xmux: XHttpXmuxSchema.optional(),
+  // UI-only toggle controlling whether the XMUX sub-form is expanded.
+  // Never present on the wire — outbound modal strips it via the
+  // form-to-wire adapter.
+  enableXmux: z.boolean().default(false),
 });
 export type XHttpStreamSettings = z.infer<typeof XHttpStreamSettingsSchema>;