Skip to content

Commit 57670ac

Browse files
authored
feat: add customizable headers for reqwest::Proxy (#2600)
1 parent d9cf60e commit 57670ac

File tree

4 files changed

+169
-3
lines changed

4 files changed

+169
-3
lines changed

src/async_impl/client.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,8 @@ impl ClientBuilder {
955955
}
956956

957957
let proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth());
958+
let proxies_maybe_http_custom_headers =
959+
proxies.iter().any(|p| p.maybe_has_http_custom_headers());
958960

959961
let redirect_policy_desc = if config.redirect_policy.is_default() {
960962
None
@@ -1007,6 +1009,7 @@ impl ClientBuilder {
10071009
hyper,
10081010
proxies,
10091011
proxies_maybe_http_auth,
1012+
proxies_maybe_http_custom_headers,
10101013
https_only: config.https_only,
10111014
redirect_policy_desc,
10121015
}),
@@ -2449,6 +2452,7 @@ impl Client {
24492452
};
24502453

24512454
self.proxy_auth(&uri, &mut headers);
2455+
self.proxy_custom_headers(&uri, &mut headers);
24522456

24532457
let builder = hyper::Request::builder()
24542458
.method(method.clone())
@@ -2528,6 +2532,26 @@ impl Client {
25282532
break;
25292533
}
25302534
}
2535+
2536+
fn proxy_custom_headers(&self, dst: &Uri, headers: &mut HeaderMap) {
2537+
if !self.inner.proxies_maybe_http_custom_headers {
2538+
return;
2539+
}
2540+
2541+
if dst.scheme() != Some(&Scheme::HTTP) {
2542+
return;
2543+
}
2544+
2545+
for proxy in self.inner.proxies.iter() {
2546+
if let Some(iter) = proxy.http_non_tunnel_custom_headers(dst) {
2547+
iter.iter().for_each(|(key, value)| {
2548+
headers.insert(key, value.clone());
2549+
});
2550+
}
2551+
2552+
break;
2553+
}
2554+
}
25312555
}
25322556

25332557
impl fmt::Debug for Client {
@@ -2716,6 +2740,7 @@ struct ClientRef {
27162740
read_timeout: Option<Duration>,
27172741
proxies: Arc<Vec<ProxyMatcher>>,
27182742
proxies_maybe_http_auth: bool,
2743+
proxies_maybe_http_custom_headers: bool,
27192744
https_only: bool,
27202745
redirect_policy_desc: Option<String>,
27212746
}

src/connect.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,9 @@ impl ConnectorService {
628628
#[cfg(feature = "__tls")]
629629
let auth = proxy.basic_auth().cloned();
630630

631+
#[cfg(feature = "__tls")]
632+
let misc = proxy.custom_headers().clone();
633+
631634
match &self.inner {
632635
#[cfg(feature = "default-tls")]
633636
Inner::DefaultTls(http, tls) => {
@@ -647,6 +650,10 @@ impl ConnectorService {
647650
headers.insert(http::header::USER_AGENT, ua);
648651
tunnel = tunnel.with_headers(headers);
649652
}
653+
// Note that custom headers may override the user agent header.
654+
if let Some(custom_headers) = misc {
655+
tunnel = tunnel.with_headers(custom_headers.clone());
656+
}
650657
// We don't wrap this again in an HttpsConnector since that uses Maybe,
651658
// and we know this is definitely HTTPS.
652659
let tunneled = tunnel.call(dst.clone()).await?;
@@ -683,6 +690,9 @@ impl ConnectorService {
683690
if let Some(auth) = auth {
684691
tunnel = tunnel.with_auth(auth);
685692
}
693+
if let Some(custom_headers) = misc {
694+
tunnel = tunnel.with_headers(custom_headers.clone());
695+
}
686696
if let Some(ua) = self.user_agent {
687697
let mut headers = http::HeaderMap::new();
688698
headers.insert(http::header::USER_AGENT, ua);

src/proxy.rs

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::error::Error;
22
use std::fmt;
33
use std::sync::Arc;
44

5-
use http::{header::HeaderValue, Uri};
5+
use http::{header::HeaderValue, HeaderMap, Uri};
66
use hyper_util::client::proxy::matcher;
77

88
use crate::into_url::{IntoUrl, IntoUrlSealed};
@@ -67,6 +67,7 @@ pub struct NoProxy {
6767
#[derive(Clone)]
6868
struct Extra {
6969
auth: Option<HeaderValue>,
70+
misc: Option<HeaderMap>,
7071
}
7172

7273
// ===== Internal =====
@@ -75,6 +76,7 @@ pub(crate) struct Matcher {
7576
inner: Matcher_,
7677
extra: Extra,
7778
maybe_has_http_auth: bool,
79+
maybe_has_http_custom_headers: bool,
7880
}
7981

8082
enum Matcher_ {
@@ -100,6 +102,14 @@ impl ProxyScheme {
100102
_ => None,
101103
}
102104
}
105+
106+
fn maybe_http_custom_headers(&self) -> Option<&HeaderMap> {
107+
match self {
108+
ProxyScheme::Http { misc, .. } | ProxyScheme::Https { misc, .. } => misc.as_ref(),
109+
#[cfg(feature = "socks")]
110+
_ => None,
111+
}
112+
}
103113
}
104114
*/
105115

@@ -245,7 +255,10 @@ impl Proxy {
245255

246256
fn new(intercept: Intercept) -> Proxy {
247257
Proxy {
248-
extra: Extra { auth: None },
258+
extra: Extra {
259+
auth: None,
260+
misc: None,
261+
},
249262
intercept,
250263
no_proxy: None,
251264
}
@@ -297,6 +310,32 @@ impl Proxy {
297310
self
298311
}
299312

313+
/// Adds a Custom Headers to Proxy
314+
/// Adds custom headers to this Proxy
315+
///
316+
/// # Example
317+
/// ```
318+
/// # extern crate reqwest;
319+
/// # use reqwest::header::*;
320+
/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
321+
/// let mut headers = HeaderMap::new();
322+
/// headers.insert(USER_AGENT, "reqwest".parse().unwrap());
323+
/// let proxy = reqwest::Proxy::https("http://localhost:1234")?
324+
/// .headers(headers);
325+
/// # Ok(())
326+
/// # }
327+
/// # fn main() {}
328+
/// ```
329+
pub fn headers(mut self, headers: HeaderMap) -> Proxy {
330+
match self.intercept {
331+
Intercept::All(_) | Intercept::Http(_) | Intercept::Https(_) | Intercept::Custom(_) => {
332+
self.extra.misc = Some(headers);
333+
}
334+
}
335+
336+
self
337+
}
338+
300339
/// Adds a `No Proxy` exclusion list to this Proxy
301340
///
302341
/// # Example
@@ -323,10 +362,13 @@ impl Proxy {
323362
} = self;
324363

325364
let maybe_has_http_auth;
365+
let maybe_has_http_custom_headers;
326366

327367
let inner = match intercept {
328368
Intercept::All(url) => {
329369
maybe_has_http_auth = cache_maybe_has_http_auth(&url, &extra.auth);
370+
maybe_has_http_custom_headers =
371+
cache_maybe_has_http_custom_headers(&url, &extra.misc);
330372
Matcher_::Util(
331373
matcher::Matcher::builder()
332374
.all(String::from(url))
@@ -336,6 +378,8 @@ impl Proxy {
336378
}
337379
Intercept::Http(url) => {
338380
maybe_has_http_auth = cache_maybe_has_http_auth(&url, &extra.auth);
381+
maybe_has_http_custom_headers =
382+
cache_maybe_has_http_custom_headers(&url, &extra.misc);
339383
Matcher_::Util(
340384
matcher::Matcher::builder()
341385
.http(String::from(url))
@@ -345,6 +389,8 @@ impl Proxy {
345389
}
346390
Intercept::Https(url) => {
347391
maybe_has_http_auth = cache_maybe_has_http_auth(&url, &extra.auth);
392+
maybe_has_http_custom_headers =
393+
cache_maybe_has_http_custom_headers(&url, &extra.misc);
348394
Matcher_::Util(
349395
matcher::Matcher::builder()
350396
.https(String::from(url))
@@ -354,6 +400,7 @@ impl Proxy {
354400
}
355401
Intercept::Custom(mut custom) => {
356402
maybe_has_http_auth = true; // never know
403+
maybe_has_http_custom_headers = true;
357404
custom.no_proxy = no_proxy;
358405
Matcher_::Custom(custom)
359406
}
@@ -363,6 +410,7 @@ impl Proxy {
363410
inner,
364411
extra,
365412
maybe_has_http_auth,
413+
maybe_has_http_custom_headers,
366414
}
367415
}
368416

@@ -399,6 +447,10 @@ fn cache_maybe_has_http_auth(url: &Url, extra: &Option<HeaderValue>) -> bool {
399447
url.scheme() == "http" && (url.password().is_some() || extra.is_some())
400448
}
401449

450+
fn cache_maybe_has_http_custom_headers(url: &Url, extra: &Option<HeaderMap>) -> bool {
451+
url.scheme() == "http" && extra.is_some()
452+
}
453+
402454
impl fmt::Debug for Proxy {
403455
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
404456
f.debug_tuple("Proxy")
@@ -453,9 +505,13 @@ impl Matcher {
453505
pub(crate) fn system() -> Self {
454506
Self {
455507
inner: Matcher_::Util(matcher::Matcher::from_system()),
456-
extra: Extra { auth: None },
508+
extra: Extra {
509+
auth: None,
510+
misc: None,
511+
},
457512
// maybe env vars have auth!
458513
maybe_has_http_auth: true,
514+
maybe_has_http_custom_headers: true,
459515
}
460516
}
461517

@@ -493,6 +549,20 @@ impl Matcher {
493549

494550
None
495551
}
552+
553+
pub(crate) fn maybe_has_http_custom_headers(&self) -> bool {
554+
self.maybe_has_http_custom_headers
555+
}
556+
557+
pub(crate) fn http_non_tunnel_custom_headers(&self, dst: &Uri) -> Option<HeaderMap> {
558+
if let Some(proxy) = self.intercept(dst) {
559+
if proxy.uri().scheme_str() == Some("http") {
560+
return proxy.custom_headers().cloned();
561+
}
562+
}
563+
564+
None
565+
}
496566
}
497567

498568
impl fmt::Debug for Matcher {
@@ -516,6 +586,13 @@ impl Intercepted {
516586
self.inner.basic_auth()
517587
}
518588

589+
pub(crate) fn custom_headers(&self) -> Option<&HeaderMap> {
590+
if let Some(ref val) = self.extra.misc {
591+
return Some(val);
592+
}
593+
None
594+
}
595+
519596
#[cfg(feature = "socks")]
520597
pub(crate) fn raw_auth(&self) -> Option<(&str, &str)> {
521598
self.inner.raw_auth()
@@ -580,6 +657,25 @@ impl ProxyScheme {
580657
}
581658
}
582659
660+
fn set_custom_headers(&mut self, headers: HeaderMap) {
661+
match *self {
662+
ProxyScheme::Http { ref mut misc, .. } => {
663+
misc.get_or_insert_with(HeaderMap::new).extend(headers)
664+
}
665+
ProxyScheme::Https { ref mut misc, .. } => {
666+
misc.get_or_insert_with(HeaderMap::new).extend(headers)
667+
}
668+
#[cfg(feature = "socks")]
669+
ProxyScheme::Socks4 { .. } => {
670+
panic!("Socks4 is not supported for this method")
671+
}
672+
#[cfg(feature = "socks")]
673+
ProxyScheme::Socks5 { .. } => {
674+
panic!("Socks5 is not supported for this method")
675+
}
676+
}
677+
}
678+
583679
fn if_no_auth(mut self, update: &Option<HeaderValue>) -> Self {
584680
match self {
585681
ProxyScheme::Http { ref mut auth, .. } => {

tests/proxy.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,41 @@ async fn test_no_proxy() {
172172
assert_eq!(res.status(), reqwest::StatusCode::OK);
173173
}
174174

175+
#[tokio::test]
176+
async fn test_custom_headers() {
177+
let url = "http://hyper.rs.local/prox";
178+
let server = server::http(move |req| {
179+
assert_eq!(req.method(), "GET");
180+
assert_eq!(req.uri(), url);
181+
assert_eq!(req.headers()["host"], "hyper.rs.local");
182+
assert_eq!(
183+
req.headers()["proxy-authorization"],
184+
"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
185+
);
186+
async { http::Response::default() }
187+
});
188+
189+
let proxy = format!("http://{}", server.addr());
190+
let mut headers = reqwest::header::HeaderMap::new();
191+
headers.insert(
192+
// reqwest::header::HeaderName::from_static("Proxy-Authorization"),
193+
reqwest::header::PROXY_AUTHORIZATION,
194+
"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==".parse().unwrap(),
195+
);
196+
197+
let res = reqwest::Client::builder()
198+
.proxy(reqwest::Proxy::http(&proxy).unwrap().headers(headers))
199+
.build()
200+
.unwrap()
201+
.get(url)
202+
.send()
203+
.await
204+
.unwrap();
205+
206+
assert_eq!(res.url().as_str(), url);
207+
assert_eq!(res.status(), reqwest::StatusCode::OK);
208+
}
209+
175210
#[tokio::test]
176211
async fn test_using_system_proxy() {
177212
let url = "http://not.a.real.sub.hyper.rs.local/prox";

0 commit comments

Comments
 (0)