miku_http_util/request/misc/
proxy.rs1use 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)]
12pub enum Error {
14 #[error("Invalid proxy uri: {0}")]
15 InvalidUri(#[from] http::uri::InvalidUri),
17
18 #[error("Invalid proxy uri: unsupported scheme")]
19 UnsupportedScheme,
21
22 #[error("Invalid proxy uri: general error")]
23 General,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum ProxyScheme {
39 Http {
41 is_https: bool,
43
44 basic_auth: Option<HeaderValue>,
46
47 authority: http::uri::Authority,
49 },
50
51 Socks5 {
53 remote_dns: bool,
55
56 password_auth: Option<(Arc<str>, Arc<str>)>,
58
59 host: Arc<str>,
61
62 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 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 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() }
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() }
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() }
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() }
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() }
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}