1
0

subscription.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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. listen: '127.0.0.1',
  138. port: 10808,
  139. protocol: 'socks',
  140. settings: { auth: 'noauth', udp: true, userLevel: 8 },
  141. sniffing: { destOverride: ['http', 'tls', 'quic', 'fakedns'], enabled: true },
  142. tag: 'mixed',
  143. },
  144. {
  145. listen: '127.0.0.1',
  146. port: 10809,
  147. protocol: 'http',
  148. settings: { userLevel: 8 },
  149. tag: 'http',
  150. },
  151. ],
  152. log: { loglevel: 'warning' },
  153. policy: {
  154. levels: { '8': { connIdle: 300, downlinkOnly: 1, handshake: 4, uplinkOnly: 1 } },
  155. system: { statsOutboundUplink: true, statsOutboundDownlink: true },
  156. },
  157. routing: {
  158. domainStrategy: 'AsIs',
  159. rules: [{ type: 'field', network: 'tcp,udp', outboundTag: 'proxy' }],
  160. },
  161. stats: {},
  162. };
  163. }
  164. function skeletonOutbounds(): Record<string, unknown>[] {
  165. return [
  166. {
  167. tag: 'direct',
  168. protocol: 'freedom',
  169. settings: { domainStrategy: 'AsIs', redirect: '', noises: [] },
  170. },
  171. { tag: 'block', protocol: 'blackhole', settings: { response: { type: 'http' } } },
  172. ];
  173. }
  174. function proxyOutbound(c: SubClient): Record<string, unknown> {
  175. const streamSettings = buildStreamSettings({
  176. network: c.network ?? 'tcp',
  177. security: c.security ?? 'none',
  178. sni: c.sni,
  179. fingerprint: c.fingerprint,
  180. path: c.path,
  181. host: c.host,
  182. serviceName: c.serviceName,
  183. publicKey: c.publicKey,
  184. shortId: c.shortId,
  185. });
  186. let settings: Record<string, unknown>;
  187. switch (c.protocol) {
  188. case 'vless': {
  189. const s: Record<string, unknown> = {
  190. address: c.address,
  191. port: c.port,
  192. id: c.id ?? '',
  193. encryption: c.encryption || 'none',
  194. level: 8,
  195. };
  196. if (c.flow) s.flow = c.flow;
  197. settings = s;
  198. break;
  199. }
  200. case 'vmess':
  201. settings = {
  202. address: c.address,
  203. port: c.port,
  204. id: c.id ?? '',
  205. security: c.vmessSecurity || 'auto',
  206. level: 8,
  207. };
  208. break;
  209. case 'trojan':
  210. settings = {
  211. servers: [{ address: c.address, port: c.port, password: c.password ?? '', level: 8 }],
  212. };
  213. break;
  214. case 'ss':
  215. settings = {
  216. servers: [
  217. {
  218. address: c.address,
  219. port: c.port,
  220. password: c.password ?? '',
  221. level: 8,
  222. method: c.method || '',
  223. },
  224. ],
  225. };
  226. break;
  227. }
  228. return {
  229. protocol: c.protocol === 'ss' ? 'shadowsocks' : c.protocol,
  230. tag: 'proxy',
  231. streamSettings,
  232. settings,
  233. };
  234. }
  235. // Mirrors the one-document-per-client model only; the panel also emits
  236. // balancer documents (sub_balancers) that are intentionally out of scope here.
  237. function jsonConfig(c: SubClient): Record<string, unknown> {
  238. return {
  239. remarks: c.remark,
  240. ...subJsonSkeleton(),
  241. outbounds: [proxyOutbound(c), ...skeletonOutbounds()],
  242. };
  243. }
  244. export function buildJsonSubscription(clients: SubClient[]): string {
  245. if (clients.length === 0) return '';
  246. const configs = clients.map(jsonConfig);
  247. // 3x-ui returns a single object for one client, an array for several.
  248. return JSON.stringify(configs.length === 1 ? configs[0] : configs, null, 2);
  249. }