subscription.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. // Pure builders for 3x-ui's subscription server: the subscription URLs plus
  2. // previews of the two body formats — Base64 (newline-joined share links,
  3. // standard base64) and JSON (Xray client config, one per client). Grounded in
  4. // internal/sub/{controller,build_urls_test}.go, json_service.go, default.json.
  5. // Reuses links.ts (share-link builders), base64.ts, and outbounds.ts
  6. // (buildStreamSettings). No React/DOM imports.
  7. import { textToBase64 } from './base64';
  8. import { buildVless, buildVmess, buildTrojan, buildShadowsocks } from './links';
  9. import { buildStreamSettings, type Network, type Security } from './outbounds';
  10. export interface SubUrlInput {
  11. scheme: 'http' | 'https';
  12. host: string;
  13. port: number;
  14. subPath: string; // e.g. '/sub/'
  15. jsonPath: string; // e.g. '/json/'
  16. subId: string;
  17. /** When behind a reverse proxy the public URL omits the sub-server port. */
  18. behindProxy?: boolean;
  19. }
  20. export interface SubUrls {
  21. base64: string;
  22. json: string;
  23. }
  24. export interface SubClient {
  25. protocol: 'vless' | 'vmess' | 'trojan' | 'ss';
  26. remark: string;
  27. address: string;
  28. port: number;
  29. // credentials
  30. id?: string; // vless / vmess uuid
  31. password?: string; // trojan / ss
  32. method?: string; // ss cipher
  33. flow?: string; // vless
  34. encryption?: string; // vless server encryption, default 'none'
  35. vmessSecurity?: string; // vmess scy, default 'auto'
  36. // stream (subset, mirrored into share-link params + JSON streamSettings)
  37. network?: Network;
  38. security?: Security;
  39. sni?: string;
  40. fingerprint?: string;
  41. path?: string;
  42. host?: string;
  43. serviceName?: string;
  44. publicKey?: string; // reality
  45. shortId?: string; // reality
  46. }
  47. function normPath(p: string): string {
  48. let s = p.trim();
  49. if (!s.startsWith('/')) s = `/${s}`;
  50. if (!s.endsWith('/')) s = `${s}/`;
  51. return s;
  52. }
  53. export function buildSubscriptionUrls(i: SubUrlInput): SubUrls {
  54. if (!i.subId) return { base64: '', json: '' };
  55. const origin = i.behindProxy ? `${i.scheme}://${i.host}` : `${i.scheme}://${i.host}:${i.port}`;
  56. return {
  57. base64: `${origin}${normPath(i.subPath)}${i.subId}`,
  58. json: `${origin}${normPath(i.jsonPath)}${i.subId}`,
  59. };
  60. }
  61. function streamParams(c: SubClient): Record<string, string> {
  62. const p: Record<string, string> = {
  63. type: c.network ?? 'tcp',
  64. security: c.security ?? 'none',
  65. };
  66. if (c.sni) p.sni = c.sni;
  67. if (c.fingerprint) p.fp = c.fingerprint;
  68. if (c.path) p.path = c.path;
  69. if (c.host) p.host = c.host;
  70. if (c.serviceName) p.serviceName = c.serviceName;
  71. if (c.publicKey) p.pbk = c.publicKey;
  72. if (c.shortId) p.sid = c.shortId;
  73. return p;
  74. }
  75. function shareLink(c: SubClient): string {
  76. switch (c.protocol) {
  77. case 'vless': {
  78. const params = streamParams(c);
  79. if (c.flow) params.flow = c.flow;
  80. return buildVless({
  81. credential: c.id ?? '',
  82. address: c.address,
  83. port: c.port,
  84. name: c.remark,
  85. params,
  86. });
  87. }
  88. case 'trojan':
  89. return buildTrojan({
  90. credential: c.password ?? '',
  91. address: c.address,
  92. port: c.port,
  93. name: c.remark,
  94. params: streamParams(c),
  95. });
  96. case 'vmess':
  97. return buildVmess({
  98. ps: c.remark,
  99. add: c.address,
  100. port: c.port,
  101. id: c.id ?? '',
  102. scy: c.vmessSecurity || 'auto',
  103. net: c.network ?? 'tcp',
  104. tls: c.security === 'tls' ? 'tls' : '',
  105. sni: c.sni ?? '',
  106. host: c.host ?? '',
  107. path: c.path ?? '',
  108. });
  109. case 'ss':
  110. return buildShadowsocks({
  111. method: c.method || '',
  112. password: c.password ?? '',
  113. address: c.address,
  114. port: c.port,
  115. name: c.remark,
  116. });
  117. }
  118. }
  119. export function buildShareLinks(clients: SubClient[]): string[] {
  120. return clients.map(shareLink);
  121. }
  122. export function buildBase64Subscription(clients: SubClient[]): string {
  123. if (clients.length === 0) return '';
  124. return textToBase64(buildShareLinks(clients).join('\n'));
  125. }
  126. // The non-outbound skeleton of internal/sub/default.json. A factory so every
  127. // call returns a fresh object (pure, no shared mutation).
  128. function subJsonSkeleton(): Record<string, unknown> {
  129. return {
  130. dns: {
  131. tag: 'dns_out',
  132. queryStrategy: 'UseIP',
  133. servers: [{ address: '8.8.8.8', skipFallback: false }],
  134. },
  135. inbounds: [
  136. {
  137. port: 10808,
  138. protocol: 'mixed',
  139. settings: { auth: 'noauth', udp: true, userLevel: 8 },
  140. sniffing: { destOverride: ['http', 'tls', 'quic', 'fakedns'], enabled: true },
  141. tag: 'mixed',
  142. },
  143. { port: 10809, protocol: 'http', settings: { userLevel: 8 }, tag: 'http' },
  144. ],
  145. log: { loglevel: 'warning' },
  146. policy: {
  147. levels: { '8': { connIdle: 300, downlinkOnly: 1, handshake: 4, uplinkOnly: 1 } },
  148. system: { statsOutboundUplink: true, statsOutboundDownlink: true },
  149. },
  150. routing: {
  151. domainStrategy: 'AsIs',
  152. rules: [{ type: 'field', network: 'tcp,udp', outboundTag: 'proxy' }],
  153. },
  154. stats: {},
  155. };
  156. }
  157. function skeletonOutbounds(): Record<string, unknown>[] {
  158. return [
  159. {
  160. tag: 'direct',
  161. protocol: 'freedom',
  162. settings: { domainStrategy: 'AsIs', redirect: '', noises: [] },
  163. },
  164. { tag: 'block', protocol: 'blackhole', settings: { response: { type: 'http' } } },
  165. ];
  166. }
  167. function proxyOutbound(c: SubClient): Record<string, unknown> {
  168. const streamSettings = buildStreamSettings({
  169. network: c.network ?? 'tcp',
  170. security: c.security ?? 'none',
  171. sni: c.sni,
  172. fingerprint: c.fingerprint,
  173. path: c.path,
  174. host: c.host,
  175. serviceName: c.serviceName,
  176. publicKey: c.publicKey,
  177. shortId: c.shortId,
  178. });
  179. let settings: Record<string, unknown>;
  180. switch (c.protocol) {
  181. case 'vless': {
  182. const s: Record<string, unknown> = {
  183. address: c.address,
  184. port: c.port,
  185. id: c.id ?? '',
  186. encryption: c.encryption || 'none',
  187. level: 8,
  188. };
  189. if (c.flow) s.flow = c.flow;
  190. settings = s;
  191. break;
  192. }
  193. case 'vmess':
  194. settings = {
  195. address: c.address,
  196. port: c.port,
  197. id: c.id ?? '',
  198. security: c.vmessSecurity || 'auto',
  199. level: 8,
  200. };
  201. break;
  202. case 'trojan':
  203. settings = { servers: [{ address: c.address, port: c.port, password: c.password ?? '', level: 8 }] };
  204. break;
  205. case 'ss':
  206. settings = {
  207. servers: [
  208. { address: c.address, port: c.port, password: c.password ?? '', level: 8, method: c.method || '' },
  209. ],
  210. };
  211. break;
  212. }
  213. return {
  214. protocol: c.protocol === 'ss' ? 'shadowsocks' : c.protocol,
  215. tag: 'proxy',
  216. streamSettings,
  217. settings,
  218. };
  219. }
  220. function jsonConfig(c: SubClient): Record<string, unknown> {
  221. return {
  222. remarks: c.remark,
  223. ...subJsonSkeleton(),
  224. outbounds: [proxyOutbound(c), ...skeletonOutbounds()],
  225. };
  226. }
  227. export function buildJsonSubscription(clients: SubClient[]): string {
  228. if (clients.length === 0) return '';
  229. const configs = clients.map(jsonConfig);
  230. // 3x-ui returns a single object for one client, an array for several.
  231. return JSON.stringify(configs.length === 1 ? configs[0] : configs, null, 2);
  232. }