Skip to main content

deltachat/net/
proxy.rs

1//! # Proxy support.
2//!
3//! Delta Chat supports HTTP(S) CONNECT, SOCKS5 and Shadowsocks protocols.
4
5use 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
28/// Default SOCKS5 port according to [RFC 1928](https://tools.ietf.org/html/rfc1928).
29pub 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    /// HTTP proxy host.
53    pub host: url::Host,
54
55    /// HTTP proxy port.
56    pub port: u16,
57
58    /// Username and password for basic authentication.
59    ///
60    /// If set, `Proxy-Authorization` header is sent.
61    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    /// Hostname or IP address.
105    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/// Configuration for the proxy through which all traffic
159/// (except for iroh p2p connections)
160/// will be sent.
161#[derive(Debug, Clone, PartialEq, Eq)]
162#[expect(clippy::large_enum_variant)]
163pub enum ProxyConfig {
164    /// HTTP proxy.
165    Http(HttpConfig),
166
167    /// HTTPS proxy.
168    Https(HttpConfig),
169
170    /// SOCKS5 proxy.
171    Socks5(Socks5Config),
172
173    /// Shadowsocks proxy.
174    Shadowsocks(ShadowsocksConfig),
175}
176
177/// Constructs HTTP/1.1 `CONNECT` request for HTTP(S) proxy.
178fn http_connect_request(host: &str, port: u16, auth: Option<(&str, &str)>) -> String {
179    // According to <https://datatracker.ietf.org/doc/html/rfc7230#section-5.4>
180    // clients MUST send `Host:` header in HTTP/1.1 requests,
181    // so repeat the host there.
182    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
192/// Sends HTTP/1.1 `CONNECT` request over given connection
193/// to establish an HTTP tunnel.
194///
195/// Returns the same connection back so actual data can be tunneled over it.
196async 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    // Send HTTP/1.1 CONNECT request.
201    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            // End of response is not reached, read more.
218            break res;
219        }
220    };
221
222    // Normally response looks like
223    // `HTTP/1.1 200 Connection established\r\n\r\n`.
224    if !res.starts_with(b"HTTP/") {
225        bail!("Unexpected HTTP CONNECT response: {res:?}");
226    }
227
228    // HTTP-version followed by space has fixed length
229    // according to RFC 7230:
230    // <https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2>
231    //
232    // Normally status line starts with `HTTP/1.1 `.
233    // We only care about 3-digit status code.
234    let status_code = res
235        .get(9..12)
236        .context("HTTP status line does not contain a status code")?;
237
238    // Interpret status code according to
239    // <https://datatracker.ietf.org/doc/html/rfc7231#section-6>.
240    if status_code == b"407" {
241        Err(format_err!("Proxy Authentication Required"))
242    } else if status_code.starts_with(b"2") {
243        // Success.
244        Ok(conn)
245    } else {
246        Err(format_err!(
247            "Failed to establish HTTP CONNECT tunnel: {res:?}"
248        ))
249    }
250}
251
252impl ProxyConfig {
253    /// Creates a new proxy configuration by parsing given proxy URL.
254    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            // Because of `curl` convention,
272            // `socks5` URL scheme may be expected to resolve domain names locally
273            // with `socks5h` URL scheme meaning that hostnames are passed to the proxy.
274            // Resolving hostnames locally is not supported
275            // in Delta Chat when using a proxy
276            // to prevent DNS leaks.
277            // Because of this we do not distinguish
278            // between `socks5` and `socks5h`.
279            "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    /// Serializes proxy config into an URL.
307    ///
308    /// This function can be used to normalize proxy URL
309    /// by parsing it and serializing back.
310    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    /// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password`
320    /// config into `proxy_url` if `proxy_url` is unset or empty.
321    ///
322    /// Unsets `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` in any case.
323    async fn migrate_socks_config(sql: &Sql) -> Result<()> {
324        if sql.get_raw_config("proxy_url").await?.is_none() {
325            // Load legacy SOCKS5 settings.
326            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    /// Reads proxy configuration from the database.
371    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    /// If `load_dns_cache` is true, loads cached DNS resolution results.
395    /// Use this only if the connection is going to be protected with TLS checks.
396    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                // IPv4 address is parsed as a domain and not url::Host::Ipv4.
516                // This is expected: <https://github.com/servo/rust-url/issues/767>.
517                // We only need a distinction for IPv6 to remove square brackets
518                // before passing the address to `lookup_host()`.
519                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                // IPv6 address should be recognized as IPv6 address and not "[::1]" hostname.
531                // Otherwise we may try to resolve "[::1]" and fail to connect.
532                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        // Example URL from <https://shadowsocks.org/doc/sip002.html>.
759        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        // This caused panic before shadowsocks 1.22.0.
771        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        // Test that config is migrated on attempt to load even if disabled.
779        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        // Even though proxy is not enabled, config should be migrated.
784        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    // Test SOCKS5 setting migration if proxy was never configured.
794    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
795    async fn test_socks5_migration_unconfigured() -> Result<()> {
796        let t = TestContext::new().await;
797
798        // Try to load config to trigger migration.
799        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    // Test SOCKS5 setting migration if SOCKS5 host is empty.
810    #[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        // Try to load config to trigger migration.
817        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}