libsubconverter/parser/explodes/
ss.rs

1use crate::models::{Proxy, SS_DEFAULT_GROUP};
2use crate::utils::url::url_decode;
3use base64::engine::general_purpose::STANDARD;
4use base64::Engine;
5use serde_json::Value;
6
7/// Parse a Shadowsocks link into a Proxy object
8/// Based on the C++ implementation in explodeSS function
9pub fn explode_ss(ss: &str, node: &mut Proxy) -> bool {
10    // Check if the link starts with ss://
11    if !ss.starts_with("ss://") {
12        return false;
13    }
14
15    // Extract the content part after ss://
16    let mut ss_content = ss[5..].to_string();
17    // Replace "/?" with "?" like in C++ replaceAllDistinct
18    ss_content = ss_content.replace("/?", "?");
19
20    // Extract fragment (remark) if present
21    let mut ps = String::new();
22    if let Some(hash_pos) = ss_content.find('#') {
23        ps = url_decode(&ss_content[hash_pos + 1..]);
24        ss_content = ss_content[..hash_pos].to_string();
25    }
26
27    // Extract plugin and other query parameters
28    let mut plugin = String::new();
29    let mut plugin_opts = String::new();
30    let mut group = SS_DEFAULT_GROUP.to_string();
31
32    if let Some(query_pos) = ss_content.find('?') {
33        let addition = ss_content[query_pos + 1..].to_string();
34        ss_content = ss_content[..query_pos].to_string();
35
36        // Parse query parameters
37        for (key, value) in url::form_urlencoded::parse(addition.as_bytes()) {
38            if key == "plugin" {
39                let plugins = url_decode(&value);
40                if let Some(semicolon_pos) = plugins.find(';') {
41                    plugin = plugins[..semicolon_pos].to_string();
42                    plugin_opts = plugins[semicolon_pos + 1..].to_string();
43                } else {
44                    plugin = plugins;
45                }
46            } else if key == "group" {
47                if !value.is_empty() {
48                    group = crate::utils::base64::url_safe_base64_decode(&value);
49                }
50            }
51        }
52    }
53
54    // Parse the main part of the URL
55    let method;
56    let password;
57    let server;
58    let port;
59
60    if ss_content.contains('@') {
61        // SIP002 format (method:password@server:port)
62        let parts: Vec<&str> = ss_content.split('@').collect();
63        if parts.len() < 2 {
64            return false;
65        }
66
67        let secret = parts[0];
68        let server_port = parts[1];
69
70        // Parse server and port
71        let server_port_parts: Vec<&str> = server_port.split(':').collect();
72        if server_port_parts.len() < 2 {
73            return false;
74        }
75        server = server_port_parts[0].to_string();
76        port = match server_port_parts[1].parse::<u16>() {
77            Ok(p) => p,
78            Err(_) => return false,
79        };
80
81        // Decode the secret part
82        let decoded_secret = crate::utils::base64::url_safe_base64_decode(secret);
83        let method_pass: Vec<&str> = decoded_secret.split(':').collect();
84        if method_pass.len() < 2 {
85            return false;
86        }
87        method = method_pass[0].to_string();
88        password = method_pass[1..].join(":"); // In case password contains colons
89    } else {
90        // Legacy format
91        let decoded = crate::utils::base64::url_safe_base64_decode(&ss_content);
92        if decoded.is_empty() {
93            return false;
94        }
95
96        // Parse method:password@server:port
97        let parts: Vec<&str> = decoded.split('@').collect();
98        if parts.len() < 2 {
99            return false;
100        }
101
102        let method_pass = parts[0];
103        let server_port = parts[1];
104
105        // Parse method and password
106        let method_pass_parts: Vec<&str> = method_pass.split(':').collect();
107        if method_pass_parts.len() < 2 {
108            return false;
109        }
110        method = method_pass_parts[0].to_string();
111        password = method_pass_parts[1..].join(":"); // In case password contains colons
112
113        // Parse server and port
114        let server_port_parts: Vec<&str> = server_port.split(':').collect();
115        if server_port_parts.len() < 2 {
116            return false;
117        }
118        server = server_port_parts[0].to_string();
119        port = match server_port_parts[1].parse::<u16>() {
120            Ok(p) => p,
121            Err(_) => return false,
122        };
123    }
124
125    // Skip if port is 0
126    if port == 0 {
127        return false;
128    }
129
130    // Use server:port as remark if none provided
131    if ps.is_empty() {
132        ps = format!("{} ({})", server, port);
133    }
134
135    // Create the proxy
136    *node = Proxy::ss_construct(
137        &group,
138        &ps,
139        &server,
140        port,
141        &password,
142        &method,
143        &plugin,
144        &plugin_opts,
145        None,
146        None,
147        None,
148        None,
149        "",
150    );
151
152    true
153}
154
155/// Parse a SSD (Shadowsocks subscription) link into a vector of Proxy objects
156pub fn explode_ssd(link: &str, nodes: &mut Vec<Proxy>) -> bool {
157    // Check if the link starts with ssd://
158    if !link.starts_with("ssd://") {
159        return false;
160    }
161
162    // Extract the base64 part
163    let encoded = &link[6..];
164
165    // Decode base64
166    let decoded = match STANDARD.decode(encoded) {
167        Ok(bytes) => match String::from_utf8(bytes) {
168            Ok(s) => s,
169            Err(_) => return false,
170        },
171        Err(_) => return false,
172    };
173
174    // Parse as JSON
175    let json: Value = match serde_json::from_str(&decoded) {
176        Ok(json) => json,
177        Err(_) => return false,
178    };
179
180    // Extract common fields
181    let airport = json["airport"].as_str().unwrap_or("");
182    let port = json["port"].as_u64().unwrap_or(0) as u16;
183    let encryption = json["encryption"].as_str().unwrap_or("");
184    let password = json["password"].as_str().unwrap_or("");
185
186    // Extract servers
187    if !json["servers"].is_array() {
188        return false;
189    }
190
191    let servers = json["servers"].as_array().unwrap();
192
193    for server in servers {
194        let server_host = server["server"].as_str().unwrap_or("");
195        let server_port = server["port"].as_u64().unwrap_or(port as u64) as u16;
196        let server_encryption = server["encryption"].as_str().unwrap_or(encryption);
197        let server_password = server["password"].as_str().unwrap_or(password);
198        let server_remark = server["remarks"].as_str().unwrap_or("");
199        let server_plugin = server["plugin"].as_str().unwrap_or("");
200        let server_plugin_opts = server["plugin_options"].as_str().unwrap_or("");
201
202        // Create formatted remark
203        let formatted_remark = format!("{} - {}", airport, server_remark);
204
205        // Create the proxy object
206        let node = Proxy::ss_construct(
207            SS_DEFAULT_GROUP,
208            &formatted_remark,
209            server_host,
210            server_port,
211            server_password,
212            server_encryption,
213            server_plugin,
214            server_plugin_opts,
215            None,
216            None,
217            None,
218            None,
219            "",
220        );
221
222        nodes.push(node);
223    }
224
225    !nodes.is_empty()
226}
227
228/// Parse Android Shadowsocks configuration into a vector of Proxy objects
229pub fn explode_ss_android(content: &str, nodes: &mut Vec<Proxy>) -> bool {
230    // Try to parse as JSON
231    let json: Value = match serde_json::from_str(content) {
232        Ok(json) => json,
233        Err(_) => {
234            println!(
235                "Error parsing Android Shadowsocks configuration: {}",
236                content
237            );
238            return false;
239        }
240    };
241
242    // Check if it contains profiles
243    if !json["configs"].is_array() && !json["proxies"].is_array() {
244        return false;
245    }
246
247    // Determine which field to use
248    let configs = if json["configs"].is_array() {
249        json["configs"].as_array().unwrap()
250    } else {
251        json["proxies"].as_array().unwrap()
252    };
253
254    let mut index = nodes.len();
255
256    for config in configs {
257        // Extract fields
258        let server = config["server"].as_str().unwrap_or("");
259        if server.is_empty() {
260            continue;
261        }
262
263        let port_num = config["server_port"].as_u64().unwrap_or(0) as u16;
264        if port_num == 0 {
265            continue;
266        }
267
268        let method = config["method"].as_str().unwrap_or("");
269        let password = config["password"].as_str().unwrap_or("");
270
271        // Get remark, try both "remarks" and "name" fields
272        let remark = if config["remarks"].is_string() {
273            config["remarks"].as_str().unwrap_or("").to_string()
274        } else if config["name"].is_string() {
275            config["name"].as_str().unwrap_or("").to_string()
276        } else {
277            format!("{} ({})", server, port_num)
278        };
279
280        // Get plugin and plugin_opts
281        let plugin = config["plugin"].as_str().unwrap_or("");
282        let plugin_opts = config["plugin_opts"].as_str().unwrap_or("");
283
284        // Create the proxy object
285        let mut node = Proxy::ss_construct(
286            SS_DEFAULT_GROUP,
287            &remark,
288            server,
289            port_num,
290            password,
291            method,
292            plugin,
293            plugin_opts,
294            None,
295            None,
296            None,
297            None,
298            "",
299        );
300
301        node.id = index as u32;
302        nodes.push(node);
303        index += 1;
304    }
305
306    !nodes.is_empty()
307}
308
309/// Parse a Shadowsocks configuration file into a vector of Proxy objects
310pub fn explode_ss_conf(content: &str, nodes: &mut Vec<Proxy>) -> bool {
311    // Try to parse as JSON
312    let json: Value = match serde_json::from_str(content) {
313        Ok(json) => json,
314        Err(_) => return false,
315    };
316
317    // Check for different configuration formats
318    if json["configs"].is_array() || json["proxies"].is_array() {
319        return explode_ss_android(content, nodes);
320    }
321
322    // Check for single server configuration
323    if json["server"].is_string() && json["server_port"].is_u64() {
324        let index = nodes.len();
325
326        // Extract fields
327        let server = json["server"].as_str().unwrap_or("");
328        let port_num = json["server_port"].as_u64().unwrap_or(0) as u16;
329        if server.is_empty() || port_num == 0 {
330            return false;
331        }
332
333        let method = json["method"].as_str().unwrap_or("");
334        let password = json["password"].as_str().unwrap_or("");
335
336        // Get remark
337        let remark = if json["remarks"].is_string() {
338            json["remarks"].as_str().unwrap_or("")
339        } else {
340            &format!("{} ({})", server, port_num)
341        };
342
343        // Get plugin and plugin_opts
344        let plugin = json["plugin"].as_str().unwrap_or("");
345        let plugin_opts = json["plugin_opts"].as_str().unwrap_or("");
346
347        // Create the proxy object
348        let mut node = Proxy::ss_construct(
349            SS_DEFAULT_GROUP,
350            remark,
351            server,
352            port_num,
353            password,
354            method,
355            plugin,
356            plugin_opts,
357            None,
358            None,
359            None,
360            None,
361            "",
362        );
363
364        node.id = index as u32;
365        nodes.push(node);
366
367        return true;
368    }
369
370    // Check for server list configuration
371    if json["servers"].is_array() {
372        let servers = json["servers"].as_array().unwrap();
373        let mut index = nodes.len();
374
375        for server_json in servers {
376            // Extract fields
377            let server = server_json["server"].as_str().unwrap_or("");
378            let port_num = server_json["server_port"].as_u64().unwrap_or(0) as u16;
379            if server.is_empty() || port_num == 0 {
380                continue;
381            }
382
383            let method = server_json["method"].as_str().unwrap_or("");
384            let password = server_json["password"].as_str().unwrap_or("");
385
386            // Get remark
387            let remark = if server_json["remarks"].is_string() {
388                server_json["remarks"].as_str().unwrap_or("")
389            } else {
390                &format!("{} ({})", server, port_num)
391            };
392
393            // Get plugin and plugin_opts
394            let plugin = server_json["plugin"].as_str().unwrap_or("");
395            let plugin_opts = server_json["plugin_opts"].as_str().unwrap_or("");
396
397            // Create the proxy object
398            let mut node = Proxy::ss_construct(
399                SS_DEFAULT_GROUP,
400                remark,
401                server,
402                port_num,
403                password,
404                method,
405                plugin,
406                plugin_opts,
407                None,
408                None,
409                None,
410                None,
411                "",
412            );
413
414            node.id = index as u32;
415            nodes.push(node);
416            index += 1;
417        }
418
419        return !nodes.is_empty();
420    }
421
422    false
423}