ureq/
proxy.rs

1use std::convert::{TryFrom, TryInto};
2use std::fmt;
3use std::sync::Arc;
4use ureq_proto::http::uri::{PathAndQuery, Scheme};
5
6use http::Uri;
7
8use crate::http;
9use crate::util::{AuthorityExt, DebugUri};
10use crate::Error;
11
12/// Proxy protocol
13#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
14#[non_exhaustive]
15pub enum ProxyProtocol {
16    /// CONNECT proxy over HTTP
17    Http,
18    /// CONNECT proxy over HTTPS
19    Https,
20    /// A SOCKS4 proxy
21    Socks4,
22    /// A SOCKS4a proxy (proxy can resolve domain name)
23    Socks4A,
24    /// SOCKS5 proxy
25    Socks5,
26}
27
28impl ProxyProtocol {
29    pub(crate) fn default_port(&self) -> u16 {
30        match self {
31            ProxyProtocol::Http => 80,
32            ProxyProtocol::Https => 443,
33            ProxyProtocol::Socks4 | ProxyProtocol::Socks4A | ProxyProtocol::Socks5 => 1080,
34        }
35    }
36
37    pub(crate) fn is_socks(&self) -> bool {
38        matches!(self, Self::Socks4 | Self::Socks4A | Self::Socks5)
39    }
40
41    pub(crate) fn is_connect(&self) -> bool {
42        matches!(self, Self::Http | Self::Https)
43    }
44
45    fn default_resolve_target(&self) -> bool {
46        match self {
47            ProxyProtocol::Http => false,
48            ProxyProtocol::Https => false,
49            ProxyProtocol::Socks4 => true, // we must locally resolve before using proxy
50            ProxyProtocol::Socks4A => false,
51            ProxyProtocol::Socks5 => false,
52        }
53    }
54}
55
56/// Proxy server settings
57///
58/// This struct represents a proxy server configuration that can be used to route HTTP/HTTPS
59/// requests through a proxy server. It supports various proxy protocols including HTTP CONNECT,
60/// HTTPS CONNECT, SOCKS4, SOCKS4A, and SOCKS5.
61///
62/// # Protocol Support
63///
64/// * `HTTP`: HTTP CONNECT proxy
65/// * `HTTPS`: HTTPS CONNECT proxy (requires a TLS provider)
66/// * `SOCKS4`: SOCKS4 proxy (requires **socks-proxy** feature)
67/// * `SOCKS4A`: SOCKS4A proxy (requires **socks-proxy** feature)
68/// * `SOCKS5`: SOCKS5 proxy (requires **socks-proxy** feature)
69///
70/// # DNS Resolution
71///
72/// The `resolve_target` setting controls where DNS resolution happens:
73///
74/// * When `true`: DNS resolution happens locally before connecting to the proxy.
75///   The resolved IP address is sent to the proxy.
76/// * When `false`: The hostname is sent to the proxy, which performs DNS resolution.
77///
78/// Default behavior:
79/// * For SOCKS4: `true` (local resolution required)
80/// * For all other protocols: `false` (proxy performs resolution)
81///
82/// # Examples
83///
84/// ```rust
85/// use ureq::{Proxy, ProxyProtocol};
86///
87/// // Create a proxy from a URI string
88/// let proxy = Proxy::new("http://localhost:8080").unwrap();
89///
90/// // Create a proxy using the builder pattern
91/// let proxy = Proxy::builder(ProxyProtocol::Socks5)
92///     .host("proxy.example.com")
93///     .port(1080)
94///     .username("user")
95///     .password("pass")
96///     .resolve_target(true)  // Force local DNS resolution
97///     .build()
98///     .unwrap();
99///
100/// // Read proxy settings from environment variables
101/// if let Some(proxy) = Proxy::try_from_env() {
102///     // Use proxy from environment
103/// }
104/// ```
105#[derive(Clone, Eq, Hash, PartialEq)]
106pub struct Proxy {
107    inner: Arc<ProxyInner>,
108}
109
110#[derive(Eq, Hash, PartialEq)]
111struct ProxyInner {
112    proto: ProxyProtocol,
113    uri: Uri,
114    from_env: bool,
115    resolve_target: bool,
116}
117
118impl Proxy {
119    /// Create a proxy from a uri.
120    ///
121    /// # Arguments:
122    ///
123    /// * `proxy` - a str of format `<protocol>://<user>:<password>@<host>:port` . All parts
124    ///   except host are optional.
125    ///
126    /// ###  Protocols
127    ///
128    /// * `http`: HTTP CONNECT proxy
129    /// * `https`: HTTPS CONNECT proxy (requires a TLS provider)
130    /// * `socks4`: SOCKS4 (requires **socks-proxy** feature)
131    /// * `socks4a`: SOCKS4A (requires **socks-proxy** feature)
132    /// * `socks5` and `socks`: SOCKS5 (requires **socks-proxy** feature)
133    ///
134    /// # Examples proxy formats
135    ///
136    /// * `http://127.0.0.1:8080`
137    /// * `socks5://john:smith@socks.google.com`
138    /// * `john:smith@socks.google.com:8000`
139    /// * `localhost`
140    pub fn new(proxy: &str) -> Result<Self, Error> {
141        Self::new_with_flag(proxy, false, None)
142    }
143
144    /// Creates a proxy config using a builder.
145    pub fn builder(p: ProxyProtocol) -> ProxyBuilder {
146        ProxyBuilder {
147            protocol: p,
148            host: None,
149            port: None,
150            username: None,
151            password: None,
152            resolve_target: p.default_resolve_target(),
153        }
154    }
155
156    fn new_with_flag(
157        proxy: &str,
158        from_env: bool,
159        resolve_target: Option<bool>,
160    ) -> Result<Self, Error> {
161        let mut uri = proxy.parse::<Uri>().or(Err(Error::InvalidProxyUrl))?;
162
163        // The uri must have an authority part (with the host), or
164        // it is invalid.
165        let _ = uri.authority().ok_or(Error::InvalidProxyUrl)?;
166
167        let scheme = match uri.scheme_str() {
168            Some(v) => v,
169            None => {
170                // The default protocol is Proto::HTTP, and it is missing in
171                // the uri. Let's put it in place.
172                uri = insert_default_scheme(uri);
173                "http"
174            }
175        };
176
177        let proto: ProxyProtocol = scheme.try_into()?;
178        let resolve_target = resolve_target.unwrap_or(proto.default_resolve_target());
179
180        let inner = ProxyInner {
181            proto,
182            uri,
183            from_env,
184            resolve_target,
185        };
186
187        Ok(Self {
188            inner: Arc::new(inner),
189        })
190    }
191
192    /// Read proxy settings from environment variables.
193    ///
194    /// The environment variable is expected to contain a proxy URI. The following
195    /// environment variables are attempted:
196    ///
197    /// * `ALL_PROXY`
198    /// * `HTTPS_PROXY`
199    /// * `HTTP_PROXY`
200    ///
201    /// Returns `None` if no environment variable is set or the URI is invalid.
202    pub fn try_from_env() -> Option<Self> {
203        const TRY_ENV: &[&str] = &[
204            "ALL_PROXY",
205            "all_proxy",
206            "HTTPS_PROXY",
207            "https_proxy",
208            "HTTP_PROXY",
209            "http_proxy",
210        ];
211
212        for attempt in TRY_ENV {
213            if let Ok(env) = std::env::var(attempt) {
214                if let Ok(proxy) = Self::new_with_flag(&env, true, None) {
215                    return Some(proxy);
216                }
217            }
218        }
219
220        None
221    }
222
223    /// The configured protocol.
224    pub fn protocol(&self) -> ProxyProtocol {
225        self.inner.proto
226    }
227
228    /// The proxy uri
229    pub fn uri(&self) -> &Uri {
230        &self.inner.uri
231    }
232
233    /// The host part of the proxy uri
234    pub fn host(&self) -> &str {
235        self.inner
236            .uri
237            .authority()
238            .map(|a| a.host())
239            .expect("constructor to ensure there is an authority")
240    }
241
242    /// The port of the proxy uri
243    pub fn port(&self) -> u16 {
244        self.inner
245            .uri
246            .authority()
247            .and_then(|a| a.port_u16())
248            .unwrap_or_else(|| self.inner.proto.default_port())
249    }
250
251    /// The username of the proxy uri
252    pub fn username(&self) -> Option<&str> {
253        self.inner.uri.authority().and_then(|a| a.username())
254    }
255
256    /// The password of the proxy uri
257    pub fn password(&self) -> Option<&str> {
258        self.inner.uri.authority().and_then(|a| a.password())
259    }
260
261    /// Whether this proxy setting was created manually or from
262    /// environment variables.
263    pub fn is_from_env(&self) -> bool {
264        self.inner.from_env
265    }
266
267    /// Whether to resolve target locally before calling the proxy.
268    ///
269    /// * `true` - resolve the DNS before calling proxy.
270    /// * `false` - send the target host to the proxy and let it resolve.
271    ///
272    /// Defaults to `false` for all proxies protocols except `SOCKS4`. I.e. the normal
273    /// case is to let the proxy resolve the target host.
274    pub fn resolve_target(&self) -> bool {
275        self.inner.resolve_target
276    }
277}
278
279fn insert_default_scheme(uri: Uri) -> Uri {
280    let mut parts = uri.into_parts();
281
282    parts.scheme = Some(Scheme::HTTP);
283
284    // For some reason uri.into_parts can produce None for
285    // the path, but Uri::from_parts does not accept that.
286    parts.path_and_query = parts
287        .path_and_query
288        .or_else(|| Some(PathAndQuery::from_static("/")));
289
290    Uri::from_parts(parts).unwrap()
291}
292
293/// Builder for configuring a proxy.
294///
295/// Obtained via [`Proxy::builder()`].
296pub struct ProxyBuilder {
297    protocol: ProxyProtocol,
298    host: Option<String>,
299    port: Option<u16>,
300    username: Option<String>,
301    password: Option<String>,
302    resolve_target: bool,
303}
304
305impl ProxyBuilder {
306    /// Set the proxy hostname
307    ///
308    /// Defaults to `localhost`. Invalid hostnames surface in [`ProxyBuilder::build()`].
309    pub fn host(mut self, host: &str) -> Self {
310        self.host = Some(host.to_string());
311        self
312    }
313
314    /// Set the proxy port
315    ///
316    /// Defaults to whatever is default for the chosen [`ProxyProtocol`].
317    pub fn port(mut self, port: u16) -> Self {
318        self.port = Some(port);
319        self
320    }
321
322    /// Set the username
323    ///
324    /// Defaults to none. Invalid usernames surface in [`ProxyBuilder::build()`].
325    pub fn username(mut self, v: &str) -> Self {
326        self.username = Some(v.to_string());
327        self
328    }
329
330    /// Set the password
331    ///
332    /// If you want to set only a password, no username, i.e. `https://secret@foo.com`,
333    /// you need to set it as [`ProxyBuilder::username()`].
334    ///
335    /// Defaults to none.  Invalid passwords surface in [`ProxyBuilder::build()`].
336    pub fn password(mut self, v: &str) -> Self {
337        self.password = Some(v.to_string());
338        self
339    }
340
341    /// Whether to resolve the target host locally before calling the proxy.
342    ///
343    /// * `true` - resolve target host locally before calling proxy.
344    /// * `false` - let proxy resolve the host.
345    ///
346    /// For SOCKS4, this defaults to `true`, for all other protocols `false`. I.e.
347    /// in the "normal" case, we let the proxy itself resolve host names.
348    pub fn resolve_target(mut self, do_resolve: bool) -> Self {
349        self.resolve_target = do_resolve;
350        self
351    }
352
353    /// Construct the [`Proxy`]
354    pub fn build(self) -> Result<Proxy, Error> {
355        let host = self.host.as_deref().unwrap_or("localhost");
356        let port = self.port.unwrap_or(self.protocol.default_port());
357
358        let mut userpass = String::new();
359        if let Some(username) = self.username {
360            userpass.push_str(&username);
361            if let Some(password) = self.password {
362                userpass.push(':');
363                userpass.push_str(&password);
364            }
365            userpass.push('@');
366        }
367
368        // TODO(martin): This incurs as a somewhat unnecessary allocation, but we get some
369        // validation and normalization in new_with_flag. This could be refactored
370        // in the future.
371        let proxy = format!("{}://{}{}:{}", self.protocol, userpass, host, port);
372        Proxy::new_with_flag(&proxy, false, Some(self.resolve_target))
373    }
374}
375
376impl TryFrom<&str> for ProxyProtocol {
377    type Error = Error;
378
379    fn try_from(scheme: &str) -> Result<Self, Self::Error> {
380        match scheme.to_ascii_lowercase().as_str() {
381            "http" => Ok(ProxyProtocol::Http),
382            "https" => Ok(ProxyProtocol::Https),
383            "socks4" => Ok(ProxyProtocol::Socks4),
384            "socks4a" => Ok(ProxyProtocol::Socks4A),
385            "socks" => Ok(ProxyProtocol::Socks5),
386            "socks5" => Ok(ProxyProtocol::Socks5),
387            _ => Err(Error::InvalidProxyUrl),
388        }
389    }
390}
391
392impl fmt::Debug for Proxy {
393    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394        f.debug_struct("Proxy")
395            .field("proto", &self.inner.proto)
396            .field("uri", &DebugUri(&self.inner.uri))
397            .field("from_env", &self.inner.from_env)
398            .finish()
399    }
400}
401
402impl fmt::Display for ProxyProtocol {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            ProxyProtocol::Http => write!(f, "HTTP"),
406            ProxyProtocol::Https => write!(f, "HTTPS"),
407            ProxyProtocol::Socks4 => write!(f, "SOCKS4"),
408            ProxyProtocol::Socks4A => write!(f, "SOCKS4a"),
409            ProxyProtocol::Socks5 => write!(f, "SOCKS5"),
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn parse_proxy_fakeproto() {
420        assert!(Proxy::new("fakeproto://localhost").is_err());
421    }
422
423    #[test]
424    fn parse_proxy_http_user_pass_server_port() {
425        let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999").unwrap();
426        assert_eq!(proxy.username(), Some("user"));
427        assert_eq!(proxy.password(), Some("p@ssw0rd"));
428        assert_eq!(proxy.host(), "localhost");
429        assert_eq!(proxy.port(), 9999);
430        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
431    }
432
433    #[test]
434    fn parse_proxy_http_user_pass_server_port_trailing_slash() {
435        let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999/").unwrap();
436        assert_eq!(proxy.username(), Some("user"));
437        assert_eq!(proxy.password(), Some("p@ssw0rd"));
438        assert_eq!(proxy.host(), "localhost");
439        assert_eq!(proxy.port(), 9999);
440        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
441    }
442
443    #[test]
444    fn parse_proxy_socks4_user_pass_server_port() {
445        let proxy = Proxy::new("socks4://user:p@ssw0rd@localhost:9999").unwrap();
446        assert_eq!(proxy.username(), Some("user"));
447        assert_eq!(proxy.password(), Some("p@ssw0rd"));
448        assert_eq!(proxy.host(), "localhost");
449        assert_eq!(proxy.port(), 9999);
450        assert_eq!(proxy.inner.proto, ProxyProtocol::Socks4);
451    }
452
453    #[test]
454    fn parse_proxy_socks4a_user_pass_server_port() {
455        let proxy = Proxy::new("socks4a://user:p@ssw0rd@localhost:9999").unwrap();
456        assert_eq!(proxy.username(), Some("user"));
457        assert_eq!(proxy.password(), Some("p@ssw0rd"));
458        assert_eq!(proxy.host(), "localhost");
459        assert_eq!(proxy.port(), 9999);
460        assert_eq!(proxy.inner.proto, ProxyProtocol::Socks4A);
461    }
462
463    #[test]
464    fn parse_proxy_socks_user_pass_server_port() {
465        let proxy = Proxy::new("socks://user:p@ssw0rd@localhost:9999").unwrap();
466        assert_eq!(proxy.username(), Some("user"));
467        assert_eq!(proxy.password(), Some("p@ssw0rd"));
468        assert_eq!(proxy.host(), "localhost");
469        assert_eq!(proxy.port(), 9999);
470        assert_eq!(proxy.inner.proto, ProxyProtocol::Socks5);
471    }
472
473    #[test]
474    fn parse_proxy_socks5_user_pass_server_port() {
475        let proxy = Proxy::new("socks5://user:p@ssw0rd@localhost:9999").unwrap();
476        assert_eq!(proxy.username(), Some("user"));
477        assert_eq!(proxy.password(), Some("p@ssw0rd"));
478        assert_eq!(proxy.host(), "localhost");
479        assert_eq!(proxy.port(), 9999);
480        assert_eq!(proxy.inner.proto, ProxyProtocol::Socks5);
481    }
482
483    #[test]
484    fn parse_proxy_user_pass_server_port() {
485        let proxy = Proxy::new("user:p@ssw0rd@localhost:9999").unwrap();
486        assert_eq!(proxy.username(), Some("user"));
487        assert_eq!(proxy.password(), Some("p@ssw0rd"));
488        assert_eq!(proxy.host(), "localhost");
489        assert_eq!(proxy.port(), 9999);
490        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
491    }
492
493    #[test]
494    fn parse_proxy_server_port() {
495        let proxy = Proxy::new("localhost:9999").unwrap();
496        assert_eq!(proxy.username(), None);
497        assert_eq!(proxy.password(), None);
498        assert_eq!(proxy.host(), "localhost");
499        assert_eq!(proxy.port(), 9999);
500        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
501    }
502
503    #[test]
504    fn parse_proxy_server() {
505        let proxy = Proxy::new("localhost").unwrap();
506        assert_eq!(proxy.username(), None);
507        assert_eq!(proxy.password(), None);
508        assert_eq!(proxy.host(), "localhost");
509        assert_eq!(proxy.port(), 80);
510        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
511    }
512}
513
514#[cfg(test)]
515mod test {
516    use super::*;
517    use assert_no_alloc::*;
518
519    #[test]
520    fn proxy_clone_does_not_allocate() {
521        let c = Proxy::new("socks://1.2.3.4").unwrap();
522        assert_no_alloc(|| c.clone());
523    }
524
525    #[test]
526    fn proxy_new_default_scheme() {
527        let c = Proxy::new("localhost:1234").unwrap();
528        assert_eq!(c.protocol(), ProxyProtocol::Http);
529        assert_eq!(c.uri(), "http://localhost:1234");
530    }
531
532    #[test]
533    fn proxy_empty_env_url() {
534        let result = Proxy::new_with_flag("", false, None);
535        assert!(result.is_err());
536    }
537
538    #[test]
539    fn proxy_invalid_env_url() {
540        let result = Proxy::new_with_flag("r32/?//52:**", false, None);
541        assert!(result.is_err());
542    }
543
544    #[test]
545    fn proxy_builder() {
546        let proxy = Proxy::builder(ProxyProtocol::Socks4)
547            .host("my-proxy.com")
548            .port(5551)
549            .resolve_target(false)
550            .build()
551            .unwrap();
552
553        assert_eq!(proxy.protocol(), ProxyProtocol::Socks4);
554        assert_eq!(proxy.uri(), "SOCKS4://my-proxy.com:5551/");
555        assert_eq!(proxy.host(), "my-proxy.com");
556        assert_eq!(proxy.port(), 5551);
557        assert_eq!(proxy.username(), None);
558        assert_eq!(proxy.password(), None);
559        assert_eq!(proxy.is_from_env(), false);
560        assert_eq!(proxy.resolve_target(), false);
561    }
562
563    #[test]
564    fn proxy_builder_username() {
565        let proxy = Proxy::builder(ProxyProtocol::Https)
566            .username("hemligearne")
567            .build()
568            .unwrap();
569
570        assert_eq!(proxy.protocol(), ProxyProtocol::Https);
571        assert_eq!(proxy.uri(), "https://hemligearne@localhost:443/");
572        assert_eq!(proxy.host(), "localhost");
573        assert_eq!(proxy.port(), 443);
574        assert_eq!(proxy.username(), Some("hemligearne"));
575        assert_eq!(proxy.password(), None);
576        assert_eq!(proxy.is_from_env(), false);
577        assert_eq!(proxy.resolve_target(), false);
578    }
579
580    #[test]
581    fn proxy_builder_username_password() {
582        let proxy = Proxy::builder(ProxyProtocol::Https)
583            .username("hemligearne")
584            .password("kulgrej")
585            .build()
586            .unwrap();
587
588        assert_eq!(proxy.protocol(), ProxyProtocol::Https);
589        assert_eq!(proxy.uri(), "https://hemligearne:kulgrej@localhost:443/");
590        assert_eq!(proxy.host(), "localhost");
591        assert_eq!(proxy.port(), 443);
592        assert_eq!(proxy.username(), Some("hemligearne"));
593        assert_eq!(proxy.password(), Some("kulgrej"));
594        assert_eq!(proxy.is_from_env(), false);
595        assert_eq!(proxy.resolve_target(), false);
596    }
597}