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    /// Request headers
110    pub request_headers: Option<HashMap<String, String>>,
111}
112
113/// Parse a query string into a HashMap
114pub 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/// Struct to represent a subscription process response
127#[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
160/// Handler for subscription conversion
161pub 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    // not initialized, in wasm that's common for cold start.
168    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    // Start building configuration
178    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                    // TODO: Check user agent and set target accordingly
187                    // if let Some(user_agent) = req.headers().get("User-Agent") {
188                    //     if let Ok(user_agent) = user_agent.to_str() {
189
190                    //         // match_user_agent(
191                    //         //     user_agent,
192                    //         //     &target,
193                    //         //      query.new_name,
194                    //         //      &query.ver);
195                    //     }
196                    // }
197                    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    // Check if we should authorize the request, if we are in API mode
223    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        // 加在前面还是加在后面
251        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    // TODO: what if urls still empty after insert?
267
268    // Create template args from request parameters and other settings
269    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    // flags
282    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    // 这部分参数有优先级:query > external > global
309    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        // In WebAssembly environment, we can't use std::thread::spawn
338        // Instead, we use the async version directly
339        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    // 请求参数的覆盖优先级最高
397    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        // loading custom groups
427        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        // loading custom rulesets
439        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    // TODO: process with the script runtime
455
456    // parse settings
457
458    // Process group name
459    builder.group_name(query.group.clone());
460    builder.filename(query.filename.clone());
461    builder.upload(query.upload.unwrap_or_default());
462
463    // // Process filter script
464    // if let Some(filter) = &query.filter {
465    //     builder = builder.filter_script(Some(filter.clone()));
466    // }
467
468    // // Process device ID
469    // if let Some(dev_id) = &query.dev_id {
470    //     builder = builder.device_id(Some(dev_id.clone()));
471    // }
472
473    // // Set managed config prefix from global settings
474    // if !global.managed_config_prefix.is_empty() {
475    //     builder =
476    // builder.managed_config_prefix(global.managed_config_prefix.clone()); }
477
478    if let Some(request_headers) = &query.request_headers {
479        builder.request_headers(request_headers.clone());
480    }
481
482    // Build and validate configuration
483    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    // Run subconverter directly instead of spawning a thread
495    // This is necessary for WebAssembly compatibility
496    debug!("Running subconverter with config: {:?}", config);
497    let subconverter_result = subconverter(config).await;
498
499    match subconverter_result {
500        Ok(result) => {
501            // Determine content type based on target
502            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    // Parse the query from JSON
525    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    // Create a future for the async sub_process
534    let future = async move {
535        match sub_process(None, query).await {
536            Ok(response) => {
537                // Convert the SubResponse to JSON string
538                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    // Convert the future to a JavaScript Promise
554    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}