mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
refactor(frontend): extract toHeaders + toV2Headers to lib/xray/headers.ts
First Step 3d extraction. The XrayCommonClass static helpers toHeaders/toV2Headers are pure data shape conversions with no class hierarchy needs, so they move to a standalone module that callers can import without dragging in models/inbound.ts. The new module exports HeaderEntry + V2HeaderMap as named types so consumers stop reaching into the legacy class for type shapes. A new test file (headers.test.ts) asserts byte-equality with the legacy XrayCommonClass.toHeaders / .toV2Headers across 18 cases — null / undefined / primitive inputs, single-string headers, array-valued headers, duplicate names, empty-name and empty-value filtering, both arr=true (TCP request/response shape) and arr=false (WS / xHTTP / sockopt shape). Drift between the legacy and new impls fails these tests, so the follow-up call-site swap stays safe. Callers (TcpStreamSettings, WsStreamSettings, HTTPUpgradeStreamSettings, TunnelSettings, etc.) still go through XrayCommonClass for now — those swaps land alongside class-method extractions in subsequent turns. Suite is now 44 tests across 5 files; typecheck + lint clean.
This commit is contained in:
parent
a7a8041b13
commit
922a442264
2 changed files with 116 additions and 0 deletions
57
frontend/src/lib/xray/headers.ts
Normal file
57
frontend/src/lib/xray/headers.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Pure helpers for header-shape conversion between the panel's internal
|
||||||
|
// HeaderEntry[] form and Xray's V2-style header map. Extracted from
|
||||||
|
// XrayCommonClass.toHeaders / .toV2Headers so callers can stop relying on
|
||||||
|
// the class hierarchy. Behavior is byte-equivalent to the legacy methods —
|
||||||
|
// the shadow tests in src/test/headers.test.ts pin that.
|
||||||
|
|
||||||
|
export interface HeaderEntry {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type V2HeaderMap = Record<string, string | string[]>;
|
||||||
|
|
||||||
|
// Expand a V2-style header map into the panel's flat HeaderEntry[]. A
|
||||||
|
// header whose value is an array yields one entry per item, preserving
|
||||||
|
// order; a string value yields a single entry. Non-object inputs (null,
|
||||||
|
// undefined, primitives) yield [].
|
||||||
|
export function toHeaders(v2Headers: unknown): HeaderEntry[] {
|
||||||
|
const out: HeaderEntry[] = [];
|
||||||
|
if (!v2Headers || typeof v2Headers !== 'object') return out;
|
||||||
|
const map = v2Headers as Record<string, unknown>;
|
||||||
|
for (const key of Object.keys(map)) {
|
||||||
|
const values = map[key];
|
||||||
|
if (typeof values === 'string') {
|
||||||
|
out.push({ name: key, value: values });
|
||||||
|
} else if (Array.isArray(values)) {
|
||||||
|
for (const v of values) {
|
||||||
|
if (typeof v === 'string') out.push({ name: key, value: v });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse a HeaderEntry[] back into a V2-style header map. When `arr` is
|
||||||
|
// true (the default — matches Xray's TCP/WS/HTTP request/response shape),
|
||||||
|
// duplicate header names accumulate into a string[]. When false (used for
|
||||||
|
// WS/HTTPUpgrade/xHTTP top-level headers, sockopt portMap, etc.), the
|
||||||
|
// last value wins. Entries with empty name or value are skipped — same as
|
||||||
|
// the legacy ObjectUtil.isEmpty() filter.
|
||||||
|
export function toV2Headers(headers: HeaderEntry[], arr: boolean = true): V2HeaderMap {
|
||||||
|
const out: V2HeaderMap = {};
|
||||||
|
for (const { name, value } of headers) {
|
||||||
|
if (name == null || name === '' || value == null || value === '') continue;
|
||||||
|
if (!(name in out)) {
|
||||||
|
out[name] = arr ? [value] : value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const existing = out[name];
|
||||||
|
if (arr && Array.isArray(existing)) {
|
||||||
|
existing.push(value);
|
||||||
|
} else {
|
||||||
|
out[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
59
frontend/src/test/headers.test.ts
Normal file
59
frontend/src/test/headers.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { toHeaders, toV2Headers, type HeaderEntry } from '@/lib/xray/headers';
|
||||||
|
import { XrayCommonClass } from '@/models/inbound';
|
||||||
|
|
||||||
|
// Shadow harness: the new pure helpers must agree byte-for-byte with the
|
||||||
|
// legacy XrayCommonClass static methods. Drift here is a regression.
|
||||||
|
|
||||||
|
const headerMapCases: Array<[string, unknown]> = [
|
||||||
|
['null', null],
|
||||||
|
['undefined', undefined],
|
||||||
|
['primitive', 'not-an-object'],
|
||||||
|
['empty', {}],
|
||||||
|
['single string', { Host: 'example.test' }],
|
||||||
|
['single array', { Host: ['a.example.test'] }],
|
||||||
|
['multi array', { Accept: ['text/html', 'application/json'] }],
|
||||||
|
['mixed', { Host: 'a.example.test', 'X-Trace': ['1', '2'] }],
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('toHeaders parity with XrayCommonClass.toHeaders', () => {
|
||||||
|
for (const [label, input] of headerMapCases) {
|
||||||
|
it(label, () => {
|
||||||
|
expect(toHeaders(input)).toEqual(XrayCommonClass.toHeaders(input));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const entryCases: Array<[string, HeaderEntry[]]> = [
|
||||||
|
['empty', []],
|
||||||
|
['single', [{ name: 'Host', value: 'example.test' }]],
|
||||||
|
['duplicate name', [
|
||||||
|
{ name: 'Accept', value: 'text/html' },
|
||||||
|
{ name: 'Accept', value: 'application/json' },
|
||||||
|
]],
|
||||||
|
['empty name skipped', [
|
||||||
|
{ name: '', value: 'ignored' },
|
||||||
|
{ name: 'X-Real', value: 'kept' },
|
||||||
|
]],
|
||||||
|
['empty value skipped', [
|
||||||
|
{ name: 'X-Empty', value: '' },
|
||||||
|
{ name: 'X-Real', value: 'kept' },
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('toV2Headers parity (arr=true)', () => {
|
||||||
|
for (const [label, input] of entryCases) {
|
||||||
|
it(label, () => {
|
||||||
|
expect(toV2Headers(input, true)).toEqual(XrayCommonClass.toV2Headers(input, true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toV2Headers parity (arr=false)', () => {
|
||||||
|
for (const [label, input] of entryCases) {
|
||||||
|
it(label, () => {
|
||||||
|
expect(toV2Headers(input, false)).toEqual(XrayCommonClass.toV2Headers(input, false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue