mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-09 22:55:00 +03:00
aee45ca3fe
Invariant: an inbound's enabled Hosts are the endpoints every client config for it advertises, whichever surface renders that config. WireGuard and AmneziaWG broke it. Their raw generators ignored the externalProxy entries Hosts are injected as and always emitted resolveInboundAddress, so the raw subscription, the sub page .conf, the clients links API and "export all links" gave out the panel address while the JSON and Clash formats of the same inbound used the Host. advertisedEndpoints now states the fan-out once for mtproto, wireguard and amneziawg. The browser-built configs had the same gap. The Clients page WireGuard and AmneziaWG config blocks and QR panels, and its TUIC Clash config, used the panel hostname next to server links that already used Hosts; the Inbounds page peer configs, QR and export ignored them too. withMtprotoHostEndpoints becomes withHostEndpoints over a shared hostEndpointsFor mirror of the backend, the tunnel fan-outs render one config per Host, and the clients page waits for the hosts list the way the inbounds page does, so an empty list means "no hosts" rather than "not loaded yet".
202 lines
6.8 KiB
TypeScript
202 lines
6.8 KiB
TypeScript
import { getMessage } from '@/utils/messageBus';
|
|
import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
|
|
import { Protocols } from '@/schemas/primitives';
|
|
import { coerceInboundJsonField } from '@/models/dbinbound';
|
|
import {
|
|
canEnableTlsFlow,
|
|
isSS2022 as isSS2022Helper,
|
|
isSSMultiUser as isSSMultiUserHelper,
|
|
} from '@/lib/xray/protocol-capabilities';
|
|
|
|
import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
|
|
|
|
const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
|
|
Protocols.VMESS,
|
|
Protocols.VLESS,
|
|
Protocols.TROJAN,
|
|
Protocols.SHADOWSOCKS,
|
|
Protocols.HYSTERIA,
|
|
Protocols.MTPROTO,
|
|
Protocols.TUIC,
|
|
]);
|
|
|
|
export function hasShareLink(protocol: string): boolean {
|
|
return LINK_PROTOCOLS.has(protocol);
|
|
}
|
|
|
|
function readHeader(headers: unknown, name: string): string {
|
|
const needle = name.toLowerCase();
|
|
if (Array.isArray(headers)) {
|
|
for (const h of headers) {
|
|
if (
|
|
h &&
|
|
typeof h === 'object' &&
|
|
String((h as { name?: string }).name ?? '').toLowerCase() === needle
|
|
) {
|
|
return String((h as { value?: unknown }).value ?? '');
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
if (headers && typeof headers === 'object') {
|
|
for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
|
|
if (k.toLowerCase() === needle) {
|
|
return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
|
|
switch (network) {
|
|
case 'tcp': {
|
|
const tcp = stream.tcpSettings as
|
|
| { header?: { request?: { headers?: unknown } } }
|
|
| undefined;
|
|
return readHeader(tcp?.header?.request?.headers, 'host');
|
|
}
|
|
case 'ws': {
|
|
const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
|
|
return ws?.host && ws.host.length > 0 ? ws.host : readHeader(ws?.headers, 'host');
|
|
}
|
|
case 'httpupgrade': {
|
|
const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
|
|
return hu?.host && hu.host.length > 0 ? hu.host : readHeader(hu?.headers, 'host');
|
|
}
|
|
case 'xhttp': {
|
|
const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
|
|
return xh?.host && xh.host.length > 0 ? xh.host : readHeader(xh?.headers, 'host');
|
|
}
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
|
|
switch (network) {
|
|
case 'tcp': {
|
|
const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
|
|
return tcp?.header?.request?.path?.[0] ?? null;
|
|
}
|
|
case 'ws':
|
|
return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
|
|
case 'httpupgrade':
|
|
return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
|
|
case 'xhttp':
|
|
return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
|
|
const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
|
|
const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
|
|
const network = (stream.network as string | undefined) ?? '';
|
|
const security = (stream.security as string | undefined) ?? 'none';
|
|
const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
|
|
const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
|
|
const grpcSettings = stream.grpcSettings as
|
|
| { multiMode?: boolean; serviceName?: string }
|
|
| undefined;
|
|
let serverName = '';
|
|
if (security === 'tls') {
|
|
const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
|
|
serverName = tls?.sni ?? tls?.serverName ?? '';
|
|
} else if (security === 'reality') {
|
|
const reality = stream.realitySettings as
|
|
| { serverNames?: string[]; serverName?: string }
|
|
| undefined;
|
|
if (Array.isArray(reality?.serverNames)) {
|
|
serverName = reality.serverNames.join(', ');
|
|
} else if (reality?.serverName) {
|
|
serverName = reality.serverName;
|
|
}
|
|
}
|
|
return {
|
|
protocol: dbInbound.protocol,
|
|
clients,
|
|
settings,
|
|
isTcp: network === 'tcp',
|
|
isWs: network === 'ws',
|
|
isHttpupgrade: network === 'httpupgrade',
|
|
isXHTTP: network === 'xhttp',
|
|
isGrpc: network === 'grpc',
|
|
isSSMultiUser: isSSMultiUserHelper({
|
|
protocol: dbInbound.protocol,
|
|
settings: settings as { method?: string },
|
|
}),
|
|
isSS2022: isSS2022Helper({
|
|
protocol: dbInbound.protocol,
|
|
settings: settings as { method?: string },
|
|
}),
|
|
isVlessTlsFlow: canEnableTlsFlow({
|
|
protocol: dbInbound.protocol,
|
|
settings: {
|
|
encryption: settings.encryption as string | undefined,
|
|
decryption: settings.decryption as string | undefined,
|
|
},
|
|
streamSettings: { network, security },
|
|
}),
|
|
host: readNetworkHost(stream, network),
|
|
path: readNetworkPath(stream, network),
|
|
serviceName: grpcSettings?.serviceName ?? '',
|
|
serverName,
|
|
stream: {
|
|
network,
|
|
security,
|
|
xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
|
|
grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function copyText(value: unknown, t: (k: string) => string) {
|
|
ClipboardManager.copyText(String(value ?? '')).then((ok) => {
|
|
if (ok) getMessage().success(t('copied'));
|
|
});
|
|
}
|
|
|
|
export function downloadText(content: string, filename: string) {
|
|
FileManager.downloadTextFile(content, filename);
|
|
}
|
|
|
|
// One file per advertised Host, so a peer behind two Hosts gets two names.
|
|
export function peerConfFileName(peerIndex: number, endpointIndex: number, endpointCount: number) {
|
|
const suffix = endpointCount > 1 ? `-${endpointIndex + 1}` : '';
|
|
return `peer-${peerIndex + 1}${suffix}.conf`;
|
|
}
|
|
|
|
export function statsColor(stats: ClientStats, trafficDiff: number) {
|
|
return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
|
|
}
|
|
|
|
export function formatIpInfo(record: unknown) {
|
|
if (record == null) return '';
|
|
if (typeof record === 'string' || typeof record === 'number') return String(record);
|
|
const r = record as {
|
|
ip?: string;
|
|
IP?: string;
|
|
timestamp?: number | string;
|
|
Timestamp?: number | string;
|
|
};
|
|
const ip = r.ip || r.IP || '';
|
|
const ts = r.timestamp || r.Timestamp || 0;
|
|
if (!ip) return String(record);
|
|
if (!ts) return String(ip);
|
|
const date = new Date(Number(ts) * 1000);
|
|
const timeStr = date
|
|
.toLocaleString('en-GB', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false,
|
|
})
|
|
.replace(',', '');
|
|
return `${ip} (${timeStr})`;
|
|
}
|