libsubconverter/api/
sub.rs

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, UploadStatus};
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// START Helper function for deserializing boolean-like values
23mod bool_deserializer {
24    use serde::{self, Deserialize, Deserializer};
25
26    pub fn deserialize_option_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
27    where
28        D: Deserializer<'de>,
29    {
30        #[derive(Deserialize)]
31        #[serde(untagged)]
32        enum BoolOrString {
33            Bool(bool),
34            String(String),
35            Int(i64),
36        }
37
38        match Option::<BoolOrString>::deserialize(deserializer)? {
39            Some(BoolOrString::Bool(b)) => Ok(Some(b)),
40            Some(BoolOrString::Int(i)) => match i {
41                0 => Ok(Some(false)),
42                1 => Ok(Some(true)),
43                _ => Ok(None), /* Or return an error: Err(serde::de::Error::custom("invalid
44                                * integer for bool")) */
45            },
46            Some(BoolOrString::String(s)) => match s.to_lowercase().as_str() {
47                "true" | "yes" | "1" | "on" => Ok(Some(true)),
48                "false" | "no" | "0" | "off" => Ok(Some(false)),
49                _ => Ok(None), /* Or return an error:
50                                * Err(serde::de::Error::custom(format!("invalid string for bool:
51                                * {}", s))) */
52            },
53            None => Ok(None),
54        }
55    }
56}
57// END Helper function
58
59/// Query parameters for subscription conversion
60#[derive(Deserialize, Serialize, Debug, Default, Clone)]
61pub struct SubconverterQuery {
62    /// Target format
63    pub target: Option<String>,
64    /// Surge version number
65    #[serde(default = "default_ver")]
66    pub ver: u32,
67    /// Clash new field name
68    #[serde(
69        default,
70        deserialize_with = "bool_deserializer::deserialize_option_bool"
71    )]
72    pub new_name: Option<bool>,
73    /// URLs to convert (pipe separated)
74    pub url: Option<String>,
75    /// Custom group name
76    pub group: Option<String>,
77    /// Upload path (optional)
78    pub upload_path: Option<String>,
79    /// Include remarks regex, multiple regexes separated by '|'
80    pub include: Option<String>,
81    /// Exclude remarks regex, multiple regexes separated by '|'
82    pub exclude: Option<String>,
83    /// custom groups
84    pub groups: Option<String>,
85    /// Ruleset contents
86    pub ruleset: Option<String>,
87    /// External configuration file (optional)
88    pub config: Option<String>,
89
90    /// Device ID (for device-specific configurations)
91    pub dev_id: Option<String>,
92    /// Whether to insert nodes
93    #[serde(
94        default,
95        deserialize_with = "bool_deserializer::deserialize_option_bool"
96    )]
97    pub insert: Option<bool>,
98    /// Whether to prepend insert nodes
99    #[serde(
100        default,
101        deserialize_with = "bool_deserializer::deserialize_option_bool"
102    )]
103    pub prepend: Option<bool>,
104    /// Custom filename for download
105    pub filename: Option<String>,
106    /// Append proxy type to remarks
107    #[serde(
108        default,
109        deserialize_with = "bool_deserializer::deserialize_option_bool"
110    )]
111    pub append_type: Option<bool>,
112    /// Whether to remove old emoji and add new emoji
113    #[serde(
114        default,
115        deserialize_with = "bool_deserializer::deserialize_option_bool"
116    )]
117    pub emoji: Option<bool>,
118    /// Whether to add emoji
119    #[serde(
120        default,
121        deserialize_with = "bool_deserializer::deserialize_option_bool"
122    )]
123    pub add_emoji: Option<bool>,
124    /// Whether to remove emoji
125    #[serde(
126        default,
127        deserialize_with = "bool_deserializer::deserialize_option_bool"
128    )]
129    pub remove_emoji: Option<bool>,
130    /// List mode (node list only)
131    #[serde(
132        default,
133        deserialize_with = "bool_deserializer::deserialize_option_bool"
134    )]
135    pub list: Option<bool>,
136    /// Sort nodes
137    #[serde(
138        default,
139        deserialize_with = "bool_deserializer::deserialize_option_bool"
140    )]
141    pub sort: Option<bool>,
142
143    /// Sort Script
144    pub sort_script: Option<String>,
145
146    /// argFilterDeprecated
147    #[serde(
148        default,
149        deserialize_with = "bool_deserializer::deserialize_option_bool"
150    )]
151    pub fdn: Option<bool>,
152
153    /// Information for filtering, rename, emoji addition
154    pub rename: Option<String>,
155    /// Whether to enable TCP Fast Open
156    #[serde(
157        default,
158        deserialize_with = "bool_deserializer::deserialize_option_bool"
159    )]
160    pub tfo: Option<bool>,
161    /// Whether to enable UDP
162    #[serde(
163        default,
164        deserialize_with = "bool_deserializer::deserialize_option_bool"
165    )]
166    pub udp: Option<bool>,
167    /// Whether to skip certificate verification
168    #[serde(
169        default,
170        deserialize_with = "bool_deserializer::deserialize_option_bool"
171    )]
172    pub scv: Option<bool>,
173    /// Whether to enable TLS 1.3
174    #[serde(
175        default,
176        deserialize_with = "bool_deserializer::deserialize_option_bool"
177    )]
178    pub tls13: Option<bool>,
179    /// Enable rule generator
180    #[serde(
181        default,
182        deserialize_with = "bool_deserializer::deserialize_option_bool"
183    )]
184    pub rename_node: Option<bool>,
185    /// Update interval in seconds
186    pub interval: Option<u32>,
187    /// Update strict mode
188    #[serde(
189        default,
190        deserialize_with = "bool_deserializer::deserialize_option_bool"
191    )]
192    pub strict: Option<bool>,
193    /// Upload to gist
194    #[serde(
195        default,
196        deserialize_with = "bool_deserializer::deserialize_option_bool"
197    )]
198    pub upload: Option<bool>,
199    /// Authentication token
200    pub token: Option<String>,
201    /// Filter script
202    pub filter: Option<String>,
203
204    /// Clash script
205    #[serde(
206        default,
207        deserialize_with = "bool_deserializer::deserialize_option_bool"
208    )]
209    pub script: Option<bool>,
210    #[serde(
211        default,
212        deserialize_with = "bool_deserializer::deserialize_option_bool"
213    )]
214    pub classic: Option<bool>,
215
216    #[serde(
217        default,
218        deserialize_with = "bool_deserializer::deserialize_option_bool"
219    )]
220    pub expand: Option<bool>,
221
222    /// Singbox specific parameters
223    #[serde(default)]
224    pub singbox: HashMap<String, String>,
225
226    /// Request headers
227    pub request_headers: Option<HashMap<String, String>>,
228}
229
230/// Parse a query string into a HashMap
231pub fn parse_query_string(query: &str) -> HashMap<String, String> {
232    let mut params = HashMap::new();
233    for pair in query.split('&') {
234        let mut parts = pair.splitn(2, '=');
235        if let Some(key) = parts.next() {
236            let value = parts.next().unwrap_or("");
237            params.insert(key.to_string(), value.to_string());
238        }
239    }
240    params
241}
242
243/// Struct to represent a subscription process response
244#[derive(Debug, Serialize)]
245pub struct SubResponse {
246    pub content: String,
247    pub content_type: String,
248    pub headers: HashMap<String, String>,
249    pub status_code: u16,
250    #[serde(skip_serializing_if = "is_not_attempted")] // Don't include if upload wasn't attempted
251    pub upload_status: UploadStatus,
252}
253
254// Helper function for skip_serializing_if
255fn is_not_attempted(status: &UploadStatus) -> bool {
256    matches!(status, UploadStatus::NotAttempted)
257}
258
259impl SubResponse {
260    pub fn ok(content: String, content_type: String) -> Self {
261        Self {
262            content,
263            content_type,
264            headers: HashMap::new(),
265            status_code: 200,
266            upload_status: UploadStatus::NotAttempted, // Default to not attempted
267        }
268    }
269
270    pub fn error(content: String, status_code: u16) -> Self {
271        Self {
272            content,
273            content_type: "text/plain".to_string(),
274            headers: HashMap::new(),
275            status_code,
276            upload_status: UploadStatus::NotAttempted, // Default to not attempted
277        }
278    }
279
280    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
281        self.headers = headers;
282        self
283    }
284
285    pub fn with_upload_status(mut self, status: UploadStatus) -> Self {
286        self.upload_status = status;
287        self
288    }
289}
290
291/// Handler for subscription conversion
292pub async fn sub_process(
293    req_url: Option<String>,
294    query: SubconverterQuery,
295) -> Result<SubResponse, Box<dyn std::error::Error>> {
296    let mut global = Settings::current();
297
298    // not initialized, in wasm that's common for cold start.
299    if global.pref_path.is_empty() {
300        debug!("Global config not initialized, reloading");
301        init_settings("").await?;
302        global = Settings::current();
303    } else if global.reload_conf_on_request && !global.api_mode && !global.generator_mode {
304        refresh_configuration().await;
305        global = Settings::current();
306    }
307
308    // Start building configuration
309    let mut builder = SubconverterConfigBuilder::new();
310
311    let target;
312    if let Some(_target) = &query.target {
313        match SubconverterTarget::from_str(&_target) {
314            Some(_target) => {
315                target = _target.clone();
316                if _target == SubconverterTarget::Auto {
317                    // TODO: Check user agent and set target accordingly
318                    // if let Some(user_agent) = req.headers().get("User-Agent") {
319                    //     if let Ok(user_agent) = user_agent.to_str() {
320
321                    //         // match_user_agent(
322                    //         //     user_agent,
323                    //         //     &target,
324                    //         //      query.new_name,
325                    //         //      &query.ver);
326                    //     }
327                    // }
328                    return Ok(SubResponse::error(
329                        "Auto user agent is not supported for now.".to_string(),
330                        400,
331                    ));
332                }
333                builder.target(_target);
334            }
335            None => {
336                return Ok(SubResponse::error(
337                    "Invalid target parameter".to_string(),
338                    400,
339                ));
340            }
341        }
342    } else {
343        return Ok(SubResponse::error(
344            "Missing target parameter".to_string(),
345            400,
346        ));
347    }
348
349    builder.update_interval(match query.interval {
350        Some(interval) => interval,
351        None => global.update_interval,
352    });
353    // Check if we should authorize the request, if we are in API mode
354    #[cfg(not(feature = "js-runtime"))]
355    let authorized = false;
356
357    #[cfg(feature = "js-runtime")]
358    let authorized =
359        !global.api_mode || query.token.as_deref().unwrap_or_default() == global.api_access_token;
360    builder.authorized(authorized);
361    builder.update_strict(query.strict.unwrap_or(global.update_strict));
362
363    if query
364        .include
365        .clone()
366        .is_some_and(|include| REGEX_BLACK_LIST.contains(&include))
367        || query
368            .exclude
369            .clone()
370            .is_some_and(|exclude| REGEX_BLACK_LIST.contains(&exclude))
371    {
372        return Ok(SubResponse::error(
373            "Invalid regex in request!".to_string(),
374            400,
375        ));
376    }
377
378    let enable_insert = match query.insert {
379        Some(insert) => insert,
380        None => global.enable_insert,
381    };
382
383    if enable_insert {
384        builder.insert_urls(global.insert_urls.clone());
385        // 加在前面还是加在后面
386        builder.prepend_insert(query.prepend.unwrap_or(global.prepend_insert));
387    }
388
389    let urls = match query.url.as_deref() {
390        Some(query_url) => query_url.split('|').map(|s| s.to_owned()).collect(),
391        None => {
392            if authorized {
393                global.default_urls.clone()
394            } else {
395                vec![]
396            }
397        }
398    };
399    builder.urls(urls);
400
401    // TODO: what if urls still empty after insert?
402
403    // Create template args from request parameters and other settings
404    let mut template_args = TemplateArgs::default();
405    template_args.global_vars = global.template_vars.clone();
406
407    template_args.request_params = query.clone();
408
409    builder.append_proxy_type(query.append_type.unwrap_or(global.append_type));
410
411    let mut arg_expand_rulesets = query.expand;
412    if target.is_clash() && query.script.is_none() {
413        arg_expand_rulesets = Some(true);
414    }
415
416    // flags
417    builder.tfo(query.tfo.or(global.tfo_flag));
418    builder.udp(query.udp.or(global.udp_flag));
419    builder.skip_cert_verify(query.scv.or(global.skip_cert_verify));
420    builder.tls13(query.tls13.or(global.tls13_flag));
421    builder.sort(query.sort.unwrap_or(global.enable_sort));
422    builder.sort_script(query.sort_script.unwrap_or(global.sort_script.clone()));
423
424    builder.filter_deprecated(query.fdn.unwrap_or(global.filter_deprecated));
425    builder.clash_new_field_name(query.new_name.unwrap_or(global.clash_use_new_field));
426    builder.clash_script(query.script.unwrap_or_default());
427    builder.clash_classical_ruleset(query.classic.unwrap_or_default());
428    let nodelist = query.list.unwrap_or_default();
429    builder.nodelist(nodelist);
430
431    if arg_expand_rulesets != Some(true) {
432        builder.clash_new_field_name(true);
433    } else {
434        builder.managed_config_prefix(global.managed_config_prefix.clone());
435        builder.clash_script(false);
436    }
437
438    let mut ruleset_configs = global.custom_rulesets.clone();
439    let mut custom_group_configs = global.custom_proxy_groups.clone();
440
441    // 这部分参数有优先级:query > external > global
442    builder.include_remarks(global.include_remarks.clone());
443    builder.exclude_remarks(global.exclude_remarks.clone());
444    builder.rename_array(global.renames.clone());
445    builder.emoji_array(global.emojis.clone());
446    builder.add_emoji(global.add_emoji);
447    builder.remove_emoji(global.remove_emoji);
448    builder.enable_rule_generator(global.enable_rule_gen);
449    let mut rule_bases = RuleBases {
450        clash_rule_base: global.clash_base.clone(),
451        surge_rule_base: global.surge_base.clone(),
452        surfboard_rule_base: global.surfboard_base.clone(),
453        mellow_rule_base: global.mellow_base.clone(),
454        quan_rule_base: global.quan_base.clone(),
455        quanx_rule_base: global.quanx_base.clone(),
456        loon_rule_base: global.loon_base.clone(),
457        sssub_rule_base: global.ssub_base.clone(),
458        singbox_rule_base: global.singbox_base.clone(),
459    };
460    builder.rule_bases(rule_bases.clone());
461    builder.template_args(template_args.clone());
462
463    let ext_config = match query.config.as_deref() {
464        Some(config) => config.to_owned(),
465        None => global.default_ext_config.clone(),
466    };
467    if !ext_config.is_empty() {
468        debug!("Loading external config from {}", ext_config);
469
470        // In WebAssembly environment, we can't use std::thread::spawn
471        // Instead, we use the async version directly
472        let extconf_result = ExternalSettings::load_from_file(&ext_config).await;
473
474        match extconf_result {
475            Ok(extconf) => {
476                debug!("Successfully loaded external config from {}", ext_config);
477                if !nodelist {
478                    rule_bases
479                        .check_external_bases(&extconf, &global.base_path)
480                        .await;
481                    builder.rule_bases(rule_bases);
482
483                    if let Some(tpl_args) = extconf.tpl_args {
484                        template_args.local_vars = tpl_args;
485                    }
486
487                    builder.template_args(template_args);
488
489                    if !target.is_simple() {
490                        if !extconf.custom_rulesets.is_empty() {
491                            ruleset_configs = extconf.custom_rulesets;
492                        }
493                        if !extconf.custom_proxy_groups.is_empty() {
494                            custom_group_configs = extconf.custom_proxy_groups;
495                        }
496                        if let Some(enable_rule_gen) = extconf.enable_rule_generator {
497                            builder.enable_rule_generator(enable_rule_gen);
498                        }
499                        if let Some(overwrite_original_rules) = extconf.overwrite_original_rules {
500                            builder.overwrite_original_rules(overwrite_original_rules);
501                        }
502                    }
503                }
504                if !extconf.rename_nodes.is_empty() {
505                    builder.rename_array(extconf.rename_nodes);
506                }
507                if !extconf.emojis.is_empty() {
508                    builder.emoji_array(extconf.emojis);
509                }
510                if !extconf.include_remarks.is_empty() {
511                    builder.include_remarks(extconf.include_remarks);
512                }
513                if !extconf.exclude_remarks.is_empty() {
514                    builder.exclude_remarks(extconf.exclude_remarks);
515                }
516                if extconf.add_emoji.is_some() {
517                    builder.add_emoji(extconf.add_emoji.unwrap());
518                }
519                if extconf.remove_old_emoji.is_some() {
520                    builder.remove_emoji(extconf.remove_old_emoji.unwrap());
521                }
522            }
523            Err(e) => {
524                error!("Failed to load external config from {}: {}", ext_config, e);
525            }
526        }
527    }
528
529    // 请求参数的覆盖优先级最高
530    if let Some(include) = query.include.as_deref() {
531        if reg_valid(&include) {
532            builder.include_remarks(vec![include.to_owned()]);
533        }
534    }
535    if let Some(exclude) = query.exclude.as_deref() {
536        if reg_valid(&exclude) {
537            builder.exclude_remarks(vec![exclude.to_owned()]);
538        }
539    }
540    if let Some(emoji) = query.emoji {
541        builder.add_emoji(emoji);
542        builder.remove_emoji(true);
543    }
544
545    if let Some(add_emoji) = query.add_emoji {
546        builder.add_emoji(add_emoji);
547    }
548    if let Some(remove_emoji) = query.remove_emoji {
549        builder.remove_emoji(remove_emoji);
550    }
551    if let Some(rename) = query.rename.as_deref() {
552        if !rename.is_empty() {
553            let v_array: Vec<String> = rename.split('`').map(|s| s.to_string()).collect();
554            builder.rename_array(RegexMatchConfigs::from_ini_with_delimiter(&v_array, "@"));
555        }
556    }
557
558    if !target.is_simple() {
559        // loading custom groups
560        if !query
561            .groups
562            .as_deref()
563            .is_none_or(|groups| groups.is_empty())
564            && !nodelist
565        {
566            if let Some(groups) = query.groups.as_deref() {
567                let v_array: Vec<String> = groups.split('@').map(|s| s.to_string()).collect();
568                custom_group_configs = ProxyGroupConfigs::from_ini(&v_array);
569            }
570        }
571        // loading custom rulesets
572        if !query
573            .ruleset
574            .as_deref()
575            .is_none_or(|ruleset| ruleset.is_empty())
576            && !nodelist
577        {
578            if let Some(ruleset) = query.ruleset.as_deref() {
579                let v_array: Vec<String> = ruleset.split('@').map(|s| s.to_string()).collect();
580                ruleset_configs = RulesetConfigs::from_ini(&v_array);
581            }
582        }
583    }
584    builder.proxy_groups(custom_group_configs);
585    builder.ruleset_configs(ruleset_configs);
586
587    // TODO: process with the script runtime
588
589    // parse settings
590
591    // Process group name
592    builder.group_name(query.group.clone());
593    builder.filename(query.filename.clone());
594    builder.upload(query.upload.unwrap_or_default());
595
596    // Process filter script
597    let filter = query.filter.unwrap_or(global.filter_script.clone());
598    if !filter.is_empty() {
599        builder.filter_script(Some(filter));
600    }
601
602    // // Process device ID
603    // if let Some(dev_id) = &query.dev_id {
604    //     builder.device_id(Some(dev_id.clone()));
605    // }
606
607    // // Set managed config prefix from global settings
608    // if !global.managed_config_prefix.is_empty() {
609    //     builder =
610    // builder.managed_config_prefix(global.managed_config_prefix.clone()); }
611
612    if let Some(request_headers) = &query.request_headers {
613        builder.request_headers(request_headers.clone());
614    }
615
616    // Build and validate configuration
617    let config = match builder.build() {
618        Ok(cfg) => cfg,
619        Err(e) => {
620            error!("Failed to build subconverter config: {}", e);
621            return Ok(SubResponse::error(
622                format!("Configuration error: {}", e),
623                400,
624            ));
625        }
626    };
627
628    // Run subconverter directly instead of spawning a thread
629    // This is necessary for WebAssembly compatibility
630    debug!("Running subconverter with config: {:?}", config);
631    let subconverter_result = subconverter(config).await;
632
633    match subconverter_result {
634        Ok(result) => {
635            // Determine content type based on target
636            let content_type = match target {
637                SubconverterTarget::Clash
638                | SubconverterTarget::ClashR
639                | SubconverterTarget::SingBox => "application/yaml",
640                SubconverterTarget::SSSub | SubconverterTarget::SSD => "application/json",
641                _ => "text/plain",
642            };
643
644            debug!("Subconverter completed successfully");
645            Ok(SubResponse::ok(result.content, content_type.to_string())
646                .with_headers(result.headers)
647                .with_upload_status(result.upload_status))
648        }
649        Err(e) => {
650            error!("Subconverter error: {}", e);
651            Ok(SubResponse::error(format!("Conversion error: {}", e), 500))
652        }
653    }
654}
655
656#[cfg(target_arch = "wasm32")]
657#[wasm_bindgen]
658pub fn sub_process_wasm(query_json: &str) -> Promise {
659    // Parse the query from JSON
660    let query = match serde_json::from_str::<SubconverterQuery>(query_json) {
661        Ok(q) => q,
662        Err(e) => {
663            return Promise::reject(&JsValue::from_str(&format!("Failed to parse query: {}", e)));
664        }
665    };
666
667    let query_json_string = Some(query_json.to_string());
668    // Create a future for the async sub_process
669    let future = async move {
670        match sub_process(None, query).await {
671            Ok(response) => {
672                // Convert the SubResponse to JSON string
673                match serde_json::to_string(&response) {
674                    Ok(json) => Ok(JsValue::from_str(&json)),
675                    Err(e) => Err(JsValue::from_str(&format!(
676                        "Failed to serialize response: {}",
677                        e
678                    ))),
679                }
680            }
681            Err(e) => Err(JsValue::from_str(&format!(
682                "Subscription processing error: {}",
683                e
684            ))),
685        }
686    };
687
688    // Convert the future to a JavaScript Promise
689    future_to_promise(future)
690}
691
692#[cfg(target_arch = "wasm32")]
693#[wasm_bindgen]
694pub fn init_settings_wasm(pref_path: &str) -> Promise {
695    let pref_path = pref_path.to_string();
696    let future = async move {
697        match init_settings(&pref_path).await {
698            Ok(_) => Ok(JsValue::from_bool(true)),
699            Err(e) => Err(JsValue::from_str(&format!(
700                "Failed to initialize settings: {}",
701                e
702            ))),
703        }
704    };
705
706    future_to_promise(future)
707}