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