libsubconverter/parser/
subparser.rs

1use crate::models::Proxy;
2use crate::parser::explodes::*;
3use crate::parser::infoparser::{get_sub_info_from_nodes, get_sub_info_from_ssd};
4use crate::parser::parse_settings::ParseSettings;
5use crate::utils::http::get_sub_info_from_header;
6use crate::utils::matcher::{apply_matcher, reg_find};
7use crate::utils::network::is_link;
8use crate::utils::url::url_decode;
9use crate::utils::{file_exists, file_get_async, web_get_async};
10use log::warn;
11
12/// Equivalent to ConfType enum in C++
13#[derive(Debug, PartialEq, Eq)]
14pub enum ConfType {
15    SOCKS,
16    HTTP,
17    SUB,
18    Netch,
19    Local,
20    Unknown,
21}
22
23/// Transform of C++ addNodes function
24/// Adds nodes from a link to the provided vector
25///
26/// # Arguments
27/// * `link` - Link to parse for proxies
28/// * `all_nodes` - Vector to add nodes to
29/// * `group_id` - Group ID to assign to nodes
30/// * `parse_settings` - Settings for parsing
31///
32/// # Returns
33/// * `Ok(())` on success
34/// * `Err(String)` with error message on failure
35pub async fn add_nodes(
36    mut link: String,
37    all_nodes: &mut Vec<Proxy>,
38    group_id: i32,
39    parse_settings: &mut ParseSettings,
40) -> Result<(), String> {
41    // Extract references to settings for easier access
42    let proxy = &parse_settings.proxy;
43    let exclude_remarks = parse_settings.exclude_remarks.as_ref();
44    let include_remarks = parse_settings.include_remarks.as_ref();
45    let stream_rules = parse_settings.stream_rules.as_ref();
46    let time_rules = parse_settings.time_rules.as_ref();
47    let request_header = parse_settings.request_header.as_ref();
48    let authorized = parse_settings.authorized;
49
50    // Variables to store data during processing
51    let mut nodes: Vec<Proxy> = Vec::new();
52    let mut node = Proxy::default();
53    let mut custom_group = String::new();
54
55    // Clean up the link string - remove quotes
56    link = link.replace("\"", "");
57
58    // Handle JavaScript scripts (Not implementing JS support here)
59    #[cfg(feature = "js_runtime")]
60    if authorized && link.starts_with("script:") {
61        // Script processing would go here
62        return Err("Script processing not implemented".to_string());
63    }
64
65    // Handle tag: prefix for custom group
66    if link.starts_with("tag:") {
67        if let Some(pos) = link.find(',') {
68            custom_group = link[4..pos].to_string();
69            link = link[pos + 1..].to_string();
70        }
71    }
72
73    // Handle null node
74    if link == "nullnode" {
75        let mut null_node = Proxy::default();
76        null_node.group_id = 0;
77        all_nodes.push(null_node);
78        return Ok(());
79    }
80
81    // Determine link type
82    let link_type = if link.starts_with("https://t.me/socks") || link.starts_with("tg://socks") {
83        ConfType::SOCKS
84    } else if link.starts_with("https://t.me/http") || link.starts_with("tg://http") {
85        ConfType::HTTP
86    } else if is_link(&link) || link.starts_with("surge:///install-config") {
87        ConfType::SUB
88    } else if link.starts_with("Netch://") {
89        ConfType::Netch
90    } else if file_exists(&link).await {
91        ConfType::Local
92    } else {
93        // Default to Unknown for direct proxy links or invalid links
94        ConfType::Unknown
95    };
96
97    match link_type {
98        ConfType::SUB => {
99            // Handle subscription links
100            if link.starts_with("surge:///install-config") {
101                // Extract URL from Surge config link
102                if let Some(url_arg) = get_url_arg(&link, "url") {
103                    link = url_decode(&url_arg);
104                }
105            }
106
107            // Download subscription content
108            let response = match web_get_async(&link, proxy, request_header).await {
109                Ok(response) => response,
110                Err(e) => {
111                    warn!("Failed to get subscription content from {}: {}", link, e);
112                    return Err(format!("HTTP request failed: {}", e));
113                }
114            };
115
116            let sub_content = response.body;
117            let headers = response.headers;
118
119            if !sub_content.is_empty() {
120                // Parse the subscription content
121                let result = explode_conf_content(&sub_content, &mut nodes);
122                if result > 0 {
123                    // Get subscription info
124                    if sub_content.starts_with("ssd://") {
125                        // Extract info from SSD subscription
126                        if let Some(info) = get_sub_info_from_ssd(&sub_content) {
127                            parse_settings.sub_info = Some(info);
128                        }
129                    } else {
130                        // Try to get info from header first
131                        let header_info = get_sub_info_from_header(&headers);
132                        if !header_info.is_empty() {
133                            parse_settings.sub_info = Some(header_info);
134                        } else {
135                            // If no header info, try from nodes
136                            if let (Some(stream_rules_unwrapped), Some(time_rules_unwrapped)) =
137                                (stream_rules, time_rules)
138                            {
139                                if let Some(info) = get_sub_info_from_nodes(
140                                    &nodes,
141                                    stream_rules_unwrapped,
142                                    time_rules_unwrapped,
143                                ) {
144                                    parse_settings.sub_info = Some(info);
145                                }
146                            }
147                        }
148                    }
149
150                    // Filter nodes and set group info
151                    filter_nodes(&mut nodes, exclude_remarks, include_remarks, group_id);
152
153                    // Set group_id and custom_group for all nodes
154                    for node in &mut nodes {
155                        node.group_id = group_id;
156                        if !custom_group.is_empty() {
157                            node.group = custom_group.clone();
158                        }
159                    }
160
161                    // Add nodes to result vector
162                    all_nodes.append(&mut nodes);
163                    Ok(())
164                } else {
165                    Err(format!("Invalid subscription: '{}'", sub_content))
166                }
167            } else {
168                Err("Cannot download subscription data".to_string())
169            }
170        }
171        ConfType::Local => {
172            if !authorized {
173                return Err("Not authorized to access local files".to_string());
174            }
175
176            // Read and parse local file
177            let result = explode_conf(&link, &mut nodes).await;
178            if result > 0 {
179                // The rest is similar to SUB case
180                // Get subscription info
181                if link.starts_with("ssd://") {
182                    // Extract info from SSD subscription
183                    if let Some(info) = get_sub_info_from_ssd(&link) {
184                        parse_settings.sub_info = Some(info);
185                    }
186                } else {
187                    // Try to get info from nodes
188                    if let (Some(stream_rules_unwrapped), Some(time_rules_unwrapped)) =
189                        (stream_rules, time_rules)
190                    {
191                        if let Some(info) = get_sub_info_from_nodes(
192                            &nodes,
193                            stream_rules_unwrapped,
194                            time_rules_unwrapped,
195                        ) {
196                            parse_settings.sub_info = Some(info);
197                        }
198                    }
199                }
200
201                filter_nodes(&mut nodes, exclude_remarks, include_remarks, group_id);
202
203                // Set group_id and custom_group for all nodes
204                for node in &mut nodes {
205                    node.group_id = group_id;
206                    if !custom_group.is_empty() {
207                        node.group = custom_group.clone();
208                    }
209                }
210
211                all_nodes.append(&mut nodes);
212                Ok(())
213            } else {
214                Err("Invalid configuration file".to_string())
215            }
216        }
217        _ => {
218            // Handle direct link to a single proxy
219            if explode(&link, &mut node) {
220                if node.proxy_type == crate::models::ProxyType::Unknown {
221                    return Err("No valid link found".to_string());
222                }
223                node.group_id = group_id;
224                if !custom_group.is_empty() {
225                    node.group = custom_group;
226                }
227                all_nodes.push(node);
228                Ok(())
229            } else {
230                Err("No valid link found".to_string())
231            }
232        }
233    }
234}
235
236/// Extracts a specific argument from a URL
237fn get_url_arg(url: &str, arg_name: &str) -> Option<String> {
238    if let Some(query_start) = url.find('?') {
239        let query = &url[query_start + 1..];
240        for pair in query.split('&') {
241            let parts: Vec<&str> = pair.split('=').collect();
242            if parts.len() == 2 && parts[0] == arg_name {
243                return Some(parts[1].to_string());
244            }
245        }
246    }
247    None
248}
249
250/// Parses a configuration file into a vector of Proxy objects
251/// Returns the number of proxies parsed
252async fn explode_conf(path: &str, nodes: &mut Vec<Proxy>) -> i32 {
253    // TODO: 安全问题,但是旧版subconverter也有……
254    match file_get_async(path, None).await {
255        Ok(content) => explode_conf_content(&content, nodes),
256        Err(_) => 0,
257    }
258}
259
260/// Filters nodes based on include/exclude rules
261fn filter_nodes(
262    nodes: &mut Vec<Proxy>,
263    exclude_remarks: Option<&Vec<String>>,
264    include_remarks: Option<&Vec<String>>,
265    group_id: i32,
266) {
267    let mut node_index = 0;
268    let mut i = 0;
269
270    while i < nodes.len() {
271        if should_ignore(&nodes[i], exclude_remarks, include_remarks) {
272            // Log that node is ignored
273            println!(
274                "Node {} - {} has been ignored and will not be added.",
275                nodes[i].group, nodes[i].remark
276            );
277            nodes.remove(i);
278        } else {
279            // Log that node is added
280            println!(
281                "Node {} - {} has been added.",
282                nodes[i].group, nodes[i].remark
283            );
284            nodes[i].id = node_index;
285            nodes[i].group_id = group_id;
286            node_index += 1;
287            i += 1;
288        }
289    }
290}
291
292/// Determines if a node should be ignored based on its remarks and the filtering rules
293fn should_ignore(
294    node: &Proxy,
295    exclude_remarks: Option<&Vec<String>>,
296    include_remarks: Option<&Vec<String>>,
297) -> bool {
298    let mut excluded = false;
299    let mut included = true; // Default to true if no include rules
300
301    // Check exclude rules
302    if let Some(excludes) = exclude_remarks {
303        excluded = excludes.iter().any(|pattern| {
304            let mut real_rule = String::new();
305            if apply_matcher(pattern, &mut real_rule, node) {
306                if !real_rule.is_empty() {
307                    reg_find(&node.remark, &real_rule)
308                } else {
309                    pattern == &node.remark
310                }
311            } else {
312                false
313            }
314        });
315    }
316
317    // Check include rules if they exist
318    if let Some(includes) = include_remarks {
319        if !includes.is_empty() {
320            included = includes.iter().any(|pattern| {
321                let mut real_rule = String::new();
322                if apply_matcher(pattern, &mut real_rule, node) {
323                    if !real_rule.is_empty() {
324                        reg_find(&node.remark, &real_rule)
325                    } else {
326                        pattern == &node.remark
327                    }
328                } else {
329                    false
330                }
331            });
332        }
333    }
334
335    // A node is ignored if it's excluded OR not included
336    excluded || !included
337}