Skip to content

Commit

Permalink
Fix Proxy URL parse error handling. (#1539)
Browse files Browse the repository at this point in the history
* Check for schema during URL parse error handling.  Lots of unit tests.
* Introduce BadScheme; an error source.  Change schema to scheme.  Use BadScheme instead of the error text to determine that a scheme is not present.
  • Loading branch information
Brian Cook authored May 5, 2022
1 parent 6ca5f3e commit 2a6e012
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 12 deletions.
13 changes: 12 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ pub(crate) fn status_code(url: Url, status: StatusCode) -> Error {
}

pub(crate) fn url_bad_scheme(url: Url) -> Error {
Error::new(Kind::Builder, Some("URL scheme is not allowed")).with_url(url)
Error::new(Kind::Builder, Some(BadScheme)).with_url(url)
}

if_wasm! {
Expand Down Expand Up @@ -306,6 +306,17 @@ impl fmt::Display for TimedOut {

impl StdError for TimedOut {}

#[derive(Debug)]
pub(crate) struct BadScheme;

impl fmt::Display for BadScheme {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("URL scheme is not allowed")
}
}

impl StdError for BadScheme {}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
261 changes: 250 additions & 11 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use ipnet::IpNet;
use percent_encoding::percent_decode;
use std::collections::HashMap;
use std::env;
#[cfg(target_os = "windows")]
use std::error::Error;
use std::net::IpAddr;
#[cfg(target_os = "windows")]
Expand Down Expand Up @@ -124,13 +123,33 @@ impl<S: IntoUrl> IntoProxyScheme for S {
let url = match self.as_str().into_url() {
Ok(ok) => ok,
Err(e) => {
// the issue could have been caused by a missing scheme, so we try adding http://
format!("http://{}", self.as_str())
.into_url()
.map_err(|_| {
let mut presumed_to_have_scheme = true;
let mut source = e.source();
while let Some(err) = source {
if let Some(parse_error) = err.downcast_ref::<url::ParseError>() {
match parse_error {
url::ParseError::RelativeUrlWithoutBase => {
presumed_to_have_scheme = false;
break;
}
_ => {}
}
} else if let Some(_) = err.downcast_ref::<crate::error::BadScheme>() {
presumed_to_have_scheme = false;
break;
}
source = err.source();
}
if !presumed_to_have_scheme {
// the issue could have been caused by a missing scheme, so we try adding http://
let try_this = format!("http://{}", self.as_str());
try_this.into_url().map_err(|_| {
// return the original error
crate::error::builder(e)
})?
} else {
return Err(crate::error::builder(e));
}
}
};
ProxyScheme::parse(url)
Expand Down Expand Up @@ -1107,14 +1126,14 @@ mod tests {
let disabled_proxies = get_sys_proxies(Some((0, String::from("http://127.0.0.1/"))));
// set valid proxy
let valid_proxies = get_sys_proxies(Some((1, String::from("http://127.0.0.1/"))));
let valid_proxies_no_schema = get_sys_proxies(Some((1, String::from("127.0.0.1"))));
let valid_proxies_no_scheme = get_sys_proxies(Some((1, String::from("127.0.0.1"))));
let valid_proxies_explicit_https =
get_sys_proxies(Some((1, String::from("https://127.0.0.1/"))));
let multiple_proxies = get_sys_proxies(Some((
1,
String::from("http=127.0.0.1:8888;https=127.0.0.2:8888"),
)));
let multiple_proxies_explicit_schema = get_sys_proxies(Some((
let multiple_proxies_explicit_scheme = get_sys_proxies(Some((
1,
String::from("http=http://127.0.0.1:8888;https=https://127.0.0.2:8888"),
)));
Expand All @@ -1132,11 +1151,11 @@ mod tests {
assert_eq!(p.scheme(), "http");
assert_eq!(p.host(), "127.0.0.1");

let p = &valid_proxies_no_schema["http"];
let p = &valid_proxies_no_scheme["http"];
assert_eq!(p.scheme(), "http");
assert_eq!(p.host(), "127.0.0.1");

let p = &valid_proxies_no_schema["https"];
let p = &valid_proxies_no_scheme["https"];
assert_eq!(p.scheme(), "http");
assert_eq!(p.host(), "127.0.0.1");

Expand All @@ -1152,11 +1171,11 @@ mod tests {
assert_eq!(p.scheme(), "http");
assert_eq!(p.host(), "127.0.0.2:8888");

let p = &multiple_proxies_explicit_schema["http"];
let p = &multiple_proxies_explicit_scheme["http"];
assert_eq!(p.scheme(), "http");
assert_eq!(p.host(), "127.0.0.1:8888");

let p = &multiple_proxies_explicit_schema["https"];
let p = &multiple_proxies_explicit_scheme["https"];
assert_eq!(p.scheme(), "https");
assert_eq!(p.host(), "127.0.0.2:8888");
}
Expand Down Expand Up @@ -1511,3 +1530,223 @@ mod tests {
);
}
}

#[cfg(test)]
mod test {
mod into_proxy_scheme {
use crate::Proxy;
use std::error::Error;
use std::mem::discriminant;

fn includes(haystack: &crate::error::Error, needle: url::ParseError) -> bool {
let mut source = haystack.source();
while let Some(error) = source {
if let Some(parse_error) = error.downcast_ref::<url::ParseError>() {
if discriminant(parse_error) == discriminant(&needle) {
return true;
}
}
source = error.source();
}
false
}

fn check_parse_error(url: &str, needle: url::ParseError) {
let error = Proxy::http(url).unwrap_err();
if !includes(&error, needle) {
panic!("{:?} expected; {:?}, {} found", needle, error, error);
}
}

mod when_scheme_missing {
mod and_url_is_valid {
use crate::Proxy;

#[test]
fn lookback_works() {
let _ = Proxy::http("127.0.0.1").unwrap();
}

#[test]
fn loopback_port_works() {
let _ = Proxy::http("127.0.0.1:8080").unwrap();
}

#[test]
fn loopback_username_works() {
let _ = Proxy::http("username@127.0.0.1").unwrap();
}

#[test]
fn loopback_username_password_works() {
let _ = Proxy::http("username:password@127.0.0.1").unwrap();
}

#[test]
fn loopback_username_password_port_works() {
let _ = Proxy::http("ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap();
}

#[test]
fn domain_works() {
let _ = Proxy::http("proxy.example.com").unwrap();
}

#[test]
fn domain_port_works() {
let _ = Proxy::http("proxy.example.com:8080").unwrap();
}

#[test]
fn domain_username_works() {
let _ = Proxy::http("username@proxy.example.com").unwrap();
}

#[test]
fn domain_username_password_works() {
let _ = Proxy::http("username:password@proxy.example.com").unwrap();
}

#[test]
fn domain_username_password_port_works() {
let _ =
Proxy::http("ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080").unwrap();
}
}
mod and_url_has_bad {
use super::super::check_parse_error;

#[test]
fn host() {
check_parse_error("username@", url::ParseError::RelativeUrlWithoutBase);
}

#[test]
fn idna_encoding() {
check_parse_error("xn---", url::ParseError::RelativeUrlWithoutBase);
}

#[test]
fn port() {
check_parse_error("127.0.0.1:808080", url::ParseError::RelativeUrlWithoutBase);
}

#[test]
fn ip_v4_address() {
check_parse_error("421.627.718.469", url::ParseError::RelativeUrlWithoutBase);
}

#[test]
fn ip_v6_address() {
check_parse_error(
"[56FE::2159:5BBC::6594]",
url::ParseError::RelativeUrlWithoutBase,
);
}

#[test]
fn invalid_domain_character() {
check_parse_error("abc 123", url::ParseError::RelativeUrlWithoutBase);
}
}
}

mod when_scheme_present {
mod and_url_is_valid {
use crate::Proxy;

#[test]
fn loopback_works() {
let _ = Proxy::http("http://127.0.0.1").unwrap();
}

#[test]
fn loopback_port_works() {
let _ = Proxy::http("https://127.0.0.1:8080").unwrap();
}

#[test]
fn loopback_username_works() {
let _ = Proxy::http("http://username@127.0.0.1").unwrap();
}

#[test]
fn loopback_username_password_works() {
let _ = Proxy::http("https://username:password@127.0.0.1").unwrap();
}

#[test]
fn loopback_username_password_port_works() {
let _ =
Proxy::http("http://ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap();
}

#[test]
fn domain_works() {
let _ = Proxy::http("https://proxy.example.com").unwrap();
}

#[test]
fn domain_port_works() {
let _ = Proxy::http("http://proxy.example.com:8080").unwrap();
}

#[test]
fn domain_username_works() {
let _ = Proxy::http("https://username@proxy.example.com").unwrap();
}

#[test]
fn domain_username_password_works() {
let _ = Proxy::http("http://username:password@proxy.example.com").unwrap();
}

#[test]
fn domain_username_password_port_works() {
let _ =
Proxy::http("https://ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080")
.unwrap();
}
}
mod and_url_has_bad {
use super::super::check_parse_error;

#[test]
fn host() {
check_parse_error("http://username@", url::ParseError::EmptyHost);
}

#[test]
fn idna_encoding() {
check_parse_error("http://xn---", url::ParseError::IdnaError);
}

#[test]
fn port() {
check_parse_error("http://127.0.0.1:808080", url::ParseError::InvalidPort);
}

#[test]
fn ip_v4_address() {
check_parse_error(
"http://421.627.718.469",
url::ParseError::InvalidIpv4Address,
);
}

#[test]
fn ip_v6_address() {
check_parse_error(
"http://[56FE::2159:5BBC::6594]",
url::ParseError::InvalidIpv6Address,
);
}

#[test]
fn invalid_domain_character() {
check_parse_error("http://abc 123/", url::ParseError::InvalidDomainCharacter);
}
}
}
}
}

0 comments on commit 2a6e012

Please sign in to comment.