libsubconverter/parser/explodes/
surge.rs

1use crate::models::{
2    Proxy, HTTP_DEFAULT_GROUP, SNELL_DEFAULT_GROUP, SOCKS_DEFAULT_GROUP, SS_DEFAULT_GROUP,
3    TROJAN_DEFAULT_GROUP, V2RAY_DEFAULT_GROUP,
4};
5
6/// Parse a Surge configuration into a vector of Proxy objects
7pub fn explode_surge(content: &str, nodes: &mut Vec<Proxy>) -> bool {
8    // Split the content into lines
9    let lines: Vec<&str> = content.lines().collect();
10
11    // Track the section we're currently in
12    let mut in_proxy_section = false;
13    let mut success = false;
14
15    for line in lines {
16        // Skip empty lines and comments
17        let line = line.trim();
18        if line.is_empty() || line.starts_with('#') {
19            continue;
20        }
21
22        // Check section headers
23        if line.starts_with('[') && line.ends_with(']') {
24            in_proxy_section = line == "[Proxy]";
25            continue;
26        }
27
28        // Only process lines in the [Proxy] section
29        if !in_proxy_section {
30            continue;
31        }
32
33        // Split by = to get name and configuration
34        let parts: Vec<&str> = line.splitn(2, '=').collect();
35        if parts.len() != 2 {
36            continue;
37        }
38
39        let name = parts[0].trim();
40        let config = parts[1].trim();
41
42        // Skip direct, reject, and reject-tinygif
43        if config.starts_with("direct")
44            || config.starts_with("reject")
45            || config.starts_with("reject-tinygif")
46        {
47            continue;
48        }
49
50        // Parse the proxy based on the configuration format
51        let mut node = Proxy::default();
52
53        if config.starts_with("custom,") {
54            // Surge 2 style custom proxy (essentially a shadowsocks proxy)
55            if parse_surge_custom_ss(config, name, &mut node) {
56                nodes.push(node);
57                success = true;
58            }
59        } else if config.starts_with("ss,") || config.starts_with("shadowsocks,") {
60            // Surge 3 style ss proxy
61            if parse_surge_ss(config, name, &mut node) {
62                nodes.push(node);
63                success = true;
64            }
65        } else if config.starts_with("socks5") || config.starts_with("socks5-tls") {
66            if parse_surge_socks(config, name, &mut node) {
67                nodes.push(node);
68                success = true;
69            }
70        } else if config.starts_with("vmess,") {
71            // Surge 4 style vmess proxy
72            if parse_surge_vmess(config, name, &mut node) {
73                nodes.push(node);
74                success = true;
75            }
76        } else if config.starts_with("http") || config.starts_with("https") {
77            if parse_surge_http(config, name, &mut node) {
78                nodes.push(node);
79                success = true;
80            }
81        } else if config.starts_with("trojan") {
82            if parse_surge_trojan(config, name, &mut node) {
83                nodes.push(node);
84                success = true;
85            }
86        } else if config.starts_with("snell") {
87            if parse_surge_snell(config, name, &mut node) {
88                nodes.push(node);
89                success = true;
90            }
91        }
92    }
93
94    success
95}
96
97/// Parse a Surge 2 custom Shadowsocks configuration line
98fn parse_surge_custom_ss(config: &str, name: &str, node: &mut Proxy) -> bool {
99    // Split the configuration into parts
100    let parts: Vec<&str> = config.split(',').map(|s| s.trim()).collect();
101
102    // Check minimum required parts (custom,server,port,method,password,module)
103    if parts.len() < 5 {
104        return false;
105    }
106
107    // Extract the server, port, method, and password
108    let server = parts[1];
109    let port_str = parts[2];
110    let port = match port_str.parse::<u16>() {
111        Ok(p) => p,
112        Err(_) => return false,
113    };
114    if port == 0 {
115        return false;
116    }
117
118    let method = parts[3];
119    let password = parts[4];
120
121    // Default values
122    let mut plugin = String::new();
123    let mut plugin_opts = String::new();
124    let mut pluginopts_mode = String::new();
125    let mut pluginopts_host = String::new();
126    let mut udp = None;
127    let mut tfo = None;
128    let scv = None;
129
130    // Parse additional parameters
131    for i in 6..parts.len() {
132        if parts[i].contains('=') {
133            let param_parts: Vec<&str> = parts[i].split('=').collect();
134            if param_parts.len() != 2 {
135                continue;
136            }
137            let key = param_parts[0].trim();
138            let value = param_parts[1].trim();
139
140            match key {
141                "obfs" => {
142                    plugin = "simple-obfs".to_string();
143                    pluginopts_mode = value.to_string();
144                }
145                "obfs-host" => {
146                    pluginopts_host = value.to_string();
147                }
148                "udp-relay" => {
149                    udp = Some(value == "true" || value == "1");
150                }
151                "tfo" => {
152                    tfo = Some(value == "true" || value == "1");
153                }
154                _ => {}
155            }
156        }
157    }
158
159    // Build plugin options if plugin is not empty
160    if !plugin.is_empty() {
161        plugin_opts = format!("obfs={}", pluginopts_mode);
162        if !pluginopts_host.is_empty() {
163            plugin_opts.push_str(&format!(";obfs-host={}", pluginopts_host));
164        }
165    }
166
167    // Create the proxy object
168    *node = Proxy::ss_construct(
169        SS_DEFAULT_GROUP,
170        name,
171        server,
172        port,
173        password,
174        method,
175        &plugin,
176        &plugin_opts,
177        udp,
178        tfo,
179        scv,
180        None,
181        "",
182    );
183
184    true
185}
186
187/// Parse a Surge Shadowsocks configuration line
188fn parse_surge_ss(config: &str, name: &str, node: &mut Proxy) -> bool {
189    // Split the configuration into parts
190    let parts: Vec<&str> = config.split(',').map(|s| s.trim()).collect();
191
192    // Check minimum required parts
193    if parts.len() < 3 {
194        return false;
195    }
196
197    // Extract the server and port
198    let server = parts[1];
199    let port_str = parts[2];
200    let port = match port_str.parse::<u16>() {
201        Ok(p) => p,
202        Err(_) => return false,
203    };
204    if port == 0 {
205        return false;
206    }
207
208    // Default values
209    let mut method = String::new();
210    let mut password = String::new();
211    let mut plugin = String::new();
212    let mut plugin_opts = String::new();
213    let mut pluginopts_mode = String::new();
214    let mut pluginopts_host = String::new();
215    let mut udp = None;
216    let mut tfo = None;
217    let mut scv = None;
218
219    // Parse additional parameters
220    for i in 3..parts.len() {
221        if parts[i].contains('=') {
222            let param_parts: Vec<&str> = parts[i].split('=').collect();
223            if param_parts.len() != 2 {
224                continue;
225            }
226            let key = param_parts[0].trim();
227            let value = param_parts[1].trim();
228
229            match key {
230                "encrypt-method" => {
231                    method = value.to_string();
232                }
233                "password" => {
234                    password = value.to_string();
235                }
236                "obfs" => {
237                    plugin = "simple-obfs".to_string();
238                    pluginopts_mode = value.to_string();
239                }
240                "obfs-host" => {
241                    pluginopts_host = value.to_string();
242                }
243                "udp-relay" => {
244                    udp = Some(value == "true" || value == "1");
245                }
246                "tfo" => {
247                    tfo = Some(value == "true" || value == "1");
248                }
249                "skip-cert-verify" => {
250                    scv = Some(value == "true" || value == "1");
251                }
252                _ => {}
253            }
254        }
255    }
256
257    // Build plugin options if plugin is not empty
258    if !plugin.is_empty() {
259        plugin_opts = format!("obfs={}", pluginopts_mode);
260        if !pluginopts_host.is_empty() {
261            plugin_opts.push_str(&format!(";obfs-host={}", pluginopts_host));
262        }
263    }
264
265    // Create the proxy object
266    *node = Proxy::ss_construct(
267        SS_DEFAULT_GROUP,
268        name,
269        server,
270        port,
271        &password,
272        &method,
273        &plugin,
274        &plugin_opts,
275        udp,
276        tfo,
277        scv,
278        None,
279        "",
280    );
281
282    true
283}
284
285/// Parse a Surge HTTP/HTTPS configuration line
286fn parse_surge_http(config: &str, name: &str, node: &mut Proxy) -> bool {
287    // Split the configuration into parts
288    let parts: Vec<&str> = config.split(',').map(|s| s.trim()).collect();
289
290    // Check minimum required parts
291    if parts.len() < 3 {
292        return false;
293    }
294
295    // Extract the server and port
296    let server = parts[1];
297    let port_str = parts[2];
298    let port = match port_str.parse::<u16>() {
299        Ok(p) => p,
300        Err(_) => return false,
301    };
302
303    // Determine if it's HTTPS
304    let is_https = parts[0] == "https";
305
306    // Default values
307    let mut username = "";
308    let mut password = "";
309    let mut tfo = None;
310    let mut scv = None;
311
312    // Parse additional parameters
313    for i in 3..parts.len() {
314        if parts[i].starts_with("username=") {
315            username = &parts[i][9..];
316        } else if parts[i].starts_with("password=") {
317            password = &parts[i][9..];
318        } else if parts[i] == "tfo=true" {
319            tfo = Some(true);
320        } else if parts[i] == "skip-cert-verify=true" {
321            scv = Some(true);
322        }
323    }
324
325    // Create the proxy object
326    *node = Proxy::http_construct(
327        HTTP_DEFAULT_GROUP,
328        name,
329        server,
330        port,
331        username,
332        password,
333        is_https,
334        tfo,
335        scv,
336        None,
337        "",
338    );
339
340    true
341}
342
343/// Parse a Surge SOCKS5 configuration line
344fn parse_surge_socks(config: &str, name: &str, node: &mut Proxy) -> bool {
345    // Split the configuration into parts
346    let parts: Vec<&str> = config.split(',').map(|s| s.trim()).collect();
347
348    // Check minimum required parts
349    if parts.len() < 3 {
350        return false;
351    }
352
353    // Extract the server and port
354    let server = parts[1];
355    let port_str = parts[2];
356    let port = match port_str.parse::<u16>() {
357        Ok(p) => p,
358        Err(_) => return false,
359    };
360
361    // Default values
362    let mut username = "";
363    let mut password = "";
364    let mut udp = None;
365    let mut tfo = None;
366    let mut scv = None;
367
368    // Parse additional parameters
369    if parts.len() >= 5 {
370        username = parts[3];
371        password = parts[4];
372    }
373
374    // Parse additional parameters
375    for i in 5..parts.len() {
376        if parts[i].contains('=') {
377            let param_parts: Vec<&str> = parts[i].split('=').collect();
378            if param_parts.len() != 2 {
379                continue;
380            }
381            let key = param_parts[0].trim();
382            let value = param_parts[1].trim();
383
384            match key {
385                "udp-relay" => {
386                    udp = Some(value == "true" || value == "1");
387                }
388                "tfo" => {
389                    tfo = Some(value == "true" || value == "1");
390                }
391                "skip-cert-verify" => {
392                    scv = Some(value == "true" || value == "1");
393                }
394                _ => {}
395            }
396        }
397    }
398
399    // Create the proxy object
400    *node = Proxy::socks_construct(
401        SOCKS_DEFAULT_GROUP,
402        name,
403        server,
404        port,
405        username,
406        password,
407        udp,
408        tfo,
409        scv,
410        "",
411    );
412
413    true
414}
415
416/// Parse a Surge VMess configuration line
417fn parse_surge_vmess(config: &str, name: &str, node: &mut Proxy) -> bool {
418    // Split the configuration into parts
419    let parts: Vec<&str> = config.split(',').map(|s| s.trim()).collect();
420
421    // Check minimum required parts
422    if parts.len() < 3 {
423        return false;
424    }
425
426    // Extract the server and port
427    let server = parts[1];
428    let port_str = parts[2];
429    let port = match port_str.parse::<u16>() {
430        Ok(p) => p,
431        Err(_) => return false,
432    };
433    if port == 0 {
434        return false;
435    }
436
437    // Default values
438    let mut id = String::new();
439    let mut net = "tcp".to_string();
440    let method = "auto".to_string();
441    let mut path = String::new();
442    let mut host = String::new();
443    let mut edge = String::new();
444    let mut tls = String::new();
445    let mut udp = None;
446    let mut tfo = None;
447    let mut scv = None;
448    let mut tls13 = None;
449    let mut aead = "1".to_string(); // Default to 1 for non-AEAD mode
450
451    // Parse additional parameters
452    for i in 3..parts.len() {
453        if parts[i].contains('=') {
454            let param_parts: Vec<&str> = parts[i].split('=').collect();
455            if param_parts.len() != 2 {
456                continue;
457            }
458            let key = param_parts[0].trim();
459            let value = param_parts[1].trim();
460
461            match key {
462                "username" => {
463                    id = value.to_string();
464                }
465                "ws" => {
466                    net = if value == "true" {
467                        "ws".to_string()
468                    } else {
469                        "tcp".to_string()
470                    };
471                }
472                "tls" => {
473                    tls = if value == "true" {
474                        "tls".to_string()
475                    } else {
476                        String::new()
477                    };
478                }
479                "ws-path" => {
480                    path = value.to_string();
481                }
482                "obfs-host" => {
483                    host = value.to_string();
484                }
485                "ws-headers" => {
486                    // Parse headers in the format "Host:example.com|Edge:example.edge"
487                    let headers: Vec<&str> = value.split('|').collect();
488                    for header in headers {
489                        let header_parts: Vec<&str> = header.split(':').collect();
490                        if header_parts.len() == 2 {
491                            let header_name = header_parts[0].trim().to_lowercase();
492                            let header_value = header_parts[1].trim();
493                            if header_name == "host" {
494                                host = header_value.trim_matches('"').to_string();
495                            } else if header_name == "edge" {
496                                edge = header_value.trim_matches('"').to_string();
497                            }
498                        }
499                    }
500                }
501                "udp-relay" => {
502                    udp = Some(value == "true" || value == "1");
503                }
504                "tfo" => {
505                    tfo = Some(value == "true" || value == "1");
506                }
507                "skip-cert-verify" => {
508                    scv = Some(value == "true" || value == "1");
509                }
510                "tls13" => {
511                    tls13 = Some(value == "true" || value == "1");
512                }
513                "vmess-aead" => {
514                    aead = if value == "true" {
515                        "0".to_string()
516                    } else {
517                        "1".to_string()
518                    };
519                }
520                _ => {}
521            }
522        }
523    }
524
525    // Create the proxy object
526    *node = Proxy::vmess_construct(
527        V2RAY_DEFAULT_GROUP,
528        name,
529        server,
530        port,
531        "",
532        &id,
533        aead.parse::<u16>().unwrap_or(0),
534        &net,
535        &method,
536        &path,
537        &host,
538        &edge,
539        &tls,
540        "",
541        udp,
542        tfo,
543        scv,
544        tls13,
545        "",
546    );
547
548    true
549}
550
551/// Parse a Surge Trojan configuration line
552fn parse_surge_trojan(config: &str, name: &str, node: &mut Proxy) -> bool {
553    // Split the configuration into parts
554    let parts: Vec<&str> = config.split(',').map(|s| s.trim()).collect();
555
556    // Check minimum required parts
557    if parts.len() < 4 {
558        return false;
559    }
560
561    // Extract the server and port
562    let server = parts[1];
563    let port_str = parts[2];
564    let port = match port_str.parse::<u16>() {
565        Ok(p) => p,
566        Err(_) => return false,
567    };
568    if port == 0 {
569        return false;
570    }
571
572    // Default values
573    let mut password = String::new();
574    let mut host = String::new();
575    let mut udp = None;
576    let mut tfo = None;
577    let mut scv = None;
578
579    // Parse additional parameters
580    for i in 3..parts.len() {
581        if parts[i].contains('=') {
582            let param_parts: Vec<&str> = parts[i].split('=').collect();
583            if param_parts.len() != 2 {
584                continue;
585            }
586            let key = param_parts[0].trim();
587            let value = param_parts[1].trim();
588
589            match key {
590                "password" => {
591                    password = value.to_string();
592                }
593                "sni" => {
594                    host = value.to_string();
595                }
596                "udp-relay" => {
597                    udp = Some(value == "true" || value == "1");
598                }
599                "tfo" => {
600                    tfo = Some(value == "true" || value == "1");
601                }
602                "skip-cert-verify" => {
603                    scv = Some(value == "true" || value == "1");
604                }
605                _ => {}
606            }
607        }
608    }
609
610    // If password parameter not found, use the 4th part directly
611    if password.is_empty() {
612        password = parts[3].to_string();
613        // Check if it has password= prefix
614        if password.starts_with("password=") {
615            password = password[9..].to_string();
616        }
617    }
618
619    // Create the proxy object
620    *node = Proxy::trojan_construct(
621        TROJAN_DEFAULT_GROUP.to_string(),
622        name.to_string(),
623        server.to_string(),
624        port,
625        password,
626        None,
627        if host.is_empty() { None } else { Some(host) },
628        None,
629        None,
630        true,
631        udp,
632        tfo,
633        scv,
634        None,
635        None,
636    );
637
638    true
639}
640
641/// Parse a Surge Snell configuration line
642fn parse_surge_snell(config: &str, name: &str, node: &mut Proxy) -> bool {
643    // Split the configuration into parts
644    let parts: Vec<&str> = config.split(',').map(|s| s.trim()).collect();
645
646    // Check minimum required parts
647    if parts.len() < 3 {
648        return false;
649    }
650
651    // Extract the server and port
652    let server = parts[1];
653    let port_str = parts[2];
654    let port = match port_str.parse::<u16>() {
655        Ok(p) => p,
656        Err(_) => return false,
657    };
658    if port == 0 {
659        return false; // Skip if port is 0
660    }
661
662    // Default values
663    let mut password = String::new();
664    let mut plugin = String::new();
665    let mut host = String::new();
666    let mut version = String::new();
667    let mut udp = None;
668    let mut tfo = None;
669    let mut scv = None;
670
671    // Parse additional parameters
672    for i in 3..parts.len() {
673        // Split by equals sign
674        let param_parts: Vec<&str> = parts[i].split('=').collect();
675        if param_parts.len() != 2 {
676            continue;
677        }
678        let key = param_parts[0].trim();
679        let value = param_parts[1].trim();
680
681        match key {
682            "psk" => password = value.to_string(),
683            "obfs" => plugin = value.to_string(),
684            "obfs-host" => host = value.to_string(),
685            "udp-relay" => udp = Some(value == "true" || value == "1"),
686            "tfo" => tfo = Some(value == "true" || value == "1"),
687            "skip-cert-verify" => scv = Some(value == "true" || value == "1"),
688            "version" => version = value.to_string(),
689            _ => {}
690        }
691    }
692
693    if password.is_empty() {
694        return false;
695    }
696
697    // Create the proxy object
698    *node = Proxy::snell_construct(
699        SNELL_DEFAULT_GROUP.to_string(),
700        name.to_string(),
701        server.to_string(),
702        port,
703        password.to_string(),
704        plugin,
705        host,
706        version.parse::<u16>().unwrap_or(1),
707        udp,
708        tfo,
709        scv,
710        None,
711    );
712
713    true
714}