1use crate::error::{ProxyError, Result};
2use crate::i18n::t;
3use crate::types::{ProxyNode, ProxyType};
4use crate::utils::{decode_base64_utf8, safe_decode_uri};
5use indexmap::IndexMap;
6use serde_yaml::Value as YamlValue;
7
8fn parse_ss(uri: &str) -> Result<ProxyNode> {
9 let without_scheme = &uri[5..];
10
11 let hash_idx = without_scheme.rfind('#');
12 let raw_name = match hash_idx {
13 Some(idx) => safe_decode_uri(Some(&without_scheme[idx + 1..])),
14 None => String::new(),
15 };
16 let body = match hash_idx {
17 Some(idx) => &without_scheme[..idx],
18 None => without_scheme,
19 };
20
21 if let Some(at_idx) = body.find('@') {
22 let b64_part = &body[..at_idx];
23 let host_port = &body[at_idx + 1..];
24 let (server, port) = split_host_port(host_port).ok_or_else(|| {
25 ProxyError::msg(t("err_ss_address", &[("host", host_port)]))
26 })?;
27
28 let decoded = decode_base64_utf8(b64_part).map_err(|_| {
29 ProxyError::msg(t("err_ss_sip002_credentials", &[("raw", b64_part)]))
30 })?;
31
32 if let Some(at_in_decoded) = decoded.find('@') {
33 let cred = &decoded[..at_in_decoded];
34 let (method, password) = split_cred(cred).ok_or_else(|| {
35 ProxyError::msg(t(
36 "err_ss_sip002_credentials",
37 &[("raw", cred)],
38 ))
39 })?;
40 let mut node = ProxyNode::new(
41 if raw_name.is_empty() {
42 server.clone()
43 } else {
44 raw_name
45 },
46 ProxyType::Ss,
47 server,
48 port,
49 );
50 node.set_str("cipher", method);
51 node.set_str("password", password);
52 return Ok(node);
53 }
54
55 let colon_idx = decoded.find(':').ok_or_else(|| {
56 ProxyError::msg(t(
57 "err_ss_sip002_credentials",
58 &[("raw", &decoded)],
59 ))
60 })?;
61 let mut node = ProxyNode::new(
62 if raw_name.is_empty() {
63 server.clone()
64 } else {
65 raw_name
66 },
67 ProxyType::Ss,
68 server,
69 port,
70 );
71 node.set_str("cipher", &decoded[..colon_idx]);
72 node.set_str("password", &decoded[colon_idx + 1..]);
73 return Ok(node);
74 }
75
76 let decoded = decode_base64_utf8(body).map_err(|_| {
77 ProxyError::msg(t("err_ss_legacy_no_at", &[("raw", body)]))
78 })?;
79 let at_in_decoded = decoded.find('@').ok_or_else(|| {
80 ProxyError::msg(t("err_ss_legacy_no_at", &[("raw", &decoded)]))
81 })?;
82 let cred = &decoded[..at_in_decoded];
83 let host_port = &decoded[at_in_decoded + 1..];
84 let (server, port) = split_host_port(host_port).ok_or_else(|| {
85 ProxyError::msg(t("err_ss_legacy_address", &[("host", host_port)]))
86 })?;
87 let (method, password) = split_cred(cred).ok_or_else(|| {
88 ProxyError::msg(t("err_ss_legacy_no_method", &[("raw", cred)]))
89 })?;
90
91 let mut node = ProxyNode::new(
92 if raw_name.is_empty() {
93 server.clone()
94 } else {
95 raw_name
96 },
97 ProxyType::Ss,
98 server,
99 port,
100 );
101 node.set_str("cipher", method);
102 node.set_str("password", password);
103 Ok(node)
104}
105
106fn indexmap_to_yaml_mapping(map: IndexMap<String, YamlValue>) -> YamlValue {
107 let mut mapping = serde_yaml::Mapping::new();
108 for (k, v) in map {
109 mapping.insert(YamlValue::String(k), v);
110 }
111 YamlValue::Mapping(mapping)
112}
113
114fn split_host_port(host_port: &str) -> Option<(String, u16)> {
115 let (server, port_str) = host_port.rsplit_once(':')?;
116 let port = port_str.parse().ok()?;
117 if server.is_empty() {
118 return None;
119 }
120 Some((server.to_string(), port))
121}
122
123fn split_cred(cred: &str) -> Option<(String, String)> {
124 let colon_idx = cred.find(':')?;
125 let method = cred[..colon_idx].trim();
126 if method.is_empty() {
127 return None;
128 }
129 Some((method.to_string(), cred[colon_idx + 1..].to_string()))
130}
131
132fn vmess_field_str(obj: &serde_json::Value, key: &str) -> Option<String> {
133 match obj.get(key)? {
134 serde_json::Value::String(s) => Some(s.clone()),
135 serde_json::Value::Number(n) => Some(n.to_string()),
136 serde_json::Value::Bool(b) => Some(b.to_string()),
137 _ => None,
138 }
139}
140
141fn vmess_field_u16(obj: &serde_json::Value, key: &str) -> Option<u16> {
142 match obj.get(key)? {
143 serde_json::Value::Number(n) => n.as_u64().and_then(|v| u16::try_from(v).ok()),
144 serde_json::Value::String(s) => s.parse().ok(),
145 _ => None,
146 }
147}
148
149fn parse_vmess(uri: &str) -> Result<ProxyNode> {
150 let b64 = &uri[8..];
151 let json_str = decode_base64_utf8(b64).map_err(|_| ProxyError::msg(t("err_vmess_parse", &[])))?;
152 let obj: serde_json::Value =
153 serde_json::from_str(&json_str).map_err(|_| ProxyError::msg(t("err_vmess_parse", &[])))?;
154
155 let ps = vmess_field_str(&obj, "ps");
156 let remarks = vmess_field_str(&obj, "remarks");
157 let name = {
158 let raw = safe_decode_uri(ps.as_deref().or(remarks.as_deref()));
159 if raw.is_empty() {
160 "Unnamed".to_string()
161 } else {
162 raw
163 }
164 };
165 let ws_host = vmess_field_str(&obj, "host");
166 let server = vmess_field_str(&obj, "add")
167 .or_else(|| vmess_field_str(&obj, "host"))
168 .unwrap_or_default();
169 let port = vmess_field_u16(&obj, "port").unwrap_or(0);
170
171 if server.is_empty() || port == 0 {
172 return Err(ProxyError::msg(t("err_vmess_no_server", &[])));
173 }
174
175 let mut node = ProxyNode::new(name, ProxyType::Vmess, server, port);
176 if let Some(uuid) = vmess_field_str(&obj, "id") {
177 node.set_str("uuid", uuid);
178 }
179 node.set_u64(
180 "alterId",
181 vmess_field_u16(&obj, "aid").map(u64::from).unwrap_or(0),
182 );
183 node.set_str(
184 "cipher",
185 vmess_field_str(&obj, "scy").as_deref().unwrap_or("auto"),
186 );
187
188 let net = vmess_field_str(&obj, "net").unwrap_or_else(|| "tcp".to_string());
189 match net.as_str() {
190 "ws" => {
191 node.set_str("network", "ws");
192 let mut ws_opts = IndexMap::new();
193 ws_opts.insert(
194 "path".to_string(),
195 YamlValue::String(vmess_field_str(&obj, "path").unwrap_or_else(|| "/".to_string())),
196 );
197 if let Some(host) = ws_host.clone() {
198 let mut headers = IndexMap::new();
199 headers.insert("Host".to_string(), YamlValue::String(host));
200 ws_opts.insert("headers".to_string(), indexmap_to_yaml_mapping(headers));
201 }
202 node.set_value("ws-opts", indexmap_to_yaml_mapping(ws_opts));
203 }
204 "grpc" => {
205 node.set_str("network", "grpc");
206 let mut grpc_opts = IndexMap::new();
207 grpc_opts.insert(
208 "grpc-service-name".to_string(),
209 YamlValue::String(vmess_field_str(&obj, "serviceName").unwrap_or_default()),
210 );
211 node.set_value("grpc-opts", indexmap_to_yaml_mapping(grpc_opts));
212 }
213 "h2" => {
214 node.set_str("network", "h2");
215 let mut h2_opts = IndexMap::new();
216 h2_opts.insert(
217 "path".to_string(),
218 YamlValue::String(vmess_field_str(&obj, "path").unwrap_or_else(|| "/".to_string())),
219 );
220 if let Some(host) = ws_host.clone() {
221 h2_opts.insert("host".to_string(), YamlValue::String(host));
222 }
223 node.set_value("h2-opts", indexmap_to_yaml_mapping(h2_opts));
224 }
225 "quic" => {
226 node.set_str("network", "quic");
227 let mut quic_opts = IndexMap::new();
228 quic_opts.insert(
229 "security".to_string(),
230 YamlValue::String(
231 vmess_field_str(&obj, "security").unwrap_or_else(|| "none".to_string()),
232 ),
233 );
234 quic_opts.insert(
235 "key".to_string(),
236 YamlValue::String(vmess_field_str(&obj, "key").unwrap_or_default()),
237 );
238 node.set_value("quic-opts", indexmap_to_yaml_mapping(quic_opts));
239 }
240 "kcp" => {
241 node.set_str("network", "kcp");
242 let mut kcp_opts = IndexMap::new();
243 kcp_opts.insert("mtu".to_string(), YamlValue::Number(1350.into()));
244 kcp_opts.insert("mptcp".to_string(), YamlValue::Bool(false));
245 node.set_value("kcp-opts", indexmap_to_yaml_mapping(kcp_opts));
246 }
247 _ => {}
248 }
249
250 let tls = vmess_field_str(&obj, "tls");
251 match tls.as_deref() {
252 Some("tls") => {
253 node.set_bool("tls", true);
254 if let Some(sni) = vmess_field_str(&obj, "sni") {
255 node.set_str("sni", sni);
256 }
257 if let Some(alpn) = vmess_field_str(&obj, "alpn") {
258 let values: Vec<YamlValue> = alpn
259 .split(',')
260 .map(|s| YamlValue::String(s.trim().to_string()))
261 .collect();
262 node.set_value("alpn", YamlValue::Sequence(values));
263 }
264 if let Some(fp) = vmess_field_str(&obj, "fp") {
265 node.set_str("client-fingerprint", fp);
266 }
267 let skip = ["skip-cert-verify", "skip_cert_verify", "allow_insecure", "allowInsecure"]
268 .iter()
269 .filter_map(|key| vmess_field_str(&obj, key))
270 .any(|v| v == "true" || v == "1");
271 if skip {
272 node.set_bool("skip-cert-verify", true);
273 }
274 }
275 Some("none") => {
276 node.set_bool("tls", false);
277 node.set_bool("skip-cert-verify", true);
278 }
279 _ => {}
280 }
281
282 Ok(node)
283}
284
285fn parse_trojan(uri: &str) -> Result<ProxyNode> {
286 let parsed = url::Url::parse(uri)?;
287 let name = {
288 let raw = safe_decode_uri(parsed.fragment());
289 if raw.is_empty() {
290 parsed.host_str().unwrap_or_default().to_string()
291 } else {
292 raw
293 }
294 };
295 let server = parsed.host_str().unwrap_or_default().to_string();
296 let port = parsed.port().unwrap_or(443);
297 let password = if parsed.username().is_empty() {
298 parsed.password().unwrap_or_default().to_string()
299 } else {
300 parsed.username().to_string()
301 };
302
303 let mut node = ProxyNode::new(name, ProxyType::Trojan, server.clone(), port);
304 node.set_str("password", password);
305 node.set_str(
306 "sni",
307 parsed
308 .query_pairs()
309 .find(|(k, _)| k == "sni")
310 .map(|(_, v)| v.into_owned())
311 .unwrap_or(server),
312 );
313
314 let allow_insecure = parsed
315 .query_pairs()
316 .find(|(k, _)| k == "allowInsecure" || k == "allow_insecure")
317 .map(|(_, v)| v.into_owned());
318 if allow_insecure.as_deref() == Some("1") || allow_insecure.as_deref() == Some("true") {
319 node.set_bool("skip-cert-verify", true);
320 }
321
322 Ok(node)
323}
324
325fn parse_hysteria2(uri: &str) -> Result<ProxyNode> {
326 let parsed = url::Url::parse(uri)?;
327 let name = {
328 let raw = safe_decode_uri(parsed.fragment());
329 if raw.is_empty() {
330 parsed.host_str().unwrap_or_default().to_string()
331 } else {
332 raw
333 }
334 };
335 let server = parsed.host_str().unwrap_or_default().to_string();
336 let port = parsed.port().unwrap_or(443);
337 let password = if parsed.username().is_empty() {
338 parsed.password().unwrap_or_default().to_string()
339 } else {
340 parsed.username().to_string()
341 };
342
343 let mut node = ProxyNode::new(name, ProxyType::Hysteria2, server, port);
344 node.set_str("password", password);
345
346 if let Some(sni) = parsed
347 .query_pairs()
348 .find(|(k, _)| k == "sni" || k == "peer")
349 .map(|(_, v)| v.into_owned())
350 {
351 node.set_str("sni", sni);
352 }
353
354 let insecure = parsed
355 .query_pairs()
356 .find(|(k, _)| k == "insecure" || k == "allowInsecure" || k == "skip-cert-verify")
357 .map(|(_, v)| v.into_owned());
358 if insecure.as_deref() == Some("1") || insecure.as_deref() == Some("true") {
359 node.set_bool("skip-cert-verify", true);
360 }
361
362 if let Some(alpn) = parsed
363 .query_pairs()
364 .find(|(k, _)| k == "alpn")
365 .map(|(_, v)| v.into_owned())
366 {
367 let values: Vec<YamlValue> = alpn
368 .split(',')
369 .map(|s| YamlValue::String(s.trim().to_string()))
370 .collect();
371 node.set_value("alpn", YamlValue::Sequence(values));
372 }
373
374 if let Some(up) = parsed
375 .query_pairs()
376 .find(|(k, _)| k == "up" || k == "upload")
377 .and_then(|(_, v)| v.parse::<u64>().ok())
378 {
379 node.set_u64("up", up);
380 }
381
382 if let Some(down) = parsed
383 .query_pairs()
384 .find(|(k, _)| k == "down" || k == "download")
385 .and_then(|(_, v)| v.parse::<u64>().ok())
386 {
387 node.set_u64("down", down);
388 }
389
390 Ok(node)
391}
392
393pub fn parse_uri(uri: &str) -> Result<ProxyNode> {
394 if let Some(rest) = uri.strip_prefix("ss://") {
395 return parse_ss(&format!("ss://{rest}"));
396 }
397 if let Some(rest) = uri.strip_prefix("vmess://") {
398 return parse_vmess(&format!("vmess://{rest}"));
399 }
400 if uri.starts_with("trojan://") {
401 return parse_trojan(uri);
402 }
403 if uri.starts_with("hysteria2://") || uri.starts_with("hy2://") {
404 return parse_hysteria2(uri);
405 }
406
407 let prefix = uri.chars().take(10).collect::<String>();
408 Err(ProxyError::msg(t(
409 "err_unsupported_protocol",
410 &[("prefix", &prefix)],
411 )))
412}
413
414pub fn parse_lines(lines: &[impl AsRef<str>]) -> Vec<ProxyNode> {
415 let mut nodes = Vec::new();
416 for line in lines {
417 match parse_uri(line.as_ref()) {
418 Ok(node) => nodes.push(node),
419 Err(err) => {
420 eprintln!(
421 "⚠️ {}",
422 t("skip_node", &[("msg", &err.to_string())])
423 );
424 }
425 }
426 }
427 nodes
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::fetch::deduplicate_names;
434
435 #[test]
436 fn ss_sip002_standard() {
437 let uri = "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#美国-01";
438 let node = parse_uri(uri).unwrap();
439 assert_eq!(node.proxy_type, ProxyType::Ss);
440 assert_eq!(node.name, "美国-01");
441 assert_eq!(node.server, "us1.example.com");
442 assert_eq!(node.port, 8388);
443 assert_eq!(node.extra.get("cipher").and_then(|v| v.as_str()), Some("aes-256-gcm"));
444 assert_eq!(node.extra.get("password").and_then(|v| v.as_str()), Some("password"));
445 }
446
447 #[test]
448 fn ss_legacy_format() {
449 use base64::Engine;
450 let raw = "chacha20-ietf-poly1305:secret123@jp1.example.com:443";
451 let b64 = base64::engine::general_purpose::STANDARD.encode(raw);
452 let uri = format!("ss://{b64}#日本-01");
453 let node = parse_uri(&uri).unwrap();
454 assert_eq!(node.name, "日本-01");
455 assert_eq!(node.server, "jp1.example.com");
456 assert_eq!(node.port, 443);
457 }
458
459 #[test]
460 fn ss_fallback_name_to_server() {
461 let uri = "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@sg1.example.com:443";
462 let node = parse_uri(uri).unwrap();
463 assert_eq!(node.name, "sg1.example.com");
464 }
465
466 #[test]
467 fn ss_password_with_colons() {
468 use base64::Engine;
469 let raw = "2022-blake3-aes-256-gcm:long:password:with:colons";
470 let b64 = base64::engine::general_purpose::STANDARD.encode(raw);
471 let uri = format!("ss://{b64}@hk1.example.com:443#香港");
472 let node = parse_uri(&uri).unwrap();
473 assert_eq!(
474 node.extra.get("cipher").and_then(|v| v.as_str()),
475 Some("2022-blake3-aes-256-gcm")
476 );
477 assert_eq!(
478 node.extra.get("password").and_then(|v| v.as_str()),
479 Some("long:password:with:colons")
480 );
481 }
482
483 #[test]
484 fn vmess_aid_as_number() {
485 use base64::Engine;
486 let cfg = r#"{"ps":"JMS-test","port":"6191","id":"be599ec8-c3de-47d5-ac77-c57f24e13d47","aid":0,"net":"tcp","type":"none","tls":"none","add":"104.243.21.29"}"#;
487 let b64 = base64::engine::general_purpose::STANDARD.encode(cfg);
488 let node = parse_uri(&format!("vmess://{b64}")).unwrap();
489 assert_eq!(node.proxy_type, ProxyType::Vmess);
490 assert_eq!(node.server, "104.243.21.29");
491 assert_eq!(node.extra.get("alterId").and_then(|v| v.as_u64()), Some(0));
492 }
493
494 #[test]
495 fn vmess_ws_tls() {
496 use base64::Engine;
497 let cfg = serde_json::json!({
498 "v": "2",
499 "ps": "日本-01",
500 "add": "jp1.example.com",
501 "port": "443",
502 "id": "550e8400-e29b-41d4-a716-446655440000",
503 "aid": "0",
504 "net": "ws",
505 "host": "cloudflare.com",
506 "path": "/ws?ed=2048",
507 "tls": "tls",
508 "sni": "cloudflare.com"
509 });
510 let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
511 let node = parse_uri(&format!("vmess://{b64}")).unwrap();
512 assert_eq!(node.proxy_type, ProxyType::Vmess);
513 assert_eq!(node.extra.get("network").and_then(|v| v.as_str()), Some("ws"));
514 assert_eq!(node.extra.get("tls").and_then(|v| v.as_bool()), Some(true));
515 }
516
517 #[test]
518 fn trojan_standard() {
519 let uri = "trojan://my-password@sg1.example.com:443?security=tls&sni=sg1.example.com#新加坡-01";
520 let node = parse_uri(uri).unwrap();
521 assert_eq!(node.proxy_type, ProxyType::Trojan);
522 assert_eq!(node.name, "新加坡-01");
523 assert_eq!(node.extra.get("password").and_then(|v| v.as_str()), Some("my-password"));
524 }
525
526 #[test]
527 fn hysteria2_standard() {
528 let uri = "hysteria2://my-password@jp1.example.com:443?insecure=1&sni=jp1.example.com&alpn=h3#日本-Hy2";
529 let node = parse_uri(uri).unwrap();
530 assert_eq!(node.proxy_type, ProxyType::Hysteria2);
531 assert_eq!(node.name, "日本-Hy2");
532 assert_eq!(
533 node.extra.get("skip-cert-verify").and_then(|v| v.as_bool()),
534 Some(true)
535 );
536 }
537
538 #[test]
539 fn deduplicate_names_suffix() {
540 let mut nodes = vec![
541 ProxyNode::new("日本-01".into(), ProxyType::Ss, "a.com".into(), 443),
542 ProxyNode::new("日本-01".into(), ProxyType::Ss, "b.com".into(), 443),
543 ProxyNode::new("日本-01".into(), ProxyType::Ss, "c.com".into(), 443),
544 ];
545 deduplicate_names(&mut nodes);
546 assert_eq!(nodes[0].name, "日本-01");
547 assert_eq!(nodes[1].name, "日本-2");
548 assert_eq!(nodes[2].name, "日本-3");
549 }
550
551 #[test]
552 fn parse_lines_mixed_protocols() {
553 let lines = vec![
554 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#美国-01".to_string(),
555 "trojan://pass@sg1.example.com:443?security=tls&sni=sg1.example.com#新加坡-01".to_string(),
556 ];
557 let nodes = parse_lines(&lines);
558 assert_eq!(nodes.len(), 2);
559 }
560
561 #[test]
562 fn ss_sip002_nonstandard_extension() {
563 use base64::Engine;
564 let full = "aes-256-gcm:RealPassword@extra.example.com:8443";
565 let b64 = base64::engine::general_purpose::STANDARD.encode(full);
566 let uri = format!("ss://{b64}@extra.example.com:8443#测试");
567 let node = parse_uri(&uri).unwrap();
568 assert_eq!(
569 node.extra.get("cipher").and_then(|v| v.as_str()),
570 Some("aes-256-gcm")
571 );
572 assert_eq!(
573 node.extra.get("password").and_then(|v| v.as_str()),
574 Some("RealPassword")
575 );
576 assert_eq!(node.server, "extra.example.com");
577 assert_eq!(node.port, 8443);
578 }
579
580 #[test]
581 fn vmess_grpc() {
582 use base64::Engine;
583 let cfg = serde_json::json!({
584 "v": "2",
585 "ps": "日本-01",
586 "add": "jp1.example.com",
587 "port": "443",
588 "id": "550e8400-e29b-41d4-a716-446655440000",
589 "aid": "0",
590 "net": "grpc",
591 "serviceName": "my-service",
592 "tls": "tls"
593 });
594 let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
595 let node = parse_uri(&format!("vmess://{b64}")).unwrap();
596 assert_eq!(node.extra.get("network").and_then(|v| v.as_str()), Some("grpc"));
597 let grpc_opts = node.extra.get("grpc-opts").unwrap();
598 let mapping = grpc_opts.as_mapping().unwrap();
599 let key = YamlValue::String("grpc-service-name".into());
600 assert_eq!(
601 mapping.get(&key).and_then(|v| v.as_str()),
602 Some("my-service")
603 );
604 }
605
606 #[test]
607 fn vmess_h2() {
608 use base64::Engine;
609 let cfg = serde_json::json!({
610 "v": "2",
611 "ps": "日本-01",
612 "add": "jp1.example.com",
613 "port": "443",
614 "id": "550e8400-e29b-41d4-a716-446655440000",
615 "aid": "0",
616 "net": "h2",
617 "path": "/api",
618 "host": "example.com",
619 "tls": "tls"
620 });
621 let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
622 let node = parse_uri(&format!("vmess://{b64}")).unwrap();
623 assert_eq!(node.extra.get("network").and_then(|v| v.as_str()), Some("h2"));
624 let h2_opts = node.extra.get("h2-opts").unwrap().as_mapping().unwrap();
625 assert_eq!(
626 h2_opts
627 .get(&YamlValue::String("path".into()))
628 .and_then(|v| v.as_str()),
629 Some("/api")
630 );
631 assert_eq!(
632 h2_opts
633 .get(&YamlValue::String("host".into()))
634 .and_then(|v| v.as_str()),
635 Some("example.com")
636 );
637 }
638
639 #[test]
640 fn vmess_tcp_no_tls() {
641 use base64::Engine;
642 let cfg = serde_json::json!({
643 "v": "2",
644 "ps": "日本-01",
645 "add": "jp1.example.com",
646 "port": "443",
647 "id": "550e8400-e29b-41d4-a716-446655440000",
648 "aid": "0",
649 "net": "tcp",
650 "tls": "none"
651 });
652 let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
653 let node = parse_uri(&format!("vmess://{b64}")).unwrap();
654 assert!(node.extra.get("network").is_none());
655 assert_eq!(node.extra.get("tls").and_then(|v| v.as_bool()), Some(false));
656 }
657
658 #[test]
659 fn vmess_remarks_host_alias() {
660 use base64::Engine;
661 let cfg = serde_json::json!({
662 "v": "2",
663 "remarks": "香港-01",
664 "add": "hk.example.com",
665 "port": "80",
666 "id": "uuid",
667 "aid": "0",
668 "net": "tcp"
669 });
670 let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
671 let node = parse_uri(&format!("vmess://{b64}")).unwrap();
672 assert_eq!(node.name, "香港-01");
673 assert_eq!(node.server, "hk.example.com");
674 }
675
676 #[test]
677 fn vmess_client_fingerprint_alpn() {
678 use base64::Engine;
679 let cfg = serde_json::json!({
680 "v": "2",
681 "ps": "日本-01",
682 "add": "jp1.example.com",
683 "port": "443",
684 "id": "550e8400-e29b-41d4-a716-446655440000",
685 "aid": "0",
686 "net": "ws",
687 "tls": "tls",
688 "fp": "chrome",
689 "alpn": "h2,http/1.1"
690 });
691 let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
692 let node = parse_uri(&format!("vmess://{b64}")).unwrap();
693 assert_eq!(
694 node.extra
695 .get("client-fingerprint")
696 .and_then(|v| v.as_str()),
697 Some("chrome")
698 );
699 let alpn = node.extra.get("alpn").unwrap().as_sequence().unwrap();
700 assert_eq!(alpn.len(), 2);
701 assert_eq!(alpn[0].as_str(), Some("h2"));
702 assert_eq!(alpn[1].as_str(), Some("http/1.1"));
703 }
704
705 #[test]
706 fn trojan_allow_insecure() {
707 let uri = "trojan://pass@us1.example.com:443?allowInsecure=1&sni=us1.example.com#美国";
708 let node = parse_uri(uri).unwrap();
709 assert_eq!(
710 node.extra.get("skip-cert-verify").and_then(|v| v.as_bool()),
711 Some(true)
712 );
713 }
714
715 #[test]
716 fn trojan_fallback_name_to_hostname() {
717 let uri = "trojan://pass@jp1.example.com:443";
718 let node = parse_uri(uri).unwrap();
719 assert_eq!(node.name, "jp1.example.com");
720 }
721
722 #[test]
723 fn hysteria2_hy2_prefix() {
724 let uri = "hy2://pass@sg1.example.com:8443?insecure=1&alpn=h3,h2#新加坡-Hy2";
725 let node = parse_uri(uri).unwrap();
726 assert_eq!(node.proxy_type, ProxyType::Hysteria2);
727 assert_eq!(node.server, "sg1.example.com");
728 assert_eq!(node.port, 8443);
729 let alpn = node.extra.get("alpn").unwrap().as_sequence().unwrap();
730 assert_eq!(alpn.len(), 2);
731 assert_eq!(alpn[0].as_str(), Some("h3"));
732 assert_eq!(alpn[1].as_str(), Some("h2"));
733 }
734
735 #[test]
736 fn hysteria2_up_down() {
737 let uri = "hysteria2://pass@us1.example.com:443?up=50&down=150&insecure=1#美国";
738 let node = parse_uri(uri).unwrap();
739 assert_eq!(node.extra.get("up").and_then(|v| v.as_u64()), Some(50));
740 assert_eq!(node.extra.get("down").and_then(|v| v.as_u64()), Some(150));
741 }
742
743 #[test]
744 fn hysteria2_fallback_name_to_hostname() {
745 let uri = "hysteria2://pass@hk1.example.com:443";
746 let node = parse_uri(uri).unwrap();
747 assert_eq!(node.name, "hk1.example.com");
748 assert!(node.extra.get("skip-cert-verify").is_none());
749 }
750
751 #[test]
752 fn parse_lines_skips_unparseable() {
753 let lines = vec![
754 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#美国".to_string(),
755 "unknown://bad-line".to_string(),
756 "trojan://pass@sg.example.com:443?sni=sg.example.com#新加坡".to_string(),
757 ];
758 let nodes = parse_lines(&lines);
759 assert_eq!(nodes.len(), 2);
760 assert_eq!(nodes[0].proxy_type, ProxyType::Ss);
761 assert_eq!(nodes[1].proxy_type, ProxyType::Trojan);
762 }
763}