Skip to main content

acme_proxy/
http_client.rs

1//! The transport half of this server's four outbound HTTP clients.
2//!
3//! Deliberately **not** a shared client type. What
4//! [`challenge::http_01`](crate::challenge::http_01),
5//! [`signer::relay::client`](crate::signer::relay::client),
6//! [`ipam::http`](crate::ipam::http) and
7//! [`notify::webhook`](crate::notify::webhook) have in common is
8//! *plumbing*: pick a URL apart into host, scheme and port; connect through the
9//! shared resolver; wrap in TLS with the right SNI; hand the stream to hyper and
10//! spawn the connection task. That part was written out four times, and
11//! `client_tls_config` twice byte-for-byte.
12//!
13//! What differs is *policy*, and each of those modules' comment defending
14//! "per-module locality" is right about policy and wrong about plumbing:
15//! `http_01` must **not** validate the peer certificate (RFC 8555 §8.3 — what
16//! it carries is the proof, not an identity) while the other three must; each
17//! caps its response body differently; each has its own headers and its own
18//! error type. So this module owns the plumbing and nothing else.
19
20use std::pin::Pin;
21use std::sync::Arc;
22use std::task::{Context, Poll};
23
24use bytes::Bytes;
25use http_body_util::{BodyExt, Empty, Limited};
26use hyper::body::Body;
27use hyper_util::rt::TokioIo;
28use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
29use url::Url;
30
31use crate::dns::Resolver;
32use crate::proxy::{OutboundProxies, ProxyTarget};
33
34/// How much of a proxy's refusal is quoted back. A `407` body is a page, and
35/// what an operator needs from it is the first line.
36const MAX_PROXY_ERROR_BYTES: usize = 512;
37
38/// The ceiling on a JSON API response this server will read.
39///
40/// Shared by [`ipam::http`](crate::ipam::http) and
41/// [`signer::relay::client`](crate::signer::relay::client), which had the same
42/// `1024 * 1024` written out separately. `challenge::http_01` keeps its own,
43/// because there it is an operator-facing configuration key
44/// (`challenge.http_01.max_response_bytes`) rather than a constant.
45pub(crate) const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
46
47/// How much of a rejecting response's body is quoted back into an error.
48///
49/// A provider that answers a bad request with `invalid_payload` has given the
50/// whole diagnosis; one that answers with a page is not owed a log line the
51/// size of one.
52pub(crate) const MAX_ERROR_BODY_CHARS: usize = 200;
53
54/// A rejecting response's body, lossily decoded and capped for an error message.
55///
56/// Three callers had written this out: the body of a non-2xx is the operator's
57/// entire diagnosis, and it is never a value to be parsed, only quoted. Lossy
58/// rather than `from_utf8`, since a body that is not UTF-8 is exactly the case
59/// where an operator most needs to see what did arrive.
60pub(crate) fn error_excerpt(body: &[u8]) -> String {
61    String::from_utf8_lossy(body)
62        .chars()
63        .take(MAX_ERROR_BODY_CHARS)
64        .collect()
65}
66
67/// Where an outbound request is going, once its URL has been picked apart.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub(crate) struct Endpoint {
70    pub host: String,
71    pub port: u16,
72    pub https: bool,
73}
74
75impl Endpoint {
76    /// Splits a URL into host, port and scheme, rejecting anything this server
77    /// will not speak.
78    ///
79    /// Only `http` and `https`: an outbound client here talks to a CA, a NetBox
80    /// instance or a webhook, and a `file:` or `gopher:` URL in a configuration
81    /// field is a mistake worth naming rather than a scheme to support.
82    pub(crate) fn from_url(url: &Url) -> Result<Self, String> {
83        let host = url
84            .host_str()
85            .ok_or_else(|| format!("{url} has no host"))?
86            .to_string();
87        let https = match url.scheme() {
88            "https" => true,
89            "http" => false,
90            other => return Err(format!("unsupported scheme: {other}")),
91        };
92        let port = url
93            .port_or_known_default()
94            .unwrap_or(if https { 443 } else { 80 });
95
96        Ok(Self { host, port, https })
97    }
98
99    /// An endpoint for a TLS connection that never came from a URL —
100    /// [`crate::challenge::tls_alpn_01`]'s probe, which has an identifier and a
101    /// port and no scheme at all.
102    pub(crate) fn tls(host: &str, port: u16) -> Self {
103        Self {
104            host: host.to_string(),
105            port,
106            https: true,
107        }
108    }
109
110    /// The host with any IPv6 brackets removed — the form `IpAddr::from_str`
111    /// and a resolver want.
112    ///
113    /// [`Url::host_str`] hands back `[2001:db8::1]` for a literal, which
114    /// [`crate::dns::connect`] cannot parse as an address and would therefore
115    /// try to *resolve*. [`authority`](Self::authority) keeps the brackets,
116    /// because that is the form a `Host` header and a request line need.
117    pub(crate) fn host_for_lookup(&self) -> &str {
118        self.host
119            .strip_prefix('[')
120            .and_then(|rest| rest.strip_suffix(']'))
121            .unwrap_or(&self.host)
122    }
123
124    /// `host` or `host:port` — what a `Host` header should carry.
125    pub(crate) fn authority(&self) -> String {
126        let default = if self.https { 443 } else { 80 };
127        if self.port == default {
128            self.host.clone()
129        } else {
130            format!("{}:{}", self.host, self.port)
131        }
132    }
133
134    /// Always `host:port`, including a port [`authority`](Self::authority)
135    /// would elide.
136    ///
137    /// A `CONNECT` request-target is authority-form and RFC 9110 §9.3.6 requires
138    /// both components: a proxy handed `CONNECT example.com` refuses it. This is
139    /// the one place the distinction bites, and it is why the two spellings are
140    /// separate methods rather than one with a flag.
141    pub(crate) fn connect_authority(&self) -> String {
142        format!("{}:{}", self.host, self.port)
143    }
144}
145
146/// The webpki root store, for the clients that talk to a real remote service
147/// whose certificate is the only thing establishing who it is.
148///
149/// The fourth client, `challenge::http_01`, deliberately validates nothing —
150/// see [`crate::challenge::tls_alpn_01::accept_any_client_config`].
151///
152/// The provider is passed explicitly rather than installed as a process
153/// default: `CryptoProvider::install_default` panics on a second call, which
154/// would make test ordering matter.
155pub(crate) fn webpki_tls_config() -> rustls::ClientConfig {
156    let roots = rustls::RootCertStore {
157        roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
158    };
159    rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
160        .with_safe_default_protocol_versions()
161        .expect("ring provider supports the default protocol versions")
162        .with_root_certificates(roots)
163        .with_no_client_auth()
164}
165
166/// The transport under an outbound connection: a direct socket, or one
167/// tunnelled through a forward proxy's `CONNECT`.
168///
169/// An enum rather than `Box<dyn AsyncRead + AsyncWrite + Unpin + Send>`: the
170/// delegation is mechanical, it costs no allocation, and it keeps the concrete
171/// `TcpStream` visible to anyone reading the direct path.
172pub(crate) enum ClientStream {
173    Direct(tokio::net::TcpStream),
174    Tunnelled(TokioIo<hyper::upgrade::Upgraded>),
175}
176
177impl AsyncRead for ClientStream {
178    fn poll_read(
179        self: Pin<&mut Self>,
180        context: &mut Context<'_>,
181        buffer: &mut ReadBuf<'_>,
182    ) -> Poll<std::io::Result<()>> {
183        match self.get_mut() {
184            Self::Direct(stream) => Pin::new(stream).poll_read(context, buffer),
185            Self::Tunnelled(stream) => Pin::new(stream).poll_read(context, buffer),
186        }
187    }
188}
189
190impl AsyncWrite for ClientStream {
191    fn poll_write(
192        self: Pin<&mut Self>,
193        context: &mut Context<'_>,
194        buffer: &[u8],
195    ) -> Poll<std::io::Result<usize>> {
196        match self.get_mut() {
197            Self::Direct(stream) => Pin::new(stream).poll_write(context, buffer),
198            Self::Tunnelled(stream) => Pin::new(stream).poll_write(context, buffer),
199        }
200    }
201
202    fn poll_flush(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<std::io::Result<()>> {
203        match self.get_mut() {
204            Self::Direct(stream) => Pin::new(stream).poll_flush(context),
205            Self::Tunnelled(stream) => Pin::new(stream).poll_flush(context),
206        }
207    }
208
209    fn poll_shutdown(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<std::io::Result<()>> {
210        match self.get_mut() {
211            Self::Direct(stream) => Pin::new(stream).poll_shutdown(context),
212            Self::Tunnelled(stream) => Pin::new(stream).poll_shutdown(context),
213        }
214    }
215}
216
217/// How the request line must address the target on a given connection.
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub(crate) enum RequestForm {
220    /// `GET /path?query` — a direct connection, or one inside a tunnel.
221    /// RFC 9112 §3.2.1 makes this the form for an origin server.
222    Origin,
223    /// `GET http://host/path?query` — plain HTTP forwarded by a proxy, the one
224    /// case §3.2.2 is for.
225    Absolute,
226}
227
228/// An established HTTP/1 connection, plus how requests on it must be addressed
229/// and credentialed.
230///
231/// Returned instead of a bare `SendRequest` because those two facts are
232/// properties of *this connection*, not of the caller: a request built for a
233/// direct connection is malformed on a forwarding one, and the other way round.
234pub(crate) struct Connection<B> {
235    sender: hyper::client::conn::http1::SendRequest<B>,
236    form: RequestForm,
237    /// Carried only on a forwarding connection — see
238    /// [`send_request`](Self::send_request).
239    proxy_authorization: Option<String>,
240}
241
242impl<B> Connection<B>
243where
244    B: Body + 'static,
245{
246    /// The request-line target for `url` on this connection.
247    pub(crate) fn request_target(&self, url: &Url) -> String {
248        match self.form {
249            RequestForm::Absolute => url.as_str().to_string(),
250            RequestForm::Origin => {
251                let mut target = url.path().to_string();
252                if let Some(query) = url.query() {
253                    target.push('?');
254                    target.push_str(query);
255                }
256                target
257            }
258        }
259    }
260
261    /// Sends `request`, attaching `Proxy-Authorization` when this connection
262    /// reaches the origin *through* a proxy in the clear.
263    ///
264    /// Attached here rather than left to the caller so a new caller cannot
265    /// forget it — and never on a tunnelled connection: the credential was
266    /// already spent on the `CONNECT`, and repeating it inside the tunnel would
267    /// hand it to the origin server.
268    pub(crate) async fn send_request(
269        &mut self,
270        mut request: hyper::Request<B>,
271    ) -> hyper::Result<hyper::Response<hyper::body::Incoming>> {
272        if let Some(credential) = &self.proxy_authorization
273            && let Ok(value) = hyper::header::HeaderValue::from_str(credential)
274        {
275            request
276                .headers_mut()
277                .insert(hyper::header::PROXY_AUTHORIZATION, value);
278        }
279        self.sender.send_request(request).await
280    }
281}
282
283/// Where to resolve a name, and whether to go through a proxy.
284///
285/// The two travelled as separate parameters through some forty positions across
286/// `challenge/`, `ipam/`, `notify/webhook.rs` and `signer/relay/`, and they are
287/// never meaningfully apart: a caller holding one holds the other, and both
288/// come from the same `Profile::build_all`. Bundling them is what retires the
289/// crate's last `#[allow(clippy::too_many_arguments)]`
290/// (`notify::webhook::send_request`).
291///
292/// `tls` is deliberately **not** in here. It is the part that genuinely differs
293/// per caller — [`crate::challenge::http_01`] must not validate the responder's
294/// certificate (RFC 8555 §8.3: what it carries is the proof, not an identity)
295/// while the other three verify against `webpki-roots` — and this module's rule
296/// is that policy stays with its caller and only plumbing moves here.
297#[derive(Clone)]
298pub struct Outbound {
299    resolver: Arc<dyn Resolver>,
300    proxies: Arc<OutboundProxies>,
301}
302
303impl Outbound {
304    pub fn new(resolver: Arc<dyn Resolver>, proxies: Arc<OutboundProxies>) -> Self {
305        Self { resolver, proxies }
306    }
307
308    /// Connects to `endpoint` and completes the HTTP/1 handshake under the
309    /// caller's own `tls` — see [`connect`] for what a proxy changes.
310    pub(crate) async fn connect<B>(
311        &self,
312        endpoint: &Endpoint,
313        tls: &Arc<rustls::ClientConfig>,
314    ) -> Result<Connection<B>, String>
315    where
316        B: Body + Send + 'static,
317        B::Data: Send,
318        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
319    {
320        connect(self.resolver.as_ref(), &self.proxies, endpoint, tls).await
321    }
322
323    /// Opens a byte stream to `endpoint` for a caller layering its own TLS —
324    /// see [`connect_stream`].
325    pub(crate) async fn connect_stream(&self, endpoint: &Endpoint) -> Result<ClientStream, String> {
326        connect_stream(self.resolver.as_ref(), &self.proxies, endpoint).await
327    }
328}
329
330/// Opens a byte stream to `endpoint`, tunnelling when a proxy applies.
331///
332/// For callers that layer their own TLS — [`crate::challenge::tls_alpn_01`]'s
333/// probe is the one. When a proxy is selected this always uses `CONNECT`,
334/// including for a cleartext endpoint: the caller asked for an end-to-end
335/// stream, not a forwarding relationship.
336pub(crate) async fn connect_stream(
337    resolver: &dyn Resolver,
338    proxies: &OutboundProxies,
339    endpoint: &Endpoint,
340) -> Result<ClientStream, String> {
341    match proxies.select(endpoint) {
342        Some(proxy) => tunnel(resolver, proxy, endpoint)
343            .await
344            .map(ClientStream::Tunnelled),
345        None => dial(resolver, endpoint).await.map(ClientStream::Direct),
346    }
347}
348
349/// Connects to `endpoint` and completes the HTTP/1 handshake.
350///
351/// Connects through `resolver` rather than `TcpStream::connect`, so
352/// `dns.resolver` governs every outbound hop the way it already governs the
353/// `dns-01` TXT lookup — and so a dual-stack answer whose first address is
354/// unreachable falls back instead of failing outright. Three of the four
355/// clients used to bypass that, which meant an operator running a split-horizon
356/// estate would find NetBox and their upstream CA resolving differently from
357/// the challenge validators, with nothing in the code saying so.
358///
359/// Four shapes, and the table is the whole of what a proxy changes here:
360///
361/// | proxy | `https` | transport | request form |
362/// | --- | --- | --- | --- |
363/// | none | no | TCP to the origin | origin |
364/// | none | yes | TCP + TLS to the origin | origin |
365/// | some | yes | `CONNECT` tunnel, TLS **inside** it | origin |
366/// | some | no | TCP to the proxy | absolute + `Proxy-Authorization` |
367///
368/// There is deliberately no timeout here: every caller already wraps the whole
369/// operation in one, so a black-holed proxy surfaces as the same failure as a
370/// black-holed origin rather than as a second budget to keep in step.
371///
372/// The connection task is spawned and detached: hyper drives the connection
373/// there while the caller uses the sender, and it ends when the sender drops.
374pub(crate) async fn connect<B>(
375    resolver: &dyn Resolver,
376    proxies: &OutboundProxies,
377    endpoint: &Endpoint,
378    tls: &Arc<rustls::ClientConfig>,
379) -> Result<Connection<B>, String>
380where
381    B: Body + Send + 'static,
382    B::Data: Send,
383    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
384{
385    let proxy = proxies.select(endpoint);
386
387    // Cleartext through a proxy is the one case that is not a tunnel: the proxy
388    // is the peer, and it learns the target from the request line instead.
389    if let Some(proxy) = proxy
390        && !endpoint.https
391    {
392        let stream = dial(resolver, proxy.endpoint())
393            .await
394            .map_err(|error| format!("connecting to proxy {}: {error}", proxy.redacted()))?;
395        let sender = spawn_handshake(TokioIo::new(stream)).await?;
396        return Ok(Connection {
397            sender,
398            form: RequestForm::Absolute,
399            proxy_authorization: proxy.authorization().map(str::to_string),
400        });
401    }
402
403    let stream = match proxy {
404        Some(proxy) => ClientStream::Tunnelled(tunnel(resolver, proxy, endpoint).await?),
405        None => ClientStream::Direct(dial(resolver, endpoint).await?),
406    };
407
408    let sender = if endpoint.https {
409        // SNI and validation are against the *origin*, never the proxy: the
410        // tunnel is a pipe, and the certificate at the far end is the origin's.
411        let server_name =
412            rustls_pki_types::ServerName::try_from(endpoint.host_for_lookup().to_string())
413                .map_err(|error| format!("{}: {error}", endpoint.host))?;
414        let stream = tokio_rustls::TlsConnector::from(tls.clone())
415            .connect(server_name, stream)
416            .await
417            .map_err(|error| format!("TLS handshake with {}: {error}", endpoint.host))?;
418        spawn_handshake(TokioIo::new(stream)).await?
419    } else {
420        spawn_handshake(TokioIo::new(stream)).await?
421    };
422
423    Ok(Connection {
424        sender,
425        form: RequestForm::Origin,
426        proxy_authorization: None,
427    })
428}
429
430/// The TCP half, named so the error says which hop failed.
431async fn dial(
432    resolver: &dyn Resolver,
433    endpoint: &Endpoint,
434) -> Result<tokio::net::TcpStream, String> {
435    crate::dns::connect(resolver, endpoint.host_for_lookup(), endpoint.port)
436        .await
437        .map_err(|error| format!("connecting to {}:{}: {error}", endpoint.host, endpoint.port))
438}
439
440/// Opens a `CONNECT` tunnel to `endpoint` through `proxy`.
441///
442/// The proxy is reached through the shared resolver too: `dns.resolver` is
443/// documented as governing every lookup this server makes, and a proxy named by
444/// hostname is a lookup.
445async fn tunnel(
446    resolver: &dyn Resolver,
447    proxy: &ProxyTarget,
448    endpoint: &Endpoint,
449) -> Result<TokioIo<hyper::upgrade::Upgraded>, String> {
450    let socket = dial(resolver, proxy.endpoint())
451        .await
452        .map_err(|error| format!("connecting to proxy {}: {error}", proxy.redacted()))?;
453
454    let (mut sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(socket))
455        .await
456        .map_err(|error| format!("HTTP handshake with proxy {}: {error}", proxy.redacted()))?;
457
458    // `with_upgrades` is load-bearing: without it the connection task never
459    // surrenders the socket, `hyper::upgrade::on` never resolves, and the tunnel
460    // hangs rather than failing — a far worse shape than an error.
461    tokio::spawn(async move {
462        let _ = connection.with_upgrades().await;
463    });
464
465    let authority = endpoint.connect_authority();
466    // No `Connection: close` (a proxy honouring it would close the tunnel we
467    // just asked for) and no `Proxy-Connection`, which no RFC defines.
468    let mut builder = hyper::Request::connect(&authority)
469        .header(hyper::header::HOST, &authority)
470        .header(hyper::header::USER_AGENT, "acme-proxy");
471    if let Some(credential) = proxy.authorization() {
472        builder = builder.header(hyper::header::PROXY_AUTHORIZATION, credential);
473    }
474    let request = builder
475        .body(Empty::<Bytes>::new())
476        .map_err(|error| format!("building the CONNECT request: {error}"))?;
477
478    let response = sender
479        .send_request(request)
480        .await
481        .map_err(|error| format!("CONNECT {authority} via {}: {error}", proxy.redacted()))?;
482
483    // The status is checked *before* asking for the upgrade: a non-2xx has no
484    // pending upgrade to hand over, and its body is the whole diagnosis —
485    // "407 Proxy Authentication Required" is something an operator can act on,
486    // and they will never see the proxy's own log.
487    if !response.status().is_success() {
488        let status = response.status();
489        let excerpt = Limited::new(response.into_body(), MAX_PROXY_ERROR_BYTES)
490            .collect()
491            .await
492            .map(|body| {
493                String::from_utf8_lossy(&body.to_bytes())
494                    .split_whitespace()
495                    .collect::<Vec<_>>()
496                    .join(" ")
497                    .chars()
498                    .take(200)
499                    .collect::<String>()
500            })
501            .unwrap_or_default();
502        return Err(format!(
503            "proxy {} refused CONNECT {authority}: {status} {excerpt}",
504            proxy.redacted()
505        ));
506    }
507
508    hyper::upgrade::on(response)
509        .await
510        .map(TokioIo::new)
511        .map_err(|error| {
512            format!(
513                "proxy {} did not hand over the tunnel to {authority}: {error}",
514                proxy.redacted()
515            )
516        })
517}
518
519/// The `http1::handshake` + detached connection task the four clients each
520/// wrote out identically.
521async fn spawn_handshake<B, I>(io: I) -> Result<hyper::client::conn::http1::SendRequest<B>, String>
522where
523    B: Body + Send + 'static,
524    B::Data: Send,
525    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
526    I: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
527{
528    let (sender, connection) = hyper::client::conn::http1::handshake(io)
529        .await
530        .map_err(|error| format!("HTTP handshake: {error}"))?;
531    tokio::spawn(async move {
532        // Nothing to do with the result: the caller learns about a broken
533        // connection from its own request future failing.
534        let _ = connection.await;
535    });
536    Ok(sender)
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    fn url(value: &str) -> Url {
544        Url::parse(value).unwrap()
545    }
546
547    #[test]
548    fn default_ports_follow_the_scheme() {
549        let http = Endpoint::from_url(&url("http://example.com/x")).unwrap();
550        assert_eq!(http.port, 80);
551        assert!(!http.https);
552        assert_eq!(http.host, "example.com");
553
554        let https = Endpoint::from_url(&url("https://example.com/x")).unwrap();
555        assert_eq!(https.port, 443);
556        assert!(https.https);
557    }
558
559    #[test]
560    fn an_explicit_port_wins() {
561        let endpoint = Endpoint::from_url(&url("https://example.com:8443/x")).unwrap();
562        assert_eq!(endpoint.port, 8443);
563        assert!(endpoint.https);
564    }
565
566    /// The `Host` header omits the port when it is the scheme's default, which
567    /// is what a server expects to see.
568    #[test]
569    fn the_authority_omits_a_default_port() {
570        assert_eq!(
571            Endpoint::from_url(&url("https://example.com/x"))
572                .unwrap()
573                .authority(),
574            "example.com"
575        );
576        assert_eq!(
577            Endpoint::from_url(&url("http://example.com/x"))
578                .unwrap()
579                .authority(),
580            "example.com"
581        );
582        assert_eq!(
583            Endpoint::from_url(&url("https://example.com:8443/x"))
584                .unwrap()
585                .authority(),
586            "example.com:8443"
587        );
588    }
589
590    #[test]
591    fn a_url_with_no_host_is_refused() {
592        let error = Endpoint::from_url(&url("file:///etc/passwd")).unwrap_err();
593        assert!(
594            error.contains("no host") || error.contains("unsupported scheme"),
595            "{error}"
596        );
597    }
598
599    #[test]
600    fn an_unsupported_scheme_is_refused() {
601        let error = Endpoint::from_url(&url("ftp://example.com/x")).unwrap_err();
602        assert!(error.contains("unsupported scheme"), "{error}");
603    }
604
605    #[test]
606    fn an_ipv6_literal_survives_the_round_trip() {
607        let endpoint = Endpoint::from_url(&url("https://[2001:db8::1]:8443/x")).unwrap();
608        assert_eq!(endpoint.port, 8443);
609        assert_eq!(endpoint.authority(), "[2001:db8::1]:8443");
610    }
611
612    /// The brackets a `Host` header needs are exactly what
613    /// `IpAddr::from_str` chokes on, so the connect path strips them — without
614    /// this, an IPv6-literal URL is handed to the resolver as a name.
615    #[test]
616    fn an_ipv6_literal_loses_its_brackets_for_a_lookup() {
617        let endpoint = Endpoint::from_url(&url("https://[2001:db8::1]:8443/x")).unwrap();
618        assert_eq!(endpoint.host_for_lookup(), "2001:db8::1");
619        assert!(
620            endpoint
621                .host_for_lookup()
622                .parse::<std::net::IpAddr>()
623                .is_ok()
624        );
625
626        let named = Endpoint::from_url(&url("https://example.com/x")).unwrap();
627        assert_eq!(named.host_for_lookup(), "example.com");
628    }
629
630    #[test]
631    fn the_webpki_config_builds() {
632        let config = webpki_tls_config();
633        assert!(config.alpn_protocols.is_empty());
634    }
635
636    /// A `CONNECT` target must keep the port even when it is the scheme's
637    /// default — `authority()` elides it, and `CONNECT example.com` is refused
638    /// by every real proxy.
639    #[test]
640    fn a_connect_authority_always_carries_the_port() {
641        let https = Endpoint::from_url(&url("https://example.com/x")).unwrap();
642        assert_eq!(https.authority(), "example.com");
643        assert_eq!(https.connect_authority(), "example.com:443");
644
645        let http = Endpoint::from_url(&url("http://example.com/x")).unwrap();
646        assert_eq!(http.connect_authority(), "example.com:80");
647
648        let literal = Endpoint::from_url(&url("https://[2001:db8::1]/x")).unwrap();
649        assert_eq!(literal.connect_authority(), "[2001:db8::1]:443");
650    }
651
652    #[test]
653    fn an_endpoint_can_be_built_without_a_url() {
654        let endpoint = Endpoint::tls("example.com", 8443);
655        assert!(endpoint.https);
656        assert_eq!(endpoint.connect_authority(), "example.com:8443");
657    }
658
659    /// The request line is a property of the connection, not the caller: the
660    /// same URL is spelled two ways depending on who the peer is.
661    #[test]
662    fn the_request_target_follows_the_form() {
663        let target = url("http://example.com/a/b?c=d&e=f");
664        for (form, expected) in [
665            (RequestForm::Origin, "/a/b?c=d&e=f"),
666            (RequestForm::Absolute, "http://example.com/a/b?c=d&e=f"),
667        ] {
668            let connection = Connection::<Empty<Bytes>> {
669                sender: unreachable_sender(),
670                form,
671                proxy_authorization: None,
672            };
673            assert_eq!(connection.request_target(&target), expected);
674        }
675
676        let no_query = url("http://example.com/a");
677        let connection = Connection::<Empty<Bytes>> {
678            sender: unreachable_sender(),
679            form: RequestForm::Origin,
680            proxy_authorization: None,
681        };
682        assert_eq!(connection.request_target(&no_query), "/a");
683    }
684
685    /// A `SendRequest` for a connection whose peer never answers. The pure
686    /// `request_target` tests need the field, not a live socket.
687    fn unreachable_sender() -> hyper::client::conn::http1::SendRequest<Empty<Bytes>> {
688        let (sender, connection) = futures_lite_block_on(async {
689            let (client, _server) = tokio::io::duplex(64);
690            hyper::client::conn::http1::handshake(TokioIo::new(client))
691                .await
692                .unwrap()
693        });
694        drop(connection);
695        sender
696    }
697
698    /// A one-shot current-thread runtime, so the helper above stays usable from
699    /// a plain `#[test]`.
700    fn futures_lite_block_on<F: Future>(future: F) -> F::Output {
701        tokio::runtime::Builder::new_current_thread()
702            .enable_all()
703            .build()
704            .unwrap()
705            .block_on(future)
706    }
707
708    /// The transport against a loopback proxy that really speaks `CONNECT`.
709    ///
710    /// Every case builds its proxies with `OutboundProxies::always`, because
711    /// `select` bypasses loopback unconditionally — through the ordinary
712    /// constructor a proxy on `127.0.0.1` would never be selected, and every
713    /// one of these would pass for the wrong reason.
714    ///
715    /// Two of them (the tunnel cases) would **hang** rather than fail if
716    /// `with_upgrades()` were ever dropped from `tunnel`, which is the shape
717    /// that mistake actually has.
718    mod loopback {
719        use super::*;
720        use crate::proxy::{OutboundProxies, ProxyTarget};
721        use crate::testutil::{FakeProxy, ProxyBehaviour};
722        use tokio::io::{AsyncReadExt, AsyncWriteExt};
723
724        /// Everything here targets `127.0.0.1` literally, which
725        /// [`crate::dns::connect`] short-circuits before asking a resolver —
726        /// except `resolving`, below, which is the one case that needs a name.
727        struct UnreachableResolver;
728
729        #[async_trait::async_trait]
730        impl Resolver for UnreachableResolver {
731            async fn reverse(&self, _ip: std::net::IpAddr) -> Result<Vec<String>, String> {
732                unreachable!()
733            }
734            async fn forward(&self, _name: &str) -> Result<Vec<std::net::IpAddr>, String> {
735                unreachable!("a literal 127.0.0.1 must short-circuit before this is called")
736            }
737            async fn txt(&self, _name: &str) -> Result<Vec<String>, String> {
738                unreachable!()
739            }
740        }
741
742        /// Answers every name with loopback, for the cases that need a real
743        /// hostname on the wire.
744        struct LoopbackResolver;
745
746        #[async_trait::async_trait]
747        impl Resolver for LoopbackResolver {
748            async fn reverse(&self, _ip: std::net::IpAddr) -> Result<Vec<String>, String> {
749                unreachable!()
750            }
751            async fn forward(&self, _name: &str) -> Result<Vec<std::net::IpAddr>, String> {
752                Ok(vec![std::net::IpAddr::from([127, 0, 0, 1])])
753            }
754            async fn txt(&self, _name: &str) -> Result<Vec<String>, String> {
755                unreachable!()
756            }
757        }
758
759        fn through(proxy: &FakeProxy) -> OutboundProxies {
760            OutboundProxies::always(ProxyTarget::for_test(&proxy.url()))
761        }
762
763        fn tunnelling(port: u16) -> ProxyBehaviour {
764            ProxyBehaviour::Tunnel {
765                status: "HTTP/1.1 200 Connection established\r\n",
766                force_port: Some(port),
767            }
768        }
769
770        /// A plain TCP server that answers one canned response.
771        async fn origin(response: &'static str) -> u16 {
772            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
773            let port = listener.local_addr().unwrap().port();
774            tokio::spawn(async move {
775                let (mut stream, _) = listener.accept().await.unwrap();
776                let mut buffer = vec![0u8; 1024];
777                let _ = stream.read(&mut buffer).await;
778                let _ = stream.write_all(response.as_bytes()).await;
779                let _ = stream.shutdown().await;
780            });
781            port
782        }
783
784        /// The `tls_alpn_01` shape: a raw byte stream carried end to end.
785        #[tokio::test]
786        async fn connect_stream_tunnels_to_the_origin() {
787            let port = origin("pong").await;
788            let proxy = FakeProxy::start(tunnelling(port)).await;
789
790            // A name, not `127.0.0.1`: `select` bypasses loopback
791            // unconditionally, so a literal would take the direct path and this
792            // would pass without the proxy being involved at all.
793            let mut stream = connect_stream(
794                &LoopbackResolver,
795                &through(&proxy),
796                &Endpoint::tls("origin.example", 443),
797            )
798            .await
799            .expect("the tunnel must open");
800
801            stream.write_all(b"ping").await.unwrap();
802            let mut answer = String::new();
803            stream.read_to_string(&mut answer).await.unwrap();
804            assert_eq!(answer, "pong");
805
806            assert_eq!(proxy.connections(), 1);
807            let request = proxy.requests().remove(0);
808            assert!(
809                request.starts_with("CONNECT origin.example:443 HTTP/1.1"),
810                "{request}"
811            );
812            // Never on a CONNECT: a proxy honouring it would close the tunnel
813            // that was just asked for.
814            assert!(
815                !request.to_lowercase().contains("connection: close"),
816                "{request}"
817            );
818            assert!(
819                !request.to_lowercase().contains("proxy-connection"),
820                "{request}"
821            );
822        }
823
824        /// What squid actually answers: an older version and an extra header.
825        /// The framing has to survive both.
826        #[tokio::test]
827        async fn a_squid_shaped_reply_still_opens_the_tunnel() {
828            let port = origin("pong").await;
829            let proxy = FakeProxy::start(ProxyBehaviour::Tunnel {
830                status: "HTTP/1.0 200 Connection established\r\nProxy-Agent: squid/6.10\r\n",
831                force_port: Some(port),
832            })
833            .await;
834
835            let mut stream = connect_stream(
836                &LoopbackResolver,
837                &through(&proxy),
838                &Endpoint::tls("origin.example", 443),
839            )
840            .await
841            .expect("a 1.0 reply is still a tunnel");
842            stream.write_all(b"ping").await.unwrap();
843            let mut answer = String::new();
844            stream.read_to_string(&mut answer).await.unwrap();
845            assert_eq!(answer, "pong");
846        }
847
848        /// A cleartext target is forwarded rather than tunnelled: the request
849        /// line carries the whole URL and the credential rides on the request.
850        #[tokio::test]
851        async fn a_cleartext_target_is_forwarded_with_its_credentials() {
852            let proxy = FakeProxy::start(ProxyBehaviour::Forward(
853                "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok",
854            ))
855            .await;
856            let proxies = OutboundProxies::always(ProxyTarget::for_test(&format!(
857                "http://user:pass@127.0.0.1:{}",
858                proxy.port
859            )));
860
861            let target = Url::parse("http://origin.example/a?b=c").unwrap();
862            let endpoint = Endpoint::from_url(&target).unwrap();
863            let mut connection = connect::<Empty<Bytes>>(
864                &UnreachableResolver,
865                &proxies,
866                &endpoint,
867                &Arc::new(webpki_tls_config()),
868            )
869            .await
870            .expect("the proxy is the peer, so the origin need not exist");
871
872            let request = hyper::Request::builder()
873                .uri(connection.request_target(&target))
874                .header(hyper::header::HOST, endpoint.authority())
875                .body(Empty::<Bytes>::new())
876                .unwrap();
877            assert_eq!(
878                connection.send_request(request).await.unwrap().status(),
879                200
880            );
881
882            let seen = proxy.requests().remove(0);
883            assert!(
884                seen.starts_with("GET http://origin.example/a?b=c HTTP/1.1"),
885                "{seen}"
886            );
887            assert!(
888                seen.to_lowercase()
889                    .contains("proxy-authorization: basic dxnlcjpwyxnz"),
890                "{seen}"
891            );
892        }
893
894        /// The https path end to end: the tunnel carries a TLS session whose
895        /// SNI is the *origin's* name, and the credential spent on the CONNECT
896        /// is not repeated inside — where the origin would read it.
897        #[tokio::test]
898        async fn https_is_tunnelled_with_the_origin_s_own_sni() {
899            use rustls::server::{ClientHello, ResolvesServerCert};
900            use rustls::sign::CertifiedKey;
901            use std::sync::Mutex;
902
903            #[derive(Debug)]
904            struct RecordingCert {
905                key: Arc<CertifiedKey>,
906                names: Arc<Mutex<Vec<String>>>,
907            }
908
909            impl ResolvesServerCert for RecordingCert {
910                fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
911                    self.names
912                        .lock()
913                        .unwrap()
914                        .push(hello.server_name().unwrap_or_default().to_string());
915                    Some(self.key.clone())
916                }
917            }
918
919            // A self-signed certificate for the name the client will ask for;
920            // the client validates nothing (this is the transport under test,
921            // not a trust decision), so its contents do not matter.
922            let key_pair = rcgen::KeyPair::generate().unwrap();
923            let certificate = rcgen::CertificateParams::new(vec!["origin.example".to_string()])
924                .unwrap()
925                .self_signed(&key_pair)
926                .unwrap();
927            let provider = rustls::crypto::ring::default_provider();
928            let signing_key = provider
929                .key_provider
930                .load_private_key(
931                    rustls_pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der()).into(),
932                )
933                .unwrap();
934            let names = Arc::new(Mutex::new(Vec::new()));
935            let resolver = RecordingCert {
936                key: Arc::new(CertifiedKey::new(
937                    vec![certificate.der().clone()],
938                    signing_key,
939                )),
940                names: names.clone(),
941            };
942            let server_config = rustls::ServerConfig::builder_with_provider(Arc::new(provider))
943                .with_safe_default_protocol_versions()
944                .unwrap()
945                .with_no_client_auth()
946                .with_cert_resolver(Arc::new(resolver));
947
948            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
949            let origin_port = listener.local_addr().unwrap().port();
950            let seen_inside = Arc::new(Mutex::new(String::new()));
951            let recorder = seen_inside.clone();
952            tokio::spawn(async move {
953                let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config));
954                let (stream, _) = listener.accept().await.unwrap();
955                let mut stream = acceptor.accept(stream).await.unwrap();
956                let mut buffer = vec![0u8; 2048];
957                let read = stream.read(&mut buffer).await.unwrap();
958                *recorder.lock().unwrap() = String::from_utf8_lossy(&buffer[..read]).into_owned();
959                let _ = stream
960                    .write_all(b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
961                    .await;
962                let _ = stream.shutdown().await;
963            });
964
965            // `force_port` so the CONNECT target can be a real name — which is
966            // what makes the SNI assertion mean anything — without that name
967            // having to resolve for the *proxy*.
968            let proxy = FakeProxy::start(ProxyBehaviour::Tunnel {
969                status: "HTTP/1.1 200 Connection established\r\n",
970                force_port: Some(origin_port),
971            })
972            .await;
973            let proxies = OutboundProxies::always(ProxyTarget::for_test(&format!(
974                "http://user:pass@127.0.0.1:{}",
975                proxy.port
976            )));
977
978            let target = Url::parse("https://origin.example/x").unwrap();
979            let endpoint = Endpoint::from_url(&target).unwrap();
980            let mut connection = connect::<Empty<Bytes>>(
981                &LoopbackResolver,
982                &proxies,
983                &endpoint,
984                &crate::challenge::tls_alpn_01::accept_any_client_config(&[]).unwrap(),
985            )
986            .await
987            .expect("the tunnel must carry the TLS session");
988
989            let request = hyper::Request::builder()
990                .uri(connection.request_target(&target))
991                .header(hyper::header::HOST, endpoint.authority())
992                .body(Empty::<Bytes>::new())
993                .unwrap();
994            assert_eq!(
995                connection.send_request(request).await.unwrap().status(),
996                204
997            );
998
999            let connect_request = proxy.requests().remove(0);
1000            assert!(
1001                connect_request.starts_with("CONNECT origin.example:443 HTTP/1.1"),
1002                "{connect_request}"
1003            );
1004            assert!(
1005                connect_request
1006                    .to_lowercase()
1007                    .contains("proxy-authorization"),
1008                "{connect_request}"
1009            );
1010
1011            assert_eq!(names.lock().unwrap().as_slice(), ["origin.example"]);
1012
1013            let inside = seen_inside.lock().unwrap().clone();
1014            // Origin-form inside the tunnel: the peer is the origin server.
1015            assert!(inside.starts_with("GET /x HTTP/1.1"), "{inside}");
1016            // …and the proxy credential stops at the proxy.
1017            assert!(
1018                !inside.to_lowercase().contains("proxy-authorization"),
1019                "{inside}"
1020            );
1021        }
1022
1023        /// "407 Proxy Authentication Required" is the whole diagnosis, and an
1024        /// operator never sees the proxy's own log.
1025        #[tokio::test]
1026        async fn a_refused_connect_reports_the_status_and_the_body() {
1027            let proxy = FakeProxy::start(ProxyBehaviour::Refuse(
1028                "HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 20\r\n\r\n\
1029                 credentials required",
1030            ))
1031            .await;
1032            let proxies = OutboundProxies::always(ProxyTarget::for_test(&format!(
1033                "http://user:hunter2@127.0.0.1:{}",
1034                proxy.port
1035            )));
1036
1037            let Err(error) = connect_stream(
1038                &LoopbackResolver,
1039                &proxies,
1040                &Endpoint::tls("origin.example", 443),
1041            )
1042            .await
1043            else {
1044                panic!("a 407 is not a tunnel");
1045            };
1046
1047            assert!(error.contains("407"), "{error}");
1048            assert!(error.contains("credentials required"), "{error}");
1049            // …and the password stays out of the message an operator pastes
1050            // into a ticket.
1051            assert!(!error.contains("hunter2"), "{error}");
1052        }
1053
1054        /// A dead proxy must name the proxy: "connection refused" against the
1055        /// origin's address would send an operator to the wrong host.
1056        #[tokio::test]
1057        async fn an_unreachable_proxy_names_the_proxy() {
1058            // Bound then dropped, so the port is free but was recently valid.
1059            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1060            let port = listener.local_addr().unwrap().port();
1061            drop(listener);
1062
1063            let proxies =
1064                OutboundProxies::always(ProxyTarget::for_test(&format!("http://127.0.0.1:{port}")));
1065            let Err(error) = connect_stream(
1066                &LoopbackResolver,
1067                &proxies,
1068                &Endpoint::tls("origin.example", 443),
1069            )
1070            .await
1071            else {
1072                panic!("a dead proxy is not a tunnel");
1073            };
1074
1075            assert!(error.contains("proxy"), "{error}");
1076            assert!(error.contains(&port.to_string()), "{error}");
1077            assert!(!error.contains("origin.example"), "{error}");
1078        }
1079
1080        /// A `no_proxy` hit really leaves the proxy untouched — which only a
1081        /// connection counter can prove, since a successful response looks
1082        /// exactly the same either way.
1083        #[tokio::test]
1084        async fn a_bypassed_target_never_reaches_the_proxy() {
1085            let port = origin("pong").await;
1086            let proxy = FakeProxy::start(tunnelling(port)).await;
1087            let proxies = through(&proxy).with_bypass(&["bypassed.example"]).unwrap();
1088
1089            let mut stream = connect_stream(
1090                &LoopbackResolver,
1091                &proxies,
1092                &Endpoint::tls("bypassed.example", port),
1093            )
1094            .await
1095            .expect("a bypassed target still connects, just directly");
1096            stream.write_all(b"ping").await.unwrap();
1097            let mut answer = String::new();
1098            stream.read_to_string(&mut answer).await.unwrap();
1099            assert_eq!(answer, "pong");
1100
1101            assert_eq!(proxy.connections(), 0, "the proxy must not be dialled");
1102        }
1103    }
1104}