mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
refactor(frontend): add getHeaderValue wire-shape lookup to lib/xray/headers
Tiny piece of the toShareLink scaffold. The legacy Inbound.getHeader(obj, name) iterated the panel's internal HeaderEntry[] form; the new getHeaderValue reads the Record<string, string|string[]> map our Zod schemas store on the wire. Case-insensitive, returns '' on miss to match the legacy fallback so link-generator call sites stay simple. For repeated-name maps (TCP/WS-style string[] values) the first value wins — matches the legacy iteration order so the share URL's Host hint stays deterministic. Five unit tests cover undefined/null/empty inputs, case folding, string-valued and array-valued matches, empty-array edge case, and missing-key fallback. Suite: 64 tests across 6 files; typecheck + lint clean. This unblocks the next slice: per-protocol link generators (genVmessLink etc.) take a typed inbound + client and call getHeaderValue against the ws/httpupgrade/xhttp/tcp.request header maps.
This commit is contained in:
parent
e79ca42407
commit
c4f5d841b0
2 changed files with 47 additions and 1 deletions
|
|
@ -32,6 +32,27 @@ export function toHeaders(v2Headers: unknown): HeaderEntry[] {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Case-insensitive lookup against a wire-shape header map. The legacy
|
||||||
|
// `Inbound.getHeader(obj, name)` iterated `obj.headers` as a HeaderEntry[];
|
||||||
|
// this version reads the Record map our Zod schemas store. For repeated
|
||||||
|
// header names (string[] in TCP/WS-style maps) the first value wins —
|
||||||
|
// matches the legacy iteration order. Returns '' when missing, mirroring
|
||||||
|
// the legacy fallback so link-generator call sites stay simple.
|
||||||
|
export function getHeaderValue(
|
||||||
|
headers: Readonly<Record<string, string | string[]>> | undefined | null,
|
||||||
|
name: string,
|
||||||
|
): string {
|
||||||
|
if (!headers || typeof headers !== 'object') return '';
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
for (const key of Object.keys(headers)) {
|
||||||
|
if (key.toLowerCase() !== lower) continue;
|
||||||
|
const value = headers[key];
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (Array.isArray(value)) return value[0] ?? '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
// Collapse a HeaderEntry[] back into a V2-style header map. When `arr` is
|
// 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),
|
// true (the default — matches Xray's TCP/WS/HTTP request/response shape),
|
||||||
// duplicate header names accumulate into a string[]. When false (used for
|
// duplicate header names accumulate into a string[]. When false (used for
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { toHeaders, toV2Headers, type HeaderEntry } from '@/lib/xray/headers';
|
import { getHeaderValue, toHeaders, toV2Headers, type HeaderEntry } from '@/lib/xray/headers';
|
||||||
import { XrayCommonClass } from '@/models/inbound';
|
import { XrayCommonClass } from '@/models/inbound';
|
||||||
|
|
||||||
// Shadow harness: the new pure helpers must agree byte-for-byte with the
|
// Shadow harness: the new pure helpers must agree byte-for-byte with the
|
||||||
|
|
@ -57,3 +57,28 @@ describe('toV2Headers parity (arr=false)', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getHeaderValue lookups', () => {
|
||||||
|
it('returns empty string for missing map', () => {
|
||||||
|
expect(getHeaderValue(undefined, 'host')).toBe('');
|
||||||
|
expect(getHeaderValue(null, 'host')).toBe('');
|
||||||
|
expect(getHeaderValue({}, 'host')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds a string-valued header case-insensitively', () => {
|
||||||
|
expect(getHeaderValue({ Host: 'example.test' }, 'host')).toBe('example.test');
|
||||||
|
expect(getHeaderValue({ host: 'example.test' }, 'HOST')).toBe('example.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns first value when the header is an array', () => {
|
||||||
|
expect(getHeaderValue({ Accept: ['text/html', 'application/json'] }, 'accept')).toBe('text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string when the header has empty array', () => {
|
||||||
|
expect(getHeaderValue({ Host: [] }, 'host')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for missing header name', () => {
|
||||||
|
expect(getHeaderValue({ Host: 'x' }, 'origin')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue