Skip to main content

leyline/core/
response.rs

1use std::fmt;
2use std::sync::{Arc, OnceLock};
3
4use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
5use url::Url;
6
7use crate::core::body_stream::BodyStream;
8use crate::core::error::{Error, Kind};
9use crate::trace::masked;
10use crate::util::redact;
11
12mod body;
13mod download;
14mod link;
15mod relay;
16
17pub use link::Link;
18#[cfg(feature = "bench-internals")]
19pub(crate) use link::parse_links;
20pub use relay::{RelayBody, relay_headers};
21
22pub(crate) enum ResponseBody {
23    Buffered(Vec<u8>),
24    Streaming(BodyStream),
25    Taken,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum HttpVersion {
31    Http1_1,
32    Http2,
33    Http3,
34}
35
36impl HttpVersion {
37    pub(crate) fn ja4h_token(self) -> &'static str {
38        match self {
39            Self::Http1_1 => "1",
40            Self::Http2 => "2",
41            Self::Http3 => "3",
42        }
43    }
44
45    pub fn as_str(self) -> &'static str {
46        match self {
47            Self::Http1_1 => "HTTP/1.1",
48            Self::Http2 => "HTTP/2",
49            Self::Http3 => "HTTP/3",
50        }
51    }
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
55#[non_exhaustive]
56pub struct ResponseTiming {
57    pub reused: bool,
58    pub connect_ms: Option<u32>,
59    pub send_ms: u32,
60    pub total_ms: u32,
61}
62
63impl ResponseTiming {
64    pub(crate) fn accumulator() -> Self {
65        Self {
66            reused: true,
67            connect_ms: None,
68            send_ms: 0,
69            total_ms: 0,
70        }
71    }
72
73    pub(crate) fn leg(started: std::time::Instant, connect_ms: Option<u32>) -> Self {
74        let total_ms = Self::millis(started);
75        Self {
76            reused: connect_ms.is_none(),
77            connect_ms,
78            send_ms: total_ms.saturating_sub(connect_ms.unwrap_or(0)),
79            total_ms,
80        }
81    }
82
83    pub(crate) fn millis(start: std::time::Instant) -> u32 {
84        u32::try_from(start.elapsed().as_millis()).unwrap_or(u32::MAX)
85    }
86
87    pub(crate) fn add_leg(&mut self, leg: &ResponseTiming) {
88        self.total_ms = self.total_ms.saturating_add(leg.total_ms);
89        self.send_ms = self.send_ms.saturating_add(leg.send_ms);
90        if let Some(c) = leg.connect_ms {
91            self.connect_ms = Some(self.connect_ms.unwrap_or(0).saturating_add(c));
92        }
93        self.reused &= leg.reused;
94    }
95}
96
97pub struct Response {
98    pub(crate) status: StatusCode,
99    pub(crate) version: HttpVersion,
100    pub(crate) headers: HeaderMap,
101    pub(crate) trailers: Vec<(HeaderName, HeaderValue)>,
102    pub(crate) body: ResponseBody,
103    pub(crate) url: Url,
104    pub(crate) redirect_chain: Vec<Url>,
105    pub(crate) request_headers: Vec<(String, String)>,
106    pub(crate) tls: Option<crate::pool::TlsInfo>,
107    pub(crate) request_method: String,
108    pub(crate) timing: ResponseTiming,
109    pub(crate) audit_tls: Option<Arc<crate::audit::AuditTlsCache>>,
110    pub(crate) audit_cache: OnceLock<crate::audit::AuditData>,
111    pub(crate) compression: crate::core::CompressionConfig,
112    pub(crate) attempts: u32,
113    pub(crate) proxy: Option<String>,
114}
115
116impl Response {
117    pub fn status(&self) -> StatusCode {
118        self.status
119    }
120
121    pub fn version(&self) -> HttpVersion {
122        self.version
123    }
124
125    pub fn timing(&self) -> &ResponseTiming {
126        &self.timing
127    }
128
129    pub fn attempts(&self) -> u32 {
130        self.attempts
131    }
132
133    pub(crate) fn set_attempts(&mut self, attempts: u32) {
134        self.attempts = attempts.max(1);
135    }
136
137    pub fn proxy(&self) -> Option<&str> {
138        self.proxy.as_deref()
139    }
140
141    pub(crate) fn set_proxy(&mut self, proxy: Option<&str>) {
142        self.proxy = proxy.map(redact);
143    }
144
145    pub fn block(&self) -> Option<crate::core::block::BlockSignal> {
146        crate::core::block::BlockRules::builtin().check(self)
147    }
148
149    pub fn url(&self) -> &Url {
150        &self.url
151    }
152
153    pub fn redirect_chain(&self) -> &[Url] {
154        &self.redirect_chain
155    }
156
157    pub fn headers(&self) -> &HeaderMap {
158        &self.headers
159    }
160
161    pub fn trailers(&self) -> impl Iterator<Item = (&HeaderName, &HeaderValue)> {
162        self.trailers.iter().map(|(k, v)| (k, v))
163    }
164
165    pub fn cookies(&self) -> impl Iterator<Item = crate::cookie::Cookie> + '_ {
166        let url = &self.url;
167        self.headers
168            .get_all(http::header::SET_COOKIE)
169            .iter()
170            .filter_map(move |value| {
171                crate::cookie::parse::parse_set_cookie(value.to_str().ok()?, url)
172            })
173    }
174
175    pub fn tls(&self) -> Option<&crate::pool::TlsInfo> {
176        self.tls.as_ref()
177    }
178
179    pub fn request_headers(&self) -> impl Iterator<Item = (&str, &str)> {
180        self.request_headers
181            .iter()
182            .map(|(k, v)| (k.as_str(), v.as_str()))
183    }
184
185    pub fn content_length(&self) -> Option<u64> {
186        self.header("content-length").and_then(|v| v.parse().ok())
187    }
188
189    pub fn error_for_status(self) -> crate::core::Result<Self> {
190        match self.status_error() {
191            Some(err) => Err(err),
192            None => Ok(self),
193        }
194    }
195
196    pub fn error_for_status_ref(&self) -> crate::core::Result<&Self> {
197        match self.status_error() {
198            Some(err) => Err(err),
199            None => Ok(self),
200        }
201    }
202
203    fn status_error(&self) -> Option<Error> {
204        if !is_error_status(self.status.as_u16()) {
205            return None;
206        }
207        Some(
208            Error::new(Kind::Status)
209                .with_status(self.status)
210                .with_url(self.url.clone())
211                .with_headers(self.headers.clone())
212                .with_trail(self.attempts, self.proxy.clone()),
213        )
214    }
215
216    pub fn audit(&self) -> Option<&crate::audit::AuditData> {
217        let tls = self.audit_tls.as_ref()?;
218        Some(self.audit_cache.get_or_init(|| {
219            let ja4h = crate::audit::compute_ja4h(&crate::audit::Ja4hInput {
220                method: &self.request_method,
221                http_version: self.version.ja4h_token(),
222                headers: &self.request_headers,
223            });
224            crate::audit::AuditData {
225                ja4: tls.ja4.clone(),
226                ja3: tls.ja3.clone(),
227                h2_fingerprint: tls.h2_fingerprint.clone(),
228                ja4t: tls.ja4t.clone(),
229                ja4h,
230                permutes_extensions: tls.permutes_extensions,
231                request_headers: self.request_headers.clone(),
232            }
233        }))
234    }
235
236    pub fn header(&self, name: &str) -> Option<&str> {
237        self.headers.get(name).and_then(|v| v.to_str().ok())
238    }
239}
240
241impl fmt::Debug for Response {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        let chain: Vec<String> = self
244            .redirect_chain
245            .iter()
246            .map(|url| redact(url.as_str()))
247            .collect();
248        f.debug_struct("Response")
249            .field("status", &self.status)
250            .field("version", &self.version)
251            .field("url", &redact(self.url.as_str()))
252            .field("redirect_chain", &chain)
253            .field(
254                "headers",
255                &masked(self.headers.iter().map(|(k, v)| (k.as_str(), v.as_bytes()))),
256            )
257            .field(
258                "trailers",
259                &masked(
260                    self.trailers
261                        .iter()
262                        .map(|(k, v)| (k.as_str(), v.as_bytes())),
263                ),
264            )
265            .field(
266                "request_headers",
267                &masked(
268                    self.request_headers
269                        .iter()
270                        .map(|(k, v)| (k.as_str(), v.as_bytes())),
271                ),
272            )
273            .field("timing", &self.timing)
274            .field("attempts", &self.attempts)
275            .field("proxy", &self.proxy)
276            .finish_non_exhaustive()
277    }
278}
279
280#[cfg(test)]
281mod tests;
282
283pub(crate) fn is_error_status(status: u16) -> bool {
284    status >= 400
285}