Skip to main content

ureq/unversioned/transport/
connect.rs

1use base64::Engine;
2use base64::prelude::BASE64_STANDARD;
3use std::borrow::Cow;
4use std::fmt;
5use std::io::Write;
6use ureq_proto::parser::try_parse_response;
7
8use http::StatusCode;
9
10use crate::Error;
11use crate::config::DEFAULT_USER_AGENT;
12use crate::http;
13use crate::transport::{Buffers, ConnectionDetails, Connector, Either};
14use crate::transport::{NextTimeout, Transport, TransportAdapter};
15use crate::util::{SchemeExt, UriExt};
16
17/// Connector for CONNECT proxy settings.
18///
19/// This operates on the previous chained transport typically a TcpConnector optionally
20/// wrapped in TLS.
21#[derive(Default)]
22pub struct ConnectProxyConnector(());
23
24impl<In: Transport> Connector<In> for ConnectProxyConnector {
25    type Out = Either<In, Box<dyn Transport>>;
26
27    fn connect(
28        &self,
29        details: &ConnectionDetails,
30        chained: Option<In>,
31    ) -> Result<Option<Self::Out>, Error> {
32        // If there is already a connection, do nothing.
33        if let Some(transport) = chained {
34            return Ok(Some(Either::A(transport)));
35        }
36
37        // If we're using a CONNECT proxy, we need to resolve that hostname.
38        let maybe_connect_uri = details.config.connect_proxy_uri();
39
40        let Some(connect_uri) = maybe_connect_uri else {
41            // Not using CONNECT
42            return Ok(None);
43        };
44
45        let target = details.uri;
46        let target_addrs = &details.addrs;
47
48        // Check if this host matches NO_PROXY
49        let is_no_proxy = details
50            .config
51            .proxy()
52            .map(|p| p.is_no_proxy(target))
53            .unwrap_or(false);
54
55        if is_no_proxy {
56            return Ok(None);
57        }
58
59        // TODO(martin): it's a bit weird to put the CONNECT proxy
60        // resolver timeout as part of Timeout::Connect, but we don't
61        // have anything more granular for now.
62        let proxy_addrs = details
63            .resolver
64            .resolve(connect_uri, details.config, details.timeout)?;
65
66        let proxy_config = details.config.clone_without_proxy();
67
68        // ConnectionDetails to establish a connection to the CONNECT
69        // proxy itself.
70        let proxy_details = ConnectionDetails {
71            uri: connect_uri,
72            addrs: proxy_addrs,
73            config: &proxy_config,
74            request_level: details.request_level,
75            resolver: details.resolver,
76            now: details.now,
77            timeout: details.timeout,
78            current_time: details.current_time.clone(),
79            run_connector: details.run_connector.clone(),
80        };
81
82        let transport = (details.run_connector)(&proxy_details)?;
83
84        // unwrap is ok because connect_proxy_uri() above checks it.
85        let proxy = details.config.proxy().unwrap();
86
87        let mut w = TransportAdapter::new(transport);
88
89        target.ensure_valid_url()?;
90
91        // unwraps are ok because ensure_valid_url() checks it.
92        let mut target_host = Cow::Borrowed(target.host().unwrap());
93        let target_port = target
94            .port_u16()
95            .unwrap_or(target.scheme().unwrap().default_port().unwrap());
96
97        if proxy.resolve_target() {
98            // In run() we do the resolution of the target (proxied) host, at this
99            // point we should have at least one IP address.
100            //
101            // TODO(martin): On fail try more addresses
102            let resolved = target_addrs.first().expect("at least one resolved address");
103
104            target_host = Cow::Owned(resolved.to_string());
105        }
106
107        write!(w, "CONNECT {}:{} HTTP/1.1\r\n", target_host, target_port)?;
108        write!(w, "Host: {}:{}\r\n", target_host, target_port)?;
109        if let Some(v) = details.config.user_agent().as_str(DEFAULT_USER_AGENT) {
110            write!(w, "User-Agent: {}\r\n", v)?;
111        }
112        write!(w, "Proxy-Connection: Keep-Alive\r\n")?;
113
114        let use_creds = proxy.username().is_some() || proxy.password().is_some();
115
116        if use_creds {
117            let user = proxy.username().unwrap_or_default();
118            let pass = proxy.password().unwrap_or_default();
119            let creds = BASE64_STANDARD.encode(format!("{}:{}", user, pass));
120            // "Basic" in canonical casing: the scheme is case-insensitive per
121            // RFC 9110, but some proxies reject the lowercase form outright.
122            write!(w, "Proxy-Authorization: Basic {}\r\n", creds)?;
123        }
124
125        write!(w, "\r\n")?;
126        w.flush()?;
127
128        let mut transport = w.into_inner();
129
130        let response = loop {
131            let made_progress = transport.maybe_await_input(details.timeout)?;
132            let buffers = transport.buffers();
133            let input = buffers.input();
134            let Some((used_input, response)) = try_parse_response::<20>(input)? else {
135                if !made_progress {
136                    let reason = "proxy server did not respond".to_string();
137                    return Err(Error::ConnectProxyFailed(reason));
138                }
139                continue;
140            };
141            buffers.input_consume(used_input);
142            break response;
143        };
144
145        match response.status() {
146            StatusCode::OK => {
147                trace!("CONNECT proxy connected");
148            }
149            x => {
150                let reason = format!("proxy server responded {}/{}", x.as_u16(), x.as_str());
151                return Err(Error::ConnectProxyFailed(reason));
152            }
153        }
154
155        // CONNECT establishes a new connection context for the target. The transport
156        // might have TLS to the proxy, but it does not have TLS to the target yet.
157        Ok(Some(Either::B(TunnelTransport(transport).boxed())))
158    }
159}
160
161#[derive(Debug)]
162struct TunnelTransport(Box<dyn Transport>);
163
164impl Transport for TunnelTransport {
165    fn buffers(&mut self) -> &mut dyn Buffers {
166        self.0.buffers()
167    }
168
169    fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error> {
170        self.0.transmit_output(amount, timeout)
171    }
172
173    fn await_input(&mut self, timeout: NextTimeout) -> Result<bool, Error> {
174        self.0.await_input(timeout)
175    }
176
177    fn is_open(&mut self) -> bool {
178        self.0.is_open()
179    }
180
181    fn is_tls(&self) -> bool {
182        false
183    }
184}
185
186impl fmt::Debug for ConnectProxyConnector {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        f.debug_struct("ProxyConnector").finish()
189    }
190}