Skip to content

Commit 672b8b9

Browse files
committed
feat: SetHost and Http1RequestTarget middlewares
1 parent 55b5171 commit 672b8b9

File tree

5 files changed

+146
-1
lines changed

5 files changed

+146
-1
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ repository = "https://github.com/hyperium/hyper-util"
99
license = "MIT"
1010
authors = ["Sean McArthur <[email protected]>"]
1111
keywords = ["http", "hyper", "hyperium"]
12-
categories = ["network-programming", "web-programming::http-client", "web-programming::http-server"]
12+
categories = [
13+
"network-programming",
14+
"web-programming::http-client",
15+
"web-programming::http-server",
16+
]
1317
edition = "2018"
1418

1519
[package.metadata.docs.rs]

src/client/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
/// Legacy implementations of `connect` module and `Client`
44
#[cfg(feature = "client-legacy")]
55
pub mod legacy;
6+
#[cfg(any(feature = "http1", feature = "http2"))]
7+
pub mod services;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use http::{uri::Scheme, Method, Request, Uri};
2+
use hyper::service::Service;
3+
use tracing::warn;
4+
5+
pub struct Http1RequestTarget<S> {
6+
inner: S,
7+
is_proxied: bool,
8+
}
9+
10+
impl<S> Http1RequestTarget<S> {
11+
pub fn new(inner: S, is_proxied: bool) -> Self {
12+
Self { inner, is_proxied }
13+
}
14+
}
15+
16+
impl<S, B> Service<Request<B>> for Http1RequestTarget<S>
17+
where
18+
S: Service<Request<B>>,
19+
{
20+
type Response = S::Response;
21+
type Error = S::Error;
22+
type Future = S::Future;
23+
24+
fn call(&self, mut req: Request<B>) -> Self::Future {
25+
// CONNECT always sends authority-form, so check it first...
26+
if req.method() == Method::CONNECT {
27+
authority_form(req.uri_mut());
28+
} else if self.is_proxied {
29+
absolute_form(req.uri_mut());
30+
} else {
31+
origin_form(req.uri_mut());
32+
}
33+
self.inner.call(req)
34+
}
35+
}
36+
37+
fn origin_form(uri: &mut Uri) {
38+
let path = match uri.path_and_query() {
39+
Some(path) if path.as_str() != "/" => {
40+
let mut parts = ::http::uri::Parts::default();
41+
parts.path_and_query = Some(path.clone());
42+
Uri::from_parts(parts).expect("path is valid uri")
43+
}
44+
_none_or_just_slash => {
45+
debug_assert!(Uri::default() == "/");
46+
Uri::default()
47+
}
48+
};
49+
*uri = path
50+
}
51+
52+
fn absolute_form(uri: &mut Uri) {
53+
debug_assert!(uri.scheme().is_some(), "absolute_form needs a scheme");
54+
debug_assert!(
55+
uri.authority().is_some(),
56+
"absolute_form needs an authority"
57+
);
58+
// If the URI is to HTTPS, and the connector claimed to be a proxy,
59+
// then it *should* have tunneled, and so we don't want to send
60+
// absolute-form in that case.
61+
if uri.scheme() == Some(&Scheme::HTTPS) {
62+
origin_form(uri);
63+
}
64+
}
65+
66+
fn authority_form(uri: &mut Uri) {
67+
if let Some(path) = uri.path_and_query() {
68+
// `https://hyper.rs` would parse with `/` path, don't
69+
// annoy people about that...
70+
if path != "/" {
71+
warn!("HTTP/1.1 CONNECT request stripping path: {:?}", path);
72+
}
73+
}
74+
*uri = match uri.authority() {
75+
Some(auth) => {
76+
let mut parts = ::http::uri::Parts::default();
77+
parts.authority = Some(auth.clone());
78+
Uri::from_parts(parts).expect("authority is valid")
79+
}
80+
None => {
81+
unreachable!("authority_form with relative uri");
82+
}
83+
};
84+
}

src/client/services/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod http1_request_target;
2+
mod set_host;
3+
4+
pub use http1_request_target::Http1RequestTarget;
5+
pub use set_host::SetHost;

src/client/services/set_host.rs

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use http::{header::HOST, uri::Port, HeaderValue, Request, Uri};
2+
use hyper::service::Service;
3+
4+
pub struct SetHost<S> {
5+
inner: S,
6+
}
7+
8+
impl<S> SetHost<S> {
9+
pub fn new(inner: S) -> Self {
10+
Self { inner }
11+
}
12+
}
13+
14+
impl<S, B> Service<Request<B>> for SetHost<S>
15+
where
16+
S: Service<Request<B>>,
17+
{
18+
type Response = S::Response;
19+
type Error = S::Error;
20+
type Future = S::Future;
21+
22+
fn call(&self, mut req: Request<B>) -> Self::Future {
23+
let uri = req.uri().clone();
24+
req.headers_mut().entry(HOST).or_insert_with(|| {
25+
let hostname = uri.host().expect("authority implies host");
26+
if let Some(port) = get_non_default_port(&uri) {
27+
let s = format!("{}:{}", hostname, port);
28+
HeaderValue::from_str(&s)
29+
} else {
30+
HeaderValue::from_str(hostname)
31+
}
32+
.expect("uri host is valid header value")
33+
});
34+
self.inner.call(req)
35+
}
36+
}
37+
38+
fn get_non_default_port(uri: &Uri) -> Option<Port<&str>> {
39+
match (uri.port().map(|p| p.as_u16()), is_schema_secure(uri)) {
40+
(Some(443), true) => None,
41+
(Some(80), false) => None,
42+
_ => uri.port(),
43+
}
44+
}
45+
46+
fn is_schema_secure(uri: &Uri) -> bool {
47+
uri.scheme_str()
48+
.map(|scheme_str| matches!(scheme_str, "wss" | "https"))
49+
.unwrap_or_default()
50+
}

0 commit comments

Comments
 (0)