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