Skip to main content

rust_web_server/proxy/
status.rs

1//! `Proxy-Status` (RFC 9209, 18.17.0): on every proxied response, this
2//! hop's name and either the status it received (`received-status=200`) or
3//! why there was no response (`error=connection_refused`).
4//!
5//! `[server] proxy_status = "always"` (default), `"errors"` or `"off"`;
6//! `proxy_name` (default `rws`) names this hop; `proxy_status_next_hop =
7//! true` adds the backend (`next-hop`) and the failure's text (`details`),
8//! which reveal internal addresses, so they're off by default.
9
10use crate::header::Header;
11use crate::response::Response;
12
13/// RFC 9209 §2.3 error types rws reports.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ProxyError {
16    DnsError,
17    DestinationUnavailable,
18    DestinationIpUnroutable,
19    ConnectionRefused,
20    ConnectionTerminated,
21    ConnectionTimeout,
22    TlsProtocolError,
23    TlsCertificateError,
24    HttpResponseTimeout,
25    HttpResponseIncomplete,
26    HttpProtocolError,
27    ProxyConfigurationError,
28    ProxyInternalError,
29}
30
31impl ProxyError {
32    /// The RFC 9209 name.
33    pub fn as_str(self) -> &'static str {
34        match self {
35            ProxyError::DnsError => "dns_error",
36            ProxyError::DestinationUnavailable => "destination_unavailable",
37            ProxyError::DestinationIpUnroutable => "destination_ip_unroutable",
38            ProxyError::ConnectionRefused => "connection_refused",
39            ProxyError::ConnectionTerminated => "connection_terminated",
40            ProxyError::ConnectionTimeout => "connection_timeout",
41            ProxyError::TlsProtocolError => "tls_protocol_error",
42            ProxyError::TlsCertificateError => "tls_certificate_error",
43            ProxyError::HttpResponseTimeout => "http_response_timeout",
44            ProxyError::HttpResponseIncomplete => "http_response_incomplete",
45            ProxyError::HttpProtocolError => "http_protocol_error",
46            ProxyError::ProxyConfigurationError => "proxy_configuration_error",
47            ProxyError::ProxyInternalError => "proxy_internal_error",
48        }
49    }
50
51    /// The error type of a forwarder's failure text.
52    pub fn classify(failure: &str) -> ProxyError {
53        let f = failure.to_ascii_lowercase();
54        let has = |s: &str| f.contains(s);
55        if has("dns lookup") || has("no address resolved") || has("failed to lookup address") {
56            ProxyError::DnsError
57        } else if has("invalid upstream") || has("invalid tls server name") || has("cannot read") || has("cannot parse") || has("need the http2") {
58            ProxyError::ProxyConfigurationError
59        } else if has("tls") || has("handshake") || has("certificate") {
60            if has("certificate") || has("unknownissuer") || has("notvalidforname") || has("expired") {
61                ProxyError::TlsCertificateError
62            } else if has("timed out") {
63                ProxyError::ConnectionTimeout
64            } else {
65                ProxyError::TlsProtocolError
66            }
67        } else if has("connect to") || has("connect(") {
68            if has("timed out") {
69                ProxyError::ConnectionTimeout
70            } else if has("unreachable") {
71                ProxyError::DestinationIpUnroutable
72            } else {
73                ProxyError::ConnectionRefused
74            }
75        } else if has("mid-body") || has("premature eof") {
76            ProxyError::HttpResponseIncomplete
77        } else if has("timed out") || has("would block") || has("temporarily unavailable") {
78            ProxyError::HttpResponseTimeout
79        } else if has("closed connection") || has("reset") || has("broken pipe") || has("write to backend") || has("aborted") {
80            ProxyError::ConnectionTerminated
81        } else if has("interim") || has("parse") || has("invalid") || has("malformed") || has("chunk") || has("status line") {
82            ProxyError::HttpProtocolError
83        } else {
84            ProxyError::ProxyInternalError
85        }
86    }
87}
88
89/// When to send the header.
90fn mode() -> (bool, bool) {
91    let config = crate::server_config::ServerConfig::current();
92    match config.proxy_status.as_str() {
93        "off" => (false, false),
94        "errors" => (false, true),
95        _ => (true, true),
96    }
97}
98
99/// A successful hop: `received-status`.
100pub fn received(response: &mut Response, next_hop: Option<&str>) {
101    if mode().0 {
102        let status = response.status_code;
103        add(response, &format!("received-status={status}"), next_hop, None);
104    }
105}
106
107/// A hop that got no usable response: `error=…`.
108pub fn failed(response: &mut Response, error: ProxyError, next_hop: Option<&str>, details: Option<&str>) {
109    if mode().1 {
110        add(response, &format!("error={}", error.as_str()), next_hop, details);
111    }
112}
113
114/// A failure from its text (what the forwarders return).
115pub fn failed_with(response: &mut Response, failure: &str, next_hop: Option<&str>) {
116    failed(response, ProxyError::classify(failure), next_hop, Some(failure));
117}
118
119fn add(response: &mut Response, parameter: &str, next_hop: Option<&str>, details: Option<&str>) {
120    let config = crate::server_config::ServerConfig::current();
121    let mut member = format!("{}; {parameter}", crate::header::sf::item(&config.proxy_name));
122    if config.proxy_status_next_hop {
123        if let Some(hop) = next_hop.filter(|h| !h.is_empty()) {
124            member.push_str(&format!("; next-hop={}", crate::header::sf::string(hop)));
125        }
126        if let Some(details) = details.filter(|d| !d.is_empty()) {
127            member.push_str(&format!("; details={}", crate::header::sf::string(details)));
128        }
129    }
130    // RFC 9209 §2: the last member is the hop closest to the client.
131    match response.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("proxy-status")) {
132        Some(existing) => existing.value = format!("{}, {member}", existing.value).into(),
133        None => response.headers.push(Header::new("Proxy-Status", member)),
134    }
135}
136
137#[cfg(test)]
138mod tests;