libsubconverter/parser/explodes/
explode_clash.rs

1use crate::{
2    models::{
3        Proxy, HTTP_DEFAULT_GROUP, HYSTERIA2_DEFAULT_GROUP, HYSTERIA_DEFAULT_GROUP,
4        SNELL_DEFAULT_GROUP, SOCKS_DEFAULT_GROUP, SSR_DEFAULT_GROUP, SS_DEFAULT_GROUP,
5        TROJAN_DEFAULT_GROUP, V2RAY_DEFAULT_GROUP, WG_DEFAULT_GROUP,
6    },
7    parser::yaml::clash::parse_clash_yaml,
8};
9use serde_yaml::Value;
10
11/// Parse a Clash YAML configuration into a vector of Proxy objects
12pub fn explode_clash(content: &str, nodes: &mut Vec<Proxy>) -> bool {
13    // 首先尝试使用新的YAML解析器
14    match parse_clash_yaml(content) {
15        Ok(mut proxies) => {
16            if !proxies.is_empty() {
17                nodes.append(&mut proxies);
18                return true;
19            }
20        }
21        Err(e) => {
22            // 失败时记录错误并尝试旧的解析方式
23            eprintln!("新YAML解析器失败: {}", e);
24        }
25    }
26
27    // 回退到旧的解析方式
28    // Parse the YAML content
29    let yaml: Value = match serde_yaml::from_str(content) {
30        Ok(y) => y,
31        Err(_) => return false,
32    };
33
34    // Extract proxies section
35    let proxies = match yaml.get("proxies") {
36        Some(Value::Sequence(seq)) => seq,
37        _ => match yaml.get("Proxy") {
38            Some(Value::Sequence(seq)) => seq,
39            _ => return false,
40        },
41    };
42
43    let mut success = false;
44
45    // Process each proxy in the sequence
46    for proxy in proxies {
47        if let Some(node) = parse_clash_proxy(proxy) {
48            nodes.push(node);
49            success = true;
50        }
51    }
52
53    success
54}
55
56/// Parse a single proxy from Clash YAML
57fn parse_clash_proxy(proxy: &Value) -> Option<Proxy> {
58    // Extract the proxy type
59    let proxy_type = match proxy.get("type") {
60        Some(Value::String(t)) => t.to_lowercase(),
61        _ => return None,
62    };
63
64    // Extract common fields
65    let name = proxy.get("name").and_then(|v| v.as_str()).unwrap_or("");
66    let server = proxy.get("server").and_then(|v| v.as_str()).unwrap_or("");
67    let port_value = proxy.get("port").and_then(|v| v.as_u64()).unwrap_or(0);
68    let port = port_value as u16;
69
70    // Skip if missing essential information
71    if name.is_empty() || server.is_empty() || port == 0 {
72        return None;
73    }
74
75    // Extract common optional fields
76    let udp = proxy.get("udp").and_then(|v| v.as_bool());
77    let tfo = proxy.get("tfo").and_then(|v| v.as_bool());
78    let skip_cert_verify = proxy.get("skip-cert-verify").and_then(|v| v.as_bool());
79
80    // Process based on proxy type
81    match proxy_type.as_str() {
82        "ss" | "shadowsocks" => {
83            parse_clash_ss(proxy, name, server, port, udp, tfo, skip_cert_verify)
84        }
85        "ssr" | "shadowsocksr" => {
86            parse_clash_ssr(proxy, name, server, port, udp, tfo, skip_cert_verify)
87        }
88        "vmess" => parse_clash_vmess(proxy, name, server, port, udp, tfo, skip_cert_verify),
89        "socks" | "socks5" => {
90            parse_clash_socks(proxy, name, server, port, udp, tfo, skip_cert_verify)
91        }
92        "http" => parse_clash_http(proxy, name, server, port, false, tfo, skip_cert_verify),
93        "https" => parse_clash_http(proxy, name, server, port, true, tfo, skip_cert_verify),
94        "trojan" => parse_clash_trojan(proxy, name, server, port, udp, tfo, skip_cert_verify),
95        "snell" => parse_clash_snell(proxy, name, server, port, udp, tfo, skip_cert_verify),
96        "wireguard" => parse_clash_wireguard(proxy, name, server, port, udp),
97        "hysteria" => parse_clash_hysteria(proxy, name, server, port, tfo, skip_cert_verify),
98        "hysteria2" => parse_clash_hysteria2(proxy, name, server, port, tfo, skip_cert_verify),
99        _ => None,
100    }
101}
102
103/// Parse a Shadowsocks proxy from Clash YAML
104fn parse_clash_ss(
105    proxy: &Value,
106    name: &str,
107    server: &str,
108    port: u16,
109    udp: Option<bool>,
110    tfo: Option<bool>,
111    skip_cert_verify: Option<bool>,
112) -> Option<Proxy> {
113    // Extract SS-specific fields
114    let password = proxy.get("password").and_then(|v| v.as_str()).unwrap_or("");
115    let method = proxy.get("cipher").and_then(|v| v.as_str()).unwrap_or("");
116
117    if password.is_empty() || method.is_empty() {
118        return None;
119    }
120
121    // Extract underlying proxy
122    let underlying_proxy = proxy
123        .get("underlying-proxy")
124        .and_then(|v| v.as_str())
125        .unwrap_or("");
126
127    // Extract plugin information
128    let mut plugin = "";
129    let mut pluginopts_mode = "";
130    let mut pluginopts_host = "";
131    let mut path = "";
132    let mut tls = "";
133    let mut pluginopts_mux = "";
134    let mut pluginopts = String::new();
135
136    // Check if plugin is defined
137    if let Some(plugin_val) = proxy.get("plugin").and_then(|v| v.as_str()) {
138        match plugin_val {
139            "obfs" => {
140                plugin = "obfs-local";
141                if let Some(plugin_opts) = proxy.get("plugin-opts").and_then(|v| v.as_mapping()) {
142                    if let Some(mode) = plugin_opts
143                        .get(&Value::String("mode".to_string()))
144                        .and_then(|v| v.as_str())
145                    {
146                        pluginopts_mode = mode;
147                    }
148                    if let Some(host) = plugin_opts
149                        .get(&Value::String("host".to_string()))
150                        .and_then(|v| v.as_str())
151                    {
152                        pluginopts_host = host;
153                    }
154                }
155            }
156            "v2ray-plugin" => {
157                plugin = "v2ray-plugin";
158                if let Some(plugin_opts) = proxy.get("plugin-opts").and_then(|v| v.as_mapping()) {
159                    if let Some(mode) = plugin_opts
160                        .get(&Value::String("mode".to_string()))
161                        .and_then(|v| v.as_str())
162                    {
163                        pluginopts_mode = mode;
164                    }
165                    if let Some(host) = plugin_opts
166                        .get(&Value::String("host".to_string()))
167                        .and_then(|v| v.as_str())
168                    {
169                        pluginopts_host = host;
170                    }
171                    if let Some(plugin_tls) = plugin_opts
172                        .get(&Value::String("tls".to_string()))
173                        .and_then(|v| v.as_bool())
174                    {
175                        tls = if plugin_tls { "tls;" } else { "" };
176                    }
177                    if let Some(plugin_path) = plugin_opts
178                        .get(&Value::String("path".to_string()))
179                        .and_then(|v| v.as_str())
180                    {
181                        path = plugin_path;
182                    }
183                    if let Some(mux) = plugin_opts
184                        .get(&Value::String("mux".to_string()))
185                        .and_then(|v| v.as_bool())
186                    {
187                        pluginopts_mux = if mux { "mux=4;" } else { "" };
188                    }
189                }
190            }
191            _ => {}
192        }
193    } else if let Some(obfs) = proxy.get("obfs").and_then(|v| v.as_str()) {
194        // Legacy support for obfs and obfs-host fields
195        plugin = "obfs-local";
196        pluginopts_mode = obfs;
197        if let Some(obfs_host) = proxy.get("obfs-host").and_then(|v| v.as_str()) {
198            pluginopts_host = obfs_host;
199        }
200    }
201
202    // Format plugin options based on plugin type
203    match plugin {
204        "simple-obfs" | "obfs-local" => {
205            pluginopts = format!("obfs={}", pluginopts_mode);
206            if !pluginopts_host.is_empty() {
207                pluginopts.push_str(&format!(";obfs-host={}", pluginopts_host));
208            }
209        }
210        "v2ray-plugin" => {
211            pluginopts = format!("mode={};{}{}", pluginopts_mode, tls, pluginopts_mux);
212            if !pluginopts_host.is_empty() {
213                pluginopts.push_str(&format!("host={};", pluginopts_host));
214            }
215            if !path.is_empty() {
216                pluginopts.push_str(&format!("path={};", path));
217            }
218            if !pluginopts_mux.is_empty() {
219                pluginopts.push_str(&format!("mux={};", pluginopts_mux));
220            }
221        }
222        _ => {}
223    }
224
225    // Handle special cipher types (support for go-shadowsocks2)
226    let mut cipher = method;
227    if cipher == "AEAD_CHACHA20_POLY1305" {
228        cipher = "chacha20-ietf-poly1305";
229    } else if cipher.contains("AEAD") {
230        // Not implementing the full C++ transformation for now
231    }
232
233    // Convert pluginopts String to &str
234    let pluginopts_str = Box::leak(pluginopts.into_boxed_str());
235
236    Some(Proxy::ss_construct(
237        SS_DEFAULT_GROUP,
238        name,
239        server,
240        port,
241        password,
242        cipher,
243        plugin,
244        pluginopts_str,
245        udp,
246        tfo,
247        skip_cert_verify,
248        None,
249        underlying_proxy,
250    ))
251}
252
253/// Parse a ShadowsocksR proxy from Clash YAML
254fn parse_clash_ssr(
255    proxy: &Value,
256    name: &str,
257    server: &str,
258    port: u16,
259    udp: Option<bool>,
260    tfo: Option<bool>,
261    skip_cert_verify: Option<bool>,
262) -> Option<Proxy> {
263    // Extract SSR-specific fields
264    let password = proxy.get("password").and_then(|v| v.as_str()).unwrap_or("");
265    let method = proxy.get("cipher").and_then(|v| v.as_str()).unwrap_or("");
266    let protocol = proxy.get("protocol").and_then(|v| v.as_str()).unwrap_or("");
267    let protocol_param = proxy
268        .get("protocol-param")
269        .and_then(|v| v.as_str())
270        .unwrap_or("");
271    let obfs = proxy.get("obfs").and_then(|v| v.as_str()).unwrap_or("");
272    let obfs_param = proxy
273        .get("obfs-param")
274        .and_then(|v| v.as_str())
275        .unwrap_or("");
276
277    // Extract underlying proxy
278    let underlying_proxy = proxy
279        .get("underlying-proxy")
280        .and_then(|v| v.as_str())
281        .unwrap_or("");
282
283    if password.is_empty() || method.is_empty() || protocol.is_empty() || obfs.is_empty() {
284        return None;
285    }
286
287    Some(Proxy::ssr_construct(
288        SSR_DEFAULT_GROUP,
289        name,
290        server,
291        port,
292        protocol,
293        method,
294        obfs,
295        password,
296        obfs_param,
297        protocol_param,
298        udp,
299        tfo,
300        skip_cert_verify,
301        underlying_proxy,
302    ))
303}
304
305/// Parse a VMess proxy from Clash YAML
306fn parse_clash_vmess(
307    proxy: &Value,
308    name: &str,
309    server: &str,
310    port: u16,
311    udp: Option<bool>,
312    tfo: Option<bool>,
313    skip_cert_verify: Option<bool>,
314) -> Option<Proxy> {
315    // Extract VMess-specific fields
316    let uuid = proxy.get("uuid").and_then(|v| v.as_str()).unwrap_or("");
317    let alter_id_val = proxy.get("alterId").and_then(|v| v.as_u64()).unwrap_or(0);
318    let alter_id = alter_id_val as u16;
319    let cipher = proxy
320        .get("cipher")
321        .and_then(|v| v.as_str())
322        .unwrap_or("auto");
323
324    // Extract underlying proxy
325    let underlying_proxy = proxy
326        .get("underlying-proxy")
327        .and_then(|v| v.as_str())
328        .unwrap_or("");
329
330    if uuid.is_empty() {
331        return None;
332    }
333
334    // Get network settings
335    let network = proxy
336        .get("network")
337        .and_then(|v| v.as_str())
338        .unwrap_or("tcp");
339
340    // Get TLS settings
341    let tls = proxy.get("tls").and_then(|v| v.as_bool()).unwrap_or(false);
342    let sni = proxy
343        .get("servername")
344        .and_then(|v| v.as_str())
345        .unwrap_or("");
346
347    // Parse network specific options
348    let mut host = String::new();
349    let mut path = String::new();
350
351    // Handle WebSocket options
352    if let Some(ws_opts) = proxy.get("ws-opts").and_then(|v| v.as_mapping()) {
353        if let Some(path_val) = ws_opts
354            .get(&Value::String("path".to_string()))
355            .and_then(|v| v.as_str())
356        {
357            path = path_val.to_string();
358        }
359
360        if let Some(headers) = ws_opts
361            .get(&Value::String("headers".to_string()))
362            .and_then(|v| v.as_mapping())
363        {
364            if let Some(host_val) = headers
365                .get(&Value::String("Host".to_string()))
366                .and_then(|v| v.as_str())
367            {
368                host = host_val.to_string();
369            }
370        }
371    }
372    // Handle HTTP/2 options
373    else if let Some(h2_opts) = proxy.get("h2-opts").and_then(|v| v.as_mapping()) {
374        if let Some(path_val) = h2_opts
375            .get(&Value::String("path".to_string()))
376            .and_then(|v| v.as_str())
377        {
378            path = path_val.to_string();
379        }
380
381        if let Some(hosts) = h2_opts
382            .get(&Value::String("host".to_string()))
383            .and_then(|v| v.as_sequence())
384        {
385            if !hosts.is_empty() {
386                if let Some(first_host) = hosts.get(0).and_then(|v| v.as_str()) {
387                    host = first_host.to_string();
388                }
389            }
390        }
391    }
392    // Handle HTTP options
393    else if let Some(http_opts) = proxy.get("http-opts").and_then(|v| v.as_mapping()) {
394        if let Some(paths) = http_opts
395            .get(&Value::String("path".to_string()))
396            .and_then(|v| v.as_sequence())
397        {
398            if !paths.is_empty() {
399                if let Some(first_path) = paths.get(0).and_then(|v| v.as_str()) {
400                    path = first_path.to_string();
401                }
402            }
403        }
404
405        if let Some(hosts) = http_opts
406            .get(&Value::String("host".to_string()))
407            .and_then(|v| v.as_sequence())
408        {
409            if !hosts.is_empty() {
410                if let Some(first_host) = hosts.get(0).and_then(|v| v.as_str()) {
411                    host = first_host.to_string();
412                }
413            }
414        }
415    }
416    // Handle gRPC options
417    else if let Some(grpc_opts) = proxy.get("grpc-opts").and_then(|v| v.as_mapping()) {
418        if let Some(service_name) = grpc_opts
419            .get(&Value::String("grpc-service-name".to_string()))
420            .and_then(|v| v.as_str())
421        {
422            path = service_name.to_string();
423        }
424    }
425
426    // Prepare path
427    let final_path = if path.is_empty() { "/" } else { &path };
428
429    // Get edge value
430    let edge = "";
431
432    Some(Proxy::vmess_construct(
433        V2RAY_DEFAULT_GROUP,
434        name,
435        server,
436        port,
437        "", // type
438        uuid,
439        alter_id,
440        network,
441        cipher,
442        final_path,
443        &host,
444        edge,
445        if tls { "tls" } else { "" },
446        sni,
447        udp,
448        tfo,
449        skip_cert_verify,
450        None,
451        underlying_proxy,
452    ))
453}
454
455/// Parse a SOCKS5 proxy from Clash YAML
456fn parse_clash_socks(
457    proxy: &Value,
458    name: &str,
459    server: &str,
460    port: u16,
461    udp: Option<bool>,
462    tfo: Option<bool>,
463    skip_cert_verify: Option<bool>,
464) -> Option<Proxy> {
465    // Extract SOCKS-specific fields
466    let username = proxy.get("username").and_then(|v| v.as_str()).unwrap_or("");
467    let password = proxy.get("password").and_then(|v| v.as_str()).unwrap_or("");
468
469    // Extract underlying proxy
470    let underlying_proxy = proxy
471        .get("underlying-proxy")
472        .and_then(|v| v.as_str())
473        .unwrap_or("");
474
475    Some(Proxy::socks_construct(
476        SOCKS_DEFAULT_GROUP,
477        name,
478        server,
479        port,
480        username,
481        password,
482        udp,
483        tfo,
484        skip_cert_verify,
485        underlying_proxy,
486    ))
487}
488
489/// Parse an HTTP/HTTPS proxy from Clash YAML
490fn parse_clash_http(
491    proxy: &Value,
492    name: &str,
493    server: &str,
494    port: u16,
495    is_https: bool,
496    tfo: Option<bool>,
497    skip_cert_verify: Option<bool>,
498) -> Option<Proxy> {
499    // Extract HTTP-specific fields
500    let username = proxy.get("username").and_then(|v| v.as_str()).unwrap_or("");
501    let password = proxy.get("password").and_then(|v| v.as_str()).unwrap_or("");
502
503    // Extract underlying proxy
504    let underlying_proxy = proxy
505        .get("underlying-proxy")
506        .and_then(|v| v.as_str())
507        .unwrap_or("");
508
509    Some(Proxy::http_construct(
510        HTTP_DEFAULT_GROUP,
511        name,
512        server,
513        port,
514        username,
515        password,
516        is_https,
517        tfo,
518        skip_cert_verify,
519        None,
520        underlying_proxy,
521    ))
522}
523
524/// Parse a Trojan proxy from Clash YAML
525fn parse_clash_trojan(
526    proxy: &Value,
527    name: &str,
528    server: &str,
529    port: u16,
530    udp: Option<bool>,
531    tfo: Option<bool>,
532    skip_cert_verify: Option<bool>,
533) -> Option<Proxy> {
534    // Extract Trojan-specific fields
535    let password = proxy.get("password").and_then(|v| v.as_str()).unwrap_or("");
536
537    if password.is_empty() {
538        return None;
539    }
540
541    // Extract underlying proxy
542    let underlying_proxy = proxy
543        .get("underlying-proxy")
544        .and_then(|v| v.as_str())
545        .unwrap_or("");
546
547    // Get SNI and network settings
548    let sni = proxy.get("sni").and_then(|v| v.as_str()).unwrap_or("");
549    let network = proxy.get("network").and_then(|v| v.as_str()).unwrap_or("");
550
551    // Get path and host, if any
552    let mut host = String::new();
553    let mut path = String::new();
554
555    // Handle WebSocket options if specified
556    if network == "ws" && proxy.get("ws-opts").is_some() {
557        if let Some(ws_opts) = proxy.get("ws-opts").and_then(|v| v.as_mapping()) {
558            if let Some(path_val) = ws_opts
559                .get(&Value::String("path".to_string()))
560                .and_then(|v| v.as_str())
561            {
562                path = path_val.to_string();
563            }
564
565            if let Some(headers) = ws_opts
566                .get(&Value::String("headers".to_string()))
567                .and_then(|v| v.as_mapping())
568            {
569                if let Some(host_val) = headers
570                    .get(&Value::String("Host".to_string()))
571                    .and_then(|v| v.as_str())
572                {
573                    host = host_val.to_string();
574                }
575            }
576        }
577    }
578
579    Some(Proxy::trojan_construct(
580        TROJAN_DEFAULT_GROUP.to_string(),
581        name.to_string(),
582        server.to_string(),
583        port,
584        password.to_string(),
585        Some(network.to_string()),
586        Some(host),
587        Some(path),
588        Some(sni.to_owned()),
589        true, // tls_secure, Trojan always uses TLS
590        udp,
591        tfo,
592        skip_cert_verify,
593        None,
594        Some(underlying_proxy.to_string()),
595    ))
596}
597
598/// Parse a Snell proxy from Clash YAML
599fn parse_clash_snell(
600    proxy: &Value,
601    name: &str,
602    server: &str,
603    port: u16,
604    udp: Option<bool>,
605    tfo: Option<bool>,
606    skip_cert_verify: Option<bool>,
607) -> Option<Proxy> {
608    // Extract Snell-specific fields
609    let psk = proxy.get("psk").and_then(|v| v.as_str()).unwrap_or("");
610
611    if psk.is_empty() {
612        return None;
613    }
614
615    // Extract underlying proxy
616    let underlying_proxy = proxy
617        .get("underlying-proxy")
618        .and_then(|v| v.as_str())
619        .unwrap_or("");
620
621    // Get obfs settings
622    let version = proxy.get("version").and_then(|v| v.as_u64()).unwrap_or(1) as u16;
623    let obfs = proxy.get("obfs").and_then(|v| v.as_str()).unwrap_or("");
624    let obfs_host = proxy
625        .get("obfs-host")
626        .and_then(|v| v.as_str())
627        .unwrap_or("");
628
629    Some(Proxy::snell_construct(
630        SNELL_DEFAULT_GROUP.to_string(),
631        name.to_string(),
632        server.to_string(),
633        port,
634        psk.to_string(),
635        obfs.to_string(),
636        obfs_host.to_string(),
637        version,
638        udp,
639        tfo,
640        skip_cert_verify,
641        Some(underlying_proxy.to_string()),
642    ))
643}
644
645/// Parse a WireGuard proxy from Clash YAML
646fn parse_clash_wireguard(
647    proxy: &Value,
648    name: &str,
649    server: &str,
650    port: u16,
651    udp: Option<bool>,
652) -> Option<Proxy> {
653    // Extract WireGuard-specific fields
654    let private_key = proxy
655        .get("privateKey")
656        .and_then(|v| v.as_str())
657        .unwrap_or("");
658    let public_key = proxy
659        .get("publicKey")
660        .and_then(|v| v.as_str())
661        .unwrap_or("");
662    let preshared_key = proxy
663        .get("presharedKey")
664        .and_then(|v| v.as_str())
665        .unwrap_or("");
666
667    if private_key.is_empty() || public_key.is_empty() {
668        return None;
669    }
670
671    // Extract underlying proxy
672    let underlying_proxy = proxy
673        .get("underlying-proxy")
674        .and_then(|v| v.as_str())
675        .unwrap_or("");
676
677    // Get IP addresses
678    let self_ip = proxy.get("ip").and_then(|v| v.as_str()).unwrap_or("");
679    let self_ipv6 = proxy.get("ipv6").and_then(|v| v.as_str()).unwrap_or("");
680
681    // Get MTU and keepalive
682    let mtu_value = proxy.get("mtu").and_then(|v| v.as_u64()).unwrap_or(0);
683    let mtu = if mtu_value > 0 {
684        Some(mtu_value as u16)
685    } else {
686        None
687    };
688
689    let keepalive_value = proxy.get("keepalive").and_then(|v| v.as_u64()).unwrap_or(0);
690    let keepalive = if keepalive_value > 0 {
691        Some(keepalive_value as u16)
692    } else {
693        None
694    };
695
696    // Get DNS servers
697    let mut dns_servers = Vec::new();
698    if let Some(Value::Sequence(dns_seq)) = proxy.get("dns") {
699        for dns in dns_seq {
700            if let Some(dns_str) = dns.as_str() {
701                dns_servers.push(dns_str.to_string());
702            }
703        }
704    }
705
706    // Get client ID and test URL
707    let client_id = proxy.get("clientId").and_then(|v| v.as_str()).unwrap_or("");
708    let test_url = proxy.get("testUrl").and_then(|v| v.as_str()).unwrap_or("");
709
710    Some(Proxy::wireguard_construct(
711        WG_DEFAULT_GROUP.to_string(),
712        name.to_string(),
713        server.to_string(),
714        port,
715        self_ip.to_string(),
716        self_ipv6.to_string(),
717        private_key.to_string(),
718        public_key.to_string(),
719        preshared_key.to_string(),
720        dns_servers,
721        mtu,
722        keepalive,
723        test_url.to_string(),
724        client_id.to_string(),
725        udp,
726        Some(underlying_proxy.to_string()),
727    ))
728}
729
730/// Parse a Hysteria proxy from Clash YAML
731fn parse_clash_hysteria(
732    proxy: &Value,
733    name: &str,
734    server: &str,
735    port: u16,
736    tfo: Option<bool>,
737    skip_cert_verify: Option<bool>,
738) -> Option<Proxy> {
739    // Extract Hysteria-specific fields
740    let auth = proxy.get("auth").and_then(|v| v.as_str()).unwrap_or("");
741    let auth_str = proxy.get("auth-str").and_then(|v| v.as_str()).unwrap_or("");
742    let obfs = proxy.get("obfs").and_then(|v| v.as_str()).unwrap_or("");
743    let protocol = proxy
744        .get("protocol")
745        .and_then(|v| v.as_str())
746        .unwrap_or("udp");
747
748    // Extract underlying proxy
749    let underlying_proxy = proxy
750        .get("underlying-proxy")
751        .and_then(|v| v.as_str())
752        .unwrap_or("");
753
754    // Get ports range if specified
755    let ports = proxy.get("ports").and_then(|v| v.as_str()).unwrap_or("");
756
757    // Get up/down speeds
758    let up_mbps = proxy.get("up").and_then(|v| v.as_u64()).unwrap_or(0);
759    let down_mbps = proxy.get("down").and_then(|v| v.as_u64()).unwrap_or(0);
760    let up_speed = if up_mbps > 0 {
761        Some(up_mbps as u32)
762    } else {
763        None
764    };
765    let down_speed = if down_mbps > 0 {
766        Some(down_mbps as u32)
767    } else {
768        None
769    };
770
771    // Get TLS settings
772    let sni = proxy.get("sni").and_then(|v| v.as_str()).unwrap_or("");
773    let alpn_value = proxy.get("alpn").and_then(|v| v.as_str()).unwrap_or("");
774    let mut alpn = Vec::new();
775    if !alpn_value.is_empty() {
776        alpn.push(alpn_value.to_string());
777    }
778
779    let fingerprint = proxy
780        .get("fingerprint")
781        .and_then(|v| v.as_str())
782        .unwrap_or("");
783    let ca = proxy.get("ca").and_then(|v| v.as_str()).unwrap_or("");
784    let ca_str = proxy.get("ca-str").and_then(|v| v.as_str()).unwrap_or("");
785
786    // Get advanced settings
787    let recv_window_conn_value = proxy
788        .get("recv-window-conn")
789        .and_then(|v| v.as_u64())
790        .unwrap_or(0);
791    let recv_window_value = proxy
792        .get("recv-window")
793        .and_then(|v| v.as_u64())
794        .unwrap_or(0);
795    let recv_window_conn = if recv_window_conn_value > 0 {
796        Some(recv_window_conn_value as u32)
797    } else {
798        None
799    };
800    let recv_window = if recv_window_value > 0 {
801        Some(recv_window_value as u32)
802    } else {
803        None
804    };
805
806    let disable_mtu_discovery = proxy.get("disable-mtu-discovery").and_then(|v| v.as_bool());
807
808    let hop_interval_value = proxy
809        .get("hop-interval")
810        .and_then(|v| v.as_u64())
811        .unwrap_or(0);
812    let hop_interval = if hop_interval_value > 0 {
813        Some(hop_interval_value as u32)
814    } else {
815        None
816    };
817
818    Some(Proxy::hysteria_construct(
819        HYSTERIA_DEFAULT_GROUP.to_string(),
820        name.to_string(),
821        server.to_string(),
822        port,
823        ports.to_string(),
824        protocol.to_string(),
825        "".to_string(), // obfs_param
826        up_speed,
827        down_speed,
828        if !auth.is_empty() {
829            auth.to_string()
830        } else {
831            auth_str.to_string()
832        },
833        obfs.to_string(),
834        sni.to_string(),
835        fingerprint.to_string(),
836        ca.to_string(),
837        ca_str.to_string(),
838        recv_window_conn,
839        recv_window,
840        disable_mtu_discovery,
841        hop_interval,
842        alpn,
843        tfo,
844        skip_cert_verify,
845        Some(underlying_proxy.to_string()),
846    ))
847}
848
849/// Parse a Hysteria2 proxy from Clash YAML
850fn parse_clash_hysteria2(
851    proxy: &Value,
852    name: &str,
853    server: &str,
854    port: u16,
855    tfo: Option<bool>,
856    skip_cert_verify: Option<bool>,
857) -> Option<Proxy> {
858    // Extract Hysteria2-specific fields
859    let password = proxy
860        .get("password")
861        .and_then(|v| v.as_str())
862        .unwrap_or("")
863        .to_owned();
864
865    // Extract underlying proxy
866    let underlying_proxy = match proxy.get("underlying-proxy").and_then(|v| v.as_str()) {
867        Some(v) => Some(v.to_owned()),
868        None => None,
869    };
870
871    // Get obfs settings
872    let obfs = match proxy.get("obfs").and_then(|v| v.as_str()) {
873        Some(v) => Some(v.to_owned()),
874        None => None,
875    };
876    let obfs_password = match proxy.get("obfs-password").and_then(|v| v.as_str()) {
877        Some(v) => Some(v.to_owned()),
878        None => None,
879    };
880
881    // Get ports range if specified
882    let ports = match proxy.get("ports").and_then(|v| v.as_str()) {
883        Some(v) => Some(v.to_owned()),
884        None => None,
885    };
886    // Get up/down speeds
887    let up_mbps = match proxy.get("up").and_then(|v| v.as_u64()) {
888        Some(v) => Some(v as u32),
889        None => None,
890    };
891    let down_mbps = match proxy.get("down").and_then(|v| v.as_u64()) {
892        Some(v) => Some(v as u32),
893        None => None,
894    };
895
896    // Get TLS settings
897    let sni = match proxy.get("sni").and_then(|v| v.as_str()) {
898        Some(v) => Some(v.to_owned()),
899        None => None,
900    };
901    let alpn = proxy
902        .get("alpn")
903        .and_then(|v| v.as_sequence())
904        .map(|v| {
905            v.iter()
906                .map(|v| v.as_str().unwrap_or("").to_owned())
907                .collect()
908        })
909        .unwrap_or_default();
910
911    let fingerprint = match proxy.get("fingerprint").and_then(|v| v.as_str()) {
912        Some(v) => Some(v.to_owned()),
913        None => None,
914    };
915    let ca = match proxy.get("ca").and_then(|v| v.as_str()) {
916        Some(v) => Some(v.to_owned()),
917        None => None,
918    };
919    let ca_str = match proxy.get("ca-str").and_then(|v| v.as_str()) {
920        Some(v) => Some(v.to_owned()),
921        None => None,
922    };
923
924    // Get congestion window
925    let cwnd_value = proxy.get("cwnd").and_then(|v| v.as_u64()).unwrap_or(0);
926    let cwnd = if cwnd_value > 0 {
927        Some(cwnd_value as u32)
928    } else {
929        None
930    };
931
932    Some(Proxy::hysteria2_construct(
933        HYSTERIA2_DEFAULT_GROUP.to_string(),
934        name.to_string(),
935        server.to_string(),
936        port,
937        ports,
938        up_mbps,
939        down_mbps,
940        password,
941        obfs,
942        obfs_password,
943        sni,
944        fingerprint,
945        alpn,
946        ca,
947        ca_str,
948        cwnd,
949        tfo,
950        skip_cert_verify,
951        underlying_proxy,
952    ))
953}