Repository: seanmonstar/warp
Pull request #676 (2020-07-25 07:09:22)

Add host filters
Resolves issue https://github.com/seanmonstar/warp/issues/432.
Obsoletes PR https://github.com/seanmonstar/warp/pull/489 (better logic, added tests and documentation).

cc @dmeijboom

@jxs, I'd appreciate a review.

dmeijboom: Nice work 👍🏽 I closed my PR in favor of this one. 
jakajancar: > I initially read "authority" as "authorization". I wonder if others would think similarly. What about using the term "host" instead?

I was thinking about that. In the end, I went with Authority because:

 1. hyper and the http crate call it authority (and the struct you get is `http::uri::Authority`),
 2. HTTP2 and 3 normally refer to it as (server) authority, and the `Host` header is more of a historic thing.

But absolutely agree re. familiarity and happy to change.  I would keep using `http::uri::Authority` though, but let me know.
jakajancar: @seanmonstar Can you please provide answers/acknowledge that my suggestions are OK on my 2 questions?
seanmonstar: I agree that the header host is phased out for `:authority`... I suppose I'd wonder what other frameworks expose. If everything at the framework level calls it host, does that mean we should also? I don't think we have to. But maybe it's less confusing to users?
jakajancar:   - Renamed filter from `authority` to `host`
  - Removed "top-level" filter in favor of explicit `warp::host::exact()` and `warp::host::optional()`

"Host" does seem much more familiar. The returned object is still `Authority` (from `http::uri::Authority`), but hopefully the documentation bridges between the familiar & easy-to-find and the technically accurate.

Patch:
diff --git a/src/filters/host.rs b/src/filters/host.rs
new file mode 100644
index 0000000..9dff055
--- /dev/null
+++ b/src/filters/host.rs
@@ -0,0 +1,96 @@
+//! Host ("authority") filter
+//!
+use crate::filter::{filter_fn_one, Filter, One};
+use crate::reject::{self, Rejection};
+use futures::future;
+pub use http::uri::Authority;
+use std::str::FromStr;
+
+/// Creates a `Filter` that requires a specific authority (target server's
+/// host and port) in the request.
+///
+/// Authority is specified either in the `Host` header or in the target URI.
+///
+/// # Example
+///
+/// ```
+/// use warp::Filter;
+///
+/// let multihost =
+///     warp::host::exact("foo.com").map(|| "you've reached foo.com")
+///     .or(warp::host::exact("bar.com").map(|| "you've reached bar.com"));
+/// ```
+pub fn exact(expected: &str) -> impl Filter<Extract = (), Error = Rejection> + Clone {
+    let expected = Authority::from_str(expected).expect("invalid host/authority");
+    optional()
+        .and_then(move |option: Option<Authority>| match option {
+            Some(authority) if authority == expected => future::ok(()),
+            _ => future::err(reject::not_found()),
+        })
+        .untuple_one()
+}
+
+/// Creates a `Filter` that looks for an authority (target server's host
+/// and port) in the request.
+///
+/// Authority is specified either in the `Host` header or in the target URI.
+///
+/// If found, extracts the `Authority`, otherwise continues the request,
+/// extracting `None`.
+///
+/// Rejects with `400 Bad Request` if the `Host` header is malformed or if there
+/// is a mismatch between the `Host` header and the target URI.
+///
+/// # Example
+///
+/// ```
+/// use warp::{Filter, host::Authority};
+///
+/// let host = warp::host::optional()
+///     .map(|authority: Option<Authority>| {
+///         if let Some(a) = authority {
+///             format!("{} is currently not at home", a.host())
+///         } else {
+///             "please state who you're trying to reach".to_owned()
+///         }
+///     });
+/// ```
+pub fn optional() -> impl Filter<Extract = One<Option<Authority>>, Error = Rejection> + Copy {
+    filter_fn_one(move |route| {
+        // The authority can be sent by clients in various ways:
+        //
+        //  1) in the "target URI"
+        //    a) serialized in the start line (HTTP/1.1 proxy requests)
+        //    b) serialized in `:authority` pseudo-header (HTTP/2 generated - "SHOULD")
+        //  2) in the `Host` header (HTTP/1.1 origin requests, HTTP/2 converted)
+        //
+        // Hyper transparently handles 1a/1b, but not 2, so we must look at both.
+
+        let from_uri = route.uri().authority();
+
+        let name = "host";
+        let from_header = route.headers()
+            .get(name)
+            .map(|value|
+                // Header present, parse it
+                value.to_str().map_err(|_| reject::invalid_header(name))
+                    .and_then(|value| Authority::from_str(value).map_err(|_| reject::invalid_header(name)))
+            );
+
+        future::ready(match (from_uri, from_header) {
+            // no authority in the request (HTTP/1.0 or non-conforming)
+            (None, None) => Ok(None),
+
+            // authority specified in either or both matching
+            (Some(a), None) => Ok(Some(a.clone())),
+            (None, Some(Ok(a))) => Ok(Some(a)),
+            (Some(a), Some(Ok(b))) if *a == b => Ok(Some(b)),
+
+            // mismatch
+            (Some(_), Some(Ok(_))) => Err(reject::invalid_header(name)),
+
+            // parse error
+            (_, Some(Err(r))) => Err(r),
+        })
+    })
+}
diff --git a/src/filters/mod.rs b/src/filters/mod.rs
index 4e7a920..fd1fb44 100644
--- a/src/filters/mod.rs
+++ b/src/filters/mod.rs
@@ -13,6 +13,7 @@ pub mod cors;
 pub mod ext;
 pub mod fs;
 pub mod header;
+pub mod host;
 pub mod log;
 pub mod method;
 #[cfg(feature = "multipart")]
diff --git a/src/lib.rs b/src/lib.rs
index 8b4686b..65469dc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -132,6 +132,7 @@ pub use self::filters::{
     header,
     // header() function
     header::header,
+    host,
     log,
     // log() function
     log::log,


Test patch:
diff --git a/tests/host.rs b/tests/host.rs
new file mode 100644
index 0000000..2fb5f42
--- /dev/null
+++ b/tests/host.rs
@@ -0,0 +1,87 @@
+#![deny(warnings)]
+use warp::host::Authority;
+
+#[tokio::test]
+async fn exact() {
+    let filter = warp::host::exact("known.com");
+
+    // no authority
+    let req = warp::test::request();
+    assert!(req.filter(&filter).await.unwrap_err().is_not_found());
+
+    // specified in URI
+    let req = warp::test::request().path("http://known.com/about-us");
+    assert!(req.filter(&filter).await.is_ok());
+
+    let req = warp::test::request().path("http://unknown.com/about-us");
+    assert!(req.filter(&filter).await.unwrap_err().is_not_found());
+
+    // specified in Host header
+    let req = warp::test::request()
+        .header("host", "known.com")
+        .path("/about-us");
+    assert!(req.filter(&filter).await.is_ok());
+
+    let req = warp::test::request()
+        .header("host", "unknown.com")
+        .path("/about-us");
+    assert!(req.filter(&filter).await.unwrap_err().is_not_found());
+
+    // specified in both - matching
+    let req = warp::test::request()
+        .header("host", "known.com")
+        .path("http://known.com/about-us");
+    assert!(req.filter(&filter).await.is_ok());
+
+    let req = warp::test::request()
+        .header("host", "unknown.com")
+        .path("http://unknown.com/about-us");
+    assert!(req.filter(&filter).await.unwrap_err().is_not_found());
+
+    // specified in both - mismatch
+    let req = warp::test::request()
+        .header("host", "known.com")
+        .path("http://known2.com/about-us");
+    assert!(req
+        .filter(&filter)
+        .await
+        .unwrap_err()
+        .find::<warp::reject::InvalidHeader>()
+        .is_some());
+
+    // bad host header - invalid chars
+    let req = warp::test::request()
+        .header("host", "😭")
+        .path("http://known.com/about-us");
+    assert!(req
+        .filter(&filter)
+        .await
+        .unwrap_err()
+        .find::<warp::reject::InvalidHeader>()
+        .is_some());
+
+    // bad host header - invalid format
+    let req = warp::test::request()
+        .header("host", "hello space.com")
+        .path("http://known.com//about-us");
+    assert!(req
+        .filter(&filter)
+        .await
+        .unwrap_err()
+        .find::<warp::reject::InvalidHeader>()
+        .is_some());
+}
+
+#[tokio::test]
+async fn optional() {
+    let filter = warp::host::optional();
+
+    let req = warp::test::request().header("host", "example.com");
+    assert_eq!(
+        req.filter(&filter).await.unwrap(),
+        Some(Authority::from_static("example.com"))
+    );
+
+    let req = warp::test::request();
+    assert_eq!(req.filter(&filter).await.unwrap(), None);
+}