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: url::Host,
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.host().context("HTTP proxy URL has no host")?.to_owned();
67 let port = url
68 .port_or_known_default()
69 .context("HTTP(S) URLs are guaranteed to return Some port")?;
70 let user_password = if let Some(password) = url.password() {
71 let username = percent_encoding::percent_decode_str(url.username())
72 .decode_utf8()
73 .context("HTTP(S) proxy username is not a valid UTF-8")?
74 .to_string();
75 let password = percent_encoding::percent_decode_str(password)
76 .decode_utf8()
77 .context("HTTP(S) proxy password is not a valid UTF-8")?
78 .to_string();
79 Some((username, password))
80 } else {
81 None
82 };
83 let http_config = HttpConfig {
84 host,
85 port,
86 user_password,
87 };
88 Ok(http_config)
89 }
90
91 fn to_url(&self, scheme: &str) -> String {
92 if let Some((user, password)) = &self.user_password {
93 let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
94 let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
95 format!("{scheme}://{user}:{password}@{}:{}", self.host, self.port)
96 } else {
97 format!("{scheme}://{}:{}", self.host, self.port)
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct Socks5Config {
104 pub host: url::Host,
106 pub port: u16,
107 pub user_password: Option<(String, String)>,
108}
109
110impl Socks5Config {
111 async fn connect(
112 &self,
113 context: &Context,
114 target_host: &str,
115 target_port: u16,
116 load_dns_cache: bool,
117 ) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
118 let hostname = match &self.host {
119 url::Host::Domain(domain) => domain.to_string(),
120 url::Host::Ipv4(addr) => addr.to_string(),
121 url::Host::Ipv6(addr) => addr.to_string(),
122 };
123
124 let tcp_stream = connect_tcp(context, &hostname, self.port, load_dns_cache)
125 .await
126 .context("Failed to connect to SOCKS5 proxy")?;
127
128 let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
129 {
130 Some(AuthenticationMethod::Password {
131 username: username.into(),
132 password: password.into(),
133 })
134 } else {
135 None
136 };
137 let mut socks_stream =
138 Socks5Stream::use_stream(tcp_stream, authentication_method, Default::default()).await?;
139 let target_addr = (target_host, target_port).to_target_addr()?;
140 socks_stream
141 .request(Socks5Command::TCPConnect, target_addr)
142 .await?;
143
144 Ok(socks_stream)
145 }
146
147 fn to_url(&self) -> String {
148 if let Some((user, password)) = &self.user_password {
149 let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
150 let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
151 format!("socks5://{user}:{password}@{}:{}", self.host, self.port)
152 } else {
153 format!("socks5://{}:{}", self.host, self.port)
154 }
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
162#[expect(clippy::large_enum_variant)]
163pub enum ProxyConfig {
164 Http(HttpConfig),
166
167 Https(HttpConfig),
169
170 Socks5(Socks5Config),
172
173 Shadowsocks(ShadowsocksConfig),
175}
176
177fn 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.host().context("socks5 URL has no host")?.to_owned();
281 let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT);
282 let user_password = if let Some(password) = url.password() {
283 let username = percent_encoding::percent_decode_str(url.username())
284 .decode_utf8()
285 .context("SOCKS5 username is not a valid UTF-8")?
286 .to_string();
287 let password = percent_encoding::percent_decode_str(password)
288 .decode_utf8()
289 .context("SOCKS5 password is not a valid UTF-8")?
290 .to_string();
291 Some((username, password))
292 } else {
293 None
294 };
295 let socks5_config = Socks5Config {
296 host,
297 port,
298 user_password,
299 };
300 Ok(Self::Socks5(socks5_config))
301 }
302 scheme => Err(format_err!("Unknown URL scheme {scheme:?}")),
303 }
304 }
305
306 pub fn to_url(&self) -> String {
311 match self {
312 Self::Http(http_config) => http_config.to_url("http"),
313 Self::Https(http_config) => http_config.to_url("https"),
314 Self::Socks5(socks5_config) => socks5_config.to_url(),
315 Self::Shadowsocks(shadowsocks_config) => shadowsocks_config.to_url(),
316 }
317 }
318
319 async fn migrate_socks_config(sql: &Sql) -> Result<()> {
324 if sql.get_raw_config("proxy_url").await?.is_none() {
325 if let Some(host) = sql
327 .get_raw_config("socks5_host")
328 .await?
329 .filter(|s| !s.is_empty())
330 {
331 let port: u16 = sql
332 .get_raw_config_int("socks5_port")
333 .await?
334 .unwrap_or(DEFAULT_SOCKS_PORT.into()) as u16;
335 let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
336 let pass = sql
337 .get_raw_config("socks5_password")
338 .await?
339 .unwrap_or_default();
340
341 let mut proxy_url = "socks5://".to_string();
342 if !pass.is_empty() {
343 proxy_url += &percent_encode(user.as_bytes(), NON_ALPHANUMERIC).to_string();
344 proxy_url += ":";
345 proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
346 proxy_url += "@";
347 };
348 proxy_url += &host;
349 proxy_url += ":";
350 proxy_url += &port.to_string();
351
352 sql.set_raw_config("proxy_url", Some(&proxy_url)).await?;
353 } else {
354 sql.set_raw_config("proxy_url", Some("")).await?;
355 }
356
357 let socks5_enabled = sql.get_raw_config("socks5_enabled").await?;
358 sql.set_raw_config("proxy_enabled", socks5_enabled.as_deref())
359 .await?;
360 }
361
362 sql.set_raw_config("socks5_enabled", None).await?;
363 sql.set_raw_config("socks5_host", None).await?;
364 sql.set_raw_config("socks5_port", None).await?;
365 sql.set_raw_config("socks5_user", None).await?;
366 sql.set_raw_config("socks5_password", None).await?;
367 Ok(())
368 }
369
370 pub async fn load(context: &Context) -> Result<Option<Self>> {
372 Self::migrate_socks_config(&context.sql)
373 .await
374 .context("Failed to migrate legacy SOCKS config")?;
375
376 let enabled = context.get_config_bool(Config::ProxyEnabled).await?;
377 if !enabled {
378 return Ok(None);
379 }
380
381 let proxy_url = context
382 .get_config(Config::ProxyUrl)
383 .await?
384 .unwrap_or_default();
385 let proxy_url = proxy_url
386 .split_once('\n')
387 .map_or(proxy_url.clone(), |(first_url, _rest)| {
388 first_url.to_string()
389 });
390 let proxy_config = Self::from_url(&proxy_url).context("Failed to parse proxy URL")?;
391 Ok(Some(proxy_config))
392 }
393
394 pub(crate) async fn connect(
397 &self,
398 context: &Context,
399 target_host: &str,
400 target_port: u16,
401 load_dns_cache: bool,
402 ) -> Result<Box<dyn SessionStream>> {
403 match self {
404 ProxyConfig::Http(http_config) => {
405 let load_cache = false;
406 let hostname = match &http_config.host {
407 url::Host::Domain(domain) => domain.to_string(),
408 url::Host::Ipv4(addr) => addr.to_string(),
409 url::Host::Ipv6(addr) => addr.to_string(),
410 };
411 let tcp_stream =
412 crate::net::connect_tcp(context, &hostname, http_config.port, load_cache)
413 .await?;
414 let auth = if let Some((username, password)) = &http_config.user_password {
415 Some((username.as_str(), password.as_str()))
416 } else {
417 None
418 };
419 let tunnel_stream = http_tunnel(tcp_stream, target_host, target_port, auth).await?;
420 Ok(Box::new(tunnel_stream))
421 }
422 ProxyConfig::Https(https_config) => {
423 let load_cache = true;
424 let hostname = match &https_config.host {
425 url::Host::Domain(domain) => domain.to_string(),
426 url::Host::Ipv4(addr) => addr.to_string(),
427 url::Host::Ipv6(addr) => addr.to_string(),
428 };
429
430 let tcp_stream =
431 crate::net::connect_tcp(context, &hostname, https_config.port, load_cache)
432 .await?;
433 let use_sni = true;
434 let tls_stream = wrap_rustls(
435 &hostname,
436 https_config.port,
437 use_sni,
438 "",
439 tcp_stream,
440 &context.tls_session_store,
441 &context.spki_hash_store,
442 &context.sql,
443 )
444 .await?;
445 let auth = if let Some((username, password)) = &https_config.user_password {
446 Some((username.as_str(), password.as_str()))
447 } else {
448 None
449 };
450 let tunnel_stream = http_tunnel(tls_stream, target_host, target_port, auth).await?;
451 Ok(Box::new(tunnel_stream))
452 }
453 ProxyConfig::Socks5(socks5_config) => {
454 let socks5_stream = socks5_config
455 .connect(context, target_host, target_port, load_dns_cache)
456 .await?;
457 Ok(Box::new(socks5_stream))
458 }
459 ProxyConfig::Shadowsocks(ShadowsocksConfig { server_config }) => {
460 let shadowsocks_context = shadowsocks::context::Context::new_shared(
461 shadowsocks::config::ServerType::Local,
462 );
463
464 let tcp_stream = {
465 let server_addr = server_config.addr();
466 let host = server_addr.host();
467 let port = server_addr.port();
468 connect_tcp(context, &host, port, load_dns_cache)
469 .await
470 .context("Failed to connect to Shadowsocks proxy")?
471 };
472
473 let shadowsocks_stream = shadowsocks::ProxyClientStream::from_stream(
474 shadowsocks_context,
475 tcp_stream,
476 server_config,
477 (target_host.to_string(), target_port),
478 );
479
480 Ok(Box::new(shadowsocks_stream))
481 }
482 }
483 }
484}
485
486impl fmt::Display for Socks5Config {
487 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488 write!(
489 f,
490 "host:{},port:{},user_password:{}",
491 self.host,
492 self.port,
493 if let Some(user_password) = self.user_password.clone() {
494 format!("user: {}, password: ***", user_password.0)
495 } else {
496 "user: None".to_string()
497 }
498 )
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use crate::config::Config;
506 use crate::test_utils::TestContext;
507 use std::net::{Ipv4Addr, Ipv6Addr};
508
509 #[test]
510 fn test_socks5_url() {
511 let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:9050").unwrap();
512 assert_eq!(
513 proxy_config,
514 ProxyConfig::Socks5(Socks5Config {
515 host: url::Host::Domain("127.0.0.1".to_string()),
520 port: 9050,
521 user_password: None
522 })
523 );
524 assert_eq!(proxy_config.to_url(), "socks5://127.0.0.1:9050".to_string());
525
526 let proxy_config = ProxyConfig::from_url("socks5://[::1]:9050").unwrap();
527 assert_eq!(
528 proxy_config,
529 ProxyConfig::Socks5(Socks5Config {
530 host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
533 port: 9050,
534 user_password: None
535 })
536 );
537 assert_eq!(proxy_config.to_url(), "socks5://[::1]:9050".to_string());
538
539 let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap();
540 assert_eq!(
541 proxy_config,
542 ProxyConfig::Socks5(Socks5Config {
543 host: url::Host::Domain("127.0.0.1".to_string()),
544 port: 9150,
545 user_password: Some(("foo".to_string(), "bar".to_string()))
546 })
547 );
548
549 let proxy_config = ProxyConfig::from_url("socks5://%66oo:b%61r@127.0.0.1:9150").unwrap();
550 assert_eq!(
551 proxy_config,
552 ProxyConfig::Socks5(Socks5Config {
553 host: url::Host::Domain("127.0.0.1".to_string()),
554 port: 9150,
555 user_password: Some(("foo".to_string(), "bar".to_string()))
556 })
557 );
558
559 let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:80").unwrap();
560 assert_eq!(
561 proxy_config,
562 ProxyConfig::Socks5(Socks5Config {
563 host: url::Host::Domain("127.0.0.1".to_string()),
564 port: 80,
565 user_password: None
566 })
567 );
568
569 let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1").unwrap();
570 assert_eq!(
571 proxy_config,
572 ProxyConfig::Socks5(Socks5Config {
573 host: url::Host::Domain("127.0.0.1".to_string()),
574 port: 1080,
575 user_password: None
576 })
577 );
578
579 let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:1080").unwrap();
580 assert_eq!(
581 proxy_config,
582 ProxyConfig::Socks5(Socks5Config {
583 host: url::Host::Domain("127.0.0.1".to_string()),
584 port: 1080,
585 user_password: None
586 })
587 );
588
589 let proxy_config = ProxyConfig::from_url("socks5://my-proxy.example.org").unwrap();
590 assert_eq!(
591 proxy_config,
592 ProxyConfig::Socks5(Socks5Config {
593 host: url::Host::Domain("my-proxy.example.org".to_string()),
594 port: 1080,
595 user_password: None
596 })
597 );
598 assert_eq!(
599 proxy_config.to_url(),
600 "socks5://my-proxy.example.org:1080".to_string()
601 );
602 }
603
604 #[test]
605 fn test_http_url() {
606 let proxy_config = ProxyConfig::from_url("http://127.0.0.1").unwrap();
607 assert_eq!(
608 proxy_config,
609 ProxyConfig::Http(HttpConfig {
610 host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
611 port: 80,
612 user_password: None
613 })
614 );
615
616 let proxy_config = ProxyConfig::from_url("http://[::1]").unwrap();
617 assert_eq!(
618 proxy_config,
619 ProxyConfig::Http(HttpConfig {
620 host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
621 port: 80,
622 user_password: None
623 })
624 );
625 assert_eq!(proxy_config.to_url(), "http://[::1]:80".to_string());
626
627 let proxy_config = ProxyConfig::from_url("http://127.0.0.1:80").unwrap();
628 assert_eq!(
629 proxy_config,
630 ProxyConfig::Http(HttpConfig {
631 host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
632 port: 80,
633 user_password: None
634 })
635 );
636 assert_eq!(proxy_config.to_url(), "http://127.0.0.1:80".to_string());
637
638 let proxy_config = ProxyConfig::from_url("http://[::1]:80").unwrap();
639 assert_eq!(
640 proxy_config,
641 ProxyConfig::Http(HttpConfig {
642 host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
643 port: 80,
644 user_password: None
645 })
646 );
647 assert_eq!(proxy_config.to_url(), "http://[::1]:80".to_string());
648
649 let proxy_config = ProxyConfig::from_url("http://127.0.0.1:443").unwrap();
650 assert_eq!(
651 proxy_config,
652 ProxyConfig::Http(HttpConfig {
653 host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
654 port: 443,
655 user_password: None
656 })
657 );
658 assert_eq!(proxy_config.to_url(), "http://127.0.0.1:443".to_string());
659
660 let proxy_config = ProxyConfig::from_url("http://my-proxy.example.org").unwrap();
661 assert_eq!(
662 proxy_config,
663 ProxyConfig::Http(HttpConfig {
664 host: url::Host::Domain("my-proxy.example.org".to_string()),
665 port: 80,
666 user_password: None
667 })
668 );
669 assert_eq!(
670 proxy_config.to_url(),
671 "http://my-proxy.example.org:80".to_string()
672 );
673 }
674
675 #[test]
676 fn test_https_url() {
677 let proxy_config = ProxyConfig::from_url("https://127.0.0.1").unwrap();
678 assert_eq!(
679 proxy_config,
680 ProxyConfig::Https(HttpConfig {
681 host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
682 port: 443,
683 user_password: None
684 })
685 );
686
687 let proxy_config = ProxyConfig::from_url("https://127.0.0.1:80").unwrap();
688 assert_eq!(
689 proxy_config,
690 ProxyConfig::Https(HttpConfig {
691 host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
692 port: 80,
693 user_password: None
694 })
695 );
696
697 let proxy_config = ProxyConfig::from_url("https://[::1]:80").unwrap();
698 assert_eq!(
699 proxy_config,
700 ProxyConfig::Https(HttpConfig {
701 host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
702 port: 80,
703 user_password: None
704 })
705 );
706
707 let proxy_config = ProxyConfig::from_url("https://127.0.0.1:443").unwrap();
708 assert_eq!(
709 proxy_config,
710 ProxyConfig::Https(HttpConfig {
711 host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
712 port: 443,
713 user_password: None
714 })
715 );
716 assert_eq!(proxy_config.to_url(), "https://127.0.0.1:443".to_string());
717
718 let proxy_config = ProxyConfig::from_url("https://[::1]:443").unwrap();
719 assert_eq!(
720 proxy_config,
721 ProxyConfig::Https(HttpConfig {
722 host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
723 port: 443,
724 user_password: None
725 })
726 );
727 assert_eq!(proxy_config.to_url(), "https://[::1]:443".to_string());
728
729 let proxy_config = ProxyConfig::from_url("https://my-proxy.example.org").unwrap();
730 assert_eq!(
731 proxy_config,
732 ProxyConfig::Https(HttpConfig {
733 host: url::Host::Domain("my-proxy.example.org".to_string()),
734 port: 443,
735 user_password: None
736 })
737 );
738 assert_eq!(
739 proxy_config.to_url(),
740 "https://my-proxy.example.org:443".to_string()
741 );
742 }
743
744 #[test]
745 fn test_http_connect_request() {
746 assert_eq!(
747 http_connect_request("example.org", 143, Some(("aladdin", "opensesame"))),
748 "CONNECT example.org:143 HTTP/1.1\r\nHost: example.org:143\r\nProxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\r\n\r\n"
749 );
750 assert_eq!(
751 http_connect_request("example.net", 587, None),
752 "CONNECT example.net:587 HTTP/1.1\r\nHost: example.net:587\r\n\r\n"
753 );
754 }
755
756 #[test]
757 fn test_shadowsocks_url() {
758 let proxy_config =
760 ProxyConfig::from_url("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1")
761 .unwrap();
762 assert!(matches!(proxy_config, ProxyConfig::Shadowsocks(_)));
763 }
764
765 #[test]
766 fn test_invalid_proxy_url() {
767 assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err());
768 assert!(ProxyConfig::from_url("abc").is_err());
769
770 assert!(ProxyConfig::from_url("ss://foo:bar@127.0.0.1:9999").is_err());
772 }
773
774 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
775 async fn test_socks5_migration() -> Result<()> {
776 let t = TestContext::new().await;
777
778 t.set_config(Config::Socks5Host, Some("127.0.0.1")).await?;
780 t.set_config(Config::Socks5Port, Some("9050")).await?;
781
782 let proxy_config = ProxyConfig::load(&t).await?;
783 assert_eq!(proxy_config, None);
785
786 assert_eq!(
787 t.get_config(Config::ProxyUrl).await?.unwrap(),
788 "socks5://127.0.0.1:9050"
789 );
790 Ok(())
791 }
792
793 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
795 async fn test_socks5_migration_unconfigured() -> Result<()> {
796 let t = TestContext::new().await;
797
798 assert_eq!(ProxyConfig::load(&t).await?, None);
800
801 assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
802 assert_eq!(
803 t.get_config(Config::ProxyUrl).await?.unwrap(),
804 String::new()
805 );
806 Ok(())
807 }
808
809 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
811 async fn test_socks5_migration_empty() -> Result<()> {
812 let t = TestContext::new().await;
813
814 t.set_config(Config::Socks5Host, Some("")).await?;
815
816 assert_eq!(ProxyConfig::load(&t).await?, None);
818
819 assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
820 assert_eq!(
821 t.get_config(Config::ProxyUrl).await?.unwrap(),
822 String::new()
823 );
824 Ok(())
825 }
826}