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};
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/// Query parameters for subscription conversion
23#[derive(Deserialize, Serialize, Debug, Default, Clone)]
24pub struct SubconverterQuery {
25    /// Target format
26    pub target: Option<String>,
27    /// Surge version number
28    #[serde(default = "default_ver")]
29    pub ver: u32,
30    /// Clash new field name
31    pub new_name: Option<bool>,
32    /// URLs to convert (pipe separated)
33    pub url: Option<String>,
34    /// Custom group name
35    pub group: Option<String>,
36    /// Upload path (optional)
37    pub upload_path: Option<String>,
38    /// Include remarks regex, multiple regexes separated by '|'
39    pub include: Option<String>,
40    /// Exclude remarks regex, multiple regexes separated by '|'
41    pub exclude: Option<String>,
42    /// custom groups
43    pub groups: Option<String>,
44    /// Ruleset contents
45    pub ruleset: Option<String>,
46    /// External configuration file (optional)
47    pub config: Option<String>,
48
49    /// Device ID (for device-specific configurations)
50    pub dev_id: Option<String>,
51    /// Whether to insert nodes
52    pub insert: Option<bool>,
53    /// Whether to prepend insert nodes
54    pub prepend: Option<bool>,
55    /// Custom filename for download
56    pub filename: Option<String>,
57    /// Append proxy type to remarks
58    pub append_type: Option<bool>,
59    /// Whether to remove old emoji and add new emoji
60    pub emoji: Option<bool>,
61    /// Whether to add emoji
62    pub add_emoji: Option<bool>,
63    /// Whether to remove emoji
64    pub remove_emoji: Option<bool>,
65    /// List mode (node list only)
66    pub list: Option<bool>,
67    /// Sort nodes
68    pub sort: Option<bool>,
69
70    /// Sort Script
71    pub sort_script: Option<String>,
72
73    /// argFilterDeprecated
74    pub fdn: Option<bool>,
75
76    /// Information for filtering, rename, emoji addition
77    pub rename: Option<String>,
78    /// Whether to enable TCP Fast Open
79    pub tfo: Option<bool>,
80    /// Whether to enable UDP
81    pub udp: Option<bool>,
82    /// Whether to skip certificate verification
83    pub scv: Option<bool>,
84    /// Whether to enable TLS 1.3
85    pub tls13: Option<bool>,
86    /// Enable rule generator
87    pub rename_node: Option<bool>,
88    /// Update interval in seconds
89    pub interval: Option<u32>,
90    /// Update strict mode
91    pub strict: Option<bool>,
92    /// Upload to gist
93    pub upload: Option<bool>,
94    /// Authentication token
95    pub token: Option<String>,
96    /// Filter script
97    pub filter: Option<String>,
98
99    /// Clash script
100    pub script: Option<bool>,
101    pub classic: Option<bool>,
102
103    pub expand: Option<bool>,
104
105    /// Singbox specific parameters
106    #[serde(default)]
107    pub singbox: HashMap<String, String>,
108}
109
110/// Parse a query string into a HashMap
111pub 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/// Struct to represent a subscription process response
124#[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
157/// Handler for subscription conversion
158pub 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    // not initialized, in wasm that's common for cold start.
165    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    // Start building configuration
175    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                    // TODO: Check user agent and set target accordingly
184                    // if let Some(user_agent) = req.headers().get("User-Agent") {
185                    //     if let Ok(user_agent) = user_agent.to_str() {
186
187                    //         // match_user_agent(
188                    //         //     user_agent,
189                    //         //     &target,
190                    //         //      query.new_name,
191                    //         //      &query.ver);
192                    //     }
193                    // }
194                    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    // Check if we should authorize the request, if we are in API mode
220    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        // 加在前面还是加在后面
248        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    // TODO: what if urls still empty after insert?
264
265    // Create template args from request parameters and other settings
266    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    // flags
279    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    // 这部分参数有优先级:query > external > global
306    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        // In WebAssembly environment, we can't use std::thread::spawn
335        // Instead, we use the async version directly
336        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    // 请求参数的覆盖优先级最高
394    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        // loading custom groups
424        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        // loading custom rulesets
436        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    // TODO: process with the script runtime
452
453    // parse settings
454
455    // Process group name
456    builder.group_name(query.group.clone());
457    builder.filename(query.filename.clone());
458    builder.upload(query.upload.unwrap_or_default());
459
460    // // Process filter script
461    // if let Some(filter) = &query.filter {
462    //     builder = builder.filter_script(Some(filter.clone()));
463    // }
464
465    // // Process device ID
466    // if let Some(dev_id) = &query.dev_id {
467    //     builder = builder.device_id(Some(dev_id.clone()));
468    // }
469
470    // // Set managed config prefix from global settings
471    // if !global.managed_config_prefix.is_empty() {
472    //     builder = builder.managed_config_prefix(global.managed_config_prefix.clone());
473    // }
474
475    // Build and validate configuration
476    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    // Run subconverter directly instead of spawning a thread
488    // This is necessary for WebAssembly compatibility
489    debug!("Running subconverter with config: {:?}", config);
490    let subconverter_result = subconverter(config).await;
491
492    match subconverter_result {
493        Ok(result) => {
494            // Determine content type based on target
495            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    // Parse the query from JSON
518    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    // Create a future for the async sub_process
527    let future = async move {
528        match sub_process(None, query).await {
529            Ok(response) => {
530                // Convert the SubResponse to JSON string
531                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    // Convert the future to a JavaScript Promise
547    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}