miku_http_util/request/misc/
proxy.rs

1//! Proxy utilities for requests.
2
3use std::{str::FromStr, sync::Arc};
4
5use anyhow::{anyhow, Context};
6use http::HeaderValue;
7
8const DEFAULT_SOCKS5_PROXY_PORT: u16 = 7890;
9
10#[derive(Debug)]
11#[derive(thiserror::Error)]
12/// Errors for proxy utilities.
13pub enum Error {
14    #[error("Invalid proxy uri: {0}")]
15    /// Invalid proxy uri, see [`http::uri::InvalidUri`] for more details.
16    InvalidUri(#[from] http::uri::InvalidUri),
17
18    #[error("Invalid proxy uri: unsupported scheme")]
19    /// Unsupported scheme
20    UnsupportedScheme,
21
22    #[error("Invalid proxy uri: general error")]
23    /// General
24    General,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28/// A particular scheme used for proxying requests.
29///
30/// Currently only `HTTP`(s) and `SOCKS5` are supported.
31///
32/// # Examples
33///
34/// - `http://127.0.0.1:7890` // if port not specified, default to 80.
35/// - `https://127.0.0.1:7890` // if port not specified, default to 443.
36/// - `socks5://127.0.0.1:7890` // if port not specified, default to 7890.
37/// - `socks5h://127.0.0.1:7890` // if port not specified, default to 7890.
38pub enum ProxyScheme {
39    /// HTTP / HTTPS proxy
40    Http {
41        /// is HTTPS proxy
42        is_https: bool,
43
44        /// optional HTTP Basic auth
45        basic_auth: Option<HeaderValue>,
46
47        /// proxy server's host and port
48        authority: http::uri::Authority,
49    },
50
51    /// SOCKS5 proxy
52    Socks5 {
53        /// whether to resolve DNS remotely, aka.: "socks5" / "socks5h"
54        remote_dns: bool,
55
56        /// optional SOCKS5 auth, username and password
57        password_auth: Option<(Arc<str>, Arc<str>)>,
58
59        /// proxy server's host
60        host: Arc<str>,
61
62        /// proxy server's port
63        port: u16,
64    },
65}
66
67impl FromStr for ProxyScheme {
68    type Err = anyhow::Error;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        let uri = fluent_uri::Uri::parse(s).context(Error::General)?;
72
73        let scheme = uri.scheme().as_str();
74        let authority = uri.authority().ok_or(Error::General)?;
75        let user_info = authority.userinfo().map(|user_info| {
76            percent_encoding::percent_decode_str(user_info.as_str()).decode_utf8_lossy()
77        });
78
79        match scheme {
80            "http" | "https" => {
81                let authority = http::uri::Authority::try_from(format!(
82                    "{}:{}",
83                    authority.host(),
84                    authority
85                        .port_to_u16()
86                        .context(Error::General)?
87                        .unwrap_or_else(|| {
88                            if scheme == "http" {
89                                80
90                            } else {
91                                443
92                            }
93                        })
94                ))
95                .map_err(|e| {
96                    #[cfg(debug_assertions)]
97                    {
98                        unreachable!("Rare bug: http::uri::Authority reports error {e:?}");
99                    }
100
101                    #[cfg(all(not(debug_assertions), feature = "feat-tracing"))]
102                    {
103                        tracing::error!("Rare bug: http::uri::Authority reports error {e:?}");
104                    }
105
106                    #[allow(unreachable_code)]
107                    Error::InvalidUri(e)
108                })?;
109
110                let basic_auth = user_info.map(|user_info| match user_info.split_once(':') {
111                    Some((user_name, password)) => basic_auth(user_name, Some(password)),
112                    None => basic_auth(user_info, None::<&str>),
113                });
114
115                Ok(Self::Http {
116                    is_https: scheme == "https",
117                    basic_auth,
118                    authority,
119                })
120            }
121            "socks5" | "socks5h" => {
122                let password_auth = match user_info {
123                    Some(user_info) => Some(
124                        user_info
125                            .split_once(':')
126                            .map(|(user_name, password)| (user_name.into(), password.into()))
127                            .context("Invalid socks5 password auth")?,
128                    ),
129                    None => None,
130                };
131
132                Ok(Self::Socks5 {
133                    remote_dns: scheme == "socks5h",
134                    password_auth,
135                    host: authority.host().into(),
136                    port: authority
137                        .port_to_u16()
138                        .context(Error::General)?
139                        .unwrap_or(DEFAULT_SOCKS5_PROXY_PORT),
140                })
141            }
142            _ => {
143                #[cfg(feature = "feat-tracing")]
144                tracing::error!("Unsupported proxy scheme: {scheme}");
145                Err(anyhow!(Error::UnsupportedScheme))
146            }
147        }
148    }
149}
150
151impl ProxyScheme {
152    /// For `HTTP` proxies, returns the optional HTTP Basic auth.
153    pub const fn http_auth(&self) -> Option<&HeaderValue> {
154        match self {
155            ProxyScheme::Http {
156                basic_auth: auth, ..
157            } => auth.as_ref(),
158            _ => None,
159        }
160    }
161}
162
163impl serde::Serialize for ProxyScheme {
164    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
165    where
166        S: serde::Serializer,
167    {
168        match self {
169            ProxyScheme::Http {
170                is_https,
171                basic_auth,
172                authority,
173            } => serializer.serialize_str(&format!(
174                "{}://{}{}",
175                if *is_https { "https" } else { "http" },
176                basic_auth
177                    .as_ref()
178                    .map(|basic_auth| {
179                        let basic_auth = basic_auth
180                            .to_str()
181                            .unwrap_or_else(|e| unreachable!("Failed to decode basic auth: {}", e))
182                            .strip_prefix("Basic ")
183                            .unwrap_or_else(|| {
184                                unreachable!("Failed to decode basic auth: not begin with `Basic `")
185                            });
186
187                        let basic_auth = String::from_utf8(
188                            base64::Engine::decode(
189                                &base64::engine::general_purpose::STANDARD,
190                                basic_auth,
191                            )
192                            .unwrap_or_else(|e| unreachable!("Failed to decode basic auth: {}", e)),
193                        )
194                        .unwrap_or_else(|e| unreachable!("Invalid decoded basic auth: {}", e));
195
196                        match basic_auth.split_once(':') {
197                            Some((user_name, password)) => format!(
198                                "{}:{}@",
199                                percent_encoding::percent_encode(
200                                    user_name.as_bytes(),
201                                    percent_encoding::NON_ALPHANUMERIC
202                                ),
203                                percent_encoding::percent_encode(
204                                    password.as_bytes(),
205                                    percent_encoding::NON_ALPHANUMERIC
206                                )
207                            ),
208                            None => format!(
209                                "{}@",
210                                percent_encoding::percent_encode(
211                                    basic_auth.as_bytes(),
212                                    percent_encoding::NON_ALPHANUMERIC
213                                ),
214                            ),
215                        }
216                    })
217                    .unwrap_or_default(),
218                authority,
219            )),
220            ProxyScheme::Socks5 {
221                remote_dns,
222                password_auth,
223                host,
224                port,
225            } => serializer.serialize_str(&format!(
226                "{}://{}{}:{}",
227                if *remote_dns { "socks5h" } else { "socks5" },
228                password_auth
229                    .as_ref()
230                    .map(|(user_name, password)| {
231                        format!(
232                            "{}:{}@",
233                            percent_encoding::percent_encode(
234                                user_name.as_bytes(),
235                                percent_encoding::NON_ALPHANUMERIC
236                            ),
237                            percent_encoding::percent_encode(
238                                password.as_bytes(),
239                                percent_encoding::NON_ALPHANUMERIC
240                            )
241                        )
242                    })
243                    .unwrap_or_default(),
244                host,
245                port,
246            )),
247        }
248    }
249}
250
251impl<'de> serde::Deserialize<'de> for ProxyScheme {
252    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253    where
254        D: serde::Deserializer<'de>,
255    {
256        <&str>::deserialize(deserializer)?
257            .parse()
258            .map_err(serde::de::Error::custom)
259    }
260}
261
262fn basic_auth<U, P>(username: U, password: Option<P>) -> HeaderValue
263where
264    U: std::fmt::Display,
265    P: std::fmt::Display,
266{
267    use std::io::Write;
268
269    use base64::{prelude::BASE64_STANDARD, write::EncoderWriter};
270
271    let mut buf = Vec::with_capacity(64);
272
273    buf.extend(b"Basic ");
274
275    {
276        let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
277        let _ = write!(encoder, "{username}:");
278        if let Some(password) = password {
279            let _ = write!(encoder, "{password}");
280        }
281    }
282
283    // Avoid allocation when `Bytes::from(buf)`
284    buf.truncate(buf.len());
285
286    let mut header = HeaderValue::from_maybe_shared(bytes::Bytes::from(buf))
287        .expect("base64 is always valid HeaderValue");
288    header.set_sensitive(true);
289    header
290}
291
292#[cfg(test)]
293mod tests {
294    use http::HeaderValue;
295
296    use super::*;
297
298    #[test]
299    fn test_parse_proxy_scheme() {
300        assert_eq!(
301            "http://127.0.0.1:7890".parse::<ProxyScheme>().unwrap(),
302            ProxyScheme::Http {
303                is_https: false,
304                basic_auth: None,
305                authority: "127.0.0.1:7890".parse().unwrap()
306            }
307        );
308        assert_eq!(
309            "http://u:p@127.0.0.1:7890".parse::<ProxyScheme>().unwrap(),
310            ProxyScheme::Http {
311                is_https: false,
312                basic_auth: Some(HeaderValue::from_static("Basic dTpw")),
313                authority: "127.0.0.1:7890".parse().unwrap() // weird but as it is
314            }
315        );
316        assert_eq!(
317            "http://u:p@127.0.0.1".parse::<ProxyScheme>().unwrap(),
318            ProxyScheme::Http {
319                is_https: false,
320                basic_auth: Some(HeaderValue::from_static("Basic dTpw")),
321                authority: "127.0.0.1:80".parse().unwrap() // weird but as it is
322            }
323        );
324        assert_eq!(
325            "https://u:p@127.0.0.1:7890".parse::<ProxyScheme>().unwrap(),
326            ProxyScheme::Http {
327                is_https: true,
328                basic_auth: Some(HeaderValue::from_static("Basic dTpw")),
329                authority: "127.0.0.1:7890".parse().unwrap() // weird but as it is
330            }
331        );
332        assert_eq!(
333            "https://u:p%40@127.0.0.1:443"
334                .parse::<ProxyScheme>()
335                .unwrap(),
336            ProxyScheme::Http {
337                is_https: true,
338                basic_auth: Some(HeaderValue::from_static("Basic dTpwQA==")),
339                authority: "127.0.0.1:443".parse().unwrap() // weird but as it is
340            }
341        );
342        assert_eq!(
343            "https://u:p%40@127.0.0.1".parse::<ProxyScheme>().unwrap(),
344            ProxyScheme::Http {
345                is_https: true,
346                basic_auth: Some(HeaderValue::from_static("Basic dTpwQA==")),
347                authority: "127.0.0.1:443".parse().unwrap() // weird but as it is
348            }
349        );
350        assert_eq!(
351            "socks5://u:p%40@127.0.0.1:7890"
352                .parse::<ProxyScheme>()
353                .unwrap(),
354            ProxyScheme::Socks5 {
355                remote_dns: false,
356                password_auth: Some(("u".into(), "p@".into())),
357                host: "127.0.0.1".into(),
358                port: 7890
359            }
360        );
361        assert_eq!(
362            "socks5h://u:p%40@127.0.0.1:7890"
363                .parse::<ProxyScheme>()
364                .unwrap(),
365            ProxyScheme::Socks5 {
366                remote_dns: true,
367                password_auth: Some(("u".into(), "p@".into())),
368                host: "127.0.0.1".into(),
369                port: DEFAULT_SOCKS5_PROXY_PORT
370            }
371        );
372        assert_eq!(
373            "socks5h://u:p%40@127.0.0.1".parse::<ProxyScheme>().unwrap(),
374            ProxyScheme::Socks5 {
375                remote_dns: true,
376                password_auth: Some(("u".into(), "p@".into())),
377                host: "127.0.0.1".into(),
378                port: 7890
379            }
380        );
381    }
382
383    #[test]
384    #[should_panic]
385    fn empty_scheme() {
386        "127.0.0.1:7890".parse::<ProxyScheme>().unwrap();
387    }
388
389    #[test]
390    fn test_serde() {
391        let scheme = ProxyScheme::Http {
392            is_https: false,
393            basic_auth: Some(HeaderValue::from_static("Basic dTpwQA==")),
394            authority: "127.0.0.1:80".parse().unwrap(),
395        };
396        assert_eq!(
397            serde_json::to_string(&scheme).unwrap(),
398            "\"http://u:p%40@127.0.0.1:80\""
399        );
400
401        let scheme = ProxyScheme::Http {
402            is_https: true,
403            basic_auth: Some(HeaderValue::from_static("Basic dTpwQA==")),
404            authority: "127.0.0.1:443".parse().unwrap(),
405        };
406        assert_eq!(
407            serde_json::to_string(&scheme).unwrap(),
408            "\"https://u:p%40@127.0.0.1:443\""
409        );
410
411        let scheme = ProxyScheme::Socks5 {
412            remote_dns: false,
413            password_auth: Some(("u".into(), "p@".into())),
414            host: "127.0.0.1".into(),
415            port: 7890,
416        };
417        assert_eq!(
418            serde_json::to_string(&scheme).unwrap(),
419            "\"socks5://u:p%40@127.0.0.1:7890\""
420        );
421
422        let scheme = ProxyScheme::Socks5 {
423            remote_dns: true,
424            password_auth: Some(("u".into(), "p@".into())),
425            host: "127.0.0.1".into(),
426            port: 7890,
427        };
428        assert_eq!(
429            serde_json::to_string(&scheme).unwrap(),
430            "\"socks5h://u:p%40@127.0.0.1:7890\""
431        );
432    }
433}