| @@ -3,14 +3,12 @@ use std::{ · 2 unmodified lines |
| 3 | 3 | convert::TryFrom, |
| 4 | 4 | io::{Read, Write}, |
| 5 | 5 | str::FromStr, |
| 6 | + sync::{atomic, Arc}, |
| 6 | 7 | }; |
| 7 | 8 | |
| 8 | 9 | use gix_features::io::pipe; |
| 9 | 10 | |
| 10 | | −use crate::client::{ |
| 11 | | − http, |
| 12 | | − http::{reqwest::Remote, traits::PostBodyDataKind}, |
| 13 | | −}; |
| 11 | +use crate::client::http::{self, options::FollowRedirects, redirect, reqwest::Remote, traits::PostBodyDataKind}; |
| 14 | 12 | |
| 15 | 13 | /// The error returned by the 'remote' helper, a purely internal construct to perform http requests. |
| 16 | 14 | #[derive(Debug, thiserror::Error)] |
| @@ -22,6 +20,8 @@ pub enum Error { · 5 unmodified lines |
| 22 | 20 | ReadPostBody(#[from] std::io::Error), |
| 23 | 21 | #[error("Request configuration failed")] |
| 24 | 22 | ConfigureRequest(#[from] Box<dyn std::error::Error + Send + Sync + 'static>), |
| 23 | + #[error(transparent)] |
| 24 | + Redirect(#[from] redirect::Error), |
| 25 | 25 | } |
| 26 | 26 | |
| 27 | 27 | impl crate::IsSpuriousError for Error { |
| @@ -40,23 +40,57 @@ impl Default for Remote { · 12 unmodified lines |
| 40 | 40 | let (req_send, req_recv) = std::sync::mpsc::sync_channel(0); |
| 41 | 41 | let (res_send, res_recv) = std::sync::mpsc::sync_channel(0); |
| 42 | 42 | let handle = std::thread::spawn(move || -> Result<(), Error> { |
| 43 | + let mut follow = None; |
| 44 | + let mut redirected_base_url = None::<String>; |
| 45 | + let allow_redirects = Arc::new(atomic::AtomicBool::new(false)); |
| 46 | + |
| 43 | 47 | // We may error while configuring, which is expected as part of the internal protocol. The error will be |
| 44 | 48 | // received and the sender of the request might restart us. |
| 45 | 49 | let client = reqwest::blocking::ClientBuilder::new() |
| 46 | 50 | .connect_timeout(std::time::Duration::from_secs(20)) |
| 47 | 51 | .http1_title_case_headers() |
| 52 | + .redirect(reqwest::redirect::Policy::custom({ |
| 53 | + let allow_redirects = allow_redirects.clone(); |
| 54 | + move |attempt| { |
| 55 | + if allow_redirects.load(atomic::Ordering::Relaxed) { |
| 56 | + let curr_url = attempt.url(); |
| 57 | + let prev_urls = attempt.previous(); |
| 58 | + |
| 59 | + match prev_urls.first() { |
| 60 | + Some(prev_url) if prev_url.host_str() != curr_url.host_str() => { |
| 61 | + // git does not want to be redirected to a different host. |
| 62 | + attempt.stop() |
| 63 | + } |
| 64 | + _ => { |
| 65 | + // emulate default git behaviour which relies on curl default behaviour apparently. |
| 66 | + const CURL_DEFAULT_REDIRS: usize = 50; |
| 67 | + if prev_urls.len() >= CURL_DEFAULT_REDIRS { |
| 68 | + attempt.error("too many redirects") |
| 69 | + } else { |
| 70 | + attempt.follow() |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + } else { |
| 75 | + attempt.stop() |
| 76 | + } |
| 77 | + } |
| 78 | + })) |
| 48 | 79 | .build()?; |
| 80 | + |
| 49 | 81 | for Request { |
| 50 | 82 | url, |
| 83 | + base_url, |
| 51 | 84 | headers, |
| 52 | 85 | upload_body_kind, |
| 53 | 86 | config, |
| 54 | 87 | } in req_recv |
| 55 | 88 | { |
| 89 | + let effective_url = redirect::swap_tails(redirected_base_url.as_deref(), &base_url, url.clone()); |
| 56 | 90 | let mut req_builder = if upload_body_kind.is_some() { |
| 57 | | − client.post(url) |
| 91 | + client.post(&effective_url) |
| 58 | 92 | } else { |
| 59 | | − client.get(url) |
| 93 | + client.get(&effective_url) |
| 60 | 94 | } |
| 61 | 95 | .headers(headers); |
| 62 | 96 | let (post_body_tx, mut post_body_rx) = pipe::unidirectional(0); |
| @@ -91,6 +125,17 @@ impl Default for Remote { · 28 unmodified lines |
| 91 | 125 | } |
| 92 | 126 | } |
| 93 | 127 | } |
| 128 | + |
| 129 | + let follow = follow.get_or_insert(config.follow_redirects); |
| 130 | + allow_redirects.store( |
| 131 | + matches!(follow, FollowRedirects::Initial | FollowRedirects::All), |
| 132 | + atomic::Ordering::Relaxed, |
| 133 | + ); |
| 134 | + |
| 135 | + if *follow == FollowRedirects::Initial { |
| 136 | + *follow = FollowRedirects::None; |
| 137 | + } |
| 138 | + |
| 94 | 139 | let mut res = match client |
| 95 | 140 | .execute(req) |
| 96 | 141 | .and_then(reqwest::blocking::Response::error_for_status) |
| @@ -116,6 +161,11 @@ impl Default for Remote { · 19 unmodified lines |
| 116 | 161 | } |
| 117 | 162 | }; |
| 118 | 163 | |
| 164 | + let actual_url = res.url().as_str(); |
| 165 | + if actual_url != effective_url.as_str() { |
| 166 | + redirected_base_url = redirect::base_url(actual_url, &base_url, url)?.into(); |
| 167 | + } |
| 168 | + |
| 119 | 169 | let send_headers = { |
| 120 | 170 | let headers = res.headers(); |
| 121 | 171 | move || -> std::io::Result<()> { |
| @@ -172,7 +222,7 @@ impl Remote { · 50 unmodified lines |
| 172 | 222 | fn make_request( |
| 173 | 223 | &mut self, |
| 174 | 224 | url: &str, |
| 175 | | − _base_url: &str, |
| 225 | + base_url: &str, |
| 176 | 226 | headers: impl IntoIterator<Item = impl AsRef<str>>, |
| 177 | 227 | upload_body_kind: Option<PostBodyDataKind>, |
| 178 | 228 | ) -> Result<http::PostResponse<pipe::Reader, pipe::Reader, pipe::Writer>, http::Error> { |
| @@ -197,6 +247,7 @@ impl Remote { · 18 unmodified lines |
| 197 | 247 | .request |
| 198 | 248 | .send(Request { |
| 199 | 249 | url: url.to_owned(), |
| 250 | + base_url: base_url.to_owned(), |
| 200 | 251 | headers: header_map, |
| 201 | 252 | upload_body_kind, |
| 202 | 253 | config: self.config.clone(), |
| @@ -259,6 +310,7 @@ impl http::Http for Remote { · 56 unmodified lines |
| 259 | 310 | |
| 260 | 311 | pub(crate) struct Request { |
| 261 | 312 | pub url: String, |
| 313 | + pub base_url: String, |
| 262 | 314 | pub headers: reqwest::header::HeaderMap, |
| 263 | 315 | pub upload_body_kind: Option<PostBodyDataKind>, |
| 264 | 316 | pub config: http::Options, |