1use std::fmt;
6use std::pin::Pin;
7
8use anyhow::{Context as _, Result, bail, format_err};
9use base64::Engine;
10use bytes::{BufMut, BytesMut};
11use fast_socks5::AuthenticationMethod;
12use fast_socks5::Socks5Command;
13use fast_socks5::client::Socks5Stream;
14use fast_socks5::util::target_addr::ToTargetAddr;
15use percent_encoding::{NON_ALPHANUMERIC, percent_encode, utf8_percent_encode};
16use tokio::io::{AsyncReadExt, AsyncWriteExt};
17use tokio::net::TcpStream;
18use tokio_io_timeout::TimeoutStream;
19use url::Url;
20
21use crate::config::Config;
22use crate::context::Context;
23use crate::net::connect_tcp;
24use crate::net::session::SessionStream;
25use crate::net::tls::wrap_rustls;
26use crate::sql::Sql;
27
28pub const DEFAULT_SOCKS_PORT: u16 = 1080;
30
31#[derive(Debug, Clone)]
32pub struct ShadowsocksConfig {
33 pub server_config: shadowsocks::config::ServerConfig,
34}
35
36impl PartialEq for ShadowsocksConfig {
37 fn eq(&self, other: &Self) -> bool {
38 self.server_config.to_url() == other.server_config.to_url()
39 }
40}
41
42impl Eq for ShadowsocksConfig {}
43
44impl ShadowsocksConfig {
45 fn to_url(&self) -> String {
46 self.server_config.to_url()
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct HttpConfig {
52 pub host: String,
54
55 pub port: u16,
57
58 pub user_password: Option<(String, String)>,
62}
63
64impl HttpConfig {
65 fn from_url(url: Url) -> Result<Self> {
66 let host = url
67 .host_str()
68 .context("HTTP proxy URL has no host")?
69 .to_string();
70 let port = url
71 .port_or_known_default()
72 .context("HTTP(S) URLs are guaranteed to return Some port")?;
73 let user_password = if let Some(password) = url.password() {
74 let username = percent_encoding::percent_decode_str(url.username())
75 .decode_utf8()
76 .context("HTTP(S) proxy username is not a valid UTF-8")?
77 .to_string();
78 let password = percent_encoding::percent_decode_str(password)
79 .decode_utf8()
80 .context("HTTP(S) proxy password is not a valid UTF-8")?
81 .to_string();
82 Some((username, password))
83 } else {
84 None
85 };
86 let http_config = HttpConfig {
87 host,
88 port,
89 user_password,
90 };
91 Ok(http_config)
92 }
93
94 fn to_url(&self, scheme: &str) -> String {
95 if let Some((user, password)) = &self.user_password {
96 let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
97 let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
98 format!("{scheme}://{user}:{password}@{}:{}", self.host, self.port)
99 } else {
100 format!("{scheme}://{}:{}", self.host, self.port)
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct Socks5Config {
107 pub host: String,
108 pub port: u16,
109 pub user_password: Option<(String, String)>,
110}
111
112impl Socks5Config {
113 async fn connect(
114 &self,
115 context: &Context,
116 target_host: &str,
117 target_port: u16,
118 load_dns_cache: bool,
119 ) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
120 let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache)
121 .await
122 .context("Failed to connect to SOCKS5 proxy")?;
123
124 let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
125 {
126 Some(AuthenticationMethod::Password {
127 username: username.into(),
128 password: password.into(),
129 })
130 } else {
131 None
132 };
133 let mut socks_stream =
134 Socks5Stream::use_stream(tcp_stream, authentication_method, Default::default()).await?;
135 let target_addr = (target_host, target_port).to_target_addr()?;
136 socks_stream
137 .request(Socks5Command::TCPConnect, target_addr)
138 .await?;
139
140 Ok(socks_stream)
141 }
142
143 fn to_url(&self) -> String {
144 if let Some((user, password)) = &self.user_password {
145 let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
146 let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
147 format!("socks5://{user}:{password}@{}:{}", self.host, self.port)
148 } else {
149 format!("socks5://{}:{}", self.host, self.port)
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
158#[expect(clippy::large_enum_variant)]
159pub enum ProxyConfig {
160 Http(HttpConfig),
162
163 Https(HttpConfig),
165
166 Socks5(Socks5Config),
168
169 Shadowsocks(ShadowsocksConfig),
171}
172
173fn http_connect_request(host: &str, port: u16, auth: Option<(&str, &str)>) -> String {
175 let mut res = format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n");
179 if let Some((username, password)) = auth {
180 res += "Proxy-Authorization: Basic ";
181 res += &base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}"));
182 res += "\r\n";
183 }
184 res += "\r\n";
185 res
186}
187
188async fn http_tunnel<T>(mut conn: T, host: &str, port: u16, auth: Option<(&str, &str)>) -> Result<T>
193where
194 T: AsyncReadExt + AsyncWriteExt + Unpin,
195{
196 let request = http_connect_request(host, port, auth);
198 conn.write_all(request.as_bytes()).await?;
199
200 let mut buffer = BytesMut::with_capacity(4096);
201
202 let res = loop {
203 if !buffer.has_remaining_mut() {
204 bail!("CONNECT response exceeded buffer size");
205 }
206 let n = conn.read_buf(&mut buffer).await?;
207 if n == 0 {
208 bail!("Unexpected end of CONNECT response");
209 }
210
211 let res = &buffer[..];
212 if res.ends_with(b"\r\n\r\n") {
213 break res;
215 }
216 };
217
218 if !res.starts_with(b"HTTP/") {
221 bail!("Unexpected HTTP CONNECT response: {res:?}");
222 }
223
224 let status_code = res
231 .get(9..12)
232 .context("HTTP status line does not contain a status code")?;
233
234 if status_code == b"407" {
237 Err(format_err!("Proxy Authentication Required"))
238 } else if status_code.starts_with(b"2") {
239 Ok(conn)
241 } else {
242 Err(format_err!(
243 "Failed to establish HTTP CONNECT tunnel: {res:?}"
244 ))
245 }
246}
247
248impl ProxyConfig {
249 pub fn from_url(url: &str) -> Result<Self> {
251 let url = Url::parse(url).context("Cannot parse proxy URL")?;
252 match url.scheme() {
253 "http" => {
254 let http_config = HttpConfig::from_url(url)?;
255 Ok(Self::Http(http_config))
256 }
257 "https" => {
258 let https_config = HttpConfig::from_url(url)?;
259 Ok(Self::Https(https_config))
260 }
261 "ss" => {
262 let server_config = shadowsocks::config::ServerConfig::from_url(url.as_str())?;
263 let shadowsocks_config = ShadowsocksConfig { server_config };
264 Ok(Self::Shadowsocks(shadowsocks_config))
265 }
266
267 "socks5" => {
276 let host = url
277 .host_str()
278 .context("socks5 URL has no host")?
279 .to_string();
280 let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT);
281 let user_password = if let Some(password) = url.password() {
282 let username = percent_encoding::percent_decode_str(url.username())
283 .decode_utf8()
284 .context("SOCKS5 username is not a valid UTF-8")?
285 .to_string();
286 let password = percent_encoding::percent_decode_str(password)
287 .decode_utf8()
288 .context("SOCKS5 password is not a valid UTF-8")?
289 .to_string();
290 Some((username, password))
291 } else {
292 None
293 };
294 let socks5_config = Socks5Config {
295 host,
296 port,
297 user_password,
298 };
299 Ok(Self::Socks5(socks5_config))
300 }
301 scheme => Err(format_err!("Unknown URL scheme {scheme:?}")),
302 }
303 }
304
305 pub fn to_url(&self) -> String {
310 match self {
311 Self::Http(http_config) => http_config.to_url("http"),
312 Self::Https(http_config) => http_config.to_url("https"),
313 Self::Socks5(socks5_config) => socks5_config.to_url(),
314 Self::Shadowsocks(shadowsocks_config) => shadowsocks_config.to_url(),
315 }
316 }
317
318 async fn migrate_socks_config(sql: &Sql) -> Result<()> {
323 if sql.get_raw_config("proxy_url").await?.is_none() {
324 if let Some(host) = sql
326 .get_raw_config("socks5_host")
327 .await?
328 .filter(|s| !s.is_empty())
329 {
330 let port: u16 = sql
331 .get_raw_config_int("socks5_port")
332 .await?
333 .unwrap_or(DEFAULT_SOCKS_PORT.into()) as u16;
334 let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
335 let pass = sql
336 .get_raw_config("socks5_password")
337 .await?
338 .unwrap_or_default();
339
340 let mut proxy_url = "socks5://".to_string();
341 if !pass.is_empty() {
342 proxy_url += &percent_encode(user.as_bytes(), NON_ALPHANUMERIC).to_string();
343 proxy_url += ":";
344 proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
345 proxy_url += "@";
346 };
347 proxy_url += &host;
348 proxy_url += ":";
349 proxy_url += &port.to_string();
350
351 sql.set_raw_config("proxy_url", Some(&proxy_url)).await?;
352 } else {
353 sql.set_raw_config("proxy_url", Some("")).await?;
354 }
355
356 let socks5_enabled = sql.get_raw_config("socks5_enabled").await?;
357 sql.set_raw_config("proxy_enabled", socks5_enabled.as_deref())
358 .await?;
359 }
360
361 sql.set_raw_config("socks5_enabled", None).await?;
362 sql.set_raw_config("socks5_host", None).await?;
363 sql.set_raw_config("socks5_port", None).await?;
364 sql.set_raw_config("socks5_user", None).await?;
365 sql.set_raw_config("socks5_password", None).await?;
366 Ok(())
367 }
368
369 pub async fn load(context: &Context) -> Result<Option<Self>> {
371 Self::migrate_socks_config(&context.sql)
372 .await
373 .context("Failed to migrate legacy SOCKS config")?;
374
375 let enabled = context.get_config_bool(Config::ProxyEnabled).await?;
376 if !enabled {
377 return Ok(None);
378 }
379
380 let proxy_url = context
381 .get_config(Config::ProxyUrl)
382 .await?
383 .unwrap_or_default();
384 let proxy_url = proxy_url
385 .split_once('\n')
386 .map_or(proxy_url.clone(), |(first_url, _rest)| {
387 first_url.to_string()
388 });
389 let proxy_config = Self::from_url(&proxy_url).context("Failed to parse proxy URL")?;
390 Ok(Some(proxy_config))
391 }
392
393 pub(crate) async fn connect(
396 &self,
397 context: &Context,
398 target_host: &str,
399 target_port: u16,
400 load_dns_cache: bool,
401 ) -> Result<Box<dyn SessionStream>> {
402 match self {
403 ProxyConfig::Http(http_config) => {
404 let load_cache = false;
405 let tcp_stream = crate::net::connect_tcp(
406 context,
407 &http_config.host,
408 http_config.port,
409 load_cache,
410 )
411 .await?;
412 let auth = if let Some((username, password)) = &http_config.user_password {
413 Some((username.as_str(), password.as_str()))
414 } else {
415 None
416 };
417 let tunnel_stream = http_tunnel(tcp_stream, target_host, target_port, auth).await?;
418 Ok(Box::new(tunnel_stream))
419 }
420 ProxyConfig::Https(https_config) => {
421 let load_cache = true;
422 let tcp_stream = crate::net::connect_tcp(
423 context,
424 &https_config.host,
425 https_config.port,
426 load_cache,
427 )
428 .await?;
429 let use_sni = true;
430 let tls_stream = wrap_rustls(
431 &https_config.host,
432 https_config.port,
433 use_sni,
434 "",
435 tcp_stream,
436 &context.tls_session_store,
437 &context.spki_hash_store,
438 &context.sql,
439 )
440 .await?;
441 let auth = if let Some((username, password)) = &https_config.user_password {
442 Some((username.as_str(), password.as_str()))
443 } else {
444 None
445 };
446 let tunnel_stream = http_tunnel(tls_stream, target_host, target_port, auth).await?;
447 Ok(Box::new(tunnel_stream))
448 }
449 ProxyConfig::Socks5(socks5_config) => {
450 let socks5_stream = socks5_config
451 .connect(context, target_host, target_port, load_dns_cache)
452 .await?;
453 Ok(Box::new(socks5_stream))
454 }
455 ProxyConfig::Shadowsocks(ShadowsocksConfig { server_config }) => {
456 let shadowsocks_context = shadowsocks::context::Context::new_shared(
457 shadowsocks::config::ServerType::Local,
458 );
459
460 let tcp_stream = {
461 let server_addr = server_config.addr();
462 let host = server_addr.host();
463 let port = server_addr.port();
464 connect_tcp(context, &host, port, load_dns_cache)
465 .await
466 .context("Failed to connect to Shadowsocks proxy")?
467 };
468
469 let shadowsocks_stream = shadowsocks::ProxyClientStream::from_stream(
470 shadowsocks_context,
471 tcp_stream,
472 server_config,
473 (target_host.to_string(), target_port),
474 );
475
476 Ok(Box::new(shadowsocks_stream))
477 }
478 }
479 }
480}
481
482impl fmt::Display for Socks5Config {
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 write!(
485 f,
486 "host:{},port:{},user_password:{}",
487 self.host,
488 self.port,
489 if let Some(user_password) = self.user_password.clone() {
490 format!("user: {}, password: ***", user_password.0)
491 } else {
492 "user: None".to_string()
493 }
494 )
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::config::Config;
502 use crate::test_utils::TestContext;
503
504 #[test]
505 fn test_socks5_url() {
506 let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:9050").unwrap();
507 assert_eq!(
508 proxy_config,
509 ProxyConfig::Socks5(Socks5Config {
510 host: "127.0.0.1".to_string(),
511 port: 9050,
512 user_password: None
513 })
514 );
515
516 let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap();
517 assert_eq!(
518 proxy_config,
519 ProxyConfig::Socks5(Socks5Config {
520 host: "127.0.0.1".to_string(),
521 port: 9150,
522 user_password: Some(("foo".to_string(), "bar".to_string()))
523 })
524 );
525
526 let proxy_config = ProxyConfig::from_url("socks5://%66oo:b%61r@127.0.0.1:9150").unwrap();
527 assert_eq!(
528 proxy_config,
529 ProxyConfig::Socks5(Socks5Config {
530 host: "127.0.0.1".to_string(),
531 port: 9150,
532 user_password: Some(("foo".to_string(), "bar".to_string()))
533 })
534 );
535
536 let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:80").unwrap();
537 assert_eq!(
538 proxy_config,
539 ProxyConfig::Socks5(Socks5Config {
540 host: "127.0.0.1".to_string(),
541 port: 80,
542 user_password: None
543 })
544 );
545
546 let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1").unwrap();
547 assert_eq!(
548 proxy_config,
549 ProxyConfig::Socks5(Socks5Config {
550 host: "127.0.0.1".to_string(),
551 port: 1080,
552 user_password: None
553 })
554 );
555
556 let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:1080").unwrap();
557 assert_eq!(
558 proxy_config,
559 ProxyConfig::Socks5(Socks5Config {
560 host: "127.0.0.1".to_string(),
561 port: 1080,
562 user_password: None
563 })
564 );
565
566 let proxy_config = ProxyConfig::from_url("socks5://my-proxy.example.org").unwrap();
567 assert_eq!(
568 proxy_config,
569 ProxyConfig::Socks5(Socks5Config {
570 host: "my-proxy.example.org".to_string(),
571 port: 1080,
572 user_password: None
573 })
574 );
575 assert_eq!(
576 proxy_config.to_url(),
577 "socks5://my-proxy.example.org:1080".to_string()
578 );
579 }
580
581 #[test]
582 fn test_http_url() {
583 let proxy_config = ProxyConfig::from_url("http://127.0.0.1").unwrap();
584 assert_eq!(
585 proxy_config,
586 ProxyConfig::Http(HttpConfig {
587 host: "127.0.0.1".to_string(),
588 port: 80,
589 user_password: None
590 })
591 );
592
593 let proxy_config = ProxyConfig::from_url("http://127.0.0.1:80").unwrap();
594 assert_eq!(
595 proxy_config,
596 ProxyConfig::Http(HttpConfig {
597 host: "127.0.0.1".to_string(),
598 port: 80,
599 user_password: None
600 })
601 );
602
603 let proxy_config = ProxyConfig::from_url("http://127.0.0.1:443").unwrap();
604 assert_eq!(
605 proxy_config,
606 ProxyConfig::Http(HttpConfig {
607 host: "127.0.0.1".to_string(),
608 port: 443,
609 user_password: None
610 })
611 );
612
613 let proxy_config = ProxyConfig::from_url("http://my-proxy.example.org").unwrap();
614 assert_eq!(
615 proxy_config,
616 ProxyConfig::Http(HttpConfig {
617 host: "my-proxy.example.org".to_string(),
618 port: 80,
619 user_password: None
620 })
621 );
622 assert_eq!(
623 proxy_config.to_url(),
624 "http://my-proxy.example.org:80".to_string()
625 );
626 }
627
628 #[test]
629 fn test_https_url() {
630 let proxy_config = ProxyConfig::from_url("https://127.0.0.1").unwrap();
631 assert_eq!(
632 proxy_config,
633 ProxyConfig::Https(HttpConfig {
634 host: "127.0.0.1".to_string(),
635 port: 443,
636 user_password: None
637 })
638 );
639
640 let proxy_config = ProxyConfig::from_url("https://127.0.0.1:80").unwrap();
641 assert_eq!(
642 proxy_config,
643 ProxyConfig::Https(HttpConfig {
644 host: "127.0.0.1".to_string(),
645 port: 80,
646 user_password: None
647 })
648 );
649
650 let proxy_config = ProxyConfig::from_url("https://127.0.0.1:443").unwrap();
651 assert_eq!(
652 proxy_config,
653 ProxyConfig::Https(HttpConfig {
654 host: "127.0.0.1".to_string(),
655 port: 443,
656 user_password: None
657 })
658 );
659
660 let proxy_config = ProxyConfig::from_url("https://my-proxy.example.org").unwrap();
661 assert_eq!(
662 proxy_config,
663 ProxyConfig::Https(HttpConfig {
664 host: "my-proxy.example.org".to_string(),
665 port: 443,
666 user_password: None
667 })
668 );
669 assert_eq!(
670 proxy_config.to_url(),
671 "https://my-proxy.example.org:443".to_string()
672 );
673 }
674
675 #[test]
676 fn test_http_connect_request() {
677 assert_eq!(
678 http_connect_request("example.org", 143, Some(("aladdin", "opensesame"))),
679 "CONNECT example.org:143 HTTP/1.1\r\nHost: example.org:143\r\nProxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\r\n\r\n"
680 );
681 assert_eq!(
682 http_connect_request("example.net", 587, None),
683 "CONNECT example.net:587 HTTP/1.1\r\nHost: example.net:587\r\n\r\n"
684 );
685 }
686
687 #[test]
688 fn test_shadowsocks_url() {
689 let proxy_config =
691 ProxyConfig::from_url("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1")
692 .unwrap();
693 assert!(matches!(proxy_config, ProxyConfig::Shadowsocks(_)));
694 }
695
696 #[test]
697 fn test_invalid_proxy_url() {
698 assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err());
699 assert!(ProxyConfig::from_url("abc").is_err());
700
701 assert!(ProxyConfig::from_url("ss://foo:bar@127.0.0.1:9999").is_err());
703 }
704
705 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
706 async fn test_socks5_migration() -> Result<()> {
707 let t = TestContext::new().await;
708
709 t.set_config(Config::Socks5Host, Some("127.0.0.1")).await?;
711 t.set_config(Config::Socks5Port, Some("9050")).await?;
712
713 let proxy_config = ProxyConfig::load(&t).await?;
714 assert_eq!(proxy_config, None);
716
717 assert_eq!(
718 t.get_config(Config::ProxyUrl).await?.unwrap(),
719 "socks5://127.0.0.1:9050"
720 );
721 Ok(())
722 }
723
724 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
726 async fn test_socks5_migration_unconfigured() -> Result<()> {
727 let t = TestContext::new().await;
728
729 assert_eq!(ProxyConfig::load(&t).await?, None);
731
732 assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
733 assert_eq!(
734 t.get_config(Config::ProxyUrl).await?.unwrap(),
735 String::new()
736 );
737 Ok(())
738 }
739
740 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
742 async fn test_socks5_migration_empty() -> Result<()> {
743 let t = TestContext::new().await;
744
745 t.set_config(Config::Socks5Host, Some("")).await?;
746
747 assert_eq!(ProxyConfig::load(&t).await?, None);
749
750 assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
751 assert_eq!(
752 t.get_config(Config::ProxyUrl).await?.unwrap(),
753 String::new()
754 );
755 Ok(())
756 }
757}