useXraySetting.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
  2. import { HttpUtil, PromiseUtil } from '@/utils';
  3. const DIRTY_POLL_MS = 1000;
  4. export interface OutboundTrafficRow {
  5. tag: string;
  6. up: number;
  7. down: number;
  8. }
  9. export interface OutboundTestResult {
  10. success: boolean;
  11. delay?: number;
  12. error?: string;
  13. mode?: string;
  14. ttfbMs?: number;
  15. tlsMs?: number;
  16. connectMs?: number;
  17. dnsMs?: number;
  18. statusCode?: number;
  19. endpoints?: { address: string; delay?: number; success: boolean; error?: string }[];
  20. }
  21. export interface OutboundTestState {
  22. testing?: boolean;
  23. result?: OutboundTestResult | null;
  24. mode?: string;
  25. }
  26. export interface XraySettingsValue {
  27. inbounds?: unknown[];
  28. outbounds?: { tag?: string; protocol?: string; settings?: unknown; streamSettings?: unknown }[];
  29. routing?: {
  30. rules?: { type?: string; outboundTag?: string; balancerTag?: string; [key: string]: unknown }[];
  31. balancers?: unknown[];
  32. domainStrategy?: string;
  33. };
  34. dns?: { tag?: string; servers?: unknown[] };
  35. log?: Record<string, unknown>;
  36. policy?: { system?: Record<string, boolean> };
  37. observatory?: unknown;
  38. burstObservatory?: unknown;
  39. fakedns?: unknown;
  40. [key: string]: unknown;
  41. }
  42. export type SetTemplate = (
  43. next: XraySettingsValue | null | ((prev: XraySettingsValue | null) => XraySettingsValue | null),
  44. ) => void;
  45. export interface UseXraySettingResult {
  46. fetched: boolean;
  47. spinning: boolean;
  48. saveDisabled: boolean;
  49. fetchError: string;
  50. xraySetting: string;
  51. setXraySetting: (next: string) => void;
  52. templateSettings: XraySettingsValue | null;
  53. setTemplateSettings: SetTemplate;
  54. outboundTestUrl: string;
  55. setOutboundTestUrl: (v: string) => void;
  56. inboundTags: string[];
  57. clientReverseTags: string[];
  58. restartResult: string;
  59. outboundsTraffic: OutboundTrafficRow[];
  60. outboundTestStates: Record<number, OutboundTestState>;
  61. testingAll: boolean;
  62. fetchAll: () => Promise<void>;
  63. fetchOutboundsTraffic: () => Promise<void>;
  64. resetOutboundsTraffic: (tag: string) => Promise<void>;
  65. applyOutboundsEvent: (payload: unknown) => void;
  66. testOutbound: (
  67. index: number,
  68. outbound: unknown,
  69. mode?: string,
  70. ) => Promise<OutboundTestResult | null>;
  71. testAllOutbounds: (mode?: string) => Promise<void>;
  72. saveAll: () => Promise<void>;
  73. resetToDefault: () => Promise<void>;
  74. restartXray: () => Promise<void>;
  75. }
  76. export function useXraySetting(): UseXraySettingResult {
  77. const [fetched, setFetched] = useState(false);
  78. const [spinning, setSpinning] = useState(false);
  79. const [saveDisabled, setSaveDisabled] = useState(true);
  80. const [fetchError, setFetchError] = useState('');
  81. const [xraySetting, setXraySettingState] = useState('');
  82. const [templateSettings, setTemplateSettingsState] = useState<XraySettingsValue | null>(null);
  83. const [outboundTestUrl, setOutboundTestUrlState] = useState('https://www.google.com/generate_204');
  84. const [inboundTags, setInboundTags] = useState<string[]>([]);
  85. const [clientReverseTags, setClientReverseTags] = useState<string[]>([]);
  86. const [restartResult, setRestartResult] = useState('');
  87. const [outboundsTraffic, setOutboundsTraffic] = useState<OutboundTrafficRow[]>([]);
  88. const [outboundTestStates, setOutboundTestStates] = useState<Record<number, OutboundTestState>>({});
  89. const [testingAll, setTestingAll] = useState(false);
  90. const oldXraySettingRef = useRef('');
  91. const oldOutboundTestUrlRef = useRef('');
  92. const syncingRef = useRef(false);
  93. const xraySettingRef = useRef('');
  94. const outboundTestUrlRef = useRef(outboundTestUrl);
  95. const templateSettingsRef = useRef<XraySettingsValue | null>(null);
  96. xraySettingRef.current = xraySetting;
  97. outboundTestUrlRef.current = outboundTestUrl;
  98. templateSettingsRef.current = templateSettings;
  99. const setXraySetting = useCallback((next: string) => {
  100. setXraySettingState(next);
  101. if (syncingRef.current) return;
  102. try {
  103. const parsed = JSON.parse(next);
  104. syncingRef.current = true;
  105. setTemplateSettingsState(parsed);
  106. syncingRef.current = false;
  107. } catch {
  108. /* ignore — wait for user to finish */
  109. }
  110. }, []);
  111. const setTemplateSettings: SetTemplate = useCallback((nextOrFn) => {
  112. setTemplateSettingsState((prev) => {
  113. const next = typeof nextOrFn === 'function' ? nextOrFn(prev) : nextOrFn;
  114. if (next == null) return next;
  115. if (!syncingRef.current) {
  116. try {
  117. syncingRef.current = true;
  118. setXraySettingState(JSON.stringify(next, null, 2));
  119. } finally {
  120. syncingRef.current = false;
  121. }
  122. }
  123. return next;
  124. });
  125. }, []);
  126. const setOutboundTestUrl = useCallback((v: string) => {
  127. setOutboundTestUrlState(v);
  128. }, []);
  129. const fetchAll = useCallback(async () => {
  130. setFetchError('');
  131. const msg = await HttpUtil.post('/panel/xray/');
  132. if (!msg?.success) {
  133. setFetchError(msg?.msg || 'Failed to load xray config');
  134. setFetched(true);
  135. return;
  136. }
  137. let obj;
  138. try {
  139. obj = JSON.parse(msg.obj);
  140. } catch (e) {
  141. const err = e as Error;
  142. setFetchError(`Malformed xray config response: ${err?.message || String(err)}`);
  143. setFetched(true);
  144. return;
  145. }
  146. const pretty = JSON.stringify(obj.xraySetting, null, 2);
  147. syncingRef.current = true;
  148. setXraySettingState(pretty);
  149. setTemplateSettingsState(obj.xraySetting);
  150. oldXraySettingRef.current = pretty;
  151. syncingRef.current = false;
  152. setInboundTags(obj.inboundTags || []);
  153. setClientReverseTags(obj.clientReverseTags || []);
  154. const nextUrl = obj.outboundTestUrl || 'https://www.google.com/generate_204';
  155. setOutboundTestUrlState(nextUrl);
  156. oldOutboundTestUrlRef.current = nextUrl;
  157. setFetched(true);
  158. setSaveDisabled(true);
  159. }, []);
  160. const saveAll = useCallback(async () => {
  161. setSpinning(true);
  162. try {
  163. const msg = await HttpUtil.post('/panel/xray/update', {
  164. xraySetting: xraySettingRef.current,
  165. outboundTestUrl: outboundTestUrlRef.current || 'https://www.google.com/generate_204',
  166. });
  167. if (msg?.success) await fetchAll();
  168. } finally {
  169. setSpinning(false);
  170. }
  171. }, [fetchAll]);
  172. const fetchOutboundsTraffic = useCallback(async () => {
  173. const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic');
  174. if (msg?.success) setOutboundsTraffic(msg.obj || []);
  175. }, []);
  176. const resetOutboundsTraffic = useCallback(async (tag: string) => {
  177. const msg = await HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag });
  178. if (msg?.success) await fetchOutboundsTraffic();
  179. }, [fetchOutboundsTraffic]);
  180. const applyOutboundsEvent = useCallback((payload: unknown) => {
  181. if (Array.isArray(payload)) setOutboundsTraffic(payload as OutboundTrafficRow[]);
  182. }, []);
  183. const testOutbound = useCallback(
  184. async (index: number, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
  185. if (!outbound) return null;
  186. setOutboundTestStates((prev) => ({
  187. ...prev,
  188. [index]: { testing: true, result: null, mode },
  189. }));
  190. try {
  191. const msg = await HttpUtil.post('/panel/xray/testOutbound', {
  192. outbound: JSON.stringify(outbound),
  193. allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
  194. mode,
  195. });
  196. if (msg?.success) {
  197. setOutboundTestStates((prev) => ({
  198. ...prev,
  199. [index]: { testing: false, result: msg.obj },
  200. }));
  201. return msg.obj;
  202. }
  203. setOutboundTestStates((prev) => ({
  204. ...prev,
  205. [index]: {
  206. testing: false,
  207. result: { success: false, error: msg?.msg || 'Unknown error', mode },
  208. },
  209. }));
  210. } catch (e) {
  211. setOutboundTestStates((prev) => ({
  212. ...prev,
  213. [index]: {
  214. testing: false,
  215. result: { success: false, error: String(e), mode },
  216. },
  217. }));
  218. }
  219. return null;
  220. },
  221. [],
  222. );
  223. const testAllOutbounds = useCallback(async (mode = 'tcp') => {
  224. const list = templateSettingsRef.current?.outbounds || [];
  225. if (list.length === 0 || testingAll) return;
  226. setTestingAll(true);
  227. try {
  228. const concurrency = mode === 'tcp' ? 8 : 1;
  229. const queue = list
  230. .map((ob, i) => ({ index: i, outbound: ob }))
  231. .filter(({ outbound }) => {
  232. const tag = outbound?.tag;
  233. const proto = outbound?.protocol;
  234. if (proto === 'blackhole' || proto === 'loopback' || tag === 'blocked') return false;
  235. if (mode === 'tcp' && (proto === 'freedom' || proto === 'dns')) return false;
  236. return true;
  237. });
  238. async function worker() {
  239. while (queue.length > 0) {
  240. const item = queue.shift();
  241. if (!item) break;
  242. await testOutbound(item.index, item.outbound, mode);
  243. }
  244. }
  245. const workers = Array.from(
  246. { length: Math.min(concurrency, queue.length) },
  247. () => worker(),
  248. );
  249. await Promise.all(workers);
  250. } finally {
  251. setTestingAll(false);
  252. }
  253. }, [testingAll, testOutbound]);
  254. const resetToDefault = useCallback(async () => {
  255. setSpinning(true);
  256. try {
  257. const msg = await HttpUtil.get('/panel/setting/getDefaultJsonConfig');
  258. if (msg?.success) {
  259. const cloned = JSON.parse(JSON.stringify(msg.obj));
  260. setTemplateSettings(cloned);
  261. }
  262. } finally {
  263. setSpinning(false);
  264. }
  265. }, [setTemplateSettings]);
  266. const restartXray = useCallback(async () => {
  267. setSpinning(true);
  268. try {
  269. const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
  270. if (msg?.success) {
  271. await PromiseUtil.sleep(500);
  272. const r = await HttpUtil.get('/panel/xray/getXrayResult');
  273. if (r?.success) setRestartResult(r.obj || '');
  274. }
  275. } finally {
  276. setSpinning(false);
  277. }
  278. }, []);
  279. useEffect(() => {
  280. fetchAll();
  281. fetchOutboundsTraffic();
  282. const timer = window.setInterval(() => {
  283. const dirtyXray = oldXraySettingRef.current !== xraySettingRef.current;
  284. const dirtyUrl = oldOutboundTestUrlRef.current !== outboundTestUrlRef.current;
  285. setSaveDisabled(!(dirtyXray || dirtyUrl));
  286. }, DIRTY_POLL_MS);
  287. return () => window.clearInterval(timer);
  288. }, [fetchAll, fetchOutboundsTraffic]);
  289. return useMemo(
  290. () => ({
  291. fetched,
  292. spinning,
  293. saveDisabled,
  294. fetchError,
  295. xraySetting,
  296. setXraySetting,
  297. templateSettings,
  298. setTemplateSettings,
  299. outboundTestUrl,
  300. setOutboundTestUrl,
  301. inboundTags,
  302. clientReverseTags,
  303. restartResult,
  304. outboundsTraffic,
  305. outboundTestStates,
  306. testingAll,
  307. fetchAll,
  308. fetchOutboundsTraffic,
  309. resetOutboundsTraffic,
  310. applyOutboundsEvent,
  311. testOutbound,
  312. testAllOutbounds,
  313. saveAll,
  314. resetToDefault,
  315. restartXray,
  316. }),
  317. [
  318. fetched,
  319. spinning,
  320. saveDisabled,
  321. fetchError,
  322. xraySetting,
  323. setXraySetting,
  324. templateSettings,
  325. setTemplateSettings,
  326. outboundTestUrl,
  327. setOutboundTestUrl,
  328. inboundTags,
  329. clientReverseTags,
  330. restartResult,
  331. outboundsTraffic,
  332. outboundTestStates,
  333. testingAll,
  334. fetchAll,
  335. fetchOutboundsTraffic,
  336. resetOutboundsTraffic,
  337. applyOutboundsEvent,
  338. testOutbound,
  339. testAllOutbounds,
  340. saveAll,
  341. resetToDefault,
  342. restartXray,
  343. ],
  344. );
  345. }