Files
3x-ui/frontend/src/pages/inbounds/info/helpers.ts
T
MHSanaei c20082d254 refactor(inbounds): drop the single-user shadowsocks inbound paths
isSSMultiUser treated 2022-blake3-chacha20-poly1305 as a single-user
inbound, and the inbounds page grew a row QR modal, a separate link
panel in the info modal and a client-count skip for that case. The
method is no longer allowed on inbounds, so every shadowsocks inbound
carries clients and those paths cannot be reached: remove them along
with the inbound-level QrCodeModal they alone opened.
2026-10-11 21:24:25 +02:00

194 lines
6.6 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 } 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',
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})`;
}