Skip to content

Commit 2a6e012

Browse files
author
Brian Cook
authored
Fix Proxy URL parse error handling. (#1539)
* 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.
1 parent 6ca5f3e commit 2a6e012

2 files changed

Lines changed: 262 additions & 12 deletions

File tree

src/error.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ pub(crate) fn status_code(url: Url, status: StatusCode) -> Error {
265265
}
266266

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

271271
if_wasm! {
@@ -306,6 +306,17 @@ impl fmt::Display for TimedOut {
306306

307307
impl StdError for TimedOut {}
308308

309+
#[derive(Debug)]
310+
pub(crate) struct BadScheme;
311+
312+
impl fmt::Display for BadScheme {
313+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
314+
f.write_str("URL scheme is not allowed")
315+
}
316+
}
317+
318+
impl StdError for BadScheme {}
319+
309320
#[cfg(test)]
310321
mod tests {
311322
use super::*;

src/proxy.rs

Lines changed: 250 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use ipnet::IpNet;
1010
use percent_encoding::percent_decode;
1111
use std::collections::HashMap;
1212
use std::env;
13-
#[cfg(target_os = "windows")]
1413
use std::error::Error;
1514
use std::net::IpAddr;
1615
#[cfg(target_os = "windows")]
@@ -124,13 +123,33 @@ impl<S: IntoUrl> IntoProxyScheme for S {
124123
let url = match self.as_str().into_url() {
125124
Ok(ok) => ok,
126125
Err(e) => {
127-
// the issue could have been caused by a missing scheme, so we try adding http://
128-
format!("http://{}", self.as_str())
129-
.into_url()
130-
.map_err(|_| {
126+
let mut presumed_to_have_scheme = true;
127+
let mut source = e.source();
128+
while let Some(err) = source {
129+
if let Some(parse_error) = err.downcast_ref::<url::ParseError>() {
130+
match parse_error {
131+
url::ParseError::RelativeUrlWithoutBase => {
132+
presumed_to_have_scheme = false;
133+
break;
134+
}
135+
_ => {}
136+
}
137+
} else if let Some(_) = err.downcast_ref::<crate::error::BadScheme>() {
138+
presumed_to_have_scheme = false;
139+
break;
140+
}
141+
source = err.source();
142+
}
143+
if !presumed_to_have_scheme {
144+
// the issue could have been caused by a missing scheme, so we try adding http://
145+
let try_this = format!("http://{}", self.as_str());
146+
try_this.into_url().map_err(|_| {
131147
// return the original error
132148
crate::error::builder(e)
133149
})?
150+
} else {
151+
return Err(crate::error::builder(e));
152+
}
134153
}
135154
};
136155
ProxyScheme::parse(url)
@@ -1107,14 +1126,14 @@ mod tests {
11071126
let disabled_proxies = get_sys_proxies(Some((0, String::from("http://127.0.0.1/"))));
11081127
// set valid proxy
11091128
let valid_proxies = get_sys_proxies(Some((1, String::from("http://127.0.0.1/"))));
1110-
let valid_proxies_no_schema = get_sys_proxies(Some((1, String::from("127.0.0.1"))));
1129+
let valid_proxies_no_scheme = get_sys_proxies(Some((1, String::from("127.0.0.1"))));
11111130
let valid_proxies_explicit_https =
11121131
get_sys_proxies(Some((1, String::from("https://127.0.0.1/"))));
11131132
let multiple_proxies = get_sys_proxies(Some((
11141133
1,
11151134
String::from("http=127.0.0.1:8888;https=127.0.0.2:8888"),
11161135
)));
1117-
let multiple_proxies_explicit_schema = get_sys_proxies(Some((
1136+
let multiple_proxies_explicit_scheme = get_sys_proxies(Some((
11181137
1,
11191138
String::from("http=http://127.0.0.1:8888;https=https://127.0.0.2:8888"),
11201139
)));
@@ -1132,11 +1151,11 @@ mod tests {
11321151
assert_eq!(p.scheme(), "http");
11331152
assert_eq!(p.host(), "127.0.0.1");
11341153

1135-
let p = &valid_proxies_no_schema["http"];
1154+
let p = &valid_proxies_no_scheme["http"];
11361155
assert_eq!(p.scheme(), "http");
11371156
assert_eq!(p.host(), "127.0.0.1");
11381157

1139-
let p = &valid_proxies_no_schema["https"];
1158+
let p = &valid_proxies_no_scheme["https"];
11401159
assert_eq!(p.scheme(), "http");
11411160
assert_eq!(p.host(), "127.0.0.1");
11421161

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

1155-
let p = &multiple_proxies_explicit_schema["http"];
1174+
let p = &multiple_proxies_explicit_scheme["http"];
11561175
assert_eq!(p.scheme(), "http");
11571176
assert_eq!(p.host(), "127.0.0.1:8888");
11581177

1159-
let p = &multiple_proxies_explicit_schema["https"];
1178+
let p = &multiple_proxies_explicit_scheme["https"];
11601179
assert_eq!(p.scheme(), "https");
11611180
assert_eq!(p.host(), "127.0.0.2:8888");
11621181
}
@@ -1511,3 +1530,223 @@ mod tests {
15111530
);
15121531
}
15131532
}
1533+
1534+
#[cfg(test)]
1535+
mod test {
1536+
mod into_proxy_scheme {
1537+
use crate::Proxy;
1538+
use std::error::Error;
1539+
use std::mem::discriminant;
1540+
1541+
fn includes(haystack: &crate::error::Error, needle: url::ParseError) -> bool {
1542+
let mut source = haystack.source();
1543+
while let Some(error) = source {
1544+
if let Some(parse_error) = error.downcast_ref::<url::ParseError>() {
1545+
if discriminant(parse_error) == discriminant(&needle) {
1546+
return true;
1547+
}
1548+
}
1549+
source = error.source();
1550+
}
1551+
false
1552+
}
1553+
1554+
fn check_parse_error(url: &str, needle: url::ParseError) {
1555+
let error = Proxy::http(url).unwrap_err();
1556+
if !includes(&error, needle) {
1557+
panic!("{:?} expected; {:?}, {} found", needle, error, error);
1558+
}
1559+
}
1560+
1561+
mod when_scheme_missing {
1562+
mod and_url_is_valid {
1563+
use crate::Proxy;
1564+
1565+
#[test]
1566+
fn lookback_works() {
1567+
let _ = Proxy::http("127.0.0.1").unwrap();
1568+
}
1569+
1570+
#[test]
1571+
fn loopback_port_works() {
1572+
let _ = Proxy::http("127.0.0.1:8080").unwrap();
1573+
}
1574+
1575+
#[test]
1576+
fn loopback_username_works() {
1577+
let _ = Proxy::http("[email protected]").unwrap();
1578+
}
1579+
1580+
#[test]
1581+
fn loopback_username_password_works() {
1582+
let _ = Proxy::http("username:[email protected]").unwrap();
1583+
}
1584+
1585+
#[test]
1586+
fn loopback_username_password_port_works() {
1587+
let _ = Proxy::http("ldap%5Cgremlin:pass%[email protected]:8080").unwrap();
1588+
}
1589+
1590+
#[test]
1591+
fn domain_works() {
1592+
let _ = Proxy::http("proxy.example.com").unwrap();
1593+
}
1594+
1595+
#[test]
1596+
fn domain_port_works() {
1597+
let _ = Proxy::http("proxy.example.com:8080").unwrap();
1598+
}
1599+
1600+
#[test]
1601+
fn domain_username_works() {
1602+
let _ = Proxy::http("[email protected]").unwrap();
1603+
}
1604+
1605+
#[test]
1606+
fn domain_username_password_works() {
1607+
let _ = Proxy::http("username:[email protected]").unwrap();
1608+
}
1609+
1610+
#[test]
1611+
fn domain_username_password_port_works() {
1612+
let _ =
1613+
Proxy::http("ldap%5Cgremlin:pass%[email protected]:8080").unwrap();
1614+
}
1615+
}
1616+
mod and_url_has_bad {
1617+
use super::super::check_parse_error;
1618+
1619+
#[test]
1620+
fn host() {
1621+
check_parse_error("username@", url::ParseError::RelativeUrlWithoutBase);
1622+
}
1623+
1624+
#[test]
1625+
fn idna_encoding() {
1626+
check_parse_error("xn---", url::ParseError::RelativeUrlWithoutBase);
1627+
}
1628+
1629+
#[test]
1630+
fn port() {
1631+
check_parse_error("127.0.0.1:808080", url::ParseError::RelativeUrlWithoutBase);
1632+
}
1633+
1634+
#[test]
1635+
fn ip_v4_address() {
1636+
check_parse_error("421.627.718.469", url::ParseError::RelativeUrlWithoutBase);
1637+
}
1638+
1639+
#[test]
1640+
fn ip_v6_address() {
1641+
check_parse_error(
1642+
"[56FE::2159:5BBC::6594]",
1643+
url::ParseError::RelativeUrlWithoutBase,
1644+
);
1645+
}
1646+
1647+
#[test]
1648+
fn invalid_domain_character() {
1649+
check_parse_error("abc 123", url::ParseError::RelativeUrlWithoutBase);
1650+
}
1651+
}
1652+
}
1653+
1654+
mod when_scheme_present {
1655+
mod and_url_is_valid {
1656+
use crate::Proxy;
1657+
1658+
#[test]
1659+
fn loopback_works() {
1660+
let _ = Proxy::http("http://127.0.0.1").unwrap();
1661+
}
1662+
1663+
#[test]
1664+
fn loopback_port_works() {
1665+
let _ = Proxy::http("https://127.0.0.1:8080").unwrap();
1666+
}
1667+
1668+
#[test]
1669+
fn loopback_username_works() {
1670+
let _ = Proxy::http("http://[email protected]").unwrap();
1671+
}
1672+
1673+
#[test]
1674+
fn loopback_username_password_works() {
1675+
let _ = Proxy::http("https://username:[email protected]").unwrap();
1676+
}
1677+
1678+
#[test]
1679+
fn loopback_username_password_port_works() {
1680+
let _ =
1681+
Proxy::http("http://ldap%5Cgremlin:pass%[email protected]:8080").unwrap();
1682+
}
1683+
1684+
#[test]
1685+
fn domain_works() {
1686+
let _ = Proxy::http("https://proxy.example.com").unwrap();
1687+
}
1688+
1689+
#[test]
1690+
fn domain_port_works() {
1691+
let _ = Proxy::http("http://proxy.example.com:8080").unwrap();
1692+
}
1693+
1694+
#[test]
1695+
fn domain_username_works() {
1696+
let _ = Proxy::http("https://[email protected]").unwrap();
1697+
}
1698+
1699+
#[test]
1700+
fn domain_username_password_works() {
1701+
let _ = Proxy::http("http://username:[email protected]").unwrap();
1702+
}
1703+
1704+
#[test]
1705+
fn domain_username_password_port_works() {
1706+
let _ =
1707+
Proxy::http("https://ldap%5Cgremlin:pass%[email protected]:8080")
1708+
.unwrap();
1709+
}
1710+
}
1711+
mod and_url_has_bad {
1712+
use super::super::check_parse_error;
1713+
1714+
#[test]
1715+
fn host() {
1716+
check_parse_error("http://username@", url::ParseError::EmptyHost);
1717+
}
1718+
1719+
#[test]
1720+
fn idna_encoding() {
1721+
check_parse_error("http://xn---", url::ParseError::IdnaError);
1722+
}
1723+
1724+
#[test]
1725+
fn port() {
1726+
check_parse_error("http://127.0.0.1:808080", url::ParseError::InvalidPort);
1727+
}
1728+
1729+
#[test]
1730+
fn ip_v4_address() {
1731+
check_parse_error(
1732+
"http://421.627.718.469",
1733+
url::ParseError::InvalidIpv4Address,
1734+
);
1735+
}
1736+
1737+
#[test]
1738+
fn ip_v6_address() {
1739+
check_parse_error(
1740+
"http://[56FE::2159:5BBC::6594]",
1741+
url::ParseError::InvalidIpv6Address,
1742+
);
1743+
}
1744+
1745+
#[test]
1746+
fn invalid_domain_character() {
1747+
check_parse_error("http://abc 123/", url::ParseError::InvalidDomainCharacter);
1748+
}
1749+
}
1750+
}
1751+
}
1752+
}

0 commit comments

Comments
 (0)