1use log::{debug, error};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::constants::regex_black_list::REGEX_BLACK_LIST;
6use crate::interfaces::subconverter::{subconverter, SubconverterConfigBuilder};
7use crate::models::ruleset::RulesetConfigs;
8use crate::models::{ProxyGroupConfigs, RegexMatchConfigs, SubconverterTarget};
9use crate::settings::external::ExternalSettings;
10use crate::settings::settings::init_settings;
11use crate::settings::{refresh_configuration, FromIni, FromIniWithDelimiter};
12use crate::utils::reg_valid;
13use crate::{RuleBases, Settings, TemplateArgs};
14
15#[cfg(target_arch = "wasm32")]
16use {js_sys::Promise, wasm_bindgen::prelude::*, wasm_bindgen_futures::future_to_promise};
17
18fn default_ver() -> u32 {
19 3
20}
21
22#[derive(Deserialize, Serialize, Debug, Default, Clone)]
24pub struct SubconverterQuery {
25 pub target: Option<String>,
27 #[serde(default = "default_ver")]
29 pub ver: u32,
30 pub new_name: Option<bool>,
32 pub url: Option<String>,
34 pub group: Option<String>,
36 pub upload_path: Option<String>,
38 pub include: Option<String>,
40 pub exclude: Option<String>,
42 pub groups: Option<String>,
44 pub ruleset: Option<String>,
46 pub config: Option<String>,
48
49 pub dev_id: Option<String>,
51 pub insert: Option<bool>,
53 pub prepend: Option<bool>,
55 pub filename: Option<String>,
57 pub append_type: Option<bool>,
59 pub emoji: Option<bool>,
61 pub add_emoji: Option<bool>,
63 pub remove_emoji: Option<bool>,
65 pub list: Option<bool>,
67 pub sort: Option<bool>,
69
70 pub sort_script: Option<String>,
72
73 pub fdn: Option<bool>,
75
76 pub rename: Option<String>,
78 pub tfo: Option<bool>,
80 pub udp: Option<bool>,
82 pub scv: Option<bool>,
84 pub tls13: Option<bool>,
86 pub rename_node: Option<bool>,
88 pub interval: Option<u32>,
90 pub strict: Option<bool>,
92 pub upload: Option<bool>,
94 pub token: Option<String>,
96 pub filter: Option<String>,
98
99 pub script: Option<bool>,
101 pub classic: Option<bool>,
102
103 pub expand: Option<bool>,
104
105 #[serde(default)]
107 pub singbox: HashMap<String, String>,
108}
109
110pub fn parse_query_string(query: &str) -> HashMap<String, String> {
112 let mut params = HashMap::new();
113 for pair in query.split('&') {
114 let mut parts = pair.splitn(2, '=');
115 if let Some(key) = parts.next() {
116 let value = parts.next().unwrap_or("");
117 params.insert(key.to_string(), value.to_string());
118 }
119 }
120 params
121}
122
123#[derive(Debug, Serialize)]
125pub struct SubResponse {
126 pub content: String,
127 pub content_type: String,
128 pub headers: HashMap<String, String>,
129 pub status_code: u16,
130}
131
132impl SubResponse {
133 pub fn ok(content: String, content_type: String) -> Self {
134 Self {
135 content,
136 content_type,
137 headers: HashMap::new(),
138 status_code: 200,
139 }
140 }
141
142 pub fn error(content: String, status_code: u16) -> Self {
143 Self {
144 content,
145 content_type: "text/plain".to_string(),
146 headers: HashMap::new(),
147 status_code,
148 }
149 }
150
151 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
152 self.headers = headers;
153 self
154 }
155}
156
157pub async fn sub_process(
159 req_url: Option<String>,
160 query: SubconverterQuery,
161) -> Result<SubResponse, Box<dyn std::error::Error>> {
162 let mut global = Settings::current();
163
164 if global.pref_path.is_empty() {
166 debug!("Global config not initialized, reloading");
167 init_settings("").await?;
168 global = Settings::current();
169 } else if global.reload_conf_on_request && !global.api_mode && !global.generator_mode {
170 refresh_configuration().await;
171 global = Settings::current();
172 }
173
174 let mut builder = SubconverterConfigBuilder::new();
176
177 let target;
178 if let Some(_target) = &query.target {
179 match SubconverterTarget::from_str(&_target) {
180 Some(_target) => {
181 target = _target.clone();
182 if _target == SubconverterTarget::Auto {
183 return Ok(SubResponse::error(
195 "Auto user agent is not supported for now.".to_string(),
196 400,
197 ));
198 }
199 builder.target(_target);
200 }
201 None => {
202 return Ok(SubResponse::error(
203 "Invalid target parameter".to_string(),
204 400,
205 ));
206 }
207 }
208 } else {
209 return Ok(SubResponse::error(
210 "Missing target parameter".to_string(),
211 400,
212 ));
213 }
214
215 builder.update_interval(match query.interval {
216 Some(interval) => interval,
217 None => global.update_interval,
218 });
219 let authorized =
221 !global.api_mode || query.token.as_deref().unwrap_or_default() == global.api_access_token;
222 builder.authorized(authorized);
223 builder.update_strict(query.strict.unwrap_or(global.update_strict));
224
225 if query
226 .include
227 .clone()
228 .is_some_and(|include| REGEX_BLACK_LIST.contains(&include))
229 || query
230 .exclude
231 .clone()
232 .is_some_and(|exclude| REGEX_BLACK_LIST.contains(&exclude))
233 {
234 return Ok(SubResponse::error(
235 "Invalid regex in request!".to_string(),
236 400,
237 ));
238 }
239
240 let enable_insert = match query.insert {
241 Some(insert) => insert,
242 None => global.enable_insert,
243 };
244
245 if enable_insert {
246 builder.insert_urls(global.insert_urls.clone());
247 builder.prepend_insert(query.prepend.unwrap_or(global.prepend_insert));
249 }
250
251 let urls = match query.url.as_deref() {
252 Some(query_url) => query_url.split('|').map(|s| s.to_owned()).collect(),
253 None => {
254 if authorized {
255 global.default_urls.clone()
256 } else {
257 vec![]
258 }
259 }
260 };
261 builder.urls(urls);
262
263 let mut template_args = TemplateArgs::default();
267 template_args.global_vars = global.template_vars.clone();
268
269 template_args.request_params = query.clone();
270
271 builder.append_proxy_type(query.append_type.unwrap_or(global.append_type));
272
273 let mut arg_expand_rulesets = query.expand;
274 if target.is_clash() && query.script.is_none() {
275 arg_expand_rulesets = Some(true);
276 }
277
278 builder.tfo(query.tfo.or(global.tfo_flag));
280 builder.udp(query.udp.or(global.udp_flag));
281 builder.skip_cert_verify(query.scv.or(global.skip_cert_verify));
282 builder.tls13(query.tls13.or(global.tls13_flag));
283 builder.sort(query.sort.unwrap_or(global.enable_sort));
284 if let Some(script) = &query.sort_script {
285 builder.sort_script(script.clone());
286 }
287
288 builder.filter_deprecated(query.fdn.unwrap_or(global.filter_deprecated));
289 builder.clash_new_field_name(query.new_name.unwrap_or(global.clash_use_new_field));
290 builder.clash_script(query.script.unwrap_or_default());
291 builder.clash_classical_ruleset(query.classic.unwrap_or_default());
292 let nodelist = query.list.unwrap_or_default();
293 builder.nodelist(nodelist);
294
295 if arg_expand_rulesets != Some(true) {
296 builder.clash_new_field_name(true);
297 } else {
298 builder.managed_config_prefix(global.managed_config_prefix.clone());
299 builder.clash_script(false);
300 }
301
302 let mut ruleset_configs = global.custom_rulesets.clone();
303 let mut custom_group_configs = global.custom_proxy_groups.clone();
304
305 builder.include_remarks(global.include_remarks.clone());
307 builder.exclude_remarks(global.exclude_remarks.clone());
308 builder.rename_array(global.renames.clone());
309 builder.emoji_array(global.emojis.clone());
310 builder.add_emoji(global.add_emoji);
311 builder.remove_emoji(global.remove_emoji);
312 builder.enable_rule_generator(global.enable_rule_gen);
313 let mut rule_bases = RuleBases {
314 clash_rule_base: global.clash_base.clone(),
315 surge_rule_base: global.surge_base.clone(),
316 surfboard_rule_base: global.surfboard_base.clone(),
317 mellow_rule_base: global.mellow_base.clone(),
318 quan_rule_base: global.quan_base.clone(),
319 quanx_rule_base: global.quanx_base.clone(),
320 loon_rule_base: global.loon_base.clone(),
321 sssub_rule_base: global.ssub_base.clone(),
322 singbox_rule_base: global.singbox_base.clone(),
323 };
324 builder.rule_bases(rule_bases.clone());
325 builder.template_args(template_args.clone());
326
327 let ext_config = match query.config.as_deref() {
328 Some(config) => config.to_owned(),
329 None => global.default_ext_config.clone(),
330 };
331 if !ext_config.is_empty() {
332 debug!("Loading external config from {}", ext_config);
333
334 let extconf_result = ExternalSettings::load_from_file(&ext_config).await;
337
338 match extconf_result {
339 Ok(extconf) => {
340 debug!("Successfully loaded external config from {}", ext_config);
341 if !nodelist {
342 rule_bases
343 .check_external_bases(&extconf, &global.base_path)
344 .await;
345 builder.rule_bases(rule_bases);
346
347 if let Some(tpl_args) = extconf.tpl_args {
348 template_args.local_vars = tpl_args;
349 }
350
351 builder.template_args(template_args);
352
353 if !target.is_simple() {
354 if !extconf.custom_rulesets.is_empty() {
355 ruleset_configs = extconf.custom_rulesets;
356 }
357 if !extconf.custom_proxy_groups.is_empty() {
358 custom_group_configs = extconf.custom_proxy_groups;
359 }
360 if let Some(enable_rule_gen) = extconf.enable_rule_generator {
361 builder.enable_rule_generator(enable_rule_gen);
362 }
363 if let Some(overwrite_original_rules) = extconf.overwrite_original_rules {
364 builder.overwrite_original_rules(overwrite_original_rules);
365 }
366 }
367 }
368 if !extconf.rename_nodes.is_empty() {
369 builder.rename_array(extconf.rename_nodes);
370 }
371 if !extconf.emojis.is_empty() {
372 builder.emoji_array(extconf.emojis);
373 }
374 if !extconf.include_remarks.is_empty() {
375 builder.include_remarks(extconf.include_remarks);
376 }
377 if !extconf.exclude_remarks.is_empty() {
378 builder.exclude_remarks(extconf.exclude_remarks);
379 }
380 if extconf.add_emoji.is_some() {
381 builder.add_emoji(extconf.add_emoji.unwrap());
382 }
383 if extconf.remove_old_emoji.is_some() {
384 builder.remove_emoji(extconf.remove_old_emoji.unwrap());
385 }
386 }
387 Err(e) => {
388 error!("Failed to load external config from {}: {}", ext_config, e);
389 }
390 }
391 }
392
393 if let Some(include) = query.include.as_deref() {
395 if reg_valid(&include) {
396 builder.include_remarks(vec![include.to_owned()]);
397 }
398 }
399 if let Some(exclude) = query.exclude.as_deref() {
400 if reg_valid(&exclude) {
401 builder.exclude_remarks(vec![exclude.to_owned()]);
402 }
403 }
404 if let Some(emoji) = query.emoji {
405 builder.add_emoji(emoji);
406 builder.remove_emoji(true);
407 }
408
409 if let Some(add_emoji) = query.add_emoji {
410 builder.add_emoji(add_emoji);
411 }
412 if let Some(remove_emoji) = query.remove_emoji {
413 builder.remove_emoji(remove_emoji);
414 }
415 if let Some(rename) = query.rename.as_deref() {
416 if !rename.is_empty() {
417 let v_array: Vec<String> = rename.split('`').map(|s| s.to_string()).collect();
418 builder.rename_array(RegexMatchConfigs::from_ini_with_delimiter(&v_array, "@"));
419 }
420 }
421
422 if !target.is_simple() {
423 if !query
425 .groups
426 .as_deref()
427 .is_none_or(|groups| groups.is_empty())
428 && !nodelist
429 {
430 if let Some(groups) = query.groups.as_deref() {
431 let v_array: Vec<String> = groups.split('@').map(|s| s.to_string()).collect();
432 custom_group_configs = ProxyGroupConfigs::from_ini(&v_array);
433 }
434 }
435 if !query
437 .ruleset
438 .as_deref()
439 .is_none_or(|ruleset| ruleset.is_empty())
440 && !nodelist
441 {
442 if let Some(ruleset) = query.ruleset.as_deref() {
443 let v_array: Vec<String> = ruleset.split('@').map(|s| s.to_string()).collect();
444 ruleset_configs = RulesetConfigs::from_ini(&v_array);
445 }
446 }
447 }
448 builder.proxy_groups(custom_group_configs);
449 builder.ruleset_configs(ruleset_configs);
450
451 builder.group_name(query.group.clone());
457 builder.filename(query.filename.clone());
458 builder.upload(query.upload.unwrap_or_default());
459
460 let config = match builder.build() {
477 Ok(cfg) => cfg,
478 Err(e) => {
479 error!("Failed to build subconverter config: {}", e);
480 return Ok(SubResponse::error(
481 format!("Configuration error: {}", e),
482 400,
483 ));
484 }
485 };
486
487 debug!("Running subconverter with config: {:?}", config);
490 let subconverter_result = subconverter(config).await;
491
492 match subconverter_result {
493 Ok(result) => {
494 let content_type = match target {
496 SubconverterTarget::Clash
497 | SubconverterTarget::ClashR
498 | SubconverterTarget::SingBox => "application/yaml",
499 SubconverterTarget::SSSub | SubconverterTarget::SSD => "application/json",
500 _ => "text/plain",
501 };
502
503 debug!("Subconverter completed successfully");
504 Ok(SubResponse::ok(result.content, content_type.to_string())
505 .with_headers(result.headers))
506 }
507 Err(e) => {
508 error!("Subconverter error: {}", e);
509 Ok(SubResponse::error(format!("Conversion error: {}", e), 500))
510 }
511 }
512}
513
514#[cfg(target_arch = "wasm32")]
515#[wasm_bindgen]
516pub fn sub_process_wasm(query_json: &str) -> Promise {
517 let query = match serde_json::from_str::<SubconverterQuery>(query_json) {
519 Ok(q) => q,
520 Err(e) => {
521 return Promise::reject(&JsValue::from_str(&format!("Failed to parse query: {}", e)));
522 }
523 };
524
525 let query_json_string = Some(query_json.to_string());
526 let future = async move {
528 match sub_process(None, query).await {
529 Ok(response) => {
530 match serde_json::to_string(&response) {
532 Ok(json) => Ok(JsValue::from_str(&json)),
533 Err(e) => Err(JsValue::from_str(&format!(
534 "Failed to serialize response: {}",
535 e
536 ))),
537 }
538 }
539 Err(e) => Err(JsValue::from_str(&format!(
540 "Subscription processing error: {}",
541 e
542 ))),
543 }
544 };
545
546 future_to_promise(future)
548}
549
550#[cfg(target_arch = "wasm32")]
551#[wasm_bindgen]
552pub fn init_settings_wasm(pref_path: &str) -> Promise {
553 let pref_path = pref_path.to_string();
554 let future = async move {
555 match init_settings(&pref_path).await {
556 Ok(_) => Ok(JsValue::from_bool(true)),
557 Err(e) => Err(JsValue::from_str(&format!(
558 "Failed to initialize settings: {}",
559 e
560 ))),
561 }
562 };
563
564 future_to_promise(future)
565}