helpers.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { getMessage } from '@/utils/messageBus';
  2. import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
  3. import { Protocols } from '@/schemas/primitives';
  4. import { coerceInboundJsonField } from '@/models/dbinbound';
  5. import {
  6. canEnableTlsFlow,
  7. isSS2022 as isSS2022Helper,
  8. isSSMultiUser as isSSMultiUserHelper,
  9. } from '@/lib/xray/protocol-capabilities';
  10. import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
  11. const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
  12. Protocols.VMESS,
  13. Protocols.VLESS,
  14. Protocols.TROJAN,
  15. Protocols.SHADOWSOCKS,
  16. Protocols.HYSTERIA,
  17. ]);
  18. export function hasShareLink(protocol: string): boolean {
  19. return LINK_PROTOCOLS.has(protocol);
  20. }
  21. function readHeader(headers: unknown, name: string): string {
  22. const needle = name.toLowerCase();
  23. if (Array.isArray(headers)) {
  24. for (const h of headers) {
  25. if (h && typeof h === 'object' && String((h as { name?: string }).name ?? '').toLowerCase() === needle) {
  26. return String((h as { value?: unknown }).value ?? '');
  27. }
  28. }
  29. return '';
  30. }
  31. if (headers && typeof headers === 'object') {
  32. for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
  33. if (k.toLowerCase() === needle) {
  34. return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
  35. }
  36. }
  37. }
  38. return '';
  39. }
  40. function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
  41. switch (network) {
  42. case 'tcp': {
  43. const tcp = stream.tcpSettings as { header?: { request?: { headers?: unknown } } } | undefined;
  44. return readHeader(tcp?.header?.request?.headers, 'host');
  45. }
  46. case 'ws': {
  47. const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
  48. return (ws?.host && ws.host.length > 0) ? ws.host : readHeader(ws?.headers, 'host');
  49. }
  50. case 'httpupgrade': {
  51. const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
  52. return (hu?.host && hu.host.length > 0) ? hu.host : readHeader(hu?.headers, 'host');
  53. }
  54. case 'xhttp': {
  55. const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
  56. return (xh?.host && xh.host.length > 0) ? xh.host : readHeader(xh?.headers, 'host');
  57. }
  58. default:
  59. return null;
  60. }
  61. }
  62. function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
  63. switch (network) {
  64. case 'tcp': {
  65. const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
  66. return tcp?.header?.request?.path?.[0] ?? null;
  67. }
  68. case 'ws':
  69. return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
  70. case 'httpupgrade':
  71. return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
  72. case 'xhttp':
  73. return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
  74. default:
  75. return null;
  76. }
  77. }
  78. export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
  79. const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
  80. const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
  81. const network = (stream.network as string | undefined) ?? '';
  82. const security = (stream.security as string | undefined) ?? 'none';
  83. const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
  84. const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
  85. const grpcSettings = stream.grpcSettings as { multiMode?: boolean; serviceName?: string } | undefined;
  86. let serverName = '';
  87. if (security === 'tls') {
  88. const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
  89. serverName = tls?.sni ?? tls?.serverName ?? '';
  90. } else if (security === 'reality') {
  91. const reality = stream.realitySettings as { serverNames?: string[]; serverName?: string } | undefined;
  92. if (Array.isArray(reality?.serverNames)) {
  93. serverName = reality.serverNames.join(', ');
  94. } else if (reality?.serverName) {
  95. serverName = reality.serverName;
  96. }
  97. }
  98. return {
  99. protocol: dbInbound.protocol,
  100. clients,
  101. settings,
  102. isTcp: network === 'tcp',
  103. isWs: network === 'ws',
  104. isHttpupgrade: network === 'httpupgrade',
  105. isXHTTP: network === 'xhttp',
  106. isGrpc: network === 'grpc',
  107. isSSMultiUser: isSSMultiUserHelper({
  108. protocol: dbInbound.protocol,
  109. settings: settings as { method?: string },
  110. }),
  111. isSS2022: isSS2022Helper({
  112. protocol: dbInbound.protocol,
  113. settings: settings as { method?: string },
  114. }),
  115. isVlessTlsFlow: canEnableTlsFlow({
  116. protocol: dbInbound.protocol,
  117. streamSettings: { network, security },
  118. }),
  119. host: readNetworkHost(stream, network),
  120. path: readNetworkPath(stream, network),
  121. serviceName: grpcSettings?.serviceName ?? '',
  122. serverName,
  123. stream: {
  124. network,
  125. security,
  126. xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
  127. grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
  128. },
  129. };
  130. }
  131. export function copyText(value: unknown, t: (k: string) => string) {
  132. ClipboardManager.copyText(String(value ?? '')).then((ok) => {
  133. if (ok) getMessage().success(t('copied'));
  134. });
  135. }
  136. export function downloadText(content: string, filename: string) {
  137. FileManager.downloadTextFile(content, filename);
  138. }
  139. export function statsColor(stats: ClientStats, trafficDiff: number) {
  140. return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
  141. }
  142. export function formatIpInfo(record: unknown) {
  143. if (record == null) return '';
  144. if (typeof record === 'string' || typeof record === 'number') return String(record);
  145. const r = record as { ip?: string; IP?: string; timestamp?: number | string; Timestamp?: number | string };
  146. const ip = r.ip || r.IP || '';
  147. const ts = r.timestamp || r.Timestamp || 0;
  148. if (!ip) return String(record);
  149. if (!ts) return String(ip);
  150. const date = new Date(Number(ts) * 1000);
  151. const timeStr = date
  152. .toLocaleString('en-GB', {
  153. year: 'numeric', month: '2-digit', day: '2-digit',
  154. hour: '2-digit', minute: '2-digit', second: '2-digit',
  155. hour12: false,
  156. })
  157. .replace(',', '');
  158. return `${ip} (${timeStr})`;
  159. }