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 pub request_headers: Option<HashMap<String, String>>,
111}
112
113pub fn parse_query_string(query: &str) -> HashMap<String, String> {
115 let mut params = HashMap::new();
116 for pair in query.split('&') {
117 let mut parts = pair.splitn(2, '=');
118 if let Some(key) = parts.next() {
119 let value = parts.next().unwrap_or("");
120 params.insert(key.to_string(), value.to_string());
121 }
122 }
123 params
124}
125
126#[derive(Debug, Serialize)]
128pub struct SubResponse {
129 pub content: String,
130 pub content_type: String,
131 pub headers: HashMap<String, String>,
132 pub status_code: u16,
133}
134
135impl SubResponse {
136 pub fn ok(content: String, content_type: String) -> Self {
137 Self {
138 content,
139 content_type,
140 headers: HashMap::new(),
141 status_code: 200,
142 }
143 }
144
145 pub fn error(content: String, status_code: u16) -> Self {
146 Self {
147 content,
148 content_type: "text/plain".to_string(),
149 headers: HashMap::new(),
150 status_code,
151 }
152 }
153
154 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
155 self.headers = headers;
156 self
157 }
158}
159
160pub async fn sub_process(
162 req_url: Option<String>,
163 query: SubconverterQuery,
164) -> Result<SubResponse, Box<dyn std::error::Error>> {
165 let mut global = Settings::current();
166
167 if global.pref_path.is_empty() {
169 debug!("Global config not initialized, reloading");
170 init_settings("").await?;
171 global = Settings::current();
172 } else if global.reload_conf_on_request && !global.api_mode && !global.generator_mode {
173 refresh_configuration().await;
174 global = Settings::current();
175 }
176
177 let mut builder = SubconverterConfigBuilder::new();
179
180 let target;
181 if let Some(_target) = &query.target {
182 match SubconverterTarget::from_str(&_target) {
183 Some(_target) => {
184 target = _target.clone();
185 if _target == SubconverterTarget::Auto {
186 return Ok(SubResponse::error(
198 "Auto user agent is not supported for now.".to_string(),
199 400,
200 ));
201 }
202 builder.target(_target);
203 }
204 None => {
205 return Ok(SubResponse::error(
206 "Invalid target parameter".to_string(),
207 400,
208 ));
209 }
210 }
211 } else {
212 return Ok(SubResponse::error(
213 "Missing target parameter".to_string(),
214 400,
215 ));
216 }
217
218 builder.update_interval(match query.interval {
219 Some(interval) => interval,
220 None => global.update_interval,
221 });
222 let authorized =
224 !global.api_mode || query.token.as_deref().unwrap_or_default() == global.api_access_token;
225 builder.authorized(authorized);
226 builder.update_strict(query.strict.unwrap_or(global.update_strict));
227
228 if query
229 .include
230 .clone()
231 .is_some_and(|include| REGEX_BLACK_LIST.contains(&include))
232 || query
233 .exclude
234 .clone()
235 .is_some_and(|exclude| REGEX_BLACK_LIST.contains(&exclude))
236 {
237 return Ok(SubResponse::error(
238 "Invalid regex in request!".to_string(),
239 400,
240 ));
241 }
242
243 let enable_insert = match query.insert {
244 Some(insert) => insert,
245 None => global.enable_insert,
246 };
247
248 if enable_insert {
249 builder.insert_urls(global.insert_urls.clone());
250 builder.prepend_insert(query.prepend.unwrap_or(global.prepend_insert));
252 }
253
254 let urls = match query.url.as_deref() {
255 Some(query_url) => query_url.split('|').map(|s| s.to_owned()).collect(),
256 None => {
257 if authorized {
258 global.default_urls.clone()
259 } else {
260 vec![]
261 }
262 }
263 };
264 builder.urls(urls);
265
266 let mut template_args = TemplateArgs::default();
270 template_args.global_vars = global.template_vars.clone();
271
272 template_args.request_params = query.clone();
273
274 builder.append_proxy_type(query.append_type.unwrap_or(global.append_type));
275
276 let mut arg_expand_rulesets = query.expand;
277 if target.is_clash() && query.script.is_none() {
278 arg_expand_rulesets = Some(true);
279 }
280
281 builder.tfo(query.tfo.or(global.tfo_flag));
283 builder.udp(query.udp.or(global.udp_flag));
284 builder.skip_cert_verify(query.scv.or(global.skip_cert_verify));
285 builder.tls13(query.tls13.or(global.tls13_flag));
286 builder.sort(query.sort.unwrap_or(global.enable_sort));
287 if let Some(script) = &query.sort_script {
288 builder.sort_script(script.clone());
289 }
290
291 builder.filter_deprecated(query.fdn.unwrap_or(global.filter_deprecated));
292 builder.clash_new_field_name(query.new_name.unwrap_or(global.clash_use_new_field));
293 builder.clash_script(query.script.unwrap_or_default());
294 builder.clash_classical_ruleset(query.classic.unwrap_or_default());
295 let nodelist = query.list.unwrap_or_default();
296 builder.nodelist(nodelist);
297
298 if arg_expand_rulesets != Some(true) {
299 builder.clash_new_field_name(true);
300 } else {
301 builder.managed_config_prefix(global.managed_config_prefix.clone());
302 builder.clash_script(false);
303 }
304
305 let mut ruleset_configs = global.custom_rulesets.clone();
306 let mut custom_group_configs = global.custom_proxy_groups.clone();
307
308 builder.include_remarks(global.include_remarks.clone());
310 builder.exclude_remarks(global.exclude_remarks.clone());
311 builder.rename_array(global.renames.clone());
312 builder.emoji_array(global.emojis.clone());
313 builder.add_emoji(global.add_emoji);
314 builder.remove_emoji(global.remove_emoji);
315 builder.enable_rule_generator(global.enable_rule_gen);
316 let mut rule_bases = RuleBases {
317 clash_rule_base: global.clash_base.clone(),
318 surge_rule_base: global.surge_base.clone(),
319 surfboard_rule_base: global.surfboard_base.clone(),
320 mellow_rule_base: global.mellow_base.clone(),
321 quan_rule_base: global.quan_base.clone(),
322 quanx_rule_base: global.quanx_base.clone(),
323 loon_rule_base: global.loon_base.clone(),
324 sssub_rule_base: global.ssub_base.clone(),
325 singbox_rule_base: global.singbox_base.clone(),
326 };
327 builder.rule_bases(rule_bases.clone());
328 builder.template_args(template_args.clone());
329
330 let ext_config = match query.config.as_deref() {
331 Some(config) => config.to_owned(),
332 None => global.default_ext_config.clone(),
333 };
334 if !ext_config.is_empty() {
335 debug!("Loading external config from {}", ext_config);
336
337 let extconf_result = ExternalSettings::load_from_file(&ext_config).await;
340
341 match extconf_result {
342 Ok(extconf) => {
343 debug!("Successfully loaded external config from {}", ext_config);
344 if !nodelist {
345 rule_bases
346 .check_external_bases(&extconf, &global.base_path)
347 .await;
348 builder.rule_bases(rule_bases);
349
350 if let Some(tpl_args) = extconf.tpl_args {
351 template_args.local_vars = tpl_args;
352 }
353
354 builder.template_args(template_args);
355
356 if !target.is_simple() {
357 if !extconf.custom_rulesets.is_empty() {
358 ruleset_configs = extconf.custom_rulesets;
359 }
360 if !extconf.custom_proxy_groups.is_empty() {
361 custom_group_configs = extconf.custom_proxy_groups;
362 }
363 if let Some(enable_rule_gen) = extconf.enable_rule_generator {
364 builder.enable_rule_generator(enable_rule_gen);
365 }
366 if let Some(overwrite_original_rules) = extconf.overwrite_original_rules {
367 builder.overwrite_original_rules(overwrite_original_rules);
368 }
369 }
370 }
371 if !extconf.rename_nodes.is_empty() {
372 builder.rename_array(extconf.rename_nodes);
373 }
374 if !extconf.emojis.is_empty() {
375 builder.emoji_array(extconf.emojis);
376 }
377 if !extconf.include_remarks.is_empty() {
378 builder.include_remarks(extconf.include_remarks);
379 }
380 if !extconf.exclude_remarks.is_empty() {
381 builder.exclude_remarks(extconf.exclude_remarks);
382 }
383 if extconf.add_emoji.is_some() {
384 builder.add_emoji(extconf.add_emoji.unwrap());
385 }
386 if extconf.remove_old_emoji.is_some() {
387 builder.remove_emoji(extconf.remove_old_emoji.unwrap());
388 }
389 }
390 Err(e) => {
391 error!("Failed to load external config from {}: {}", ext_config, e);
392 }
393 }
394 }
395
396 if let Some(include) = query.include.as_deref() {
398 if reg_valid(&include) {
399 builder.include_remarks(vec![include.to_owned()]);
400 }
401 }
402 if let Some(exclude) = query.exclude.as_deref() {
403 if reg_valid(&exclude) {
404 builder.exclude_remarks(vec![exclude.to_owned()]);
405 }
406 }
407 if let Some(emoji) = query.emoji {
408 builder.add_emoji(emoji);
409 builder.remove_emoji(true);
410 }
411
412 if let Some(add_emoji) = query.add_emoji {
413 builder.add_emoji(add_emoji);
414 }
415 if let Some(remove_emoji) = query.remove_emoji {
416 builder.remove_emoji(remove_emoji);
417 }
418 if let Some(rename) = query.rename.as_deref() {
419 if !rename.is_empty() {
420 let v_array: Vec<String> = rename.split('`').map(|s| s.to_string()).collect();
421 builder.rename_array(RegexMatchConfigs::from_ini_with_delimiter(&v_array, "@"));
422 }
423 }
424
425 if !target.is_simple() {
426 if !query
428 .groups
429 .as_deref()
430 .is_none_or(|groups| groups.is_empty())
431 && !nodelist
432 {
433 if let Some(groups) = query.groups.as_deref() {
434 let v_array: Vec<String> = groups.split('@').map(|s| s.to_string()).collect();
435 custom_group_configs = ProxyGroupConfigs::from_ini(&v_array);
436 }
437 }
438 if !query
440 .ruleset
441 .as_deref()
442 .is_none_or(|ruleset| ruleset.is_empty())
443 && !nodelist
444 {
445 if let Some(ruleset) = query.ruleset.as_deref() {
446 let v_array: Vec<String> = ruleset.split('@').map(|s| s.to_string()).collect();
447 ruleset_configs = RulesetConfigs::from_ini(&v_array);
448 }
449 }
450 }
451 builder.proxy_groups(custom_group_configs);
452 builder.ruleset_configs(ruleset_configs);
453
454 builder.group_name(query.group.clone());
460 builder.filename(query.filename.clone());
461 builder.upload(query.upload.unwrap_or_default());
462
463 if let Some(request_headers) = &query.request_headers {
479 builder.request_headers(request_headers.clone());
480 }
481
482 let config = match builder.build() {
484 Ok(cfg) => cfg,
485 Err(e) => {
486 error!("Failed to build subconverter config: {}", e);
487 return Ok(SubResponse::error(
488 format!("Configuration error: {}", e),
489 400,
490 ));
491 }
492 };
493
494 debug!("Running subconverter with config: {:?}", config);
497 let subconverter_result = subconverter(config).await;
498
499 match subconverter_result {
500 Ok(result) => {
501 let content_type = match target {
503 SubconverterTarget::Clash
504 | SubconverterTarget::ClashR
505 | SubconverterTarget::SingBox => "application/yaml",
506 SubconverterTarget::SSSub | SubconverterTarget::SSD => "application/json",
507 _ => "text/plain",
508 };
509
510 debug!("Subconverter completed successfully");
511 Ok(SubResponse::ok(result.content, content_type.to_string())
512 .with_headers(result.headers))
513 }
514 Err(e) => {
515 error!("Subconverter error: {}", e);
516 Ok(SubResponse::error(format!("Conversion error: {}", e), 500))
517 }
518 }
519}
520
521#[cfg(target_arch = "wasm32")]
522#[wasm_bindgen]
523pub fn sub_process_wasm(query_json: &str) -> Promise {
524 let query = match serde_json::from_str::<SubconverterQuery>(query_json) {
526 Ok(q) => q,
527 Err(e) => {
528 return Promise::reject(&JsValue::from_str(&format!("Failed to parse query: {}", e)));
529 }
530 };
531
532 let query_json_string = Some(query_json.to_string());
533 let future = async move {
535 match sub_process(None, query).await {
536 Ok(response) => {
537 match serde_json::to_string(&response) {
539 Ok(json) => Ok(JsValue::from_str(&json)),
540 Err(e) => Err(JsValue::from_str(&format!(
541 "Failed to serialize response: {}",
542 e
543 ))),
544 }
545 }
546 Err(e) => Err(JsValue::from_str(&format!(
547 "Subscription processing error: {}",
548 e
549 ))),
550 }
551 };
552
553 future_to_promise(future)
555}
556
557#[cfg(target_arch = "wasm32")]
558#[wasm_bindgen]
559pub fn init_settings_wasm(pref_path: &str) -> Promise {
560 let pref_path = pref_path.to_string();
561 let future = async move {
562 match init_settings(&pref_path).await {
563 Ok(_) => Ok(JsValue::from_bool(true)),
564 Err(e) => Err(JsValue::from_str(&format!(
565 "Failed to initialize settings: {}",
566 e
567 ))),
568 }
569 };
570
571 future_to_promise(future)
572}