mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat(frontend): protocol capability predicates as pure functions
Adds lib/xray/protocol-capabilities.ts with the seven predicates the modals call: canEnableTls, canEnableReality, canEnableTlsFlow, canEnableStream, canEnableVisionSeed, isSS2022, isSSMultiUser. Each takes a minimal slice of an InboundFormValues, no class instance. The legacy isSSMultiUser returns true on non-shadowsocks protocols too (method getter resolves to "" which != blake3-chacha20-poly1305). The new function preserves this quirk and documents it inline; callers all narrow on protocol === shadowsocks before checking, so the surprising return value never surfaces. Parity harness in test/protocol-capabilities.test.ts crosses each of the 10 golden fixtures with 14 stream configurations (network × security) and asserts each predicate matches the legacy class method — 140 cases, all green.
This commit is contained in:
parent
629567db72
commit
142ed97cc0
2 changed files with 159 additions and 0 deletions
74
frontend/src/lib/xray/protocol-capabilities.ts
Normal file
74
frontend/src/lib/xray/protocol-capabilities.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Pure-function ports of the legacy Inbound class capability predicates
|
||||||
|
// (canEnableTls, canEnableReality, canEnableTlsFlow, canEnableStream,
|
||||||
|
// canEnableVisionSeed, isSS2022, isSSMultiUser). Each accepts the minimal
|
||||||
|
// slice of an InboundFormValues it needs, so the same predicate can be
|
||||||
|
// called against a partial-row, a full form value, or a hand-built test
|
||||||
|
// fixture without the caller projecting a whole object.
|
||||||
|
|
||||||
|
const TLS_ELIGIBLE_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks'];
|
||||||
|
const TLS_NETWORKS = ['tcp', 'ws', 'http', 'grpc', 'httpupgrade', 'xhttp'];
|
||||||
|
const REALITY_ELIGIBLE_PROTOCOLS = ['vless', 'trojan'];
|
||||||
|
const REALITY_NETWORKS = ['tcp', 'http', 'grpc', 'xhttp'];
|
||||||
|
const STREAM_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria'];
|
||||||
|
const VISION_FLOW = 'xtls-rprx-vision';
|
||||||
|
const SS_2022_PREFIX = '2022';
|
||||||
|
const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305';
|
||||||
|
|
||||||
|
export interface CapabilityProtocolSlice {
|
||||||
|
protocol: string;
|
||||||
|
streamSettings?: { network?: string; security?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CapabilityVlessSlice extends CapabilityProtocolSlice {
|
||||||
|
settings?: { clients?: { flow?: string }[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CapabilityShadowsocksSlice {
|
||||||
|
protocol: string;
|
||||||
|
settings?: { method?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEnableTls(values: CapabilityProtocolSlice): boolean {
|
||||||
|
if (values.protocol === 'hysteria') return true;
|
||||||
|
if (!TLS_ELIGIBLE_PROTOCOLS.includes(values.protocol)) return false;
|
||||||
|
return TLS_NETWORKS.includes(values.streamSettings?.network ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEnableReality(values: CapabilityProtocolSlice): boolean {
|
||||||
|
if (!REALITY_ELIGIBLE_PROTOCOLS.includes(values.protocol)) return false;
|
||||||
|
return REALITY_NETWORKS.includes(values.streamSettings?.network ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEnableTlsFlow(values: CapabilityProtocolSlice): boolean {
|
||||||
|
const security = values.streamSettings?.security;
|
||||||
|
if (security !== 'tls' && security !== 'reality') return false;
|
||||||
|
if (values.streamSettings?.network !== 'tcp') return false;
|
||||||
|
return values.protocol === 'vless';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEnableStream(values: { protocol: string }): boolean {
|
||||||
|
return STREAM_PROTOCOLS.includes(values.protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vision seed applies only when XTLS Vision (TCP/TLS) flow is selected
|
||||||
|
// AND at least one VLESS client uses the vision flow. Excludes UDP variant.
|
||||||
|
export function canEnableVisionSeed(values: CapabilityVlessSlice): boolean {
|
||||||
|
if (!canEnableTlsFlow(values)) return false;
|
||||||
|
const clients = values.settings?.clients;
|
||||||
|
if (!Array.isArray(clients)) return false;
|
||||||
|
return clients.some((c) => c?.flow === VISION_FLOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Why: legacy returns true on non-SS protocols too (the method getter
|
||||||
|
// resolves to "" and "" !== blake3-chacha20-poly1305). Preserved for
|
||||||
|
// parity with the legacy class; in practice the callers all narrow on
|
||||||
|
// protocol === shadowsocks before checking.
|
||||||
|
export function isSSMultiUser(values: CapabilityShadowsocksSlice): boolean {
|
||||||
|
const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
|
||||||
|
return method !== SS_BLAKE3_CHACHA20;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSS2022(values: CapabilityShadowsocksSlice): boolean {
|
||||||
|
const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
|
||||||
|
return method.substring(0, 4) === SS_2022_PREFIX;
|
||||||
|
}
|
||||||
85
frontend/src/test/protocol-capabilities.test.ts
Normal file
85
frontend/src/test/protocol-capabilities.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { Inbound } from '@/models/inbound';
|
||||||
|
import {
|
||||||
|
canEnableTls,
|
||||||
|
canEnableReality,
|
||||||
|
canEnableTlsFlow,
|
||||||
|
canEnableStream,
|
||||||
|
canEnableVisionSeed,
|
||||||
|
isSS2022,
|
||||||
|
isSSMultiUser,
|
||||||
|
} from '@/lib/xray/protocol-capabilities';
|
||||||
|
|
||||||
|
// Parity harness for the capability predicates. For each golden fixture
|
||||||
|
// (protocol+settings), cross with a matrix of stream configurations
|
||||||
|
// (network × security), build the legacy Inbound class via fromJson, and
|
||||||
|
// assert each pure-function predicate matches the class method.
|
||||||
|
//
|
||||||
|
// Only the (protocol × stream-shape) cross matters here — the predicates
|
||||||
|
// never read sniffing/port/listen, so we hold those constant.
|
||||||
|
|
||||||
|
const fixtures = import.meta.glob<unknown>(
|
||||||
|
'./golden/fixtures/inbound/*.json',
|
||||||
|
{ eager: true, import: 'default' },
|
||||||
|
);
|
||||||
|
|
||||||
|
interface FixtureShape { protocol: string; settings: Record<string, unknown> }
|
||||||
|
|
||||||
|
const STREAM_CASES: { network: string; security: string }[] = [
|
||||||
|
{ network: 'tcp', security: 'none' },
|
||||||
|
{ network: 'tcp', security: 'tls' },
|
||||||
|
{ network: 'tcp', security: 'reality' },
|
||||||
|
{ network: 'ws', security: 'none' },
|
||||||
|
{ network: 'ws', security: 'tls' },
|
||||||
|
{ network: 'grpc', security: 'none' },
|
||||||
|
{ network: 'grpc', security: 'tls' },
|
||||||
|
{ network: 'grpc', security: 'reality' },
|
||||||
|
{ network: 'kcp', security: 'none' },
|
||||||
|
{ network: 'httpupgrade', security: 'none' },
|
||||||
|
{ network: 'httpupgrade', security: 'tls' },
|
||||||
|
{ network: 'xhttp', security: 'none' },
|
||||||
|
{ network: 'xhttp', security: 'tls' },
|
||||||
|
{ network: 'xhttp', security: 'reality' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function fixtureName(path: string): string {
|
||||||
|
return (path.split('/').pop() ?? path).replace(/\.json$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('protocol capability predicates: pure ↔ legacy parity', () => {
|
||||||
|
const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
for (const [path, raw] of entries) {
|
||||||
|
const name = fixtureName(path);
|
||||||
|
const fix = raw as FixtureShape;
|
||||||
|
|
||||||
|
for (const stream of STREAM_CASES) {
|
||||||
|
|
||||||
|
it(`${name} :: ${stream.network}/${stream.security}`, () => {
|
||||||
|
const wireConfig = {
|
||||||
|
port: 12345,
|
||||||
|
listen: '127.0.0.1',
|
||||||
|
protocol: fix.protocol,
|
||||||
|
settings: fix.settings,
|
||||||
|
streamSettings: { network: stream.network, security: stream.security },
|
||||||
|
sniffing: {},
|
||||||
|
};
|
||||||
|
const legacy = Inbound.fromJson(wireConfig);
|
||||||
|
const values = {
|
||||||
|
protocol: fix.protocol,
|
||||||
|
streamSettings: { network: stream.network, security: stream.security },
|
||||||
|
settings: fix.settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(canEnableTls(values)).toBe(legacy.canEnableTls());
|
||||||
|
expect(canEnableReality(values)).toBe(legacy.canEnableReality());
|
||||||
|
expect(canEnableTlsFlow(values)).toBe(legacy.canEnableTlsFlow());
|
||||||
|
expect(canEnableStream(values)).toBe(legacy.canEnableStream());
|
||||||
|
expect(canEnableVisionSeed(values)).toBe(legacy.canEnableVisionSeed());
|
||||||
|
expect(isSS2022(values)).toBe(legacy.isSS2022);
|
||||||
|
expect(isSSMultiUser(values)).toBe(legacy.isSSMultiUser);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue