From 577d06c3635840a510f22f234962bee9847e01f8 Mon Sep 17 00:00:00 2001 From: WindSoilder Date: Tue, 2 Jul 2019 07:27:58 +0800 Subject: [PATCH] Add support for system/environment proxies (#547) --- .appveyor.yml | 2 +- .travis.yml | 2 +- Cargo.toml | 3 + src/async_impl/client.rs | 21 +++++++ src/client.rs | 15 ++++- src/lib.rs | 4 ++ src/proxy.rs | 129 +++++++++++++++++++++++++++++++++++++++ tests/client.rs | 2 +- tests/proxy.rs | 82 +++++++++++++++++++++++++ 9 files changed, 255 insertions(+), 5 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 7e05160..9d0f3dd 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -15,5 +15,5 @@ install: - cargo -vV build: false test_script: - - cargo test + - cargo test -- --test-threads=1 skip_branch_with_pr: true diff --git a/.travis.yml b/.travis.yml index 95a3708..a5ac7b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,4 +58,4 @@ env: script: - cargo build $FEATURES - - cargo test -v $FEATURES + - cargo test -v $FEATURES -- --test-threads=1 diff --git a/Cargo.toml b/Cargo.toml index 50db374..79e2c8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,3 +72,6 @@ rustls-tls = ["hyper-rustls", "tokio-rustls", "webpki-roots", "rustls", "tls"] trust-dns = ["trust-dns-resolver"] hyper-011 = ["hyper-old-types"] + +[target.'cfg(windows)'.dependencies] +winreg = "0.6" diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index a04399d..c69588f 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -36,6 +36,7 @@ use into_url::{expect_uri, try_uri}; use cookie; use redirect::{self, RedirectPolicy, remove_sensitive_headers}; use {IntoUrl, Method, Proxy, StatusCode, Url}; +use ::proxy::get_proxies; #[cfg(feature = "tls")] use {Certificate, Identity}; #[cfg(feature = "tls")] @@ -327,6 +328,26 @@ impl ClientBuilder { self } + /// Clear all `Proxies`, so `Client` will use no proxy anymore. + pub fn no_proxy(mut self) -> ClientBuilder { + self.config.proxies.clear(); + self + } + + /// Add system proxy setting to the list of proxies + pub fn use_sys_proxy(mut self) -> ClientBuilder { + let proxies = get_proxies(); + self.config.proxies.push(Proxy::custom(move |url| { + return if proxies.contains_key(url.scheme()) { + Some((*proxies.get(url.scheme()).unwrap()).clone()) + } else { + None + } + })); + self + } + + /// Set a `RedirectPolicy` for this client. /// /// Default will follow redirects up to a maximum of 10. diff --git a/src/client.rs b/src/client.rs index c042df0..7d82016 100644 --- a/src/client.rs +++ b/src/client.rs @@ -84,6 +84,16 @@ impl ClientBuilder { }) } + /// Disable proxy setting. + pub fn no_proxy(self) -> ClientBuilder { + self.with_inner(move |inner| inner.no_proxy()) + } + + /// Enable system proxy setting. + pub fn use_sys_proxy(self) -> ClientBuilder { + self.with_inner(move |inner| inner.use_sys_proxy()) + } + /// Set that all sockets have `SO_NODELAY` set to `true`. pub fn tcp_nodelay(self) -> ClientBuilder { self.with_inner(move |inner| inner.tcp_nodelay()) @@ -369,7 +379,7 @@ impl ClientBuilder { /// Enable a persistent cookie store for the client. /// - /// Cookies received in responses will be preserved and included in + /// Cookies received in responses will be preserved and included in /// additional requests. /// /// By default, no cookie store is used. @@ -406,7 +416,8 @@ impl Client { /// Creates a `ClientBuilder` to configure a `Client`. /// - /// This is the same as `ClientBuilder::new()`. + /// This builder will use system proxy setting, you can use + /// `reqwest::Client::builder().no_proxy()` to disable it. pub fn builder() -> ClientBuilder { ClientBuilder::new() } diff --git a/src/lib.rs b/src/lib.rs index 9b765b7..c2c87fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,6 +133,8 @@ //! A `Client` can be configured to make use of HTTP proxies by adding //! [`Proxy`](Proxy)s to a `ClientBuilder`. //! +//! ** NOTE** System proxies will be used in the next breaking change. +//! //! ## TLS //! //! By default, a `Client` will make use of system-native transport layer @@ -206,6 +208,8 @@ extern crate url; extern crate uuid; #[cfg(feature = "socks")] extern crate socks; +#[cfg(target_os = "windows")] +extern crate winreg; #[cfg(feature = "rustls-tls")] extern crate hyper_rustls; diff --git a/src/proxy.rs b/src/proxy.rs index a974e45..2576325 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -7,6 +7,14 @@ use http::{header::HeaderValue, Uri}; use hyper::client::connect::Destination; use url::percent_encoding::percent_decode; use {IntoUrl, Url}; +use std::collections::HashMap; +use std::env; +#[cfg(target_os = "windows")] +use std::error::Error; +#[cfg(target_os = "windows")] +use winreg::enums::HKEY_CURRENT_USER; +#[cfg(target_os = "windows")] +use winreg::RegKey; /// Configuration of a proxy that a `Client` should pass requests to. /// @@ -469,6 +477,101 @@ impl Dst for Uri { } } +/// Get system proxies information. +/// +/// It can only support Linux, Unix like, and windows system. Note that it will always +/// return a HashMap, even if something runs into error when find registry information in +/// Windows system. Note that invalid proxy url in the system setting will be ignored. +/// +/// Returns: +/// System proxies information as a hashmap like +/// {"http": Url::parse("http://127.0.0.1:80"), "https": Url::parse("https://127.0.0.1:80")} +pub fn get_proxies() -> HashMap { + let proxies: HashMap = get_from_environment(); + + // TODO: move the following #[cfg] to `if expression` when attributes on `if` expressions allowed + #[cfg(target_os = "windows")] + { + if proxies.is_empty() { + // don't care errors if can't get proxies from registry, just return an empty HashMap. + return get_from_registry(); + } + } + proxies +} + +fn insert_proxy(proxies: &mut HashMap, schema: String, addr: String) +{ + if let Ok(valid_addr) = Url::parse(&addr) { + proxies.insert(schema, valid_addr); + } +} + +fn get_from_environment() -> HashMap { + let mut proxies: HashMap = HashMap::new(); + + const PROXY_KEY_ENDS: &str = "_proxy"; + + for (key, value) in env::vars() { + let key: String = key.to_lowercase(); + if key.ends_with(PROXY_KEY_ENDS) { + let end_indx = key.len() - PROXY_KEY_ENDS.len(); + let schema = &key[..end_indx]; + insert_proxy(&mut proxies, String::from(schema), String::from(value)); + } + } + proxies +} + + +#[cfg(target_os = "windows")] +fn get_from_registry_impl() -> Result, Box> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let internet_setting: RegKey = + hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?; + // ensure the proxy is enable, if the value doesn't exist, an error will returned. + let proxy_enable: u32 = internet_setting.get_value("ProxyEnable")?; + let proxy_server: String = internet_setting.get_value("ProxyServer")?; + + if proxy_enable == 0 { + return Ok(HashMap::new()); + } + + let mut proxies: HashMap = HashMap::new(); + if proxy_server.contains("=") { + // per-protocol settings. + for p in proxy_server.split(";") { + let protocol_parts: Vec<&str> = p.split("=").collect(); + match protocol_parts.as_slice() { + [protocol, address] => { + insert_proxy(&mut proxies, String::from(*protocol), String::from(*address)); + } + _ => { + // Contains invalid protocol setting, just break the loop + // And make proxies to be empty. + proxies.clear(); + break; + } + } + } + } else { + // Use one setting for all protocols. + if proxy_server.starts_with("http:") { + insert_proxy(&mut proxies, String::from("http"), proxy_server); + } else { + insert_proxy(&mut proxies, String::from("http"), format!("http://{}", proxy_server)); + insert_proxy(&mut proxies, String::from("https"), format!("https://{}", proxy_server)); + insert_proxy(&mut proxies, String::from("ftp"), format!("https://{}", proxy_server)); + } + } + Ok(proxies) +} + +#[cfg(target_os = "windows")] +fn get_from_registry() -> HashMap { + get_from_registry_impl().unwrap_or(HashMap::new()) +} + #[cfg(test)] mod tests { use super::*; @@ -562,4 +665,30 @@ mod tests { assert_eq!(intercepted_uri(&p, https), target1); assert!(p.intercept(&url(other)).is_none()); } + + #[test] + fn test_get_proxies() { + // save system setting first. + let system_proxy = env::var("http_proxy"); + + // remove proxy. + env::remove_var("http_proxy"); + assert_eq!(get_proxies().contains_key("http"), false); + + // the system proxy setting url is invalid. + env::set_var("http_proxy", "123465"); + assert_eq!(get_proxies().contains_key("http"), false); + + // set valid proxy + env::set_var("http_proxy", "http://127.0.0.1/"); + let proxies = get_proxies(); + let http_target = proxies.get("http").unwrap().as_str(); + + assert_eq!(http_target, "http://127.0.0.1/"); + // reset user setting. + match system_proxy { + Err(_) => env::remove_var("http_proxy"), + Ok(proxy) => env::set_var("http_proxy", proxy) + } + } } diff --git a/tests/client.rs b/tests/client.rs index 084c381..b5ee6c9 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -432,7 +432,7 @@ fn test_appended_headers_not_overwritten() { let client = reqwest::Client::builder() .default_headers(headers) .build().unwrap(); - + let server = server! { request: b"\ GET /4 HTTP/1.1\r\n\ diff --git a/tests/proxy.rs b/tests/proxy.rs index 9663c15..038acde 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -3,6 +3,8 @@ extern crate reqwest; #[macro_use] mod support; +use std::env; + #[test] fn http_proxy() { let server = server! { @@ -115,3 +117,83 @@ fn http_proxy_basic_auth_parsed() { assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!(res.headers().get(reqwest::header::SERVER).unwrap(), &"proxied"); } + +#[test] +fn test_no_proxy() { + let server = server! { + request: b"\ + GET /4 HTTP/1.1\r\n\ + user-agent: $USERAGENT\r\n\ + accept: */*\r\n\ + accept-encoding: gzip\r\n\ + host: $HOST\r\n\ + \r\n\ + ", + response: b"\ + HTTP/1.1 200 OK\r\n\ + Server: test\r\n\ + Content-Length: 0\r\n\ + \r\n\ + " + }; + let proxy = format!("http://{}", server.addr()); + let url = format!("http://{}/4", server.addr()); + + // set up proxy and use no_proxy to clear up client builder proxies. + let res = reqwest::Client::builder() + .proxy( + reqwest::Proxy::http(&proxy).unwrap() + ) + .no_proxy() + .build() + .unwrap() + .get(&url) + .send() + .unwrap(); + + assert_eq!(res.url().as_str(), &url); + assert_eq!(res.status(), reqwest::StatusCode::OK); +} + +#[test] +fn test_using_system_proxy() { + let server = server! { + request: b"\ + GET http://hyper.rs/prox HTTP/1.1\r\n\ + user-agent: $USERAGENT\r\n\ + accept: */*\r\n\ + accept-encoding: gzip\r\n\ + host: hyper.rs\r\n\ + \r\n\ + ", + response: b"\ + HTTP/1.1 200 OK\r\n\ + Server: proxied\r\n\ + Content-Length: 0\r\n\ + \r\n\ + " + }; + // save system setting first. + let system_proxy = env::var("http_proxy"); + // set-up http proxy. + env::set_var("http_proxy", format!("http://{}", server.addr())); + + let url = "http://hyper.rs/prox"; + let res = reqwest::Client::builder() + .use_sys_proxy() + .build() + .unwrap() + .get(url) + .send() + .unwrap(); + + assert_eq!(res.url().as_str(), url); + assert_eq!(res.status(), reqwest::StatusCode::OK); + assert_eq!(res.headers().get(reqwest::header::SERVER).unwrap(), &"proxied"); + + // reset user setting. + match system_proxy { + Err(_) => env::remove_var("http_proxy"), + Ok(proxy) => env::set_var("http_proxy", proxy) + } +}