diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 8171061da..4cc467ddc 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -9,7 +9,7 @@ - Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868] - Implement `MessageBody` for `Pin` where `B::Target: MessageBody`. [#2868] - Automatic h2c detection via new service finalizer `HttpService::tcp_auto_h2c()`. [#2957] -- `HeaderMap::retain()` [#2955]. +- `HeaderMap::retain()`. [#2955] - Header name constants in `header` module. [#2956] [#2968] - `CACHE_STATUS` - `CDN_CACHE_CONTROL` diff --git a/actix-web/src/guard/host.rs b/actix-web/src/guard/host.rs new file mode 100644 index 000000000..f05c81183 --- /dev/null +++ b/actix-web/src/guard/host.rs @@ -0,0 +1,209 @@ +use actix_http::{header, uri::Uri, RequestHead}; + +use super::{Guard, GuardContext}; + +/// Creates a guard that matches requests targetting a specific host. +/// +/// # Matching Host +/// This guard will: +/// - match against the `Host` header, if present; +/// - fall-back to matching against the request target's host, if present; +/// - return false if host cannot be determined; +/// +/// # Matching Scheme +/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using +/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent +/// the guard from matching successfully. +/// +/// # Examples +/// The `Host` guard can be used to set up a form of [virtual hosting] within a single app. +/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard +/// definitions they become safe to use in this way. Without these host guards, only routes under +/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` +/// and `localhost` as the `Host` guards. +/// ``` +/// use actix_web::{web, http::Method, guard, App, HttpResponse}; +/// +/// App::new() +/// .service( +/// web::scope("") +/// .guard(guard::Host("www.rust-lang.org")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("marketing site") +/// })), +/// ) +/// .service( +/// web::scope("") +/// .guard(guard::Host("play.rust-lang.org")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("playground frontend") +/// })), +/// ); +/// ``` +/// +/// The example below additionally guards on the host URI's scheme. This could allow routing to +/// different handlers for `http:` vs `https:` visitors; to redirect, for example. +/// ``` +/// use actix_web::{web, guard::Host, HttpResponse}; +/// +/// web::scope("/admin") +/// .guard(Host("admin.rust-lang.org").scheme("https")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("admin connection is secure") +/// })); +/// ``` +/// +/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting +#[allow(non_snake_case)] +pub fn Host(host: impl AsRef) -> HostGuard { + HostGuard { + host: host.as_ref().to_string(), + scheme: None, + } +} + +fn get_host_uri(req: &RequestHead) -> Option { + req.headers + .get(header::HOST) + .and_then(|host_value| host_value.to_str().ok()) + .or_else(|| req.uri.host()) + .and_then(|host| host.parse().ok()) +} + +#[doc(hidden)] +pub struct HostGuard { + host: String, + scheme: Option, +} + +impl HostGuard { + /// Set request scheme to match + pub fn scheme>(mut self, scheme: H) -> HostGuard { + self.scheme = Some(scheme.as_ref().to_string()); + self + } +} + +impl Guard for HostGuard { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + // parse host URI from header or request target + let req_host_uri = match get_host_uri(ctx.head()) { + Some(uri) => uri, + + // no match if host cannot be determined + None => return false, + }; + + match req_host_uri.host() { + // fall through to scheme checks + Some(uri_host) if self.host == uri_host => {} + + // Either: + // - request's host does not match guard's host; + // - It was possible that the parsed URI from request target did not contain a host. + _ => return false, + } + + if let Some(ref scheme) = self.scheme { + if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { + return scheme == req_host_uri_scheme; + } + + // TODO: is this the correct behavior? + // falls through if scheme cannot be determined + } + + // all conditions passed + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::TestRequest; + + #[test] + fn host_from_header() { + let req = TestRequest::default() + .insert_header(( + header::HOST, + header::HeaderValue::from_static("www.rust-lang.org"), + )) + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_without_header() { + let req = TestRequest::default() + .uri("www.rust-lang.org") + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_scheme() { + let req = TestRequest::default() + .insert_header(( + header::HOST, + header::HeaderValue::from_static("https://www.rust-lang.org"), + )) + .to_srv_request(); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("http"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } +} diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index e086f8648..164032bdc 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -52,12 +52,15 @@ use std::{ rc::Rc, }; -use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; +use actix_http::{header, Extensions, Method as HttpMethod, RequestHead}; use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _}; mod acceptable; +mod host; + pub use self::acceptable::Acceptable; +pub use self::host::{Host, HostGuard}; /// Provides access to request parts that are useful during routing. #[derive(Debug)] @@ -371,124 +374,6 @@ impl Guard for HeaderGuard { } } -/// Creates a guard that matches requests targetting a specific host. -/// -/// # Matching Host -/// This guard will: -/// - match against the `Host` header, if present; -/// - fall-back to matching against the request target's host, if present; -/// - return false if host cannot be determined; -/// -/// # Matching Scheme -/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using -/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent -/// the guard from matching successfully. -/// -/// # Examples -/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards. -/// -/// The example below additionally guards on the host URI's scheme. This could allow routing to -/// different handlers for `http:` vs `https:` visitors; to redirect, for example. -/// ``` -/// use actix_web::{web, guard::Host, HttpResponse}; -/// -/// web::scope("/admin") -/// .guard(Host("admin.rust-lang.org").scheme("https")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("admin connection is secure") -/// })); -/// ``` -/// -/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app. -/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard -/// definitions they become safe to use in this way. Without these host guards, only routes under -/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` -/// and `localhost` as the `Host` guards. -/// ``` -/// use actix_web::{web, http::Method, guard, App, HttpResponse}; -/// -/// App::new() -/// .service( -/// web::scope("") -/// .guard(guard::Host("www.rust-lang.org")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("marketing site") -/// })), -/// ) -/// .service( -/// web::scope("") -/// .guard(guard::Host("play.rust-lang.org")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("playground frontend") -/// })), -/// ); -/// ``` -/// -/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting -#[allow(non_snake_case)] -pub fn Host(host: impl AsRef) -> HostGuard { - HostGuard { - host: host.as_ref().to_string(), - scheme: None, - } -} - -fn get_host_uri(req: &RequestHead) -> Option { - req.headers - .get(header::HOST) - .and_then(|host_value| host_value.to_str().ok()) - .or_else(|| req.uri.host()) - .and_then(|host| host.parse().ok()) -} - -#[doc(hidden)] -pub struct HostGuard { - host: String, - scheme: Option, -} - -impl HostGuard { - /// Set request scheme to match - pub fn scheme>(mut self, scheme: H) -> HostGuard { - self.scheme = Some(scheme.as_ref().to_string()); - self - } -} - -impl Guard for HostGuard { - fn check(&self, ctx: &GuardContext<'_>) -> bool { - // parse host URI from header or request target - let req_host_uri = match get_host_uri(ctx.head()) { - Some(uri) => uri, - - // no match if host cannot be determined - None => return false, - }; - - match req_host_uri.host() { - // fall through to scheme checks - Some(uri_host) if self.host == uri_host => {} - - // Either: - // - request's host does not match guard's host; - // - It was possible that the parsed URI from request target did not contain a host. - _ => return false, - } - - if let Some(ref scheme) = self.scheme { - if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { - return scheme == req_host_uri_scheme; - } - - // TODO: is this the correct behavior? - // falls through if scheme cannot be determined - } - - // all conditions passed - true - } -} - #[cfg(test)] mod tests { use actix_http::{header, Method}; @@ -515,90 +400,6 @@ mod tests { assert!(!hdr.check(&req.guard_ctx())); } - #[test] - fn host_from_header() { - let req = TestRequest::default() - .insert_header(( - header::HOST, - header::HeaderValue::from_static("www.rust-lang.org"), - )) - .to_srv_request(); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - - #[test] - fn host_without_header() { - let req = TestRequest::default() - .uri("www.rust-lang.org") - .to_srv_request(); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - - #[test] - fn host_scheme() { - let req = TestRequest::default() - .insert_header(( - header::HOST, - header::HeaderValue::from_static("https://www.rust-lang.org"), - )) - .to_srv_request(); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("http"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - #[test] fn method_guards() { let get_req = TestRequest::get().to_srv_request(); diff --git a/actix-web/src/info.rs b/actix-web/src/info.rs index 7c685406e..c5d9638f4 100644 --- a/actix-web/src/info.rs +++ b/actix-web/src/info.rs @@ -76,7 +76,6 @@ impl ConnectionInfo { for (name, val) in req .headers .get_all(&header::FORWARDED) - .into_iter() .filter_map(|hdr| hdr.to_str().ok()) // "for=1.2.3.4, for=5.6.7.8; scheme=https" .flat_map(|val| val.split(';'))