Skip to main content

ic_bn_lib/http/
mod.rs

1pub mod body;
2pub mod cache;
3pub mod client;
4pub mod dns;
5pub mod headers;
6pub mod middleware;
7pub mod proxy;
8pub mod server;
9pub mod shed;
10
11use axum::response::{IntoResponse, Redirect};
12use http::{HeaderMap, Method, Request, StatusCode, Uri, Version, header::HOST, uri::PathAndQuery};
13
14#[cfg(feature = "clients-hyper")]
15pub use client::clients_hyper::{HyperClient, HyperClientLeastLoaded};
16pub use client::clients_reqwest::{
17    ReqwestClient, ReqwestClientLeastLoaded, ReqwestClientRoundRobin,
18};
19pub use server::{Server, ServerBuilder};
20use url::Url;
21
22use crate::{http::headers::X_FORWARDED_HOST, network::AsyncReadWrite};
23
24/// Calculate very approximate HTTP request/response headers size in bytes.
25/// More or less accurate only for http/1.1 since in h2 headers are in HPACK-compressed.
26/// But it seems there's no better way.
27pub fn calc_headers_size(h: &HeaderMap) -> usize {
28    h.iter().map(|(k, v)| k.as_str().len() + v.len() + 2).sum()
29}
30
31/// Get a static string representing given HTTP version
32pub const fn http_version(v: Version) -> &'static str {
33    match v {
34        Version::HTTP_09 => "0.9",
35        Version::HTTP_10 => "1.0",
36        Version::HTTP_11 => "1.1",
37        Version::HTTP_2 => "2.0",
38        Version::HTTP_3 => "3.0",
39        _ => "-",
40    }
41}
42
43/// Get a static string representing given HTTP method
44pub const fn http_method(v: &Method) -> &'static str {
45    match *v {
46        Method::OPTIONS => "OPTIONS",
47        Method::GET => "GET",
48        Method::POST => "POST",
49        Method::PUT => "PUT",
50        Method::DELETE => "DELETE",
51        Method::HEAD => "HEAD",
52        Method::TRACE => "TRACE",
53        Method::CONNECT => "CONNECT",
54        Method::PATCH => "PATCH",
55        _ => "",
56    }
57}
58
59/// Attempts to extract "host" from "host:port" format.
60/// Host can be either FQDN or IPv4/IPv6 address.
61pub fn extract_host(host_port: &str) -> Option<&str> {
62    if host_port.is_empty() {
63        return None;
64    }
65
66    // Cover IPv6 case
67    if host_port.as_bytes()[0] == b'[' {
68        host_port.find(']').map(|i| &host_port[1..i])
69    } else {
70        host_port.split(':').next()
71    }
72    .filter(|x| !x.is_empty())
73}
74
75/// Attempts to extract host from `X-Forwarded-Host` header, HTTP2 "authority" pseudo-header or from HTTP/1.1 `Host` header
76/// (in this order of preference)
77pub fn extract_authority<T>(request: &Request<T>) -> Option<&str> {
78    // Try `X-Forwarded-Host` header first
79    request
80        .headers()
81        .get(X_FORWARDED_HOST)
82        .and_then(|x| x.to_str().ok())
83        // Then URI authority
84        .or_else(|| request.uri().authority().map(|x| x.host()))
85        // THen `Host` header
86        .or_else(|| request.headers().get(HOST).and_then(|x| x.to_str().ok()))
87        // Extract host w/o port
88        .and_then(extract_host)
89}
90
91/// Error that might happen during Url to Uri conversion
92#[derive(thiserror::Error, Debug)]
93pub enum UrlToUriError {
94    #[error("No Authority")]
95    NoAuthority,
96    #[error("No Host")]
97    NoHost,
98    #[error(transparent)]
99    Http(#[from] http::Error),
100}
101
102/// Converts `Url` to `Uri`
103pub fn url_to_uri(url: &Url) -> Result<Uri, UrlToUriError> {
104    if !url.has_authority() {
105        return Err(UrlToUriError::NoAuthority);
106    }
107
108    if !url.has_host() {
109        return Err(UrlToUriError::NoHost);
110    }
111
112    let scheme = url.scheme();
113    let authority = url.authority();
114
115    let authority_end = scheme.len() + "://".len() + authority.len();
116    let path_and_query = &url.as_str()[authority_end..];
117
118    Uri::builder()
119        .scheme(scheme)
120        .authority(authority)
121        .path_and_query(path_and_query)
122        .build()
123        .map_err(UrlToUriError::Http)
124}
125
126/// Redirects any request to an HTTPS scheme
127pub async fn redirect_to_https(
128    request: axum::extract::Request,
129) -> Result<impl IntoResponse, impl IntoResponse> {
130    let host = extract_authority(&request)
131        .ok_or((StatusCode::BAD_REQUEST, "Unable to extract authority"))?;
132    let uri = request.uri().clone();
133
134    let fallback_path = PathAndQuery::from_static("/");
135    let pq = uri.path_and_query().unwrap_or(&fallback_path).as_str();
136
137    Ok::<_, (_, _)>(Redirect::permanent(
138        &Uri::builder()
139            .scheme("https")
140            .authority(host)
141            .path_and_query(pq)
142            .build()
143            .map_err(|_| (StatusCode::BAD_REQUEST, "Incorrect URL"))?
144            .to_string(),
145    ))
146}
147
148#[cfg(test)]
149mod test {
150    use axum::{Router, body::Body};
151    use http::{
152        Uri,
153        header::{HOST, LOCATION},
154    };
155    use tower::ServiceExt;
156
157    use crate::hval;
158
159    use super::*;
160
161    #[test]
162    fn test_extract_host() {
163        assert_eq!(extract_host("foo.bar"), Some("foo.bar"));
164        assert_eq!(extract_host("foo.bar:443"), Some("foo.bar"));
165        assert_eq!(extract_host("foo.bar:"), Some("foo.bar"));
166        assert_eq!(extract_host("foo:443"), Some("foo"));
167
168        assert_eq!(extract_host("127.0.0.1:443"), Some("127.0.0.1"));
169        assert_eq!(extract_host("[::1]:443"), Some("::1"));
170
171        assert_eq!(
172            extract_host("[fe80::b696:91ff:fe84:3ae8]"),
173            Some("fe80::b696:91ff:fe84:3ae8")
174        );
175        assert_eq!(
176            extract_host("[fe80::b696:91ff:fe84:3ae8]:123"),
177            Some("fe80::b696:91ff:fe84:3ae8")
178        );
179
180        // Unterminated bracket
181        assert_eq!(extract_host("[fe80::b696:91ff:fe84:3ae8:123"), None);
182        // Empty
183        assert_eq!(extract_host(""), None);
184        assert_eq!(extract_host("[]:443"), None);
185    }
186
187    #[test]
188    fn test_extract_authority() {
189        // No authority & no host header
190        let mut req = Request::new(());
191        *req.uri_mut() = Uri::builder()
192            .path_and_query("/foo?bar=baz")
193            .build()
194            .unwrap();
195        assert_eq!(extract_authority(&req), None);
196
197        // Authority
198        let mut req = Request::new(());
199        *req.uri_mut() = Uri::builder()
200            .scheme("http")
201            .authority("foo.bar:443")
202            .path_and_query("/foo?bar=baz")
203            .build()
204            .unwrap();
205        assert_eq!(extract_authority(&req), Some("foo.bar"));
206
207        let mut req = Request::new(());
208        *req.uri_mut() = Uri::builder()
209            .scheme("http")
210            .authority("[::1]:443")
211            .path_and_query("/foo?bar=baz")
212            .build()
213            .unwrap();
214        assert_eq!(extract_authority(&req), Some("::1"));
215
216        // Host header
217        let mut req = Request::new(());
218        *req.uri_mut() = Uri::builder()
219            .path_and_query("/foo?bar=baz")
220            .build()
221            .unwrap();
222        (*req.headers_mut()).insert(HOST, hval!("foo.baz:443"));
223        assert_eq!(extract_authority(&req), Some("foo.baz"));
224
225        // XFH header
226        let mut req = Request::new(());
227        *req.uri_mut() = Uri::builder()
228            .path_and_query("/foo?bar=baz")
229            .build()
230            .unwrap();
231        (*req.headers_mut()).insert(X_FORWARDED_HOST, hval!("foo.baz:443"));
232        assert_eq!(extract_authority(&req), Some("foo.baz"));
233
234        // Host+Authority: authority should take precedence
235        let mut req = Request::new(());
236        *req.uri_mut() = Uri::builder()
237            .scheme("http")
238            .authority("foo.bar:443")
239            .path_and_query("/foo?bar=baz")
240            .build()
241            .unwrap();
242        (*req.headers_mut()).insert(HOST, hval!("foo.baz:443"));
243        assert_eq!(extract_authority(&req), Some("foo.bar"));
244
245        // XFH+Host+Authority: XFH should take precedence
246        let mut req = Request::new(());
247        *req.uri_mut() = Uri::builder()
248            .scheme("http")
249            .authority("foo.bar:443")
250            .path_and_query("/foo?bar=baz")
251            .build()
252            .unwrap();
253        (*req.headers_mut()).insert(HOST, hval!("foo.baz:443"));
254        (*req.headers_mut()).insert(X_FORWARDED_HOST, hval!("dead.beef:443"));
255        assert_eq!(extract_authority(&req), Some("dead.beef"));
256    }
257
258    #[test]
259    fn test_url_to_uri() {
260        let url = "https://foo.bar/baz?dead=beef".parse().unwrap();
261
262        assert_eq!(
263            url_to_uri(&url).unwrap(),
264            Uri::from_static("https://foo.bar/baz?dead=beef")
265        );
266
267        let url = "unix:/foo/bar".parse().unwrap();
268        assert!(url_to_uri(&url).is_err());
269    }
270
271    #[tokio::test]
272    async fn test_redirect_to_https() {
273        let mut request = axum::extract::Request::new(Body::empty());
274        *request.uri_mut() = Uri::from_static("http://foo/bar/baz.bin?a=b");
275
276        let router = Router::new().fallback(redirect_to_https);
277
278        let response = router.oneshot(request).await.unwrap();
279        let location = response.headers().get(LOCATION).unwrap().to_str().unwrap();
280        assert_eq!(location, "https://foo/bar/baz.bin?a=b");
281    }
282}