125 | ): ObjectExpression {
126 | return {
127 | type: "ObjectExpression",
128 | properties,
129 | start: 0, // dummy
130 | end: 0, // dummy
131 | };
132 | }
133 |
134 | static newIdentifier(name: string): Identifier {
135 | return {
136 | type: "Identifier",
137 | name: name,
138 | start: 0, // dummy
139 | end: 0, // dummy
140 | };
141 | }
142 |
143 | static newSimpleLiteral(value: string | boolean | number | null): Literal {
144 | return {
145 | type: "Literal",
146 | value: value,
147 | start: 0, // dummy
148 | end: 0, // dummy
149 | };
150 | }
151 |
152 | static newFunctionDeclaration(
153 | name: string,
154 | params: string[],
155 | body: Statement[]
156 | ): FunctionDeclaration {
157 | return {
158 | type: "FunctionDeclaration",
159 | id: this.newIdentifier(name),
160 | params: params.map((v) => this.newIdentifier(v)),
161 | body: {
162 | type: "BlockStatement",
163 | body: body,
164 | start: 0, // dummy
165 | end: 0, // dummy
166 | },
167 | generator: false,
168 | expression: false,
169 | async: false,
170 | start: 0, // dummy
171 | end: 0, // dummy
172 | };
173 | }
174 |
175 | static newFunctionExpression(
176 | params: string[],
177 | body: Statement[]
178 | ): FunctionExpression {
179 | return {
180 | type: "FunctionExpression",
181 | params: params.map((v) => this.newIdentifier(v)),
182 | body: {
183 | type: "BlockStatement",
184 | body: body,
185 | start: 0, // dummy
186 | end: 0, // dummy
187 | },
188 | generator: false,
189 | expression: false,
190 | async: false,
191 | start: 0, // dummy
192 | end: 0, // dummy
193 | };
194 | }
195 |
196 | static newReturnStatement(argument?: Expression): ReturnStatement {
197 | return {
198 | type: "ReturnStatement",
199 | argument,
200 | start: 0, // dummy
201 | end: 0, // dummy
202 | };
203 | }
204 |
205 | static newMemberExpression(
206 | object: Expression,
207 | property: Expression,
208 | computed: boolean = false
209 | ): MemberExpression {
210 | return {
211 | type: "MemberExpression",
212 | object,
213 | property,
214 | computed,
215 | optional: false,
216 | start: 0, // dummy
217 | end: 0, // dummy
218 | };
219 | }
220 |
221 | static newCallExpression(
222 | callee: Expression,
223 | _arguments: Expression[]
224 | ): CallExpression {
225 | return {
226 | type: "CallExpression",
227 | optional: false,
228 | callee,
229 | arguments: _arguments,
230 | start: 0, // dummy
231 | end: 0, // dummy
232 | };
233 | }
234 |
235 | static newIfStatement(
236 | test: Expression,
237 | consequent: Statement[],
238 | alternate?: Statement | null
239 | ): IfStatement {
240 | return {
241 | type: "IfStatement",
242 | test: test,
243 | consequent: {
244 | type: "BlockStatement",
245 | body: consequent,
246 | start: 0, // dummy
247 | end: 0, // dummy
248 | },
249 | alternate,
250 | start: 0, // dummy
251 | end: 0, // dummy
252 | };
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/src/pages/PopupPage.vue:
--------------------------------------------------------------------------------
1 |
60 |
61 |
62 |
148 |
149 |
150 |
212 |
--------------------------------------------------------------------------------
/public/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "app_desc": {
3 | "message": "A tool to help you manage and switch your proxy profiles"
4 | },
5 |
6 | "nav_preference": {
7 | "message": "Preference"
8 | },
9 | "nav_config": {
10 | "message": "Config"
11 | },
12 | "nav_custom_profiles": {
13 | "message": "Custom Profiles"
14 | },
15 | "nav_system_profiles": {
16 | "message": "Presets"
17 | },
18 |
19 | "theme_light_mode": {
20 | "message": "Light mode"
21 | },
22 | "theme_dark_mode": {
23 | "message": "Dark mode"
24 | },
25 | "theme_auto_mode": {
26 | "message": "Follow system"
27 | },
28 |
29 | "mode_auto_switch": {
30 | "message": "Auto Switch"
31 | },
32 | "mode_auto_switch_abbr": {
33 | "message": "auto"
34 | },
35 |
36 | "mode_auto_switch_detection_info": {
37 | "message": "$1 is used for this tab"
38 | },
39 | "mode_auto_switch_detection_tips": {
40 | "message": "Due to the limitation of the browser, the detection is not always accurate."
41 | },
42 |
43 | "mode_direct": {
44 | "message": "Direct"
45 | },
46 | "mode_system": {
47 | "message": "Use System Proxy"
48 | },
49 | "mode_profile_create": {
50 | "message": "Create New Profile"
51 | },
52 |
53 | "config_proxy_type": {
54 | "message": "Proxy Type"
55 | },
56 | "config_proxy_type_proxy": {
57 | "message": "Proxy"
58 | },
59 | "config_proxy_type_pac": {
60 | "message": "PAC Script"
61 | },
62 | "config_proxy_type_auto": {
63 | "message": "Auto Switch"
64 | },
65 | "config_proxy_type_default": {
66 | "message": "Same as Default"
67 | },
68 | "config_section_proxy_server": {
69 | "message": "Proxy Server"
70 | },
71 | "config_section_proxy_server_default": {
72 | "message": "Default Server"
73 | },
74 | "config_section_proxy_server_http": {
75 | "message": "HTTP"
76 | },
77 | "config_section_proxy_server_https": {
78 | "message": "HTTPS"
79 | },
80 | "config_section_proxy_server_ftp": {
81 | "message": "FTP"
82 | },
83 | "config_section_proxy_auth_tips": {
84 | "message": "Set username and password if your proxy requires authentication"
85 | },
86 | "config_section_proxy_auth_unsupported": {
87 | "message": "The current proxy type does not support authentication"
88 | },
89 | "config_section_proxy_auth_title": {
90 | "message": "Proxy Authentication"
91 | },
92 | "config_section_proxy_auth_username": {
93 | "message": "Username"
94 | },
95 | "config_section_proxy_auth_password": {
96 | "message": "Password"
97 | },
98 |
99 | "config_section_bypass_list": {
100 | "message": "Bypass List"
101 | },
102 | "config_section_advance": {
103 | "message": "Advance Config"
104 | },
105 | "config_reference_bypass_list": {
106 | "message": "Learn more about bypass list"
107 | },
108 |
109 | "config_section_auto_switch_rules": {
110 | "message": "Auto Switch Rules"
111 | },
112 | "config_section_auto_switch_type": {
113 | "message": "Condition Type"
114 | },
115 | "config_section_auto_switch_type_domain": {
116 | "message": "Domain"
117 | },
118 | "config_section_auto_switch_type_url": {
119 | "message": "URL"
120 | },
121 | "config_section_auto_switch_type_url_malformed": {
122 | "message": "Invalid URL"
123 | },
124 | "config_section_auto_switch_type_url_malformed_chrome": {
125 | "message": "For security reasons, Chrome does not support path matching in HTTPS URLs. Learn more at https://issues.chromium.org/40083832"
126 | },
127 | "config_section_auto_switch_type_cidr": {
128 | "message": "CIDR"
129 | },
130 | "config_section_auto_switch_type_cidr_malformed": {
131 | "message": "Invalid CIDR. Please use the format like `192.168.1.1/24` or `2001:db8::/32`"
132 | },
133 | "config_section_auto_switch_type_disabled": {
134 | "message": "Temporarily Skip"
135 | },
136 | "config_section_auto_switch_condition": {
137 | "message": "Condition"
138 | },
139 | "config_section_auto_switch_profile": {
140 | "message": "Route to"
141 | },
142 | "config_section_auto_switch_actions": {
143 | "message": "Actions"
144 | },
145 | "config_section_auto_switch_add_rule": {
146 | "message": "Add Rule"
147 | },
148 | "config_section_auto_switch_delete_rule": {
149 | "message": "Delete Current Rule"
150 | },
151 | "config_section_auto_switch_duplicate_rule": {
152 | "message": "Duplicate Current Rule"
153 | },
154 | "config_section_auto_switch_default_profile": {
155 | "message": "Default Profile"
156 | },
157 | "config_section_auto_switch_pac_preview": {
158 | "message": "Preview PAC Script"
159 | },
160 |
161 | "config_action_edit": {
162 | "message": "Edit"
163 | },
164 | "config_action_delete": {
165 | "message": "Delete Profile"
166 | },
167 | "config_action_delete_double_confirm": {
168 | "message": "Are you sure to delete the current profile"
169 | },
170 | "config_action_save": {
171 | "message": "Save"
172 | },
173 | "config_action_cancel": {
174 | "message": "Discard Change"
175 | },
176 | "config_action_clear": {
177 | "message": "Clear"
178 | },
179 |
180 | "config_feedback_saved": {
181 | "message": "The profile had been saved"
182 | },
183 | "config_feedback_copied": {
184 | "message": "Copied"
185 | },
186 | "config_feedback_deleted": {
187 | "message": "The profile had been deleted"
188 | },
189 | "config_feedback_unknown_profile": {
190 | "message": "Unknown profile"
191 | },
192 | "config_feedback_error_occurred": {
193 | "message": "Error occurred: $1"
194 | },
195 |
196 | "preferences_section_import_export": {
197 | "message": "Import & Export"
198 | },
199 |
200 | "preferences_section_import_export_desc": {
201 | "message": "Export your proxy profiles to a file for backup or sharing, or import them to restore profiles when needed."
202 | },
203 |
204 | "preferences_action_profile_export": {
205 | "message": "Export Profiles"
206 | },
207 |
208 | "preferences_action_profile_import": {
209 | "message": "Import Profiles"
210 | },
211 |
212 | "preferences_feedback_no_profile_to_be_exported": {
213 | "message": "No valid profile to be exported"
214 | },
215 |
216 | "preferences_tips_profile_overwrite": {
217 | "message": "Some profile(s) might be overwritten after the import. Are you sure you want to continue?"
218 | },
219 |
220 | "preferences_feedback_n_profiles_being_imported": {
221 | "message": "$1 profiles have been imported"
222 | },
223 |
224 | "form_is_required": {
225 | "message": "$1 is required"
226 | },
227 |
228 | "feedback_error": {
229 | "message": "Something wrong"
230 | },
231 |
232 | "firefox_incognito_access_error_html": {
233 | "message": " Due to Firefox's restrictions, Proxyverse does not work properly as it's not enabled in private browsing windows.
\n Please enable the extension in private windows.
\n"
234 | },
235 |
236 | "_": {
237 | "message": ""
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/public/_locales/pt_BR/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "app_desc": {
3 | "message": "Uma ferramenta para ajudá-lo a gerenciar e alternar seus perfis de proxy"
4 | },
5 |
6 | "nav_preference": {
7 | "message": "Preferência"
8 | },
9 | "nav_config": {
10 | "message": "Configuração"
11 | },
12 | "nav_custom_profiles": {
13 | "message": "Perfis personalizados"
14 | },
15 | "nav_system_profiles": {
16 | "message": "Predefinições"
17 | },
18 |
19 | "theme_light_mode": {
20 | "message": "Modo de luz"
21 | },
22 | "theme_dark_mode": {
23 | "message": "Modo escuro"
24 | },
25 | "theme_auto_mode": {
26 | "message": "Seguir o sistema"
27 | },
28 |
29 | "mode_auto_switch": {
30 | "message": "Comutação automática"
31 | },
32 | "mode_auto_switch_abbr": {
33 | "message": "automático"
34 | },
35 |
36 | "mode_auto_switch_detection_info": {
37 | "message": "$1 é usado para essa guia"
38 | },
39 | "mode_auto_switch_detection_tips": {
40 | "message": "Devido à limitação do navegador, a detecção nem sempre é precisa."
41 | },
42 |
43 | "mode_direct": {
44 | "message": "Direto"
45 | },
46 | "mode_system": {
47 | "message": "Usar proxy do sistema"
48 | },
49 | "mode_profile_create": {
50 | "message": "Criar novo perfil"
51 | },
52 |
53 | "config_proxy_type": {
54 | "message": "Tipo de proxy"
55 | },
56 | "config_proxy_type_proxy": {
57 | "message": "Proxy"
58 | },
59 | "config_proxy_type_pac": {
60 | "message": "Script PAC"
61 | },
62 | "config_proxy_type_auto": {
63 | "message": "Comutação automática"
64 | },
65 | "config_proxy_type_default": {
66 | "message": "Igual ao padrão"
67 | },
68 | "config_section_proxy_server": {
69 | "message": "Servidor proxy"
70 | },
71 | "config_section_proxy_server_default": {
72 | "message": "Servidor padrão"
73 | },
74 | "config_section_proxy_server_http": {
75 | "message": "HTTP"
76 | },
77 | "config_section_proxy_server_https": {
78 | "message": "HTTPS"
79 | },
80 | "config_section_proxy_server_ftp": {
81 | "message": "FTP"
82 | },
83 | "config_section_proxy_auth_tips": {
84 | "message": "Defina o nome de usuário e a senha se o proxy exigir autenticação"
85 | },
86 | "config_section_proxy_auth_unsupported": {
87 | "message": "O tipo de proxy atual não é compatível com a autenticação"
88 | },
89 | "config_section_proxy_auth_title": {
90 | "message": "Autenticação de proxy"
91 | },
92 | "config_section_proxy_auth_username": {
93 | "message": "Nome de usuário"
94 | },
95 | "config_section_proxy_auth_password": {
96 | "message": "Senha"
97 | },
98 |
99 | "config_section_bypass_list": {
100 | "message": "Lista de desvios"
101 | },
102 | "config_section_advance": {
103 | "message": "Configuração avançada"
104 | },
105 | "config_reference_bypass_list": {
106 | "message": "Saiba mais sobre a lista de bypass"
107 | },
108 |
109 | "config_section_auto_switch_rules": {
110 | "message": "Regras de comutação automática"
111 | },
112 | "config_section_auto_switch_type": {
113 | "message": "Tipo de condição"
114 | },
115 | "config_section_auto_switch_type_domain": {
116 | "message": "Domínio"
117 | },
118 | "config_section_auto_switch_type_url": {
119 | "message": "URL"
120 | },
121 | "config_section_auto_switch_type_url_malformed": {
122 | "message": "URL inválido"
123 | },
124 | "config_section_auto_switch_type_url_malformed_chrome": {
125 | "message": "Por motivos de segurança, o Chrome não suporta a correspondência de caminhos em URLs HTTPS. Saiba mais em https://issues.chromium.org/40083832"
126 | },
127 | "config_section_auto_switch_type_cidr": {
128 | "message": "CIDR"
129 | },
130 | "config_section_auto_switch_type_cidr_malformed": {
131 | "message": "CIDR inválido. Use o formato `192.168.1.1/24` ou `2001:db8::/32`"
132 | },
133 | "config_section_auto_switch_type_disabled": {
134 | "message": "Ignorar temporariamente"
135 | },
136 | "config_section_auto_switch_condition": {
137 | "message": "Condição"
138 | },
139 | "config_section_auto_switch_profile": {
140 | "message": "Rota para"
141 | },
142 | "config_section_auto_switch_actions": {
143 | "message": "Ações"
144 | },
145 | "config_section_auto_switch_add_rule": {
146 | "message": "Adicionar regra"
147 | },
148 | "config_section_auto_switch_delete_rule": {
149 | "message": "Excluir regra atual"
150 | },
151 | "config_section_auto_switch_duplicate_rule": {
152 | "message": "Duplicar regra atual"
153 | },
154 | "config_section_auto_switch_default_profile": {
155 | "message": "Perfil padrão"
156 | },
157 | "config_section_auto_switch_pac_preview": {
158 | "message": "Visualizar o PAC Script"
159 | },
160 |
161 | "config_action_edit": {
162 | "message": "Editar"
163 | },
164 | "config_action_delete": {
165 | "message": "Excluir perfil"
166 | },
167 | "config_action_delete_double_confirm": {
168 | "message": "Tem certeza de que deseja excluir o perfil atual?"
169 | },
170 | "config_action_save": {
171 | "message": "Salvar"
172 | },
173 | "config_action_cancel": {
174 | "message": "Mudança de descarte"
175 | },
176 | "config_action_clear": {
177 | "message": "Limpo"
178 | },
179 |
180 | "config_feedback_saved": {
181 | "message": "O perfil foi salvo"
182 | },
183 | "config_feedback_copied": {
184 | "message": "Copiado"
185 | },
186 | "config_feedback_deleted": {
187 | "message": "O perfil foi excluído"
188 | },
189 | "config_feedback_unknown_profile": {
190 | "message": "Perfil desconhecido"
191 | },
192 | "config_feedback_error_occurred": {
193 | "message": "Ocorreu um erro: $1"
194 | },
195 |
196 | "preferences_section_import_export": {
197 | "message": "Importação e exportação"
198 | },
199 |
200 | "preferences_section_import_export_desc": {
201 | "message": "Exporte seus perfis de proxy para um arquivo para backup ou compartilhamento, ou importe-os para restaurar perfis quando necessário."
202 | },
203 |
204 | "preferences_action_profile_export": {
205 | "message": "Perfis de exportação"
206 | },
207 |
208 | "preferences_action_profile_import": {
209 | "message": "Perfis de importação"
210 | },
211 |
212 | "preferences_feedback_no_profile_to_be_exported": {
213 | "message": "Nenhum perfil válido a ser exportado"
214 | },
215 |
216 | "preferences_tips_profile_overwrite": {
217 | "message": "Alguns perfis podem ser sobrescritos após a importação. Tem certeza de que deseja continuar?"
218 | },
219 |
220 | "preferences_feedback_n_profiles_being_imported": {
221 | "message": "$1 perfis foram importados"
222 | },
223 |
224 | "form_is_required": {
225 | "message": "$1 é necessário"
226 | },
227 |
228 | "feedback_error": {
229 | "message": "Algo errado"
230 | },
231 |
232 | "firefox_incognito_access_error_html": {
233 | "message": " Devido às limitações do Firefox, o Proxyverse não funciona corretamente, pois não está ativado nas janelas de navegação privada.
\n Ative a extensão em janelas privadas.
\n"
234 | },
235 |
236 | "_": {
237 | "message": ""
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/src/components/configs/AutoSwitchInput.vue:
--------------------------------------------------------------------------------
1 |
156 |
157 |
158 |
280 |
281 |
282 |
294 |
--------------------------------------------------------------------------------
/tests/services/config/local-config.test.ts:
--------------------------------------------------------------------------------
1 | import { config2json, json2config } from "@/services/config/schema";
2 | import { ProxyProfile } from "@/services/profile";
3 | import { expect, test, describe } from "vitest";
4 |
5 | const mockInnerConfig: Record<
6 | string,
7 | ProxyProfile & {
8 | [key: string]: any; // Index signature to allow additional properties
9 | }
10 | > = {
11 | "90750184-fb8f-4130-ad83-c997a7207300": {
12 | color: "#DCF190",
13 | defaultProfileID: "direct",
14 | pacScript: {
15 | data: "function FindProxyForURL(url, host) {\n // …\n return 'DIRECT';\n}",
16 | },
17 | profileID: "90750184-fb8f-4130-ad83-c997a7207300",
18 | profileName: "PAC Script",
19 | proxyRules: {
20 | bypassList: ["", "127.0.0.1", "[::1]"],
21 | default: {
22 | host: "127.0.0.1",
23 | port: 8080,
24 | scheme: "http",
25 | },
26 | },
27 | proxyType: "pac",
28 | rules: [
29 | {
30 | condition: "example.com",
31 | profileID: "direct",
32 | type: "domain",
33 | },
34 | {
35 | condition: "http://example.com/api/*",
36 | profileID: "direct",
37 | type: "url",
38 | },
39 | ],
40 | },
41 | "ac8eeebe-3a13-4969-881c-3c7419e91f95": {
42 | color: "#93BEFF",
43 | defaultProfileID: "direct",
44 | pacScript: {
45 | data: "function FindProxyForURL(url, host) {\n // …\n return 'DIRECT';\n}",
46 | },
47 | profileID: "ac8eeebe-3a13-4969-881c-3c7419e91f95",
48 | profileName: "HTTP",
49 | proxyRules: {
50 | bypassList: ["", "127.0.0.1", "[::1]"],
51 | default: {
52 | host: "localhost",
53 | port: 8080,
54 | scheme: "http",
55 | },
56 | },
57 | proxyType: "proxy",
58 | rules: [
59 | {
60 | condition: "example.com",
61 | profileID: "direct",
62 | type: "domain",
63 | },
64 | {
65 | condition: "http://example.com/api/*",
66 | profileID: "direct",
67 | type: "url",
68 | },
69 | ],
70 | },
71 | "bd4a7580-2c03-4465-acae-bde628ffbb16": {
72 | color: "#FBB0A7",
73 | defaultProfileID: "direct",
74 | pacScript: {
75 | data: "function FindProxyForURL(url, host) {\n // …\n return 'DIRECT';\n}",
76 | },
77 | profileID: "bd4a7580-2c03-4465-acae-bde628ffbb16",
78 | profileName: "HTTP (with Auth)",
79 | proxyRules: {
80 | bypassList: ["", "127.0.0.1", "[::1]"],
81 | default: {
82 | auth: {
83 | password: "securepassword",
84 | username: "admin",
85 | },
86 | host: "127.0.0.1",
87 | port: 8080,
88 | scheme: "http",
89 | },
90 | },
91 | proxyType: "proxy",
92 | rules: [
93 | {
94 | condition: "example.com",
95 | profileID: "direct",
96 | type: "domain",
97 | },
98 | {
99 | condition: "http://example.com/api/*",
100 | profileID: "direct",
101 | type: "url",
102 | },
103 | ],
104 | },
105 | "fb29f908-a284-4dca-b7e5-c606e6b3c2f1": {
106 | color: "#3491FA",
107 | defaultProfileID: "direct",
108 | pacScript: {
109 | data: "function FindProxyForURL(url, host) {\n // …\n return 'DIRECT';\n}",
110 | },
111 | profileID: "fb29f908-a284-4dca-b7e5-c606e6b3c2f1",
112 | profileName: "Auto Switch",
113 | proxyRules: {
114 | bypassList: ["", "127.0.0.1", "[::1]"],
115 | default: {
116 | host: "127.0.0.1",
117 | port: 8080,
118 | scheme: "http",
119 | },
120 | },
121 | proxyType: "auto",
122 | rules: [
123 | {
124 | condition: "example.com",
125 | profileID: "direct",
126 | type: "domain",
127 | },
128 | {
129 | condition: "http://example.com/api/*",
130 | profileID: "ac8eeebe-3a13-4969-881c-3c7419e91f95",
131 | type: "url",
132 | },
133 | ],
134 | },
135 | };
136 |
137 | const mockExportedJSON = {
138 | version: "2025-01",
139 | profiles: [
140 | {
141 | color: "#DCF190",
142 | profileID: "90750184-fb8f-4130-ad83-c997a7207300",
143 | profileName: "PAC Script",
144 | pacScript: {
145 | data: "function FindProxyForURL(url, host) {\n // …\n return 'DIRECT';\n}",
146 | },
147 | proxyType: "pac",
148 | },
149 | {
150 | color: "#93BEFF",
151 | profileID: "ac8eeebe-3a13-4969-881c-3c7419e91f95",
152 | profileName: "HTTP",
153 | proxyRules: {
154 | bypassList: ["", "127.0.0.1", "[::1]"],
155 | default: {
156 | host: "localhost",
157 | scheme: "http",
158 | port: 8080,
159 | },
160 | },
161 | proxyType: "proxy",
162 | },
163 | {
164 | color: "#FBB0A7",
165 | profileID: "bd4a7580-2c03-4465-acae-bde628ffbb16",
166 | profileName: "HTTP (with Auth)",
167 | proxyRules: {
168 | bypassList: ["", "127.0.0.1", "[::1]"],
169 | default: {
170 | host: "127.0.0.1",
171 | scheme: "http",
172 | port: 8080,
173 | auth: {
174 | username: "admin",
175 | password: "securepassword",
176 | },
177 | },
178 | },
179 | proxyType: "proxy",
180 | },
181 | {
182 | color: "#3491FA",
183 | profileID: "fb29f908-a284-4dca-b7e5-c606e6b3c2f1",
184 | profileName: "Auto Switch",
185 | defaultProfileID: "direct",
186 | proxyType: "auto",
187 | rules: [
188 | {
189 | type: "domain",
190 | condition: "example.com",
191 | profileID: "direct",
192 | },
193 | {
194 | type: "url",
195 | condition: "http://example.com/api/*",
196 | profileID: "ac8eeebe-3a13-4969-881c-3c7419e91f95",
197 | },
198 | ],
199 | },
200 | ],
201 | };
202 |
203 | const mockJSONToBeImported = {
204 | version: "2025-01",
205 | profiles: [
206 | {
207 | color: "#FBB0A7",
208 | profileID: "bd4a7580-2c03-4465-acae-bde628ffbb16",
209 | profileName: "HTTP (with Auth)",
210 | proxyRules: {
211 | bypassList: ["", "127.0.0.1", "[::1]"],
212 | default: {
213 | host: "127.0.0.1",
214 | scheme: "http",
215 | port: 8080,
216 | auth: {
217 | username: "admin",
218 | password: "securepassword",
219 | },
220 | },
221 | },
222 | proxyType: "proxy",
223 |
224 | // additional properties
225 | dummy: "dummy",
226 | },
227 | ],
228 | };
229 |
230 | const mockJSONToBeImportedWithError = {
231 | version: "2025-01",
232 | profiles: [
233 | {
234 | color: "#FBB0A7",
235 | profileID: "bd4a7580-2c03-4465-acae-bde628ffbb16",
236 | profileName: "HTTP (with Auth)",
237 | proxyRules: {
238 | bypassList: ["", "127.0.0.1", "[::1]"],
239 | default: {
240 | host: "127.0.0.1",
241 | scheme: "http",
242 | port: 65536, // invalid port
243 | auth: {
244 | username: "admin",
245 | password: "securepassword",
246 | },
247 | },
248 | },
249 | proxyType: "proxy",
250 | },
251 | ],
252 | };
253 |
254 | describe("testing exporting config", () => {
255 | test("export JSON config", async () => {
256 | const jsonStr = config2json(mockInnerConfig);
257 |
258 | expect(JSON.parse(jsonStr)).toMatchObject(mockExportedJSON);
259 | });
260 | });
261 |
262 | describe("testing parsing config", () => {
263 | test("parse malformed JSON config", async () => {
264 | expect(() => json2config("{blah")).toThrow("Invalid config data");
265 | });
266 |
267 | test("parse empty JSON config", async () => {
268 | expect(() => json2config("{}")).toThrow(/Could not validate data:/);
269 | });
270 |
271 | test("parse normal JSON config", async () => {
272 | const profiles = json2config(JSON.stringify(mockJSONToBeImported));
273 | expect(profiles).toHaveLength(1);
274 |
275 | const expectedProfile: any = {
276 | ...mockJSONToBeImported["profiles"][0],
277 | };
278 | delete expectedProfile.dummy;
279 |
280 | expect(profiles[0]).toMatchObject(expectedProfile);
281 | });
282 |
283 | test("parse normal JSON config with invalid value", async () => {
284 | expect(() =>
285 | json2config(JSON.stringify(mockJSONToBeImportedWithError))
286 | ).toThrow("Could not validate data: .profiles.0.proxyRules.default.port");
287 | });
288 | });
289 |
--------------------------------------------------------------------------------
/tests/services/proxy/pacSimulator.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test, describe } from "vitest";
2 | import { isInNet, UNKNOWN } from "@/services/proxy/pacSimulator";
3 |
4 | describe("isInNet function", () => {
5 | describe("valid IP addresses that match the subnet", () => {
6 | test("should return true for IP in /24 subnet", () => {
7 | expect(isInNet("192.168.31.100", "192.168.31.0", "255.255.255.0")).toBe(
8 | true
9 | );
10 | expect(isInNet("192.168.1.1", "192.168.1.0", "255.255.255.0")).toBe(true);
11 | expect(isInNet("192.168.1.254", "192.168.1.0", "255.255.255.0")).toBe(
12 | true
13 | );
14 | });
15 |
16 | test("should return true for IP in /16 subnet", () => {
17 | expect(isInNet("192.168.100.50", "192.168.0.0", "255.255.0.0")).toBe(
18 | true
19 | );
20 | expect(isInNet("192.168.255.255", "192.168.0.0", "255.255.0.0")).toBe(
21 | true
22 | );
23 | });
24 |
25 | test("should return true for IP in /8 subnet", () => {
26 | expect(isInNet("10.100.200.50", "10.0.0.0", "255.0.0.0")).toBe(true);
27 | expect(isInNet("10.255.255.255", "10.0.0.0", "255.0.0.0")).toBe(true);
28 | });
29 |
30 | test("should return true for exact match with /32 subnet", () => {
31 | expect(isInNet("192.168.1.1", "192.168.1.1", "255.255.255.255")).toBe(
32 | true
33 | );
34 | });
35 |
36 | test("should return true for any IP with /0 subnet (all match)", () => {
37 | expect(isInNet("1.2.3.4", "0.0.0.0", "0.0.0.0")).toBe(true);
38 | expect(isInNet("255.255.255.255", "0.0.0.0", "0.0.0.0")).toBe(true);
39 | });
40 |
41 | test("should return true for localhost in 127.0.0.0/8", () => {
42 | expect(isInNet("127.0.0.1", "127.0.0.0", "255.0.0.0")).toBe(true);
43 | expect(isInNet("127.255.255.255", "127.0.0.0", "255.0.0.0")).toBe(true);
44 | });
45 | });
46 |
47 | describe("valid IP addresses that don't match the subnet", () => {
48 | test("should return false for IP outside /24 subnet", () => {
49 | expect(isInNet("192.168.2.100", "192.168.1.0", "255.255.255.0")).toBe(
50 | false
51 | );
52 | expect(isInNet("192.169.1.100", "192.168.1.0", "255.255.255.0")).toBe(
53 | false
54 | );
55 | expect(isInNet("193.168.1.100", "192.168.1.0", "255.255.255.0")).toBe(
56 | false
57 | );
58 | });
59 |
60 | test("should return false for IP outside /16 subnet", () => {
61 | expect(isInNet("192.169.1.100", "192.168.0.0", "255.255.0.0")).toBe(
62 | false
63 | );
64 | expect(isInNet("193.168.1.100", "192.168.0.0", "255.255.0.0")).toBe(
65 | false
66 | );
67 | });
68 |
69 | test("should return false for IP outside /8 subnet", () => {
70 | expect(isInNet("11.100.200.50", "10.0.0.0", "255.0.0.0")).toBe(false);
71 | });
72 |
73 | test("should return false for different IP with /32 subnet", () => {
74 | expect(isInNet("192.168.1.2", "192.168.1.1", "255.255.255.255")).toBe(
75 | false
76 | );
77 | });
78 | });
79 |
80 | describe("invalid IP addresses (should return UNKNOWN)", () => {
81 | test("should return UNKNOWN for hostnames", () => {
82 | expect(isInNet("example.com", "192.168.1.0", "255.255.255.0")).toBe(
83 | UNKNOWN
84 | );
85 | expect(isInNet("localhost", "127.0.0.0", "255.0.0.0")).toBe(UNKNOWN);
86 | expect(
87 | isInNet("subdomain.example.com", "192.168.1.0", "255.255.255.0")
88 | ).toBe(UNKNOWN);
89 | });
90 |
91 | test("should return UNKNOWN for IPs with out-of-range octets", () => {
92 | expect(isInNet("256.168.1.1", "192.168.1.0", "255.255.255.0")).toBe(
93 | UNKNOWN
94 | );
95 | expect(isInNet("192.256.1.1", "192.168.1.0", "255.255.255.0")).toBe(
96 | UNKNOWN
97 | );
98 | expect(isInNet("192.168.256.1", "192.168.1.0", "255.255.255.0")).toBe(
99 | UNKNOWN
100 | );
101 | expect(isInNet("192.168.1.256", "192.168.1.0", "255.255.255.0")).toBe(
102 | UNKNOWN
103 | );
104 | expect(isInNet("999.999.999.999", "192.168.1.0", "255.255.255.0")).toBe(
105 | UNKNOWN
106 | );
107 | });
108 |
109 | test("should return UNKNOWN for IPs with wrong format", () => {
110 | expect(isInNet("192.168.1", "192.168.1.0", "255.255.255.0")).toBe(
111 | UNKNOWN
112 | );
113 | expect(isInNet("192.168", "192.168.1.0", "255.255.255.0")).toBe(UNKNOWN);
114 | expect(isInNet("192", "192.168.1.0", "255.255.255.0")).toBe(UNKNOWN);
115 | expect(isInNet("192.168.1.1.1", "192.168.1.0", "255.255.255.0")).toBe(
116 | UNKNOWN
117 | );
118 | });
119 |
120 | test("should return UNKNOWN for empty string", () => {
121 | expect(isInNet("", "192.168.1.0", "255.255.255.0")).toBe(UNKNOWN);
122 | });
123 |
124 | test("should return UNKNOWN for non-numeric values", () => {
125 | expect(isInNet("abc.def.ghi.jkl", "192.168.1.0", "255.255.255.0")).toBe(
126 | UNKNOWN
127 | );
128 | expect(isInNet("192.168.1.a", "192.168.1.0", "255.255.255.0")).toBe(
129 | UNKNOWN
130 | );
131 | });
132 |
133 | test("should return UNKNOWN for IPs with leading zeros that exceed 255", () => {
134 | // Note: JavaScript parseInt("0256") = 256, so this should be caught
135 | expect(isInNet("0256.168.1.1", "192.168.1.0", "255.255.255.0")).toBe(
136 | UNKNOWN
137 | );
138 | });
139 | });
140 |
141 | describe("edge cases and boundary values", () => {
142 | test("should handle all zeros", () => {
143 | expect(isInNet("0.0.0.0", "0.0.0.0", "255.255.255.255")).toBe(true);
144 | expect(isInNet("0.0.0.1", "0.0.0.0", "255.255.255.0")).toBe(true);
145 | });
146 |
147 | test("should handle all 255s", () => {
148 | expect(
149 | isInNet("255.255.255.255", "255.255.255.255", "255.255.255.255")
150 | ).toBe(true);
151 | expect(isInNet("255.255.255.254", "255.255.255.0", "255.255.255.0")).toBe(
152 | true
153 | );
154 | });
155 |
156 | test("should handle boundary values in subnet", () => {
157 | // First IP in subnet
158 | expect(isInNet("192.168.1.0", "192.168.1.0", "255.255.255.0")).toBe(true);
159 | // Last IP in subnet
160 | expect(isInNet("192.168.1.255", "192.168.1.0", "255.255.255.0")).toBe(
161 | true
162 | );
163 | // IP just before subnet
164 | expect(isInNet("192.168.0.255", "192.168.1.0", "255.255.255.0")).toBe(
165 | false
166 | );
167 | // IP just after subnet
168 | expect(isInNet("192.168.2.0", "192.168.1.0", "255.255.255.0")).toBe(
169 | false
170 | );
171 | });
172 |
173 | test("should handle single octet subnet masks", () => {
174 | // /24 mask
175 | expect(isInNet("192.168.1.50", "192.168.1.0", "255.255.255.0")).toBe(
176 | true
177 | );
178 | // /16 mask
179 | expect(isInNet("192.168.50.100", "192.168.0.0", "255.255.0.0")).toBe(
180 | true
181 | );
182 | // /8 mask
183 | expect(isInNet("192.50.100.200", "192.0.0.0", "255.0.0.0")).toBe(true);
184 | });
185 |
186 | test("should handle non-standard subnet masks", () => {
187 | // /25 subnet (255.255.255.128)
188 | expect(isInNet("192.168.1.50", "192.168.1.0", "255.255.255.128")).toBe(
189 | true
190 | );
191 | expect(isInNet("192.168.1.200", "192.168.1.0", "255.255.255.128")).toBe(
192 | false
193 | );
194 | // /17 subnet (255.255.128.0)
195 | expect(isInNet("192.168.50.100", "192.168.0.0", "255.255.128.0")).toBe(
196 | true
197 | );
198 | expect(isInNet("192.168.200.100", "192.168.0.0", "255.255.128.0")).toBe(
199 | false
200 | );
201 | });
202 | });
203 |
204 | describe("real-world scenarios", () => {
205 | test("should match private network ranges", () => {
206 | // 10.0.0.0/8
207 | expect(isInNet("10.1.2.3", "10.0.0.0", "255.0.0.0")).toBe(true);
208 | // 172.16.0.0/12
209 | expect(isInNet("172.16.1.1", "172.16.0.0", "255.240.0.0")).toBe(true);
210 | expect(isInNet("172.31.255.255", "172.16.0.0", "255.240.0.0")).toBe(true);
211 | expect(isInNet("172.32.1.1", "172.16.0.0", "255.240.0.0")).toBe(false);
212 | // 192.168.0.0/16
213 | expect(isInNet("192.168.1.1", "192.168.0.0", "255.255.0.0")).toBe(true);
214 | });
215 |
216 | test("should handle loopback addresses", () => {
217 | expect(isInNet("127.0.0.1", "127.0.0.0", "255.0.0.0")).toBe(true);
218 | expect(isInNet("127.255.255.255", "127.0.0.0", "255.0.0.0")).toBe(true);
219 | });
220 |
221 | test("should handle multicast addresses", () => {
222 | // 224.0.0.0/4
223 | expect(isInNet("224.0.0.1", "224.0.0.0", "240.0.0.0")).toBe(true);
224 | expect(isInNet("239.255.255.255", "224.0.0.0", "240.0.0.0")).toBe(true);
225 | });
226 | });
227 |
228 | describe("mask validation", () => {
229 | test("should work with valid masks", () => {
230 | expect(isInNet("192.168.1.1", "192.168.1.0", "255.255.255.0")).toBe(true);
231 | expect(isInNet("192.168.1.1", "192.168.1.0", "255.255.0.0")).toBe(true);
232 | expect(isInNet("192.168.1.1", "192.168.1.0", "255.0.0.0")).toBe(true);
233 | });
234 |
235 | test("should handle invalid mask format (but function doesn't validate mask)", () => {
236 | // Note: The function doesn't validate the mask format,
237 | // it just uses it in the bitwise operation
238 | // Invalid masks get converted to 0 by convert_addr, which matches everything
239 | // This test documents the current behavior
240 | expect(isInNet("192.168.1.1", "192.168.1.0", "invalid")).toBe(true); // Invalid mask becomes 0, which matches all IPs
241 | });
242 | });
243 | });
244 |
--------------------------------------------------------------------------------
/tests/services/proxy/profile2config.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test, describe } from "vitest";
2 | import {
3 | ProxyProfile,
4 | SystemProfile,
5 | ProfileAutoSwitch,
6 | } from "@/services/profile";
7 | import { ProfileConverter } from "@/services/proxy/profile2config";
8 |
9 | const profiles: Record = {
10 | simpleProxy: {
11 | profileID: "simpleProxy",
12 | color: "",
13 | profileName: "",
14 | proxyType: "proxy",
15 | proxyRules: {
16 | default: {
17 | scheme: "http",
18 | host: "127.0.0.1",
19 | port: 8080,
20 | },
21 | https: {
22 | scheme: "direct",
23 | host: "",
24 | },
25 | bypassList: [
26 | "",
27 | "127.0.0.1",
28 | "192.168.0.1/16",
29 | "[::1]",
30 | "fefe:13::abc/33",
31 | ],
32 | },
33 | pacScript: {},
34 | },
35 |
36 | pacProxy: {
37 | profileID: "pacProxy",
38 | color: "",
39 | profileName: "",
40 | proxyType: "pac",
41 | proxyRules: {
42 | default: {
43 | scheme: "http",
44 | host: "",
45 | },
46 | bypassList: [],
47 | },
48 | pacScript: {
49 | data: "function FindProxyForURL(url, host) { return 'DIRECT'; }",
50 | },
51 | },
52 |
53 | autoProxy: {
54 | profileID: "autoProxy",
55 | color: "",
56 | profileName: "",
57 | proxyType: "auto",
58 | rules: [
59 | {
60 | type: "domain",
61 | condition: "*.example.com",
62 | profileID: "simpleProxy",
63 | },
64 | {
65 | type: "url",
66 | condition: "http://example.com/api/*",
67 | profileID: "pacProxy",
68 | },
69 | {
70 | type: "cidr",
71 | condition: "192.168.10.1/24",
72 | profileID: "simpleProxy",
73 | },
74 | {
75 | type: "domain",
76 | condition: "*.404.com",
77 | profileID: "non-exists",
78 | },
79 | ],
80 | defaultProfileID: "direct",
81 | },
82 |
83 | direct: {
84 | profileID: "direct",
85 | color: "",
86 | profileName: "",
87 | proxyType: "direct",
88 | },
89 |
90 | autoProxy2: {
91 | profileID: "autoProxy2",
92 | color: "",
93 | profileName: "",
94 | proxyType: "auto",
95 | rules: [
96 | {
97 | type: "domain",
98 | condition: "*.example.com",
99 | profileID: "autoProxy",
100 | },
101 | ],
102 | defaultProfileID: "direct",
103 | },
104 | };
105 |
106 | describe("testing generating ProxyConfig for direct and system", () => {
107 | test("proxy config mode", async () => {
108 | const profile = new ProfileConverter(SystemProfile.DIRECT);
109 | const cfg = await profile.toProxyConfig();
110 | expect(cfg.mode).toBe("direct");
111 | });
112 |
113 | test("proxy config mode for others", async () => {
114 | const profile = new ProfileConverter(profiles.simpleProxy);
115 | const cfg = await profile.toProxyConfig();
116 | expect(cfg.mode).toBe("pac_script");
117 | });
118 | });
119 |
120 | describe("testing bypass list", () => {
121 | test("bypass list with ipv6", async () => {
122 | const profile = new ProfileConverter(profiles.simpleProxy);
123 | const cfg = await profile.toProxyConfig();
124 | expect(cfg.pacScript?.data).toMatch(
125 | /.*?isInNet\(host, '192\.168\.0\.1', '255\.255\.0\.0'\).*?/
126 | );
127 | expect(cfg.pacScript?.data).toMatch(
128 | /.*?isInNet\(host, 'fefe:13::abc', 'ffff:ffff:8000:0:0:0:0:0'\).*?/
129 | );
130 | });
131 | });
132 |
133 | describe("testing auto switch profile", () => {
134 | test("auto switch profile", async () => {
135 | const profile = new ProfileConverter(profiles.autoProxy, async (id) => {
136 | return profiles[id];
137 | });
138 | const cfg = await profile.toProxyConfig();
139 | expect(cfg.mode).toBe("pac_script");
140 |
141 | expect(cfg.pacScript?.data).toContain(`
142 | register('pacProxy', function () {
143 | function FindProxyForURL(url, host) {
144 | return 'DIRECT';
145 | }
146 | return FindProxyForURL;
147 | }());`);
148 |
149 | expect(cfg.pacScript?.data).toContain(`
150 | if (isInNet(host, '192.168.10.1', '255.255.255.0')) {
151 | return profiles['simpleProxy'](url, host);
152 | }`);
153 |
154 | expect(cfg.pacScript?.data).toContain(
155 | `alert('Profile non-exists not found, skipped');`
156 | );
157 | expect(cfg.pacScript?.data).toContain(
158 | `return profiles['direct'](url, host);`
159 | );
160 | });
161 | test("nested auto switch profile", async () => {
162 | const profile = new ProfileConverter(profiles.autoProxy2, async (id) => {
163 | return profiles[id];
164 | });
165 | const cfg = await profile.toProxyConfig();
166 | expect(cfg.mode).toBe("pac_script");
167 |
168 | expect(cfg.pacScript?.data).toContain(`
169 | if (shExpMatch(host, '*.example.com')) {
170 | return profiles['autoProxy'](url, host);
171 | }`);
172 | });
173 | });
174 |
175 | describe("testing findProfile function", () => {
176 | const profileLoader = async (id: string) => profiles[id];
177 |
178 | test("simple profiles return themselves", async () => {
179 | const url = new URL("https://example.com");
180 |
181 | const direct = new ProfileConverter(SystemProfile.DIRECT);
182 | expect((await direct.findProfile(url)).profile).toBe(direct);
183 | expect((await direct.findProfile(url)).isConfident).toBe(true);
184 |
185 | const system = new ProfileConverter(SystemProfile.SYSTEM);
186 | expect((await system.findProfile(url)).profile).toBe(system);
187 | expect((await system.findProfile(url)).isConfident).toBe(true);
188 |
189 | const pac = new ProfileConverter(profiles.pacProxy);
190 | expect((await pac.findProfile(url)).profile).toBe(pac);
191 | expect((await pac.findProfile(url)).isConfident).toBe(true);
192 | });
193 |
194 | test("auto profile matches rules and falls back to default", async () => {
195 | const profile = new ProfileConverter(profiles.autoProxy, profileLoader);
196 |
197 | // Domain rule match
198 | expect(
199 | (await profile.findProfile(new URL("https://test.example.com"))).profile
200 | ).toBeDefined();
201 |
202 | // URL rule match
203 | expect(
204 | (await profile.findProfile(new URL("http://example.com/api/v1/users")))
205 | .profile
206 | ).toBeDefined();
207 |
208 | // CIDR rule match
209 | expect(
210 | (await profile.findProfile(new URL("http://192.168.10.50"))).profile
211 | ).toBeDefined();
212 |
213 | // No match - falls back to default
214 | expect(
215 | (await profile.findProfile(new URL("https://other.com"))).profile
216 | ).toBeDefined();
217 | });
218 |
219 | test("auto profile handles edge cases", async () => {
220 | const profile = new ProfileConverter(profiles.autoProxy, profileLoader);
221 |
222 | // CIDR with hostname (non-IP) - non-confident
223 | const hostnameResult = await profile.findProfile(
224 | new URL("http://example.com")
225 | );
226 | expect(hostnameResult.profile).toBeDefined();
227 | expect(hostnameResult.isConfident).toBe(false);
228 |
229 | // Missing profile in rule - skips and uses default
230 | expect(
231 | (await profile.findProfile(new URL("https://test.404.com"))).profile
232 | ).toBeDefined();
233 |
234 | // Disabled rule - skipped
235 | const autoWithDisabled: ProfileAutoSwitch = {
236 | profileID: "autoDisabled",
237 | color: "",
238 | profileName: "",
239 | proxyType: "auto",
240 | rules: [
241 | {
242 | type: "disabled",
243 | condition: "*.example.com",
244 | profileID: "simpleProxy",
245 | },
246 | { type: "domain", condition: "*.test.com", profileID: "simpleProxy" },
247 | ],
248 | defaultProfileID: "direct",
249 | };
250 | const disabledProfile = new ProfileConverter(
251 | autoWithDisabled,
252 | profileLoader
253 | );
254 | expect(
255 | (await disabledProfile.findProfile(new URL("https://test.example.com")))
256 | .profile
257 | ).toBeDefined();
258 |
259 | // Missing default profile - falls back to DIRECT
260 | const autoMissingDefault: ProfileAutoSwitch = {
261 | profileID: "autoMissingDefault",
262 | color: "",
263 | profileName: "",
264 | proxyType: "auto",
265 | rules: [],
266 | defaultProfileID: "missing-default",
267 | };
268 | const missingDefaultProfile = new ProfileConverter(
269 | autoMissingDefault,
270 | profileLoader
271 | );
272 | const result = await missingDefaultProfile.findProfile(
273 | new URL("https://other.com")
274 | );
275 | expect(result.profile).toBeDefined();
276 | expect((await result.profile!.toProxyConfig()).mode).toBe("direct");
277 | });
278 |
279 | test("nested auto profiles work correctly", async () => {
280 | const profile = new ProfileConverter(profiles.autoProxy2, profileLoader);
281 | const result = await profile.findProfile(
282 | new URL("https://test.example.com")
283 | );
284 |
285 | expect(result.profile).toBeDefined();
286 | expect(result.isConfident).toBe(true);
287 | });
288 |
289 | test("first matching rule wins", async () => {
290 | const autoMultiple: ProfileAutoSwitch = {
291 | profileID: "autoMultiple",
292 | color: "",
293 | profileName: "",
294 | proxyType: "auto",
295 | rules: [
296 | {
297 | type: "domain",
298 | condition: "*.example.com",
299 | profileID: "simpleProxy",
300 | },
301 | { type: "domain", condition: "*.example.com", profileID: "pacProxy" },
302 | ],
303 | defaultProfileID: "direct",
304 | };
305 |
306 | const profile = new ProfileConverter(autoMultiple, profileLoader);
307 | const result = await profile.findProfile(
308 | new URL("https://test.example.com")
309 | );
310 |
311 | expect(result.profile).toBeDefined();
312 | expect(result.isConfident).toBe(true);
313 | });
314 | });
315 |
--------------------------------------------------------------------------------
/src/components/ProfileConfig.vue:
--------------------------------------------------------------------------------
1 |
257 |
258 |
259 |
263 |
264 |
265 |
271 | {{ profileConfig.profileName }}
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 | {{ $t("config_action_cancel") }}
287 |
288 |
293 |
294 |
295 |
296 | {{ $t("config_action_save") }}
297 |
298 |
306 |
307 |
308 |
309 |
310 | {{ $t("config_action_delete") }}
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 | {{ $t("config_proxy_type_proxy") }}
320 | {{ $t("config_proxy_type_pac") }}
321 | {{ $t("config_proxy_type_auto") }}
322 |
323 |
324 |
325 |
326 | {{
327 | $t("config_section_proxy_server")
328 | }}
329 |
330 |
343 |
344 |
345 | {{
346 | $t("config_section_advance")
347 | }}
348 |
349 |
350 |
360 |
361 |
371 |
372 |
380 |
381 |
382 | {{
383 | $t("config_section_bypass_list")
384 | }}
385 |
386 |
387 |
393 | {{ $t("config_reference_bypass_list") }}
394 |
395 |
396 |
402 |
403 |
404 |
405 |
412 |
413 |
414 |
415 |
416 |
417 |
418 | {{
419 | $t("config_section_auto_switch_rules")
420 | }}
421 |
422 |
423 |
427 |
428 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
449 |
--------------------------------------------------------------------------------
/src/services/proxy/profile2config.ts:
--------------------------------------------------------------------------------
1 | import { generate as generateJS } from "escodegen";
2 | import type { Program, Statement } from "acorn";
3 | import {
4 | AutoSwitchRule,
5 | ProfileAutoSwitch,
6 | ProxyProfile,
7 | ProxyServer,
8 | SystemProfile,
9 | } from "../profile";
10 | import { IPv4, IPv6, isValidCIDR, parseCIDR } from "ipaddr.js";
11 | import {
12 | newProxyString,
13 | PACScriptHelper,
14 | parsePACScript,
15 | } from "./scriptHelper";
16 | import { ProxyConfig } from "@/adapters";
17 | import { isInNet, shExpMatch, UNKNOWN } from "./pacSimulator";
18 |
19 | export type ProfileLoader = (
20 | profileID: string
21 | ) => Promise;
22 |
23 | export type ProfileResult = {
24 | profile: ProfileConverter | undefined;
25 | isConfident: boolean;
26 | };
27 |
28 | export class ProfileConverter {
29 | constructor(
30 | public readonly profile: ProxyProfile,
31 | private profileLoader?: ProfileLoader
32 | ) {}
33 |
34 | async toProxyConfig(): Promise {
35 | switch (this.profile.proxyType) {
36 | case "direct":
37 | case "system":
38 | return { mode: this.profile.proxyType };
39 |
40 | case "pac":
41 | return {
42 | mode: "pac_script",
43 | pacScript: this.profile.pacScript,
44 | };
45 |
46 | default:
47 | return {
48 | mode: "pac_script",
49 | pacScript: {
50 | data: await this.toPAC(),
51 | },
52 | };
53 | }
54 | }
55 |
56 | async findProfile(url: URL): Promise {
57 | switch (this.profile.proxyType) {
58 | case "auto":
59 | return await this.findProfileForAutoProfile(url);
60 |
61 | default:
62 | return { profile: this, isConfident: true };
63 | }
64 | }
65 |
66 | /**
67 | * Convert the `auto` profile to a PAC script
68 | * @returns the PAC script
69 | */
70 | async toPAC() {
71 | const astProgram: Program = {
72 | type: "Program",
73 | sourceType: "script",
74 | body: await this.genStatements(),
75 | start: 0, // dummy
76 | end: 0, // dummy
77 | };
78 |
79 | return generateJS(astProgram);
80 | }
81 |
82 | /**
83 | * Convert the profile to a closure, which can be used for auto profiles
84 | * (function() {
85 | * // the definition of FindProxyForURL
86 | * return FindProxyForURL;
87 | * })()
88 | * @returns
89 | */
90 | async toClosure() {
91 | const stmts = await this.genStatements();
92 | stmts.push(
93 | PACScriptHelper.newReturnStatement(
94 | PACScriptHelper.newIdentifier("FindProxyForURL")
95 | )
96 | );
97 |
98 | return PACScriptHelper.newCallExpression(
99 | PACScriptHelper.newFunctionExpression([], stmts),
100 | []
101 | );
102 | }
103 |
104 | /**
105 | * genStatements that returns a list of statements, containing the main function `FindProxyForURL`
106 | * @returns
107 | */
108 | private async genStatements() {
109 | switch (this.profile.proxyType) {
110 | case "system":
111 | case "direct":
112 | case "proxy":
113 | return this.genFindProxyForURLFunction();
114 | case "pac":
115 | return this.genFindProxyForURLFunctionForPAC();
116 | case "auto":
117 | return await this.genFindProxyForURLFunctionForAutoProfile();
118 | }
119 | }
120 | private genFindProxyForURLFunctionForPAC(): Statement[] {
121 | if (this.profile.proxyType != "pac") {
122 | throw new Error("this function should only be called for pac profile");
123 | }
124 |
125 | if (!this.profile.pacScript.data) {
126 | return [];
127 | }
128 |
129 | return parsePACScript(this.profile.pacScript.data);
130 | }
131 |
132 | /**
133 | * genFindProxyForURLFunction for `ProxySimple` and `ProxyPreset`
134 | * @returns
135 | */
136 | private genFindProxyForURLFunction(): Statement[] {
137 | const body: Statement[] = [];
138 |
139 | switch (this.profile.proxyType) {
140 | case "direct":
141 | body.push(
142 | PACScriptHelper.newReturnStatement(
143 | PACScriptHelper.newSimpleLiteral("DIRECT")
144 | )
145 | );
146 | break;
147 | case "proxy":
148 | body.push(
149 | ...this.genBypassList(),
150 | ...this.genAdvancedRules(),
151 | PACScriptHelper.newReturnStatement(
152 | newProxyString(this.profile.proxyRules.default)
153 | )
154 | );
155 | break;
156 |
157 | default:
158 | throw new Error("unexpected proxy type");
159 | }
160 |
161 | return [
162 | PACScriptHelper.newFunctionDeclaration(
163 | "FindProxyForURL",
164 | ["url", "host"],
165 | body
166 | ),
167 | ];
168 | }
169 |
170 | private async genFindProxyForURLFunctionForAutoProfile() {
171 | if (this.profile.proxyType != "auto") {
172 | throw new Error("this function should only be called for auto profile");
173 | }
174 |
175 | const { stmt, loadedProfiles } = await this.prepareAutoProfilePrecedence(
176 | this.profile
177 | );
178 | const body: Statement[] = [];
179 |
180 | // rules
181 | this.profile.rules.forEach((rule) => {
182 | switch (rule.type) {
183 | case "disabled":
184 | return; // skipped
185 | default:
186 | if (loadedProfiles.has(rule.profileID)) {
187 | return body.push(this.genAutoProfileRule(rule));
188 | }
189 | }
190 |
191 | // if a dependent profile is not loaded, skip it, and add some alerts
192 | body.push(this.genAutoProfileMissingProfileAlert(rule.profileID));
193 | });
194 |
195 | // default profile
196 | if (loadedProfiles.has(this.profile.defaultProfileID)) {
197 | body.push(
198 | PACScriptHelper.newReturnStatement(
199 | this.genAutoProfileCallExpression(this.profile.defaultProfileID)
200 | )
201 | );
202 | } else {
203 | body.push(
204 | this.genAutoProfileMissingProfileAlert(this.profile.defaultProfileID),
205 | PACScriptHelper.newReturnStatement(
206 | PACScriptHelper.newSimpleLiteral("DIRECT") // fallback to direct
207 | )
208 | );
209 | }
210 |
211 | stmt.push(
212 | PACScriptHelper.newFunctionDeclaration(
213 | "FindProxyForURL",
214 | ["url", "host"],
215 | body
216 | )
217 | );
218 |
219 | return stmt;
220 | }
221 |
222 | private async findProfileForAutoProfile(url: URL): Promise {
223 | if (this.profile.proxyType != "auto") {
224 | throw new Error("this function should only be called for auto profile");
225 | }
226 |
227 | for (let rule of this.profile.rules) {
228 | if (rule.type == "disabled") {
229 | continue;
230 | }
231 |
232 | const profile = await this.loadProfile(rule.profileID);
233 | if (!profile) {
234 | continue;
235 | }
236 |
237 | const ret = await profile.findProfileForAutoProfileRule(
238 | url,
239 | rule,
240 | profile
241 | );
242 | if (ret.profile) {
243 | return ret;
244 | }
245 | }
246 |
247 | const defaultProfile =
248 | (await this.loadProfile(this.profile.defaultProfileID)) ||
249 | new ProfileConverter(SystemProfile.DIRECT);
250 |
251 | return await defaultProfile.findProfile(url);
252 | }
253 |
254 | private genAutoProfileRule(rule: AutoSwitchRule): Statement {
255 | switch (rule.type) {
256 | case "domain":
257 | return PACScriptHelper.newIfStatement(
258 | PACScriptHelper.newCallExpression(
259 | PACScriptHelper.newIdentifier("shExpMatch"),
260 | [
261 | PACScriptHelper.newIdentifier("host"),
262 | PACScriptHelper.newSimpleLiteral(rule.condition),
263 | ]
264 | ),
265 | [
266 | PACScriptHelper.newReturnStatement(
267 | this.genAutoProfileCallExpression(rule.profileID)
268 | ),
269 | ]
270 | );
271 |
272 | case "url":
273 | return PACScriptHelper.newIfStatement(
274 | PACScriptHelper.newCallExpression(
275 | PACScriptHelper.newIdentifier("shExpMatch"),
276 | [
277 | PACScriptHelper.newIdentifier("url"),
278 | PACScriptHelper.newSimpleLiteral(rule.condition),
279 | ]
280 | ),
281 | [
282 | PACScriptHelper.newReturnStatement(
283 | this.genAutoProfileCallExpression(rule.profileID)
284 | ),
285 | ]
286 | );
287 |
288 | case "cidr":
289 | // if it's a CIDR
290 | if (isValidCIDR(rule.condition)) {
291 | try {
292 | const [ip, maskPrefixLen] = parseCIDR(rule.condition);
293 | let mask = (
294 | ip.kind() == "ipv4" ? IPv4 : IPv6
295 | ).subnetMaskFromPrefixLength(maskPrefixLen);
296 |
297 | return PACScriptHelper.newIfStatement(
298 | PACScriptHelper.newCallExpression(
299 | PACScriptHelper.newIdentifier("isInNet"),
300 | [
301 | PACScriptHelper.newIdentifier("host"),
302 | PACScriptHelper.newSimpleLiteral(ip.toString()),
303 | PACScriptHelper.newSimpleLiteral(mask.toNormalizedString()),
304 | ]
305 | ),
306 | [
307 | PACScriptHelper.newReturnStatement(
308 | this.genAutoProfileCallExpression(rule.profileID)
309 | ),
310 | ]
311 | );
312 | } catch (e) {
313 | console.error(e);
314 | }
315 | }
316 | }
317 |
318 | return PACScriptHelper.newExpressionStatement(
319 | PACScriptHelper.newCallExpression(
320 | PACScriptHelper.newIdentifier("alert"),
321 | [
322 | PACScriptHelper.newSimpleLiteral(
323 | `Invalid condition ${rule.type}: ${rule.condition}, skipped`
324 | ),
325 | ]
326 | )
327 | );
328 | }
329 |
330 | private async findProfileForAutoProfileRule(
331 | url: URL,
332 | rule: AutoSwitchRule,
333 | profile: ProfileConverter
334 | ): Promise {
335 | switch (rule.type) {
336 | case "domain":
337 | if (shExpMatch(url.hostname, rule.condition)) {
338 | return profile.findProfile(url);
339 | }
340 |
341 | break;
342 | case "url":
343 | if (shExpMatch(url.href, rule.condition)) {
344 | return profile.findProfile(url);
345 | }
346 |
347 | break;
348 | case "cidr":
349 | // if it's a CIDR
350 | if (isValidCIDR(rule.condition)) {
351 | try {
352 | const [ip, maskPrefixLen] = parseCIDR(rule.condition);
353 | let mask = (
354 | ip.kind() == "ipv4" ? IPv4 : IPv6
355 | ).subnetMaskFromPrefixLength(maskPrefixLen);
356 |
357 | switch (
358 | isInNet(url.hostname, ip.toString(), mask.toNormalizedString())
359 | ) {
360 | case true:
361 | return profile.findProfile(url);
362 | case false:
363 | break; // not in the CIDR
364 | case UNKNOWN:
365 | return { profile: profile, isConfident: false }; // unknown
366 | }
367 | } catch (e) {
368 | console.error(e);
369 | }
370 | }
371 |
372 | break;
373 | }
374 |
375 | return { profile: undefined, isConfident: true };
376 | }
377 |
378 | private genAutoProfileCallExpression(profileID: string) {
379 | return PACScriptHelper.newCallExpression(
380 | PACScriptHelper.newMemberExpression(
381 | PACScriptHelper.newIdentifier("profiles"),
382 | PACScriptHelper.newSimpleLiteral(profileID),
383 | true
384 | ),
385 | [
386 | PACScriptHelper.newIdentifier("url"),
387 | PACScriptHelper.newIdentifier("host"),
388 | ]
389 | );
390 | }
391 |
392 | private genAutoProfileMissingProfileAlert(profileID: string) {
393 | return PACScriptHelper.newExpressionStatement(
394 | PACScriptHelper.newCallExpression(
395 | PACScriptHelper.newIdentifier("alert"),
396 | [
397 | PACScriptHelper.newSimpleLiteral(
398 | `Profile ${profileID} not found, skipped`
399 | ),
400 | ]
401 | )
402 | );
403 | }
404 |
405 | private async prepareAutoProfilePrecedence(profile: ProfileAutoSwitch) {
406 | const loadedProfiles = new Set();
407 | const stmt: Statement[] = [
408 | // var profiles = profiles || {};
409 | PACScriptHelper.newVariableDeclaration(
410 | "profiles",
411 | PACScriptHelper.newLogicalExpression(
412 | "||",
413 | PACScriptHelper.newIdentifier("profiles"),
414 | PACScriptHelper.newObjectExpression([])
415 | )
416 | ),
417 |
418 | /**
419 | * function register(profileID, funcFindProxyForURL) {
420 | * profiles[profileID] = funcFindProxyForURL;
421 | * }
422 | */
423 | PACScriptHelper.newFunctionDeclaration(
424 | "register",
425 | ["profileID", "funcFindProxyForURL"],
426 | [
427 | PACScriptHelper.newExpressionStatement(
428 | PACScriptHelper.newAssignmentExpression(
429 | "=",
430 | PACScriptHelper.newMemberExpression(
431 | PACScriptHelper.newIdentifier("profiles"),
432 | PACScriptHelper.newIdentifier("profileID"),
433 | true
434 | ),
435 | PACScriptHelper.newIdentifier("funcFindProxyForURL")
436 | )
437 | ),
438 | ]
439 | ),
440 | ];
441 |
442 | // register all profiles
443 | const profileIDs = [
444 | profile.defaultProfileID,
445 | ...profile.rules.map((r) => r.profileID),
446 | ];
447 | for (let profileID of profileIDs) {
448 | if (loadedProfiles.has(profileID)) {
449 | continue;
450 | }
451 |
452 | const profile = await this.loadProfile(profileID);
453 | if (!profile) {
454 | continue;
455 | }
456 |
457 | loadedProfiles.add(profileID);
458 |
459 | stmt.push(
460 | PACScriptHelper.newExpressionStatement(
461 | PACScriptHelper.newCallExpression(
462 | PACScriptHelper.newIdentifier("register"),
463 | [
464 | PACScriptHelper.newSimpleLiteral(profileID),
465 | await profile.toClosure(),
466 | ]
467 | )
468 | )
469 | );
470 | }
471 |
472 | return { stmt, loadedProfiles };
473 | }
474 |
475 | private async loadProfile(
476 | profileID: string
477 | ): Promise {
478 | if (!this.profileLoader) {
479 | return;
480 | }
481 |
482 | const profile = await this.profileLoader(profileID);
483 | if (!profile) {
484 | return;
485 | }
486 |
487 | return new ProfileConverter(profile, this.profileLoader);
488 | }
489 |
490 | private genBypassList() {
491 | if (this.profile.proxyType != "proxy") {
492 | throw new Error("Only proxy profile can have bypass list");
493 | }
494 |
495 | const directExpr = PACScriptHelper.newReturnStatement(
496 | PACScriptHelper.newSimpleLiteral("DIRECT")
497 | );
498 | return this.profile.proxyRules.bypassList.map((item) => {
499 | if (item == "") {
500 | return PACScriptHelper.newIfStatement(
501 | PACScriptHelper.newCallExpression(
502 | PACScriptHelper.newIdentifier("isPlainHostName"),
503 | [PACScriptHelper.newIdentifier("host")]
504 | ),
505 | [directExpr]
506 | );
507 | }
508 |
509 | // if it's a CIDR
510 | if (isValidCIDR(item)) {
511 | try {
512 | const [ip, maskPrefixLen] = parseCIDR(item);
513 | let mask = (
514 | ip.kind() == "ipv4" ? IPv4 : IPv6
515 | ).subnetMaskFromPrefixLength(maskPrefixLen);
516 |
517 | return PACScriptHelper.newIfStatement(
518 | PACScriptHelper.newCallExpression(
519 | PACScriptHelper.newIdentifier("isInNet"),
520 | [
521 | PACScriptHelper.newIdentifier("host"),
522 | PACScriptHelper.newSimpleLiteral(ip.toString()),
523 | PACScriptHelper.newSimpleLiteral(mask.toNormalizedString()),
524 | ]
525 | ),
526 | [directExpr]
527 | );
528 | } catch (e) {
529 | console.error(e);
530 | }
531 | }
532 |
533 | return PACScriptHelper.newIfStatement(
534 | PACScriptHelper.newCallExpression(
535 | PACScriptHelper.newIdentifier("shExpMatch"),
536 | [
537 | PACScriptHelper.newIdentifier("host"),
538 | PACScriptHelper.newSimpleLiteral(item),
539 | ]
540 | ),
541 | [directExpr]
542 | );
543 | });
544 | }
545 |
546 | private genAdvancedRules() {
547 | if (this.profile.proxyType != "proxy") {
548 | throw new Error("Only proxy profile can have bypass list");
549 | }
550 |
551 | const ret = [];
552 |
553 | type KeyVal = "ftp" | "https" | "http";
554 | const keys: KeyVal[] = ["ftp", "https", "http"];
555 | const rules = this.profile.proxyRules as Record<
556 | KeyVal,
557 | ProxyServer | undefined
558 | >;
559 |
560 | for (let item of keys) {
561 | const cfg = rules[item];
562 | if (!cfg) {
563 | continue;
564 | }
565 |
566 | ret.push(
567 | PACScriptHelper.newIfStatement(
568 | PACScriptHelper.newCallExpression(
569 | PACScriptHelper.newMemberExpression(
570 | PACScriptHelper.newIdentifier("url"),
571 | PACScriptHelper.newIdentifier("startsWith")
572 | ),
573 | [PACScriptHelper.newSimpleLiteral(`${item}:`)]
574 | ),
575 | [PACScriptHelper.newReturnStatement(newProxyString(cfg))]
576 | )
577 | );
578 | }
579 | return ret;
580 | }
581 | }
582 |
--------------------------------------------------------------------------------