uv_configuration/
proxy_url.rs1#[cfg(feature = "schemars")]
2use std::borrow::Cow;
3use std::fmt::{self, Display, Formatter};
4use std::str::FromStr;
5
6use reqwest::Proxy;
7use serde::{Deserialize, Deserializer, Serialize};
8use url::Url;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct ProxyUrl(Url);
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum ProxyUrlKind {
19 Http,
20 Https,
21}
22
23impl ProxyUrl {
24 fn as_url(&self) -> &Url {
26 &self.0
27 }
28
29 pub fn as_proxy(&self, kind: ProxyUrlKind) -> Proxy {
31 match kind {
33 ProxyUrlKind::Http => Proxy::http(self.0.as_str())
34 .expect("Constructing a proxy from a url should never fail"),
35 ProxyUrlKind::Https => Proxy::https(self.0.as_str())
36 .expect("Constructing a proxy from a url should never fail"),
37 }
38 }
39}
40
41#[derive(Debug, thiserror::Error)]
42pub enum ProxyUrlError {
43 #[error("invalid proxy URL: {0}")]
44 InvalidUrl(#[from] url::ParseError),
45 #[error(
46 "invalid proxy URL scheme `{scheme}` in `{url}`: expected http, https, socks5, or socks5h"
47 )]
48 InvalidScheme { scheme: String, url: Url },
49}
50
51fn lacks_scheme(s: &str) -> bool {
53 !s.contains("://")
54}
55
56impl FromStr for ProxyUrl {
57 type Err = ProxyUrlError;
58
59 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 fn try_with_http_scheme(s: &str) -> Result<ProxyUrl, ProxyUrlError> {
64 let with_scheme = format!("http://{s}");
65 let url = Url::parse(&with_scheme)?;
66 ProxyUrl::try_from(url)
67 }
68
69 match Url::parse(s) {
70 Ok(url) => match Self::try_from(url) {
71 Ok(proxy) => Ok(proxy),
72 Err(ProxyUrlError::InvalidScheme { .. }) if lacks_scheme(s) => {
73 try_with_http_scheme(s)
74 }
75 Err(e) => Err(e),
76 },
77 Err(url::ParseError::RelativeUrlWithoutBase) => try_with_http_scheme(s),
78 Err(err) => Err(ProxyUrlError::InvalidUrl(err)),
79 }
80 }
81}
82
83impl TryFrom<Url> for ProxyUrl {
84 type Error = ProxyUrlError;
85
86 fn try_from(url: Url) -> Result<Self, Self::Error> {
87 match url.scheme() {
88 "http" | "https" | "socks5" | "socks5h" => Ok(Self(url)),
89 scheme => Err(ProxyUrlError::InvalidScheme {
90 scheme: scheme.to_string(),
91 url,
92 }),
93 }
94 }
95}
96
97impl Display for ProxyUrl {
98 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
99 Display::fmt(&self.0, f)
100 }
101}
102
103impl<'de> Deserialize<'de> for ProxyUrl {
104 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105 where
106 D: Deserializer<'de>,
107 {
108 let s = String::deserialize(deserializer)?;
109 Self::from_str(&s).map_err(serde::de::Error::custom)
110 }
111}
112
113impl Serialize for ProxyUrl {
114 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115 where
116 S: serde::ser::Serializer,
117 {
118 serializer.serialize_str(self.as_url().as_str())
119 }
120}
121
122#[cfg(feature = "schemars")]
123impl schemars::JsonSchema for ProxyUrl {
124 fn schema_name() -> Cow<'static, str> {
125 Cow::Borrowed("ProxyUrl")
126 }
127
128 fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
129 schemars::json_schema!({
130 "type": "string",
131 "format": "uri",
132 "description": "A proxy URL (e.g., `http://proxy.example.com:8080`)."
133 })
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn parse_valid_proxy_urls() {
143 let url = "http://proxy.example.com:8080".parse::<ProxyUrl>().unwrap();
145 assert_eq!(url.to_string(), "http://proxy.example.com:8080/");
146
147 let url = "https://proxy.example.com:8080"
149 .parse::<ProxyUrl>()
150 .unwrap();
151 assert_eq!(url.to_string(), "https://proxy.example.com:8080/");
152
153 let url = "socks5://proxy.example.com:1080"
155 .parse::<ProxyUrl>()
156 .unwrap();
157 assert_eq!(url.to_string(), "socks5://proxy.example.com:1080");
158
159 let url = "socks5h://proxy.example.com:1080"
161 .parse::<ProxyUrl>()
162 .unwrap();
163 assert_eq!(url.to_string(), "socks5h://proxy.example.com:1080");
164
165 let url = "http://user:pass@proxy.example.com:8080"
167 .parse::<ProxyUrl>()
168 .unwrap();
169 assert_eq!(url.to_string(), "http://user:pass@proxy.example.com:8080/");
170 }
171
172 #[test]
173 fn parse_proxy_url_without_scheme() {
174 let url = "proxy.example.com:8080".parse::<ProxyUrl>().unwrap();
177 assert_eq!(url.to_string(), "http://proxy.example.com:8080/");
178
179 let url = "user:pass@proxy.example.com:8080"
181 .parse::<ProxyUrl>()
182 .unwrap();
183 assert_eq!(url.to_string(), "http://user:pass@proxy.example.com:8080/");
184
185 let url = "proxy.example.com".parse::<ProxyUrl>().unwrap();
187 assert_eq!(url.to_string(), "http://proxy.example.com/");
188 }
189
190 #[test]
191 fn parse_invalid_proxy_urls() {
192 let result = "ftp://proxy.example.com:8080".parse::<ProxyUrl>();
193 assert!(matches!(result, Err(ProxyUrlError::InvalidScheme { .. })));
194 insta::assert_snapshot!(
195 result.unwrap_err().to_string(),
196 @"invalid proxy URL scheme `ftp` in `ftp://proxy.example.com:8080/`: expected http, https, socks5, or socks5h"
197 );
198
199 let result = "not a url".parse::<ProxyUrl>();
201 assert!(matches!(result, Err(ProxyUrlError::InvalidUrl(_))));
202 insta::assert_snapshot!(
203 result.unwrap_err().to_string(),
204 @"invalid proxy URL: invalid international domain name"
205 );
206
207 let result = "".parse::<ProxyUrl>();
209 assert!(matches!(result, Err(ProxyUrlError::InvalidUrl(_))));
210 insta::assert_snapshot!(
211 result.unwrap_err().to_string(),
212 @"invalid proxy URL: empty host"
213 );
214
215 let result = "file:///path/to/file".parse::<ProxyUrl>();
216 assert!(matches!(result, Err(ProxyUrlError::InvalidScheme { .. })));
217 insta::assert_snapshot!(
218 result.unwrap_err().to_string(),
219 @"invalid proxy URL scheme `file` in `file:///path/to/file`: expected http, https, socks5, or socks5h"
220 );
221 }
222
223 #[test]
224 fn deserialize_invalid_proxy_url() {
225 let result: Result<ProxyUrl, _> = serde_json::from_str(r#""ftp://proxy.example.com:8080""#);
226 insta::assert_snapshot!(
227 result.unwrap_err().to_string(),
228 @"invalid proxy URL scheme `ftp` in `ftp://proxy.example.com:8080/`: expected http, https, socks5, or socks5h"
229 );
230
231 let result: Result<ProxyUrl, _> = serde_json::from_str(r#""not a url""#);
232 insta::assert_snapshot!(
233 result.unwrap_err().to_string(),
234 @"invalid proxy URL: invalid international domain name"
235 );
236 }
237}