recover from malformed location header when redirecting

based on code from #2111
closes #2102
This commit is contained in:
Rob Ede 2022-03-08 02:00:57 +00:00
parent f983103e44
commit 9b1db1b950
No known key found for this signature in database
GPG Key ID: 97C636207D3EF933
2 changed files with 78 additions and 46 deletions

View File

@ -1,22 +1,25 @@
//! `awc` is an asynchronous HTTP and WebSocket client library. //! `awc` is an asynchronous HTTP and WebSocket client library.
//! //!
//! # Making a GET request //! # `GET` Requests
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> { //! # async fn main() -> Result<(), awc::error::SendRequestError> {
//! // create client
//! let mut client = awc::Client::default(); //! let mut client = awc::Client::default();
//! let response = client.get("http://www.rust-lang.org") // <- Create request builder
//! .insert_header(("User-Agent", "Actix-web"))
//! .send() // <- Send http request
//! .await?;
//! //!
//! println!("Response: {:?}", response); //! // construct request
//! let req = client.get("http://www.rust-lang.org")
//! .insert_header(("User-Agent", "awc/3.0"));
//!
//! // send request and await response
//! let res = req.send().await?;
//! println!("Response: {:?}", res);
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```
//! //!
//! # Making POST requests //! # `POST` Requests
//! ## Raw body contents //! ## Raw Body
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> { //! # async fn main() -> Result<(), awc::error::SendRequestError> {
@ -28,20 +31,6 @@
//! # } //! # }
//! ``` //! ```
//! //!
//! ## Forms
//! ```no_run
//! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
//! let params = [("foo", "bar"), ("baz", "quux")];
//!
//! let mut client = awc::Client::default();
//! let response = client.post("http://httpbin.org/post")
//! .send_form(&params)
//! .await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## JSON //! ## JSON
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
@ -59,6 +48,20 @@
//! # } //! # }
//! ``` //! ```
//! //!
//! ## URL Encoded Form
//! ```no_run
//! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
//! let params = [("foo", "bar"), ("baz", "quux")];
//!
//! let mut client = awc::Client::default();
//! let response = client.post("http://httpbin.org/post")
//! .send_form(&params)
//! .await?;
//! # Ok(())
//! # }
//! ```
//!
//! # Response Compression //! # Response Compression
//! All [official][iana-encodings] and common content encoding codecs are supported, optionally. //! All [official][iana-encodings] and common content encoding codecs are supported, optionally.
//! //!
@ -76,11 +79,12 @@
//! //!
//! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding //! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
//! //!
//! # WebSocket support //! # WebSockets
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> { //! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use futures_util::{sink::SinkExt, stream::StreamExt}; //! use futures_util::{sink::SinkExt as _, stream::StreamExt as _};
//!
//! let (_resp, mut connection) = awc::Client::new() //! let (_resp, mut connection) = awc::Client::new()
//! .ws("ws://echo.websocket.org") //! .ws("ws://echo.websocket.org")
//! .connect() //! .connect()
@ -89,8 +93,9 @@
//! connection //! connection
//! .send(awc::ws::Message::Text("Echo".into())) //! .send(awc::ws::Message::Text("Echo".into()))
//! .await?; //! .await?;
//!
//! let response = connection.next().await.unwrap()?; //! let response = connection.next().await.unwrap()?;
//! # assert_eq!(response, awc::ws::Frame::Text("Echo".as_bytes().into())); //! assert_eq!(response, awc::ws::Frame::Text("Echo".into()));
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```

View File

@ -246,26 +246,32 @@ where
} }
fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result<Uri, SendRequestError> { fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result<Uri, SendRequestError> {
let uri = res // responses without this header are not processed
.headers() let location = res.headers().get(header::LOCATION).unwrap();
.get(header::LOCATION)
.map(|value| { // try to parse the location and resolve to a full URI but fall back to default if it fails
// try to parse the location to a full uri let uri = Uri::try_from(location.as_bytes()).unwrap_or_else(|_| Uri::default());
let uri = Uri::try_from(value.as_bytes())
.map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?; let uri = if uri.scheme().is_none() || uri.authority().is_none() {
if uri.scheme().is_none() || uri.authority().is_none() { let builder = Uri::builder()
let uri = Uri::builder()
.scheme(prev_uri.scheme().cloned().unwrap()) .scheme(prev_uri.scheme().cloned().unwrap())
.authority(prev_uri.authority().cloned().unwrap()) .authority(prev_uri.authority().cloned().unwrap());
.path_and_query(value.as_bytes())
.build()?; // when scheme or authority is missing treat the location value as path and query
Ok::<_, SendRequestError>(uri) // recover error where location does not have leading slash
let path = if location.as_bytes().starts_with(b"/") {
location.as_bytes().to_owned()
} else { } else {
Ok(uri) [b"/", location.as_bytes()].concat()
} };
})
// TODO: this error type is wrong. builder
.ok_or(SendRequestError::Url(InvalidUrl::MissingScheme))??; .path_and_query(path)
.build()
.map_err(|err| SendRequestError::Url(InvalidUrl::HttpError(err)))?
} else {
uri
};
Ok(uri) Ok(uri)
} }
@ -294,7 +300,7 @@ mod tests {
}; };
#[actix_rt::test] #[actix_rt::test]
async fn test_basic_redirect() { async fn basic_redirect() {
let client = ClientBuilder::new() let client = ClientBuilder::new()
.disable_redirects() .disable_redirects()
.wrap(Redirect::new().max_redirect_times(10)) .wrap(Redirect::new().max_redirect_times(10))
@ -319,6 +325,27 @@ mod tests {
assert_eq!(res.status().as_u16(), 400); assert_eq!(res.status().as_u16(), 400);
} }
#[actix_rt::test]
async fn redirect_relative_without_leading_slash() {
let client = ClientBuilder::new().finish();
let srv = actix_test::start(|| {
App::new()
.service(web::resource("/").route(web::to(|| async {
HttpResponse::Found()
.insert_header(("location", "abc/"))
.finish()
})))
.service(
web::resource("/abc/")
.route(web::to(|| async { HttpResponse::Accepted().finish() })),
)
});
let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status(), StatusCode::ACCEPTED);
}
#[actix_rt::test] #[actix_rt::test]
async fn redirect_without_location() { async fn redirect_without_location() {
let client = ClientBuilder::new() let client = ClientBuilder::new()