Skip to content

Commit 8e158c3

Browse files
committed
feat!: interpret the FollowRedirects option for the curl HTTP backend.
This comes with changes to the `HTTP` trait which now requires a base-url to be provided as well.
1 parent 4ebe2ac commit 8e158c3

File tree

5 files changed

+153
-33
lines changed

5 files changed

+153
-33
lines changed

git-transport/src/client/blocking_io/http/curl/mod.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::{
2-
error::Error,
32
sync::mpsc::{Receiver, SyncSender},
43
thread,
54
};
@@ -10,10 +9,20 @@ use crate::client::blocking_io::http;
109

1110
mod remote;
1211

12+
#[derive(Debug, thiserror::Error)]
13+
pub enum Error {
14+
#[error(transparent)]
15+
Curl(#[from] curl::Error),
16+
#[error(transparent)]
17+
Redirect(#[from] http::redirect::Error),
18+
#[error(transparent)]
19+
Authenticate(#[from] git_credentials::protocol::Error),
20+
}
21+
1322
pub struct Curl {
1423
req: SyncSender<remote::Request>,
1524
res: Receiver<remote::Response>,
16-
handle: Option<thread::JoinHandle<Result<(), curl::Error>>>,
25+
handle: Option<thread::JoinHandle<Result<(), Error>>>,
1726
config: http::Options,
1827
}
1928

@@ -36,6 +45,7 @@ impl Curl {
3645
fn make_request(
3746
&mut self,
3847
url: &str,
48+
base_url: &str,
3949
headers: impl IntoIterator<Item = impl AsRef<str>>,
4050
upload: bool,
4151
) -> Result<http::PostResponse<io::pipe::Reader, io::pipe::Reader, io::pipe::Writer>, http::Error> {
@@ -47,6 +57,7 @@ impl Curl {
4757
.req
4858
.send(remote::Request {
4959
url: url.to_owned(),
60+
base_url: base_url.to_owned(),
5061
headers: list,
5162
upload,
5263
config: self.config.clone(),
@@ -92,20 +103,25 @@ impl http::Http for Curl {
92103
fn get(
93104
&mut self,
94105
url: &str,
106+
base_url: &str,
95107
headers: impl IntoIterator<Item = impl AsRef<str>>,
96108
) -> Result<http::GetResponse<Self::Headers, Self::ResponseBody>, http::Error> {
97-
self.make_request(url, headers, false).map(Into::into)
109+
self.make_request(url, base_url, headers, false).map(Into::into)
98110
}
99111

100112
fn post(
101113
&mut self,
102114
url: &str,
115+
base_url: &str,
103116
headers: impl IntoIterator<Item = impl AsRef<str>>,
104117
) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> {
105-
self.make_request(url, headers, true)
118+
self.make_request(url, base_url, headers, true)
106119
}
107120

108-
fn configure(&mut self, config: &dyn std::any::Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
121+
fn configure(
122+
&mut self,
123+
config: &dyn std::any::Any,
124+
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
109125
if let Some(config) = config.downcast_ref::<http::Options>() {
110126
self.config = config.clone();
111127
}

git-transport/src/client/blocking_io/http/curl/remote.rs

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use std::{
99
use curl::easy::{Auth, Easy2};
1010
use git_features::io::pipe;
1111

12-
use crate::client::blocking_io::http;
13-
use crate::client::http::options::ProxyAuthMethod;
12+
use crate::client::blocking_io::http::{self, curl::Error, redirect};
13+
use crate::client::http::options::{FollowRedirects, ProxyAuthMethod};
1414

1515
#[derive(Default)]
1616
struct Handler {
@@ -19,6 +19,7 @@ struct Handler {
1919
receive_body: Option<pipe::Reader>,
2020
checked_status: bool,
2121
last_status: usize,
22+
follow: FollowRedirects,
2223
}
2324

2425
impl Handler {
@@ -34,9 +35,13 @@ impl Handler {
3435
let code = std::str::from_utf8(code)?;
3536
code.parse().map_err(Into::into)
3637
}
37-
fn parse_status(data: &[u8]) -> Option<(usize, Box<dyn std::error::Error + Send + Sync>)> {
38+
fn parse_status(data: &[u8], follow: FollowRedirects) -> Option<(usize, Box<dyn std::error::Error + Send + Sync>)> {
39+
let valid_end = match follow {
40+
FollowRedirects::Initial | FollowRedirects::All => 308,
41+
FollowRedirects::None => 299,
42+
};
3843
match Self::parse_status_inner(data) {
39-
Ok(status) if !(200..=299).contains(&status) => {
44+
Ok(status) if !(200..=valid_end).contains(&status) => {
4045
Some((status, format!("Received HTTP status {}", status).into()))
4146
}
4247
Ok(_) => None,
@@ -68,7 +73,7 @@ impl curl::easy::Handler for Handler {
6873
} else {
6974
self.checked_status = true;
7075
self.last_status = 200;
71-
match Handler::parse_status(data) {
76+
match Handler::parse_status(data, self.follow) {
7277
None => true,
7378
Some((status, err)) => {
7479
self.last_status = status;
@@ -95,6 +100,7 @@ impl curl::easy::Handler for Handler {
95100

96101
pub struct Request {
97102
pub url: String,
103+
pub base_url: String,
98104
pub headers: curl::easy::List,
99105
pub upload: bool,
100106
pub config: http::Options,
@@ -107,23 +113,26 @@ pub struct Response {
107113
}
108114

109115
pub fn new() -> (
110-
thread::JoinHandle<Result<(), curl::Error>>,
116+
thread::JoinHandle<Result<(), Error>>,
111117
SyncSender<Request>,
112118
Receiver<Response>,
113119
) {
114120
let (req_send, req_recv) = sync_channel(0);
115121
let (res_send, res_recv) = sync_channel(0);
116-
let handle = std::thread::spawn(move || -> Result<(), curl::Error> {
122+
let handle = std::thread::spawn(move || -> Result<(), Error> {
117123
let mut handle = Easy2::new(Handler::default());
124+
let mut follow = None;
125+
let mut redirected_base_url = None::<String>;
118126

119127
for Request {
120128
url,
129+
base_url,
121130
mut headers,
122131
upload,
123132
config:
124133
http::Options {
125134
extra_headers,
126-
follow_redirects: _,
135+
follow_redirects,
127136
low_speed_limit_bytes_per_second,
128137
low_speed_time_seconds,
129138
connect_timeout,
@@ -135,7 +144,8 @@ pub fn new() -> (
135144
},
136145
} in req_recv
137146
{
138-
handle.url(&url)?;
147+
let effective_url = redirect::swap_tails(redirected_base_url.as_deref(), &base_url, url.clone());
148+
handle.url(&effective_url)?;
139149

140150
// GitHub sends 'chunked' to avoid unknown clients to choke on the data, I suppose
141151
handle.post(upload)?;
@@ -160,12 +170,7 @@ pub fn new() -> (
160170
handle.proxy_type(proxy_type)?;
161171

162172
if let Some((obtain_creds_action, authenticate)) = proxy_authenticate {
163-
let creds = authenticate.lock().expect("no panics in other threads")(obtain_creds_action)
164-
.map_err(|err| {
165-
let mut e = curl::Error::new(0);
166-
e.set_extra(err.to_string());
167-
e
168-
})?
173+
let creds = authenticate.lock().expect("no panics in other threads")(obtain_creds_action)?
169174
.expect("action to fetch credentials");
170175
handle.proxy_username(&creds.identity.username)?;
171176
handle.proxy_password(&creds.identity.password)?;
@@ -214,6 +219,14 @@ pub fn new() -> (
214219
(receive_data, receive_headers, send_body)
215220
};
216221

222+
let follow = follow.get_or_insert(follow_redirects);
223+
handle.get_mut().follow = *follow;
224+
handle.follow_location(matches!(*follow, FollowRedirects::Initial | FollowRedirects::All))?;
225+
226+
if *follow == FollowRedirects::Initial {
227+
*follow = FollowRedirects::None;
228+
}
229+
217230
if res_send
218231
.send(Response {
219232
headers: receive_headers,
@@ -256,24 +269,33 @@ pub fn new() -> (
256269
action.store()
257270
} else {
258271
action.erase()
259-
})
260-
.map_err(|err| {
261-
let mut e = curl::Error::new(0);
262-
e.set_extra(err.to_string());
263-
e
264272
})?;
265273
}
266274
handler.reset();
267275
handler.receive_body.take();
268276
handler.send_header.take();
269277
handler.send_data.take();
278+
let actual_url = handle
279+
.effective_url()?
280+
.expect("effective url is present and valid UTF-8");
281+
if actual_url != effective_url {
282+
redirected_base_url = redirect::base_url(actual_url, &base_url, url)?.into();
283+
}
270284
}
271285
}
272286
Ok(())
273287
});
274288
(handle, req_send, res_recv)
275289
}
276290

291+
impl From<Error> for http::Error {
292+
fn from(err: Error) -> Self {
293+
http::Error::Detail {
294+
description: err.to_string(),
295+
}
296+
}
297+
}
298+
277299
impl From<curl::Error> for http::Error {
278300
fn from(err: curl::Error) -> Self {
279301
http::Error::Detail {

git-transport/src/client/blocking_io/http/mod.rs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ impl<H: Http> client::TransportWithoutIO for Transport<H> {
252252
headers,
253253
body,
254254
post_body,
255-
} = self.http.post(&url, static_headers.iter().chain(&dynamic_headers))?;
255+
} = self
256+
.http
257+
.post(&url, &self.url, static_headers.iter().chain(&dynamic_headers))?;
256258
let line_provider = self
257259
.line_provider
258260
.as_mut()
@@ -319,9 +321,9 @@ impl<H: Http> client::Transport for Transport<H> {
319321
dynamic_headers.push(format!("Git-Protocol: {}", parameters).into());
320322
}
321323
self.add_basic_auth_if_present(&mut dynamic_headers)?;
322-
let GetResponse { headers, body } = self
323-
.http
324-
.get(url.as_ref(), static_headers.iter().chain(&dynamic_headers))?;
324+
let GetResponse { headers, body } =
325+
self.http
326+
.get(url.as_ref(), &self.url, static_headers.iter().chain(&dynamic_headers))?;
325327
<Transport<H>>::check_content_type(service, "advertisement", headers)?;
326328

327329
let line_reader = self
@@ -431,3 +433,74 @@ pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> T
431433
pub fn connect(url: &str, desired_version: Protocol) -> Transport<Impl> {
432434
Transport::new(url, desired_version)
433435
}
436+
437+
///
438+
#[cfg(feature = "http-client-curl")]
439+
pub mod redirect {
440+
/// The error provided when redirection went beyond what we deem acceptable.
441+
#[derive(Debug, thiserror::Error)]
442+
#[error("Redirect url {redirect_url:?} could not be reconciled with original url {expected_url} as they don't share the same suffix")]
443+
pub struct Error {
444+
redirect_url: String,
445+
expected_url: String,
446+
}
447+
448+
pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result<String, Error> {
449+
let tail = url
450+
.strip_prefix(base_url)
451+
.expect("BUG: caller assures `base_url` is subset of `url`");
452+
redirect_url
453+
.strip_suffix(tail)
454+
.ok_or_else(|| Error {
455+
redirect_url: redirect_url.into(),
456+
expected_url: url,
457+
})
458+
.map(ToOwned::to_owned)
459+
}
460+
461+
pub(crate) fn swap_tails(effective_base_url: Option<&str>, base_url: &str, mut url: String) -> String {
462+
match effective_base_url {
463+
Some(effective_base) => {
464+
url.replace_range(..base_url.len(), effective_base);
465+
url
466+
}
467+
None => url,
468+
}
469+
}
470+
471+
#[cfg(test)]
472+
mod tests {
473+
use super::*;
474+
475+
#[test]
476+
fn base_url_complete() {
477+
assert_eq!(
478+
base_url(
479+
"https://redirected.org/b/info/refs?hi",
480+
"https://original/a",
481+
"https://original/a/info/refs?hi".into()
482+
)
483+
.unwrap(),
484+
"https://redirected.org/b"
485+
);
486+
}
487+
488+
#[test]
489+
fn swap_tails_complete() {
490+
assert_eq!(
491+
swap_tails(None, "not interesting", "used".into()),
492+
"used",
493+
"without effective base url, it passes url, no redirect happened yet"
494+
);
495+
assert_eq!(
496+
swap_tails(
497+
Some("https://redirected.org/b"),
498+
"https://original/a",
499+
"https://original/a/info/refs?something".into()
500+
),
501+
"https://redirected.org/b/info/refs?something",
502+
"the tail stays the same if redirection happened"
503+
)
504+
}
505+
}
506+
}

git-transport/src/client/blocking_io/http/reqwest/remote.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ impl Remote {
118118
fn make_request(
119119
&mut self,
120120
url: &str,
121+
_base_url: &str,
121122
headers: impl IntoIterator<Item = impl AsRef<str>>,
122123
upload: bool,
123124
) -> Result<http::PostResponse<pipe::Reader, pipe::Reader, pipe::Writer>, http::Error> {
@@ -182,17 +183,19 @@ impl http::Http for Remote {
182183
fn get(
183184
&mut self,
184185
url: &str,
186+
base_url: &str,
185187
headers: impl IntoIterator<Item = impl AsRef<str>>,
186188
) -> Result<http::GetResponse<Self::Headers, Self::ResponseBody>, http::Error> {
187-
self.make_request(url, headers, false).map(Into::into)
189+
self.make_request(url, base_url, headers, false).map(Into::into)
188190
}
189191

190192
fn post(
191193
&mut self,
192194
url: &str,
195+
base_url: &str,
193196
headers: impl IntoIterator<Item = impl AsRef<str>>,
194197
) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> {
195-
self.make_request(url, headers, true)
198+
self.make_request(url, base_url, headers, true)
196199
}
197200

198201
fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {

git-transport/src/client/blocking_io/http/traits.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,21 @@ pub trait Http {
5353
/// A type allowing to write the content to post.
5454
type PostBody: std::io::Write;
5555

56-
/// Initiate a `GET` request to `url` provided the given `headers`.
56+
/// Initiate a `GET` request to `url` provided the given `headers`, where `base_url` is so that `base_url + tail == url`.
57+
///
58+
/// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect.
5759
///
5860
/// The `headers` are provided verbatim and include both the key as well as the value.
5961
fn get(
6062
&mut self,
6163
url: &str,
64+
base_url: &str,
6265
headers: impl IntoIterator<Item = impl AsRef<str>>,
6366
) -> Result<GetResponse<Self::Headers, Self::ResponseBody>, Error>;
6467

65-
/// Initiate a `POST` request to `url` providing with the given `headers`.
68+
/// Initiate a `POST` request to `url` providing with the given `headers`, where `base_url` is so that `base_url + tail == url`.
69+
///
70+
/// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect.
6671
///
6772
/// The `headers` are provided verbatim and include both the key as well as the value.
6873
/// Note that the [`PostResponse`] contains the [`post_body`][PostResponse::post_body] field which implements [`std::io::Write`]
@@ -71,6 +76,7 @@ pub trait Http {
7176
fn post(
7277
&mut self,
7378
url: &str,
79+
base_url: &str,
7480
headers: impl IntoIterator<Item = impl AsRef<str>>,
7581
) -> Result<PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, Error>;
7682

0 commit comments

Comments
 (0)