Skip to main content

leyline/core/session/
execute.rs

1use url::Url;
2
3use crate::FetchSite;
4use crate::core::transport::{ErrorBudget, Prepared, ResponseMode};
5use std::sync::Arc;
6
7use crate::profile::Preset;
8
9use self::auth::{DigestLeg, unreplayable_body};
10use self::journey::{Journey, redirect_location};
11use super::Session;
12use crate::core::body::Body;
13use crate::core::config::TimeoutConfig;
14use crate::core::deadline::Deadline;
15use crate::core::digest::DigestAuth;
16use crate::core::error::{Error, Kind, Result};
17use crate::core::headers::HeaderList;
18use crate::core::response::{Response, ResponseBody};
19use crate::core::{ProxyConfig, RedirectPolicy};
20use crate::trace;
21use crate::util::{lock, redact, without_userinfo};
22
23mod auth;
24mod headers;
25mod journey;
26mod response;
27
28pub(crate) struct Attempt {
29    pub(crate) method: http::Method,
30    pub(crate) url: String,
31    pub(crate) preset: Option<Preset>,
32    pub(crate) body: Body,
33    pub(crate) headers: Option<HeaderList>,
34    pub(crate) deadline: Deadline,
35    pub(crate) response: ResponseMode,
36    pub(crate) proxy: Option<ProxyConfig>,
37    pub(crate) header_order: Option<Vec<String>>,
38    pub(crate) redirect: Option<RedirectPolicy>,
39    pub(crate) digest: Option<DigestAuth>,
40    pub(crate) initiator: Option<Url>,
41    pub(crate) trusted_origin: Option<Url>,
42}
43
44impl Attempt {
45    pub(crate) fn again(&self, body: Body, headers: Option<HeaderList>) -> Attempt {
46        Attempt {
47            method: self.method.clone(),
48            url: self.url.clone(),
49            preset: self.preset,
50            body,
51            headers,
52            deadline: self.deadline,
53            response: self.response,
54            proxy: self.proxy.clone(),
55            header_order: self.header_order.clone(),
56            redirect: self.redirect.clone(),
57            digest: self.digest.clone(),
58            initiator: self.initiator.clone(),
59            trusted_origin: self.trusted_origin.clone(),
60        }
61    }
62}
63
64impl Session {
65    pub(crate) fn deadline(&self, request: Option<&TimeoutConfig>) -> Deadline {
66        Deadline::new(&self.inner.timeouts, request)
67    }
68
69    pub(crate) fn error_budget(&self, deadline: &Deadline) -> ErrorBudget {
70        let compression = &self.inner.compression;
71        ErrorBudget::new(
72            deadline.error_body_wait(),
73            compression.max_error_body.min(compression.max_body_size),
74        )
75    }
76
77    pub(crate) async fn attempt(&self, attempt: Attempt) -> Result<Response> {
78        let deadline = attempt.deadline;
79        let inner: std::pin::Pin<
80            Box<dyn std::future::Future<Output = Result<Response>> + Send + '_>,
81        > = Box::pin(self.execute_inner(attempt));
82        trace::scope(self.inner.trace.as_ref(), async move {
83            let out = self.unless_shut_down(deadline.total(inner)).await;
84            trace::done(match &out {
85                Ok(_) => Ok(()),
86                Err(e) => Err(e),
87            });
88            out
89        })
90        .await
91    }
92
93    pub(crate) async fn unless_shut_down<T>(
94        &self,
95        fut: impl std::future::Future<Output = Result<T>>,
96    ) -> Result<T> {
97        tokio::select! {
98            biased;
99            () = self.inner.shutdown.cancelled() => Err(Error::shut_down()),
100            out = fut => out,
101        }
102    }
103
104    pub fn shutdown(&self) {
105        self.inner.shutdown.cancel();
106    }
107
108    pub fn is_shut_down(&self) -> bool {
109        self.inner.shutdown.is_cancelled()
110    }
111
112    pub(crate) async fn traced<F>(
113        &self,
114        method: &str,
115        original_url: &str,
116        streamed: bool,
117        request: F,
118    ) -> Result<Response>
119    where
120        F: std::future::Future<Output = (Result<Response>, u32)>,
121    {
122        let hook = self.inner.trace.as_ref();
123        trace::scope(hook, async move {
124            let (out, attempts) = request.await;
125            if hook.is_some() {
126                let finish = trace::Finish {
127                    method,
128                    original_url,
129                    attempts,
130                    streamed,
131                };
132                trace::summary(&finish, out.as_ref());
133            }
134            out
135        })
136        .await
137    }
138
139    #[tracing::instrument(
140        name = "session.execute",
141        level = "debug",
142        skip_all,
143        fields(http.method = attempt.method.as_str(), http.url = redact(&attempt.url))
144    )]
145    async fn execute_inner(&self, attempt: Attempt) -> Result<Response> {
146        let Attempt {
147            method,
148            url: raw_url,
149            preset,
150            body,
151            headers: extra_headers,
152            deadline,
153            response,
154            proxy: request_proxy,
155            header_order,
156            redirect,
157            digest,
158            initiator,
159            trusted_origin,
160        } = attempt;
161        let redirect_policy = redirect.as_ref().unwrap_or(&self.inner.redirect_policy);
162        let request_proxy = request_proxy.as_ref();
163        let header_order = header_order.as_deref();
164        let mut journey = Journey::begin(
165            self.hsts_upgrade(&self.resolve_url(&raw_url)?),
166            method.to_string(),
167            body,
168            extra_headers,
169            initiator,
170            self.session_referer(),
171        )
172        .trusting(trusted_origin.as_ref());
173
174        let mut digest = digest.map(DigestLeg::new);
175        let redirect_cap = redirect_policy.max_redirects_hint();
176        let mut pass = None;
177        let mut answered = false;
178        loop {
179            journey.url = self.hsts_upgrade(&journey.url);
180            drop(pass.take());
181            pass = self.admit_host(&journey.url).await;
182            let headers = self.leg_headers(&journey, preset, header_order);
183            journey.authorized = None;
184            let audit_headers = self.audit_copy(&headers);
185
186            let step_body = std::mem::take(&mut journey.body);
187            let replay_body = step_body.replay();
188
189            let proxy = self
190                .proxy_for(&journey.url, request_proxy)
191                .map_err(leg_error(answered))?;
192            let send = self.send_with_policy(Prepared {
193                method: &journey.method,
194                url: &journey.url,
195                headers,
196                body: step_body,
197                proxy,
198                response,
199            });
200            let leg = deadline
201                .response_header(send)
202                .await
203                .map_err(leg_error(answered))?;
204            answered |= leg.status != http::StatusCode::UNAUTHORIZED;
205            journey.timing.add_leg(&leg.timing);
206
207            self.store_cookies(&leg.headers, &journey.url);
208            self.note_hsts(&journey.url, &leg.headers);
209            #[cfg(feature = "http3")]
210            self.note_alt_svc(&journey.url, &leg.headers);
211
212            let next = Turn {
213                code: leg.status.as_u16(),
214                headers: &leg.headers,
215                redirect_policy,
216                redirect_cap,
217            };
218            if next.take(&mut journey, digest.as_mut(), replay_body)? {
219                drop(leg.body);
220                continue;
221            }
222
223            let mut response = self
224                .assemble_response(leg, journey, audit_headers, response, &deadline)
225                .await?;
226            if let ResponseBody::Streaming(body) = &mut response.body {
227                body.hold(pass.take());
228            }
229            return Ok(response);
230        }
231    }
232
233    fn session_referer(&self) -> Option<&str> {
234        self.inner
235            .default_headers
236            .iter()
237            .rfind(|(k, _)| k.eq_ignore_ascii_case("referer"))
238            .map(|(_, v)| v.as_str())
239            .filter(|v| !v.is_empty())
240    }
241
242    fn resolve_url(&self, raw_url: &str) -> Result<Arc<Url>> {
243        let mut cache = lock(&self.inner.url_cache);
244        match cache.as_mut() {
245            Some((raw, parsed)) if raw == raw_url => Ok(Arc::clone(parsed)),
246            _ => {
247                let parsed = Arc::new(Url::parse(raw_url).map_err(Error::from_url_parse)?);
248                *cache = Some((raw_url.to_string(), Arc::clone(&parsed)));
249                Ok(parsed)
250            }
251        }
252    }
253}
254
255pub(super) struct RequestContext<'a> {
256    pub(super) origin: &'a str,
257    pub(super) origin_downgrade: bool,
258    pub(super) referer: &'a str,
259    pub(super) initiated: bool,
260    pub(super) site: SiteContext<'a>,
261}
262
263pub(in crate::core::session) struct SiteContext<'a> {
264    pub(in crate::core::session) fetch_site: FetchSite,
265    pub(in crate::core::session) initiator: Option<&'a Url>,
266    pub(in crate::core::session) chain: &'a [Url],
267}
268
269fn fetch_site_for(initiator: Option<&Url>, chain: &[Url], current: &Url) -> FetchSite {
270    let Some(initiator) = initiator else {
271        return FetchSite::CrossSite;
272    };
273    FetchSite::across(initiator, chain.iter().chain(std::iter::once(current)))
274}
275
276fn leg_error(answered: bool) -> impl Fn(Error) -> Error {
277    move |err| if answered { err.after_response() } else { err }
278}
279
280pub(crate) fn url_origin(url: &Url) -> String {
281    let host = url.host_str().unwrap_or("");
282    match url.port() {
283        Some(port) => format!("{}://{}:{}", url.scheme(), host, port),
284        None => format!("{}://{}", url.scheme(), host),
285    }
286}
287
288pub(crate) fn referer_for(prev: Option<&str>, current_origin: &str) -> String {
289    let Some(prev) = prev else {
290        return format!("{current_origin}/");
291    };
292    let Ok(parsed) = Url::parse(prev) else {
293        return format!("{current_origin}/");
294    };
295    if parsed.scheme() == "https" && !current_origin.starts_with("https:") {
296        return String::new();
297    }
298    if url_origin(&parsed) != current_origin {
299        let origin = url_origin(&parsed);
300        return format!("{origin}/");
301    }
302    let mut parsed = without_userinfo(parsed);
303    parsed.set_fragment(None);
304    parsed.to_string()
305}
306
307#[cfg(all(test, feature = "http3"))]
308mod alt_svc_tests;
309#[cfg(test)]
310mod redact_tests;
311#[cfg(test)]
312mod referer_tests;
313#[cfg(test)]
314mod reorder_tests;
315
316struct Turn<'a> {
317    code: u16,
318    headers: &'a [(http::HeaderName, http::HeaderValue)],
319    redirect_policy: &'a RedirectPolicy,
320    redirect_cap: usize,
321}
322
323impl Turn<'_> {
324    fn take(
325        &self,
326        journey: &mut Journey,
327        digest: Option<&mut DigestLeg>,
328        replay_body: Option<Body>,
329    ) -> Result<bool> {
330        if let Some(location) = redirect_location(self.code, self.headers)
331            && let Some(hop) = journey.approved_hop(self.redirect_policy, self.code, &location)
332        {
333            journey.follow(hop, self.code, &location, replay_body)?;
334            if journey.chain.len() > self.redirect_cap {
335                return Err(Error::new(Kind::Redirect)
336                    .with_message(format!("too many redirects (max {})", self.redirect_cap)));
337            }
338            if let Some(digest) = digest {
339                journey.authorized = digest.next_hop(journey)?;
340            }
341            return Ok(true);
342        }
343        if self.code == 401
344            && let Some(digest) = digest
345            && let Some(authorized) = digest.answer(journey, self.headers)?
346        {
347            journey.body = replay_body.ok_or_else(unreplayable_body)?;
348            journey.authorized = Some(authorized);
349            return Ok(true);
350        }
351        Ok(false)
352    }
353}