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