feat(ui): validate the REALITY client version range at save time (#6126)

* feat(ui): validate the REALITY client version range at save time

The impossible range from PR #6125 — a max below the effective minimum
— could still be saved; the tooltip only helps a user who hovers it.
Add save-time validation mirroring xray-core's parser (up to three
dot-separated parts, each 0-255) on both fields, plus a cross-field
check that a non-empty max is not below a non-empty min. Errors are
field-level i18n keys following the REALITY target precedent, so the
modal stays open and points at the offending field instead of storing
a config that rejects every client.

A malformed min is reported by its own field and skipped by the max
comparison, so the user sees one precise error per field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): reject untrimmed client versions and revalidate max on min edits

From review: the validators trimmed but the save path ships the value
verbatim, and xray-core's part parser accepts no surrounding
whitespace — so a green form could still save a config the core
refuses to load. Reject any value that differs from its trimmed form.

Also revalidate the max field after a min edit when max already
shows an error, so correcting the min clears the stale cross-field
message without waiting for the next submit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
PathGao
2026-07-29 05:04:03 +08:00
committed by GitHub
parent 411271b454
commit ca6955d88b
16 changed files with 171 additions and 1 deletions
@@ -104,6 +104,63 @@ export function validateRealityTarget(target: string): string | undefined {
return undefined;
}
/**
* Parses a REALITY client-version string the way xray-core's config loader
* does: one to three dot-separated numeric parts, each 0-255. Returns the
* parts padded to three entries, or undefined when the string is not a valid
* version.
*/
export function parseRealityClientVer(value: string): [number, number, number] | undefined {
const trimmed = value.trim();
if (!trimmed) return undefined;
const parts = trimmed.split('.');
if (parts.length > 3) return undefined;
const nums: number[] = [];
for (const part of parts) {
if (!/^\d+$/.test(part)) return undefined;
const n = Number(part);
if (n > 255) return undefined;
nums.push(n);
}
while (nums.length < 3) nums.push(0);
return nums as [number, number, number];
}
/**
* Validates a REALITY client-version field; empty means "not set" and is
* valid. The value is saved exactly as typed and xray-core's part parser
* accepts no surrounding whitespace, so a value that differs from its
* trimmed form is rejected rather than silently passed to the wire.
*/
export function validateRealityClientVer(value: string): string | undefined {
if (!value) return undefined;
if (value !== value.trim() || !parseRealityClientVer(value)) {
return 'pages.inbounds.form.clientVerInvalid';
}
return undefined;
}
/**
* Validates the max client-version field: format first, then that a non-empty
* max is not below a non-empty min (an inverted range rejects every client).
* An empty or malformed min is left to the min field's own validation.
*/
export function validateRealityMaxClientVer(max: string, min: string): string | undefined {
const formatError = validateRealityClientVer(max);
if (formatError) return formatError;
const maxParts = parseRealityClientVer(max);
const minParts = parseRealityClientVer(min);
if (!maxParts || !minParts) return undefined;
for (let i = 0; i < 3; i++) {
if (maxParts[i] !== minParts[i]) {
return maxParts[i] < minParts[i]
? 'pages.inbounds.form.maxClientVerBelowMin'
: undefined;
}
}
return undefined;
}
function liftLegacyXhttpSessionKeys(obj: Record<string, unknown>): void {
const lift = (legacy: string, renamed: string) => {
const v = obj[legacy];