From c4f5d841b08519d0b914880e63631cb157179944 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 25 May 2026 23:53:03 +0200 Subject: [PATCH] refactor(frontend): add getHeaderValue wire-shape lookup to lib/xray/headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- frontend/src/lib/xray/headers.ts | 21 +++++++++++++++++++++ frontend/src/test/headers.test.ts | 27 ++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/xray/headers.ts b/frontend/src/lib/xray/headers.ts index 32694986..8368f92c 100644 --- a/frontend/src/lib/xray/headers.ts +++ b/frontend/src/lib/xray/headers.ts @@ -32,6 +32,27 @@ export function toHeaders(v2Headers: unknown): HeaderEntry[] { 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> | 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 // true (the default — matches Xray's TCP/WS/HTTP request/response shape), // duplicate header names accumulate into a string[]. When false (used for diff --git a/frontend/src/test/headers.test.ts b/frontend/src/test/headers.test.ts index fd029a1f..a5ae1452 100644 --- a/frontend/src/test/headers.test.ts +++ b/frontend/src/test/headers.test.ts @@ -1,6 +1,6 @@ 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'; // 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(''); + }); +});