libsubconverter/parser/explodes/
vmess.rs

1use crate::{
2    models::{Proxy, SOCKS_DEFAULT_GROUP, SS_DEFAULT_GROUP, V2RAY_DEFAULT_GROUP},
3    utils::{base64::url_safe_base64_decode, url_decode},
4};
5use base64::{engine::general_purpose::STANDARD, Engine};
6use regex::Regex;
7use serde_json::Value;
8use std::collections::HashMap;
9use url::Url;
10
11/// Parse a VMess link into a Proxy object
12pub fn explode_vmess(vmess: &str, node: &mut Proxy) -> bool {
13    // Check if the link starts with vmess://
14    if !vmess.starts_with("vmess://") {
15        return false;
16    }
17
18    // Extract the base64 part
19    let encoded = &vmess[8..];
20
21    // Decode base64
22    let decoded = url_safe_base64_decode(encoded);
23
24    // Try to parse as JSON
25    let json: Value = match serde_json::from_str(&decoded) {
26        Ok(json) => json,
27        Err(_) => return false,
28    };
29
30    // Determine protocol version
31    let version = json["v"].as_u64().unwrap_or(1);
32
33    // Extract common fields
34    let add = json["add"].as_str().unwrap_or("").to_string();
35    let port = json["port"]
36        .as_str()
37        .map(|s| s.to_string())
38        .unwrap_or_else(|| {
39            json["port"]
40                .as_u64()
41                .map_or_else(|| "0".to_string(), |p| p.to_string())
42        });
43    let id = json["id"].as_str().unwrap_or("").to_string();
44    let aid = json["aid"]
45        .as_str()
46        .map(|s| s.to_string())
47        .unwrap_or_else(|| {
48            json["aid"]
49                .as_u64()
50                .map_or_else(|| "0".to_string(), |a| a.to_string())
51        });
52    let net = json["net"].as_str().unwrap_or("tcp").to_string();
53    let type_field = json["type"].as_str().unwrap_or("").to_string();
54    let mut host = json["host"].as_str().unwrap_or("").to_string();
55    let mut path = json["path"].as_str().unwrap_or("").to_string();
56    let tls = json["tls"].as_str().unwrap_or("").to_string();
57    let sni = json["sni"].as_str().unwrap_or("").to_string();
58
59    // Extract remark (ps field)
60    let remark = json["ps"].as_str().unwrap_or("").to_string();
61
62    // Parse port and aid as integers
63    let port = port.parse::<u16>().unwrap_or(0);
64    let aid = aid.parse::<u16>().unwrap_or(0);
65
66    // Handle host and path for different versions
67    if version == 2 {
68        if !host.is_empty() {
69            let host_str = host.clone();
70            let parts: Vec<&str> = host_str.split(';').collect();
71            if parts.len() == 2 {
72                host = parts[0].to_string();
73                path = parts[1].to_string();
74            }
75        }
76    }
77
78    // Create the proxy object
79    *node = Proxy::vmess_construct(
80        "VMess",
81        &remark,
82        &add,
83        port,
84        &type_field,
85        &id,
86        aid,
87        &net,
88        "auto",
89        &path,
90        &host,
91        "",
92        &tls,
93        &sni,
94        None,
95        None,
96        None,
97        None,
98        "",
99    );
100
101    true
102}
103
104/// Parse a standard VMess link into a Proxy object
105/// Format: vmess[+tls]://uuid-alterId@hostname:port[/?network=ws&host=xxx&
106/// path=yyy]
107pub fn explode_std_vmess(vmess: &str, node: &mut Proxy) -> bool {
108    // Check if the link starts with vmess:// or vmess+tls://
109    if !vmess.starts_with("vmess://") && !vmess.starts_with("vmess+") {
110        return false;
111    }
112
113    // Extract the protocol part and check TLS
114    let protocol_end = match vmess.find("://") {
115        Some(pos) => pos,
116        None => return false,
117    };
118
119    let protocol = vmess[..protocol_end].to_string();
120    let tls = protocol.contains("+tls");
121
122    // Extract the rest of the URL
123    let url_part = &vmess[protocol_end + 3..];
124
125    // Split URL and fragment (remark)
126    let (url_without_fragment, remark) = match url_part.find('#') {
127        Some(pos) => (url_part[..pos].to_string(), url_part[pos + 1..].to_string()),
128        None => (url_part.to_string(), String::new()),
129    };
130
131    // Parse the URL-like string
132    let re = Regex::new(
133        r"^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-(\d+)@([^:]+):(\d+)(.*)$",
134    )
135    .unwrap();
136
137    let caps = match re.captures(&url_without_fragment) {
138        Some(c) => c,
139        None => {
140            log::warn!("Failed to explode vmess link by regex: {}", vmess);
141            return false;
142        }
143    };
144
145    let id = caps.get(1).map_or("", |m| m.as_str()).to_string();
146    let aid = caps
147        .get(2)
148        .map_or("0", |m| m.as_str())
149        .parse::<u16>()
150        .unwrap_or(0);
151    let host = caps.get(3).map_or("", |m| m.as_str()).to_string();
152    let port = caps
153        .get(4)
154        .map_or("0", |m| m.as_str())
155        .parse::<u16>()
156        .unwrap_or(0);
157    let query = caps.get(5).map_or("", |m| m.as_str()).to_string();
158
159    // Default values
160    let mut net = "tcp".to_string();
161    let mut path = "/".to_string();
162    let mut host_header = host.clone();
163    let mut tls_str = if tls {
164        "tls".to_string()
165    } else {
166        String::new()
167    };
168    let mut sni = String::new();
169
170    // Parse query parameters
171    if !query.is_empty() && query.starts_with("/?") {
172        for param in query[2..].split('&') {
173            let mut kv = param.split('=');
174            if let (Some(k), Some(v)) = (kv.next(), kv.next()) {
175                match k {
176                    "network" => net = v.to_string(),
177                    "host" => host_header = v.to_string(),
178                    "path" => path = v.to_string(),
179                    "tls" => tls_str = v.to_string(),
180                    "sni" => sni = v.to_string(),
181                    _ => {}
182                }
183            }
184        }
185    }
186
187    // Create formatted remark if empty
188    let formatted_remark = if remark.is_empty() {
189        format!("{} ({})", host, port)
190    } else {
191        remark
192    };
193
194    // Create the proxy object
195    *node = Proxy::vmess_construct(
196        "VMess",
197        &formatted_remark,
198        &host,
199        port,
200        "",
201        &id,
202        aid,
203        &net,
204        "auto",
205        &path,
206        &host_header,
207        "",
208        &tls_str,
209        &sni,
210        None,
211        None,
212        None,
213        None,
214        "",
215    );
216
217    true
218}
219
220/// Parse a Shadowrocket format VMess link
221pub fn explode_shadowrocket(rocket: &str, node: &mut Proxy) -> bool {
222    // Check if the link starts with vmess://
223    if !rocket.starts_with("vmess://") {
224        return false;
225    }
226
227    // Try to parse as URL
228    let url = match Url::parse(rocket) {
229        Ok(url) => url,
230        Err(_) => return false,
231    };
232
233    // Extract host and port
234    let host = url.host_str().unwrap_or("").to_string();
235    let port = url.port().unwrap_or(0);
236    if port == 0 {
237        return false;
238    }
239
240    // Extract username (contains encoded config)
241    let username = url.username().to_string();
242    if username.is_empty() {
243        return false;
244    }
245
246    // Decode the username
247    let decoded = match STANDARD.decode(username) {
248        Ok(decoded) => match String::from_utf8(decoded) {
249            Ok(s) => s,
250            Err(_) => return false,
251        },
252        Err(_) => return false,
253    };
254
255    // Parse the decoded string
256    let parts: Vec<&str> = decoded.split(':').collect();
257    if parts.len() < 6 {
258        return false;
259    }
260
261    let method = parts[0].to_string();
262    let id = parts[1].to_string();
263    let aid = parts[2].parse::<u16>().unwrap_or(0);
264
265    // Extract parameters from the query string
266    let mut net = "tcp".to_string();
267    let mut path = "/".to_string();
268    let mut host_header = host.clone();
269    let mut tls = String::new();
270    let mut sni = String::new();
271
272    for (key, value) in url.query_pairs() {
273        let value = url_decode(&value);
274        match key.as_ref() {
275            "obfs" => net = value,
276            "path" => path = value,
277            "obfsParam" => host_header = value,
278            "tls" => {
279                tls = if value == "1" {
280                    "tls".to_string()
281                } else {
282                    String::new()
283                }
284            }
285            "peer" => sni = value,
286            _ => {}
287        }
288    }
289
290    // Extract remark from the fragment
291    let remark = url_decode(url.fragment().unwrap_or(""));
292    let formatted_remark = if remark.is_empty() {
293        format!("{} ({})", host, port)
294    } else {
295        remark
296    };
297
298    // Create the proxy object
299    *node = Proxy::vmess_construct(
300        "VMess",
301        &formatted_remark,
302        &host,
303        port,
304        "",
305        &id,
306        aid,
307        &net,
308        &method,
309        &path,
310        &host_header,
311        "",
312        &tls,
313        &sni,
314        None,
315        None,
316        None,
317        None,
318        "",
319    );
320
321    true
322}
323
324/// Parse a Kitsunebi format VMess link
325pub fn explode_kitsunebi(kit: &str, node: &mut Proxy) -> bool {
326    // Check if the link starts with vmess://
327    if !kit.starts_with("vmess://") {
328        return false;
329    }
330
331    // Extract the base64 part
332    let encoded = &kit[8..];
333
334    // Decode base64
335    let decoded = match STANDARD.decode(encoded) {
336        Ok(decoded) => match String::from_utf8(decoded) {
337            Ok(s) => s,
338            Err(_) => return false,
339        },
340        Err(_) => return false,
341    };
342
343    // Split by line breaks
344    let lines: Vec<&str> = decoded.lines().collect();
345    if lines.is_empty() {
346        return false;
347    }
348
349    // Parse the first line (main config)
350    let parts: Vec<&str> = lines[0].split(',').collect();
351    if parts.len() < 4 {
352        return false;
353    }
354
355    let add = parts[0].to_string();
356    let port = parts[1].parse::<u16>().unwrap_or(0);
357    let id = parts[2].to_string();
358    let aid = parts[3].parse::<u16>().unwrap_or(0);
359
360    // Default values
361    let mut net = "tcp".to_string();
362    let mut path = "/".to_string();
363    let mut host = add.clone();
364    let mut tls = String::new();
365    let mut sni = String::new();
366    let mut remark = format!("{} ({})", add, port);
367
368    // Parse additional parameters
369    for i in 4..parts.len() {
370        let kv: Vec<&str> = parts[i].split('=').collect();
371        if kv.len() != 2 {
372            continue;
373        }
374
375        let value = kv[1].to_string();
376        match kv[0] {
377            "net" => net = value,
378            "path" => path = value,
379            "host" => host = value,
380            "tls" => tls = value,
381            "sni" => sni = value,
382            "remarks" | "remark" => remark = value,
383            _ => {}
384        }
385    }
386
387    // Create the proxy object
388    *node = Proxy::vmess_construct(
389        "VMess", &remark, &add, port, "", &id, aid, &net, "auto", &path, &host, "", &tls, &sni,
390        None, None, None, None, "",
391    );
392
393    true
394}
395
396/// Parse a VMess configuration file into a vector of Proxy objects
397pub fn explode_vmess_conf(content: &str, nodes: &mut Vec<Proxy>) -> bool {
398    // Try to parse as JSON
399    let json: Value = match serde_json::from_str(content) {
400        Ok(json) => json,
401        Err(_) => return false,
402    };
403
404    // Check if it's a V2Ray configuration with outbounds
405    if json["outbounds"].is_array() {
406        // Extract outbounds
407        let outbounds = json["outbounds"].as_array().unwrap();
408        let mut success = false;
409
410        for outbound in outbounds {
411            // Check if it's a VMess outbound
412            if outbound["protocol"].as_str().unwrap_or("") != "vmess" {
413                continue;
414            }
415
416            // Extract settings
417            let settings = &outbound["settings"];
418            if !settings["vnext"].is_array() {
419                continue;
420            }
421
422            // Extract vnext
423            let vnext = settings["vnext"].as_array().unwrap();
424
425            for server in vnext {
426                let address = server["address"].as_str().unwrap_or("").to_string();
427                let port = server["port"].as_u64().unwrap_or(0) as u16;
428                if port == 0 {
429                    continue;
430                }
431
432                // Extract users
433                if !server["users"].is_array() {
434                    continue;
435                }
436
437                let users = server["users"].as_array().unwrap();
438
439                for user in users {
440                    let id = user["id"].as_str().unwrap_or("").to_string();
441                    let alter_id = user["alterId"].as_u64().unwrap_or(0) as u16;
442                    let security = user["security"].as_str().unwrap_or("auto").to_string();
443
444                    // Extract stream settings
445                    let stream_settings = &outbound["streamSettings"];
446                    let network = stream_settings["network"]
447                        .as_str()
448                        .unwrap_or("tcp")
449                        .to_string();
450                    let security_type = stream_settings["security"]
451                        .as_str()
452                        .unwrap_or("")
453                        .to_string();
454
455                    // Extract network-specific settings
456                    let mut host = String::new();
457                    let mut path = String::new();
458                    let mut edge = String::new();
459                    let mut tls = String::new();
460                    let mut sni = String::new();
461                    let mut type_field = String::new();
462
463                    match network.as_str() {
464                        "ws" => {
465                            let ws_settings = &stream_settings["wsSettings"];
466                            path = ws_settings["path"].as_str().unwrap_or("").to_string();
467
468                            if let Some(headers) = ws_settings["headers"].as_object() {
469                                if let Some(host_val) = headers.get("Host") {
470                                    host = host_val.as_str().unwrap_or("").to_string();
471                                }
472                                if let Some(edge_val) = headers.get("Edge") {
473                                    edge = edge_val.as_str().unwrap_or("").to_string();
474                                }
475                            }
476                        }
477                        "h2" => {
478                            let h2_settings = &stream_settings["httpSettings"];
479                            path = h2_settings["path"].as_str().unwrap_or("").to_string();
480
481                            if let Some(hosts) = h2_settings["host"].as_array() {
482                                if !hosts.is_empty() {
483                                    host = hosts[0].as_str().unwrap_or("").to_string();
484                                }
485                            }
486                        }
487                        "tcp" => {
488                            let tcp_settings = &stream_settings["tcpSettings"];
489                            if tcp_settings["header"]["type"].as_str().unwrap_or("") == "http" {
490                                type_field = "http".to_string();
491
492                                if let Some(request) = tcp_settings["header"]["request"].as_object()
493                                {
494                                    if let Some(paths) = request.get("path") {
495                                        if let Some(paths_array) = paths.as_array() {
496                                            if !paths_array.is_empty() {
497                                                path = paths_array[0]
498                                                    .as_str()
499                                                    .unwrap_or("")
500                                                    .to_string();
501                                            }
502                                        }
503                                    }
504
505                                    if let Some(headers) = request.get("headers") {
506                                        if let Some(headers_obj) = headers.as_object() {
507                                            if let Some(host_val) = headers_obj.get("Host") {
508                                                host = host_val.as_str().unwrap_or("").to_string();
509                                            }
510                                            if let Some(edge_val) = headers_obj.get("Edge") {
511                                                edge = edge_val.as_str().unwrap_or("").to_string();
512                                            }
513                                        }
514                                    }
515                                }
516                            }
517                        }
518                        _ => {}
519                    }
520
521                    if security_type == "tls" {
522                        tls = "tls".to_string();
523                        let tls_settings = &stream_settings["tlsSettings"];
524                        sni = tls_settings["serverName"]
525                            .as_str()
526                            .unwrap_or("")
527                            .to_string();
528                    }
529
530                    // Create formatted remark for the node
531                    let formatted_remark = format!("{} ({})", address, port);
532
533                    // Create the proxy object
534                    let node = Proxy::vmess_construct(
535                        "VMess",
536                        &formatted_remark,
537                        &address,
538                        port,
539                        &type_field,
540                        &id,
541                        alter_id,
542                        &network,
543                        &security,
544                        &path,
545                        &host,
546                        &edge,
547                        &tls,
548                        &sni,
549                        None,
550                        None,
551                        None,
552                        None,
553                        "",
554                    );
555
556                    nodes.push(node);
557                    success = true;
558                }
559            }
560        }
561
562        if success {
563            return true;
564        }
565    }
566
567    // Handle custom VMess array format if outbounds format didn't match
568    if json["vmess"].is_array() {
569        let mut group_map: HashMap<String, String> = HashMap::new();
570
571        // Extract subItem data for group information
572        if json["subItem"].is_array() {
573            let sub_items = json["subItem"].as_array().unwrap();
574            for sub_item in sub_items {
575                if let (Some(id), Some(remarks)) =
576                    (sub_item["id"].as_str(), sub_item["remarks"].as_str())
577                {
578                    group_map.insert(id.to_string(), remarks.to_string());
579                }
580            }
581        }
582
583        // Process each VMess entry
584        let vmess_entries = json["vmess"].as_array().unwrap();
585        let mut nodes_added = false;
586
587        for (_, entry) in vmess_entries.iter().enumerate() {
588            // Skip invalid entries
589            if entry["address"].is_null() || entry["port"].is_null() || entry["id"].is_null() {
590                continue;
591            }
592
593            // Extract common fields
594            let ps = entry["remarks"].as_str().unwrap_or("").to_string();
595            let add = entry["address"].as_str().unwrap_or("").to_string();
596            let port = entry["port"].as_u64().unwrap_or(0) as u16;
597            if port == 0 {
598                continue;
599            }
600
601            // Extract sub_id for group information
602            let sub_id = entry["subid"].as_str().unwrap_or("").to_string();
603
604            // Determine group name
605            let mut group = V2RAY_DEFAULT_GROUP.to_string();
606            if !sub_id.is_empty() {
607                if let Some(sub_group) = group_map.get(&sub_id) {
608                    group = sub_group.clone();
609                }
610            }
611
612            // Use address:port as remark if ps is empty
613            let remark = if ps.is_empty() {
614                format!("{} ({})", add, port)
615            } else {
616                ps
617            };
618
619            // Extract configType
620            let config_type = entry["configType"].as_u64().unwrap_or(1);
621
622            // Create appropriate proxy based on configType
623            match config_type {
624                1 => {
625                    // VMess config
626                    let type_field = entry["headerType"].as_str().unwrap_or("").to_string();
627                    let id = entry["id"].as_str().unwrap_or("").to_string();
628                    let aid = entry["alterId"].as_u64().unwrap_or(0) as u16;
629                    let net = entry["network"].as_str().unwrap_or("tcp").to_string();
630                    let path = entry["path"].as_str().unwrap_or("").to_string();
631                    let host = entry["requestHost"].as_str().unwrap_or("").to_string();
632                    let tls = entry["streamSecurity"].as_str().unwrap_or("").to_string();
633                    let cipher = entry["security"].as_str().unwrap_or("auto").to_string();
634                    let sni = entry["sni"].as_str().unwrap_or("").to_string();
635
636                    // Extract security settings
637                    let allow_insecure = entry["allowInsecure"].as_bool();
638
639                    let node = Proxy::vmess_construct(
640                        &group,
641                        &remark,
642                        &add,
643                        port,
644                        &type_field,
645                        &id,
646                        aid,
647                        &net,
648                        &cipher,
649                        &path,
650                        &host,
651                        "",
652                        &tls,
653                        &sni,
654                        None,
655                        None,
656                        allow_insecure,
657                        None,
658                        "",
659                    );
660
661                    nodes.push(node);
662                    nodes_added = true;
663                }
664                3 => {
665                    // SS config
666                    let id = entry["id"].as_str().unwrap_or("").to_string();
667                    let cipher = entry["security"].as_str().unwrap_or("").to_string();
668
669                    let allow_insecure = entry["allowInsecure"].as_bool();
670
671                    let node = Proxy::ss_construct(
672                        SS_DEFAULT_GROUP,
673                        &remark,
674                        &add,
675                        port,
676                        &id,
677                        &cipher,
678                        "",
679                        "",
680                        None,
681                        None,
682                        allow_insecure,
683                        None,
684                        "",
685                    );
686
687                    nodes.push(node);
688                    nodes_added = true;
689                }
690                4 => {
691                    // Socks config
692                    let allow_insecure = entry["allowInsecure"].as_bool();
693
694                    let node = Proxy::socks_construct(
695                        SOCKS_DEFAULT_GROUP,
696                        &remark,
697                        &add,
698                        port,
699                        "",
700                        "",
701                        None,
702                        None,
703                        allow_insecure,
704                        "",
705                    );
706
707                    nodes.push(node);
708                    nodes_added = true;
709                }
710                _ => continue,
711            }
712        }
713
714        return nodes_added;
715    }
716
717    false
718}
719
720/// Parse a standard VMess link using Url::parse
721/// Format examples:
722/// vmess://uuid@host:port?type=ws&path=/&host=custom.host.com&tls=true&
723/// sni=custom.sni.com#remark vmess://uuid-aid@host:port?network=tcp&
724/// encryption=aes-128-gcm#remark vmess+tls://uuid@host:port#remark
725/// Expected example:
726/// vmess://ac104f2c-b405-3116-b81a-8c0db65a1b34@ovhzhongzhuan.ewddns.net:38555?
727/// encryption=auto&path=%2F8858d045-66fe-441a-8d35-1507216fbb2f.live238.m3u8&
728/// type=ws#%F0%9F%87%B8%F0%9F%87%AC%20OVH%207
729pub fn explode_std_vmess_new(vmess_str: &str, node: &mut Proxy) -> bool {
730    let url = match Url::parse(vmess_str) {
731        Ok(u) => u,
732        Err(_) => {
733            log::debug!("Failed to parse VMess URL: {}", vmess_str);
734            return false;
735        }
736    };
737
738    // Check scheme
739    let mut initial_tls_str = String::new();
740    match url.scheme() {
741        "vmess" => { /* initial_tls_str remains empty */ }
742        "vmess+tls" => initial_tls_str = "tls".to_string(),
743        s => {
744            log::debug!("Invalid VMess scheme: {}", s);
745            return false;
746        }
747    }
748
749    let server_address = match url.host_str() {
750        Some(h) if !h.is_empty() => h.to_string(),
751        _ => {
752            log::debug!("VMess URL missing or empty host");
753            return false;
754        }
755    };
756
757    let server_port = match url.port() {
758        Some(p) => p,
759        None => {
760            log::debug!("VMess URL missing port");
761            return false;
762        }
763    };
764
765    let user_info_str = url.username();
766    if user_info_str.is_empty() {
767        log::debug!("VMess URL missing user info (uuid)");
768        return false;
769    }
770
771    let id: String;
772    let mut aid: u16 = 0;
773
774    // Try to parse uuid-aid from user_info_str
775    if let Some(last_hyphen_pos) = user_info_str.rfind('-') {
776        // Ensure hyphen is not at the start or end, and there are characters before and
777        // after
778        if last_hyphen_pos > 0 && last_hyphen_pos < user_info_str.len() - 1 {
779            let potential_id_part = &user_info_str[..last_hyphen_pos];
780            let potential_aid_str = &user_info_str[last_hyphen_pos + 1..];
781            if let Ok(parsed_aid) = potential_aid_str.parse::<u16>() {
782                id = potential_id_part.to_string();
783                aid = parsed_aid;
784            } else {
785                // Non-numeric after last hyphen, or parse failed; treat full string as id
786                id = user_info_str.to_string();
787            }
788        } else {
789            // Hyphen is at start/end or string is just "-" or "-something" or "something-"
790            id = user_info_str.to_string();
791        }
792    } else {
793        // No hyphen found, treat full string as id
794        id = user_info_str.to_string();
795    }
796
797    // ID (UUID) must not be empty
798    if id.is_empty() {
799        log::debug!("Parsed empty ID from VMess URL user info");
800        return false;
801    }
802
803    // Default values for parameters
804    let mut net = "tcp".to_string();
805    let mut path_query = "/".to_string();
806    let mut host_header = server_address.clone(); // Default Host header to server address
807    let mut tls_str = initial_tls_str; // Determined by scheme (vmess / vmess+tls)
808    let mut sni = String::new();
809    let mut security_param = "auto".to_string(); // Default encryption/security
810
811    for (key_cow, value_cow) in url.query_pairs() {
812        let key = key_cow.as_ref();
813        // value_cow is Cow<str> and already percent-decoded by query_pairs()
814        let value = value_cow.into_owned();
815
816        match key {
817            "type" | "network" => net = value,
818            "host" => host_header = value, // HTTP Host header
819            "path" => {
820                if value.is_empty() || value == "/" {
821                    path_query = "/".to_string();
822                } else if value.starts_with('/') {
823                    path_query = value;
824                } else {
825                    path_query = format!("/{}", value); // Ensure path starts
826                                                        // with a slash
827                }
828            }
829            "tls" => {
830                // Handles "true", "false", "1", "0", or a specific string like "tls"
831                if value.eq_ignore_ascii_case("true") || value == "1" {
832                    tls_str = "tls".to_string();
833                } else if value.eq_ignore_ascii_case("false") || value == "0" {
834                    tls_str = String::new();
835                } else {
836                    tls_str = value; // Allows direct assignment, e.g., tls=xtls
837                }
838            }
839            "sni" => sni = value,
840            "encryption" | "security" => security_param = value, // For cipher
841            _ => { /* Unknown query parameter, ignore */ }
842        }
843    }
844
845    let remark_from_fragment = url.fragment().map_or_else(String::new, |f| url_decode(f));
846
847    let formatted_remark = if remark_from_fragment.is_empty() {
848        format!("{} ({})", server_address, server_port)
849    } else {
850        remark_from_fragment
851    };
852
853    *node = Proxy::vmess_construct(
854        "VMess",           // name (using a generic name for standard parsing)
855        &formatted_remark, // remark
856        &server_address,   // server address
857        server_port,       // port
858        "",                /* type_field (e.g. headerType for TCP obfuscation, usually "" for
859                            * query-based) */
860        &id,             // uuid
861        aid,             // alter_id
862        &net,            // network type (e.g., "tcp", "ws", "h2")
863        &security_param, // security/cipher (e.g., "auto", "aes-128-gcm")
864        &path_query,     // path (for ws, h2)
865        &host_header,    // host (for HTTP Host header in ws, h2)
866        "",              // edge (e.g. for CDN specific features, usually "" here)
867        &tls_str,        // tls ("tls" or "" or custom like "xtls")
868        &sni,            // sni (Server Name Indication for TLS)
869        None,            // congestion_controller
870        None,            // domain_strategy
871        None,            // allow_insecure
872        None,            // fingerprint
873        "",              // flow
874    );
875
876    true
877}