enhance.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import { emit, listen, Event } from "@tauri-apps/api/event";
  2. import { appWindow } from "@tauri-apps/api/window";
  3. import ignoreCase from "@/utils/ignore-case";
  4. export const HANDLE_FIELDS = [
  5. "port",
  6. "socks-port",
  7. "redir-port",
  8. "tproxy-port",
  9. "mixed-port",
  10. "allow-lan",
  11. "mode",
  12. "log-level",
  13. "ipv6",
  14. "secret",
  15. "external-controller",
  16. ];
  17. export const DEFAULT_FIELDS = [
  18. "rules",
  19. "proxies",
  20. "proxy-groups",
  21. "proxy-providers",
  22. "rule-providers",
  23. ] as const;
  24. export const USE_FLAG_FIELDS = [
  25. "tun",
  26. "dns",
  27. "ebpf",
  28. "hosts",
  29. "script",
  30. "profile",
  31. "payload",
  32. "auto-redir",
  33. "experimental",
  34. "interface-name",
  35. "routing-mark",
  36. "tproxy-port",
  37. "iptables",
  38. "external-ui",
  39. "bind-address",
  40. "authentication",
  41. "sniffer", // meta
  42. "geodata-mode", // meta
  43. "tcp-concurrent", // meta
  44. ] as const;
  45. /**
  46. * process the merge mode
  47. */
  48. function toMerge(merge: CmdType.ProfileMerge, data: CmdType.ProfileData) {
  49. if (!merge) return { data, use: [] };
  50. const {
  51. use,
  52. "prepend-rules": preRules,
  53. "append-rules": postRules,
  54. "prepend-proxies": preProxies,
  55. "append-proxies": postProxies,
  56. "prepend-proxy-groups": preProxyGroups,
  57. "append-proxy-groups": postProxyGroups,
  58. ...mergeConfig
  59. } = merge;
  60. [...DEFAULT_FIELDS, ...USE_FLAG_FIELDS].forEach((key) => {
  61. // the value should not be null
  62. if (mergeConfig[key] != null) {
  63. data[key] = mergeConfig[key];
  64. }
  65. });
  66. // init
  67. if (!data.rules) data.rules = [];
  68. if (!data.proxies) data.proxies = [];
  69. if (!data["proxy-groups"]) data["proxy-groups"] = [];
  70. // rules
  71. if (Array.isArray(preRules)) {
  72. data.rules.unshift(...preRules);
  73. }
  74. if (Array.isArray(postRules)) {
  75. data.rules.push(...postRules);
  76. }
  77. // proxies
  78. if (Array.isArray(preProxies)) {
  79. data.proxies.unshift(...preProxies);
  80. }
  81. if (Array.isArray(postProxies)) {
  82. data.proxies.push(...postProxies);
  83. }
  84. // proxy-groups
  85. if (Array.isArray(preProxyGroups)) {
  86. data["proxy-groups"].unshift(...preProxyGroups);
  87. }
  88. if (Array.isArray(postProxyGroups)) {
  89. data["proxy-groups"].push(...postProxyGroups);
  90. }
  91. return { data, use: Array.isArray(use) ? use : [] };
  92. }
  93. /**
  94. * process the script mode
  95. */
  96. function toScript(
  97. script: string,
  98. data: CmdType.ProfileData
  99. ): Promise<CmdType.ProfileData> {
  100. if (!script) {
  101. throw new Error("miss the main function");
  102. }
  103. const paramsName = `__verge${Math.floor(Math.random() * 1000)}`;
  104. const code = `'use strict';${script};return main(${paramsName});`;
  105. const func = new Function(paramsName, code);
  106. return func(data);
  107. }
  108. export type EStatus = { status: "ok" | "error"; message?: string };
  109. export type EListener = (status: EStatus) => void;
  110. export type EUnlistener = () => void;
  111. /**
  112. * The service helps to
  113. * implement enhanced profiles
  114. */
  115. class Enhance {
  116. private isSetup = false;
  117. private listenMap: Map<string, EListener>;
  118. private resultMap: Map<string, EStatus>;
  119. // record current config fields
  120. private fieldsState = {
  121. config: [] as string[],
  122. use: [] as string[],
  123. };
  124. constructor() {
  125. this.listenMap = new Map();
  126. this.resultMap = new Map();
  127. }
  128. // setup some listener
  129. // for the enhanced running status
  130. listen(uid: string, cb: EListener): EUnlistener {
  131. this.listenMap.set(uid, cb);
  132. return () => this.listenMap.delete(uid);
  133. }
  134. // get the running status
  135. status(uid: string): EStatus | undefined {
  136. return this.resultMap.get(uid);
  137. }
  138. // get the running field state
  139. getFieldsState() {
  140. return this.fieldsState;
  141. }
  142. async enhanceHandler(event: Event<unknown>) {
  143. const payload = event.payload as CmdType.EnhancedPayload;
  144. const result = await this.runner(payload).catch((err: any) => ({
  145. data: null,
  146. status: "error",
  147. error: err.message,
  148. }));
  149. emit(payload.callback, JSON.stringify(result)).catch(console.error);
  150. }
  151. // setup the handler
  152. setup() {
  153. if (this.isSetup) return;
  154. this.isSetup = true;
  155. listen("script-handler", async (event) => {
  156. await this.enhanceHandler(event);
  157. });
  158. listen("script-handler-close", async (event) => {
  159. await this.enhanceHandler(event);
  160. appWindow.close();
  161. });
  162. }
  163. // enhanced mode runner
  164. private async runner(payload: CmdType.EnhancedPayload) {
  165. const chain = payload.chain || [];
  166. const valid = payload.valid || [];
  167. if (!Array.isArray(chain)) throw new Error("unhandle error");
  168. let pdata = payload.current || {};
  169. let useList = valid;
  170. for (const each of chain) {
  171. const { uid, type = "" } = each.item;
  172. try {
  173. // process script
  174. if (type === "script") {
  175. // support async main function
  176. pdata = await toScript(each.script!, ignoreCase(pdata));
  177. }
  178. // process merge
  179. else if (type === "merge") {
  180. const temp = toMerge(each.merge!, ignoreCase(pdata));
  181. pdata = temp.data;
  182. useList = useList.concat(temp.use || []);
  183. }
  184. // invalid type
  185. else {
  186. throw new Error(`invalid enhanced profile type "${type}"`);
  187. }
  188. this.exec(uid, { status: "ok" });
  189. } catch (err: any) {
  190. console.error(err);
  191. this.exec(uid, {
  192. status: "error",
  193. message: err.message || err.toString(),
  194. });
  195. }
  196. }
  197. pdata = ignoreCase(pdata);
  198. // save the fields state
  199. this.fieldsState.config = Object.keys(pdata);
  200. this.fieldsState.use = [...useList];
  201. // filter the data
  202. const filterData: typeof pdata = {};
  203. Object.keys(pdata).forEach((key: any) => {
  204. if (
  205. DEFAULT_FIELDS.includes(key) ||
  206. (USE_FLAG_FIELDS.includes(key) && useList.includes(key))
  207. ) {
  208. filterData[key] = pdata[key];
  209. }
  210. });
  211. return { data: filterData, status: "ok" };
  212. }
  213. // exec the listener
  214. private exec(uid: string, status: EStatus) {
  215. this.resultMap.set(uid, status);
  216. this.listenMap.get(uid)?.(status);
  217. }
  218. }
  219. export default new Enhance();