Fix relative redirect

This commit is contained in:
Fabrice Reix 2022-10-09 16:30:07 +02:00
parent 1c2e2ffc72
commit 1ae68efd82
No known key found for this signature in database
GPG Key ID: BF5213154B2E7155
10 changed files with 181 additions and 35 deletions

View File

@ -1,2 +1,5 @@
curl 'http://localhost:8000/redirect'
curl 'http://localhost:8000/redirected' curl 'http://localhost:8000/redirected'
curl 'http://localhost:8000/redirect-absolute'
curl 'http://localhost:8000/redirect-absolute' -L
curl 'http://localhost:8000/redirect-relative'
curl 'http://localhost:8000/redirect-relative' -L

View File

@ -1,13 +1,37 @@
<pre><code class="language-hurl"><span class="hurl-entry"><span class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/redirect</span></span> <pre><code class="language-hurl"><span class="hurl-entry"><span class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/redirected</span></span>
</span><span class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="number">200</span></span>
<span class="raw"><span class="line">```Redirected```</span></span>
</span></span><span class="hurl-entry"><span class="request"><span class="line"></span>
<span class="line"></span>
<span class="line"></span><span class="comment"># Absolute redirects</span>
<span class="line"></span>
<span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/redirect-absolute</span></span>
</span><span class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="number">302</span></span> </span><span class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="number">302</span></span>
<span class="line"><span class="string">Location</span><span>:</span> <span class="string">http://localhost:8000/redirected</span></span> <span class="line"><span class="string">Location</span><span>:</span> <span class="string">http://localhost:8000/redirected</span></span>
</span></span><span class="hurl-entry"><span class="request"><span class="line"></span> </span></span><span class="hurl-entry"><span class="request"><span class="line"></span>
<span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/redirected</span></span> <span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/redirect-absolute</span></span>
</span><span class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="number">200</span></span> <span class="line section-header">[Options]</span>
<span class="line"><span class="string">location</span><span>:</span> <span class="boolean">true</span></span>
</span><span class="response"><span class="line"></span>
<span class="line"><span class="version">HTTP/1.0</span> <span class="number">200</span></span>
<span class="raw"><span class="line">```Redirected```</span></span>
</span></span><span class="hurl-entry"><span class="request"><span class="line"></span>
<span class="line"></span>
<span class="line"></span><span class="comment"># Relative redirects</span>
<span class="line"></span>
<span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/redirect-relative</span></span>
</span><span class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="number">302</span></span>
<span class="line"><span class="string">Location</span><span>:</span> <span class="string">/redirected</span></span>
</span></span><span class="hurl-entry"><span class="request"><span class="line"></span>
<span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/redirect-relative</span></span>
<span class="line section-header">[Options]</span>
<span class="line"><span class="string">location</span><span>:</span> <span class="boolean">true</span></span>
</span><span class="response"><span class="line"></span>
<span class="line"><span class="version">HTTP/1.0</span> <span class="number">200</span></span>
<span class="raw"><span class="line">```Redirected```</span></span>
</span></span><span class="line"></span> </span></span><span class="line"></span>
<span class="line"></span> <span class="line"></span>
<span class="line"></span> <span class="line"></span>
<span class="line"></span> <span class="line"></span>
<span class="line"></span> <span class="line"></span>
<span class="line"></span>
</code></pre> </code></pre>

View File

@ -1,10 +1,34 @@
GET http://localhost:8000/redirect GET http://localhost:8000/redirected
HTTP/1.0 200
```Redirected```
# Absolute redirects
GET http://localhost:8000/redirect-absolute
HTTP/1.0 302 HTTP/1.0 302
Location: http://localhost:8000/redirected Location: http://localhost:8000/redirected
GET http://localhost:8000/redirected GET http://localhost:8000/redirect-absolute
[Options]
location: true
HTTP/1.0 200 HTTP/1.0 200
```Redirected```
# Relative redirects
GET http://localhost:8000/redirect-relative
HTTP/1.0 302
Location: /redirected
GET http://localhost:8000/redirect-relative
[Options]
location: true
HTTP/1.0 200
```Redirected```

View File

@ -1 +1 @@
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/redirect"},"response":{"version":"HTTP/1.0","status":302,"headers":[{"name":"Location","value":"http://localhost:8000/redirected"}]}},{"request":{"method":"GET","url":"http://localhost:8000/redirected"},"response":{"version":"HTTP/1.0","status":200}}]} {"entries":[{"request":{"method":"GET","url":"http://localhost:8000/redirected"},"response":{"version":"HTTP/1.0","status":200,"body":{"type":"raw-string","value":"Redirected"}}},{"request":{"method":"GET","url":"http://localhost:8000/redirect-absolute"},"response":{"version":"HTTP/1.0","status":302,"headers":[{"name":"Location","value":"http://localhost:8000/redirected"}]}},{"request":{"method":"GET","url":"http://localhost:8000/redirect-absolute"},"response":{"version":"HTTP/1.0","status":200,"body":{"type":"raw-string","value":"Redirected"}}},{"request":{"method":"GET","url":"http://localhost:8000/redirect-relative"},"response":{"version":"HTTP/1.0","status":302,"headers":[{"name":"Location","value":"/redirected"}]}},{"request":{"method":"GET","url":"http://localhost:8000/redirect-relative"},"response":{"version":"HTTP/1.0","status":200,"body":{"type":"raw-string","value":"Redirected"}}}]}

View File

@ -1,12 +1,20 @@
from app import app from app import app
from flask import redirect from flask import redirect, Response
@app.route("/redirect") @app.route("/redirect-absolute")
def redirectme(): def redirect_absolute():
return redirect("http://localhost:8000/redirected") return redirect("http://localhost:8000/redirected")
@app.route("/redirect-relative")
def redirect_relative():
response = Response(status=302)
response.headers["Location"] = "/redirected"
response.autocorrect_location_header = False
return response
@app.route("/redirected") @app.route("/redirected")
def redirected(): def redirected():
return "" return "Redirected"

View File

@ -82,12 +82,13 @@ impl Client {
self.redirect_count = 0; self.redirect_count = 0;
loop { loop {
let (request, response) = self.execute(&request_spec, options, logger)?; let (request, response) = self.execute(&request_spec, options, logger)?;
calls.push((request, response.clone())); calls.push((request.clone(), response.clone()));
if !options.follow_location { if !options.follow_location {
break; break;
} }
if let Some(url) = self.get_follow_location(&response) { let base_url = request.base_url()?;
if let Some(url) = self.get_follow_location(&response, &base_url) {
logger.debug(""); logger.debug("");
logger.debug(format!("=> Redirect to {}", url).as_str()); logger.debug(format!("=> Redirect to {}", url).as_str());
logger.debug(""); logger.debug("");
@ -513,14 +514,14 @@ impl Client {
/// 1. the option follow_location set to true /// 1. the option follow_location set to true
/// 2. a 3xx response code /// 2. a 3xx response code
/// 3. a header Location /// 3. a header Location
fn get_follow_location(&mut self, response: &Response) -> Option<String> { fn get_follow_location(&mut self, response: &Response, base_url: &str) -> Option<String> {
let response_code = response.status; let response_code = response.status;
if !(300..400).contains(&response_code) { if !(300..400).contains(&response_code) {
return None; return None;
} }
let location = match response.get_header_values("Location").get(0) { let location = match response.get_header_values("Location").get(0) {
None => return None, None => return None,
Some(value) => value.clone(), Some(value) => get_redirect_url(value, base_url),
}; };
if location.is_empty() { if location.is_empty() {
@ -590,6 +591,15 @@ impl Client {
} }
} }
/// Returns the redirect url.
fn get_redirect_url(location: &str, base_url: &str) -> String {
if location.starts_with('/') {
format!("{}{}", base_url, location)
} else {
location.to_string()
}
}
/// Returns cookies from both cookies from the cookie storage and the request. /// Returns cookies from both cookies from the cookie storage and the request.
pub fn all_cookies(cookie_storage: &[Cookie], request_spec: &RequestSpec) -> Vec<RequestCookie> { pub fn all_cookies(cookie_storage: &[Cookie], request_spec: &RequestSpec) -> Vec<RequestCookie> {
let mut cookies = request_spec.cookies.clone(); let mut cookies = request_spec.cookies.clone();
@ -739,4 +749,16 @@ mod tests {
assert!(match_cookie(&cookie, "http://sub.example.com/toto")); assert!(match_cookie(&cookie, "http://sub.example.com/toto"));
assert!(!match_cookie(&cookie, "http://example.com/tata")); assert!(!match_cookie(&cookie, "http://example.com/tata"));
} }
#[test]
fn test_redirect_url() {
assert_eq!(
get_redirect_url("http://localhost:8000/redirected", "http://localhost:8000"),
"http://localhost:8000/redirected".to_string()
);
assert_eq!(
get_redirect_url("/redirected", "http://localhost:8000"),
"http://localhost:8000/redirected".to_string()
);
}
} }

View File

@ -40,4 +40,5 @@ pub enum HttpError {
UnsupportedContentEncoding { UnsupportedContentEncoding {
description: String, description: String,
}, },
InvalidUrl(String),
} }

View File

@ -18,7 +18,7 @@
use super::core::*; use super::core::*;
use super::Header; use super::Header;
use crate::http::header; use crate::http::{header, HttpError};
use url::Url; use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -61,6 +61,26 @@ impl Request {
.get(0) .get(0)
.cloned() .cloned()
} }
/// Returns the base url http(s)://host(:port)
pub fn base_url(&self) -> Result<String, HttpError> {
// FIXME: is it possible to do it with libcurl?
let url = match Url::parse(&self.url) {
Ok(url) => url,
Err(_) => return Err(HttpError::InvalidUrl(self.url.clone())),
};
let base_url = format!(
"{}://{}{}",
url.scheme(),
url.host().unwrap(),
if let Some(port) = url.port() {
format!(":{}", port)
} else {
"".to_string()
}
);
Ok(base_url)
}
} }
fn parse_cookies(s: &str) -> Vec<RequestCookie> { fn parse_cookies(s: &str) -> Vec<RequestCookie> {
@ -136,20 +156,20 @@ pub mod tests {
vec![ vec![
Param { Param {
name: "param1".to_string(), name: "param1".to_string(),
value: "value1".to_string() value: "value1".to_string(),
}, },
Param { Param {
name: "param2".to_string(), name: "param2".to_string(),
value: "".to_string() value: "".to_string(),
}, },
Param { Param {
name: "param3".to_string(), name: "param3".to_string(),
value: "a=b".to_string() value: "a=b".to_string(),
}, },
Param { Param {
name: "param4".to_string(), name: "param4".to_string(),
value: "1,2,3".to_string() value: "1,2,3".to_string(),
} },
] ]
) )
} }
@ -162,12 +182,12 @@ pub mod tests {
vec![ vec![
RequestCookie { RequestCookie {
name: "cookie1".to_string(), name: "cookie1".to_string(),
value: "value1".to_string() value: "value1".to_string(),
}, },
RequestCookie { RequestCookie {
name: "cookie2".to_string(), name: "cookie2".to_string(),
value: "value2".to_string() value: "value2".to_string(),
} },
] ]
) )
} }
@ -179,12 +199,12 @@ pub mod tests {
vec![ vec![
RequestCookie { RequestCookie {
name: "cookie1".to_string(), name: "cookie1".to_string(),
value: "value1".to_string() value: "value1".to_string(),
}, },
RequestCookie { RequestCookie {
name: "cookie2".to_string(), name: "cookie2".to_string(),
value: "value2".to_string() value: "value2".to_string(),
} },
] ]
) )
} }
@ -195,8 +215,45 @@ pub mod tests {
parse_cookie("cookie1=value1"), parse_cookie("cookie1=value1"),
RequestCookie { RequestCookie {
name: "cookie1".to_string(), name: "cookie1".to_string(),
value: "value1".to_string() value: "value1".to_string(),
}, },
) )
} }
#[test]
fn test_base_url() {
assert_eq!(
Request {
url: "http://localhost".to_string(),
method: "".to_string(),
headers: vec![],
body: vec![],
}
.base_url()
.unwrap(),
"http://localhost".to_string()
);
assert_eq!(
Request {
url: "http://localhost:8000/redirect-relative".to_string(),
method: "".to_string(),
headers: vec![],
body: vec![],
}
.base_url()
.unwrap(),
"http://localhost:8000".to_string()
);
assert_eq!(
Request {
url: "https://localhost:8000".to_string(),
method: "".to_string(),
headers: vec![],
body: vec![],
}
.base_url()
.unwrap(),
"https://localhost:8000".to_string()
);
}
} }

View File

@ -172,6 +172,7 @@ impl From<HttpError> for RunnerError {
HttpError::UnsupportedContentEncoding { description } => { HttpError::UnsupportedContentEncoding { description } => {
RunnerError::UnsupportedContentEncoding(description) RunnerError::UnsupportedContentEncoding(description)
} }
HttpError::InvalidUrl(url) => RunnerError::InvalidUrl(url),
} }
} }
} }

View File

@ -358,13 +358,16 @@ fn test_form_params() {
#[test] #[test]
fn test_redirect() { fn test_redirect() {
let request_spec = default_get_request("http://localhost:8000/redirect"); let request_spec = default_get_request("http://localhost:8000/redirect-absolute");
let logger = Logger::new(false, false, "", ""); let logger = Logger::new(false, false, "", "");
let options = ClientOptions::default(); let options = ClientOptions::default();
let mut client = Client::new(None); let mut client = Client::new(None);
let (request, response) = client.execute(&request_spec, &options, &logger).unwrap(); let (request, response) = client.execute(&request_spec, &options, &logger).unwrap();
assert_eq!(request.method, "GET".to_string()); assert_eq!(request.method, "GET".to_string());
assert_eq!(request.url, "http://localhost:8000/redirect".to_string()); assert_eq!(
request.url,
"http://localhost:8000/redirect-absolute".to_string()
);
assert_eq!(request.headers.len(), 3); assert_eq!(request.headers.len(), 3);
assert_eq!(response.status, 302); assert_eq!(response.status, 302);
@ -377,7 +380,7 @@ fn test_redirect() {
#[test] #[test]
fn test_follow_location() { fn test_follow_location() {
let request_spec = default_get_request("http://localhost:8000/redirect"); let request_spec = default_get_request("http://localhost:8000/redirect-absolute");
let logger = Logger::new(false, false, "", ""); let logger = Logger::new(false, false, "", "");
let options = ClientOptions { let options = ClientOptions {
follow_location: true, follow_location: true,
@ -388,7 +391,7 @@ fn test_follow_location() {
assert_eq!(options.curl_args(), vec!["-L".to_string()]); assert_eq!(options.curl_args(), vec!["-L".to_string()]);
assert_eq!( assert_eq!(
client.curl_command_line(&request_spec, &context_dir, &options), client.curl_command_line(&request_spec, &context_dir, &options),
"curl 'http://localhost:8000/redirect' -L".to_string() "curl 'http://localhost:8000/redirect-absolute' -L".to_string()
); );
let calls = client let calls = client
@ -398,7 +401,10 @@ fn test_follow_location() {
let (request1, response1) = calls.get(0).unwrap(); let (request1, response1) = calls.get(0).unwrap();
assert_eq!(request1.method, "GET".to_string()); assert_eq!(request1.method, "GET".to_string());
assert_eq!(request1.url, "http://localhost:8000/redirect".to_string()); assert_eq!(
request1.url,
"http://localhost:8000/redirect-absolute".to_string()
);
assert_eq!(request1.headers.len(), 3); assert_eq!(request1.headers.len(), 3);
assert_eq!(response1.status, 302); assert_eq!(response1.status, 302);
assert!(response1.headers.contains(&Header { assert!(response1.headers.contains(&Header {