helpers.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import type { NodeRecord } from '@/api/queries/useNodesQuery';
  2. import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
  3. import { coerceInboundJsonField } from '@/models/dbinbound';
  4. import type { ClientCountEntry, DBInboundRecord, SortKey, StreamHints } from './types';
  5. export function readStreamHints(streamSettings: unknown): StreamHints {
  6. const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
  7. return {
  8. network: stream.network ?? '',
  9. isTls: stream.security === 'tls',
  10. isReality: stream.security === 'reality',
  11. };
  12. }
  13. // Display label for a network value. All known transports render in
  14. // upper-case for visual consistency with the TCP/UDP/TLS/Reality tags
  15. // already shown alongside; compound names (`httpupgrade`, `splithttp`,
  16. // `xhttp`) get a tiny touch of casing so they don't read as one word.
  17. export function networkLabel(network: string): string {
  18. const n = (network || '').toLowerCase();
  19. if (!n) return 'TCP';
  20. switch (n) {
  21. case 'httpupgrade': return 'HTTPUpgrade';
  22. case 'splithttp': return 'SplitHTTP';
  23. case 'xhttp': return 'XHTTP';
  24. }
  25. return n.toUpperCase();
  26. }
  27. // Returns the underlying L4 protocol for transports whose name isn't
  28. // already TCP/UDP. `kcp` and `quic` both ride on UDP; everything else
  29. // (`ws`, `grpc`, `http`, `httpupgrade`, `xhttp`) is TCP-based and gets
  30. // no extra tag (the transport name implies TCP).
  31. export function networkL4(network: string): 'UDP' | '' {
  32. const n = (network || '').toLowerCase();
  33. if (n === 'kcp' || n === 'quic') return 'UDP';
  34. return '';
  35. }
  36. // Shadowsocks settings.network ("tcp" / "udp" / "tcp,udp") and Tunnel
  37. // settings.allowedNetwork (same shape, different field name) both carry
  38. // the L4 transport list independent of streamSettings. Returns a
  39. // comma-separated label.
  40. export function commaNetworkLabel(raw: string): string {
  41. const parts = (raw || 'tcp').toLowerCase().split(',').map((p) => p.trim()).filter(Boolean);
  42. if (parts.length === 0) return 'TCP';
  43. return parts.map(networkLabel).join(',');
  44. }
  45. export function shadowsocksNetworkLabel(settings: unknown): string {
  46. return commaNetworkLabel(readSettings(settings).network || '');
  47. }
  48. export function tunnelNetworkLabel(settings: unknown): string {
  49. return commaNetworkLabel(readSettings(settings).allowedNetwork || '');
  50. }
  51. // Mixed (socks+http combo) is always TCP at L4; settings.udp=true adds
  52. // UDP-associate support on the same port (SOCKS5 UDP).
  53. export function mixedNetworkLabel(settings: unknown): string {
  54. const st = coerceInboundJsonField(settings) as { udp?: boolean };
  55. return st.udp ? 'TCP,UDP' : 'TCP';
  56. }
  57. export function readSettings(settings: unknown): { method?: string; network?: string; allowedNetwork?: string } {
  58. return coerceInboundJsonField(settings) as { method?: string; network?: string; allowedNetwork?: string };
  59. }
  60. export function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean {
  61. switch (record.protocol) {
  62. case 'vmess':
  63. case 'vless':
  64. case 'trojan':
  65. case 'hysteria':
  66. return true;
  67. case 'shadowsocks':
  68. return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
  69. default:
  70. return false;
  71. }
  72. }
  73. export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
  74. if (dbInbound.isWireguard) return true;
  75. if (dbInbound.isSS) {
  76. return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
  77. }
  78. return false;
  79. }
  80. export const SORT_FNS: Record<SortKey, (a: DBInboundRecord, b: DBInboundRecord, ctx: { nodesById: Map<number, NodeRecord>; clientCount: Record<number, ClientCountEntry> }) => number> = {
  81. id: (a, b) => a.id - b.id,
  82. enable: (a, b) => Number(a.enable) - Number(b.enable),
  83. remark: (a, b) => (a.remark || '').localeCompare(b.remark || ''),
  84. port: (a, b) => a.port - b.port,
  85. protocol: (a, b) => a.protocol.localeCompare(b.protocol),
  86. traffic: (a, b) => (a.up + a.down) - (b.up + b.down),
  87. expiryTime: (a, b) => (a.expiryTime || Infinity) - (b.expiryTime || Infinity),
  88. node: (a, b, ctx) => {
  89. const nameA = ctx.nodesById.get(a.nodeId ?? -1)?.name ?? (a.nodeId == null ? '￿' : `node #${a.nodeId}`);
  90. const nameB = ctx.nodesById.get(b.nodeId ?? -1)?.name ?? (b.nodeId == null ? '￿' : `node #${b.nodeId}`);
  91. return nameA.localeCompare(nameB);
  92. },
  93. clients: (a, b, ctx) => (ctx.clientCount[a.id]?.clients || 0) - (ctx.clientCount[b.id]?.clients || 0),
  94. };