diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index d5989982a..f795117dd 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -6,6 +6,7 @@ - Add `HttpServer::{bind,listen}_auto_h2c()` method. - Add `Resource::{get, post, etc...}` methods for more concisely adding routes that don't need additional guards. +- Add `earlydata::EarlyData` middleware. ### Changed diff --git a/actix-web/src/middleware/earlydata.rs b/actix-web/src/middleware/earlydata.rs new file mode 100644 index 000000000..ae11bfc85 --- /dev/null +++ b/actix-web/src/middleware/earlydata.rs @@ -0,0 +1,238 @@ +//! For middleware documentation, see [`Earlydata`]. + +use crate::{ + body::EitherBody, + http::{header::HeaderValue, StatusCode}, + service::{ServiceRequest, ServiceResponse}, + Error, HttpResponse, +}; +use actix_service::{Service, Transform}; +use actix_utils::future::{ready, Ready}; +use futures_util::future::LocalBoxFuture; +use std::rc::Rc; + +/// The Early Data middleware adds support for TLS 1.3's early data ("0-RTT") feature. +/// Citing [[RFC8446](https://datatracker.ietf.org/doc/html/rfc8446#section-2-3)], +/// when a client and server share a PSK, TLS 1.3 allows clients to send data on the +/// first flight ("early data") to speed up the request, effectively reducing the +/// regular 1-RTT request to a 0-RTT request. +/// +/// This 0-RTT request is susceptible to replay attacks, hence it should only be allowed when it's +/// safe to be replayed. By standard, this applies to "safe" HTTP methods. This middleware checks +/// for exactly this and if the used method is not safe, the client is asked to re-perform the +/// request without early data. +/// +/// Since the source of the `Early-Data` header has to be trusted, this middleware also allows +/// supplying a function that returns whether the reverse proxy is trusted. If the proxy is not +/// trusted, early data is not allowed. +#[derive(Clone)] +#[non_exhaustive] +pub struct Earlydata { + /// Function that returns whether the reverse proxy for a given request is trusted. + is_proxy_trusted: fn(&ServiceRequest) -> bool, + /// Function that returns whether early data is allowed. + allow_early_data: fn(&ServiceRequest) -> bool, +} + +/// Default function that determines whether early data is allowed. This is accomplished by +/// checking if the method used is safe. +pub fn default_allow_early_data(req: &ServiceRequest) -> bool { + req.method().is_safe() +} + +impl Default for Earlydata { + /// Returns a default `Earlydata` instance that trusts all proxies and that uses + /// [`default_allow_early_data`]. + fn default() -> Self { + Self { + is_proxy_trusted: |_| -> bool { true }, + allow_early_data: default_allow_early_data, + } + } +} + +impl Earlydata { + /// Creates a new `Earlydata` middleware with given functions for determining the behavior. + pub fn new( + is_proxy_trusted: fn(&ServiceRequest) -> bool, + allow_early_data: fn(&ServiceRequest) -> bool, + ) -> Self { + Self { + is_proxy_trusted, + allow_early_data, + } + } +} + +impl Transform for Earlydata +where + S: Service, Error = Error> + 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Transform = EarlydataMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(EarlydataMiddleware { + service: Rc::new(service), + is_proxy_trusted: self.is_proxy_trusted, + allow_early_data: self.allow_early_data, + })) + } +} + +pub struct EarlydataMiddleware { + service: Rc, + is_proxy_trusted: fn(&ServiceRequest) -> bool, + allow_early_data: fn(&ServiceRequest) -> bool, +} + +impl Service for EarlydataMiddleware +where + S: Service, Error = Error> + 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + actix_service::forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let service = Rc::clone(&self.service); + let is_proxy_trusted = self.is_proxy_trusted; + let allow_early_data = self.allow_early_data; + + Box::pin(async move { + // Check if this is early data + // TODO wait for the http PR + if req.headers().get("early-data") != Some(&HeaderValue::from_static("1")) { + return service.call(req).await.map(|res| res.map_into_left_body()); + } + + // Do we trust the header? + if !is_proxy_trusted(&req) { + return Ok(req.into_response( + // TODO wait for PR + HttpResponse::new(StatusCode::from_u16(425).unwrap()).map_into_right_body(), + )); + } + + if allow_early_data(&req) { + service.call(req).await.map(|res| res.map_into_left_body()) + } else { + Ok(req.into_response( + // TODO wait for PR + HttpResponse::new(StatusCode::from_u16(425).unwrap()).map_into_right_body(), + )) + } + }) + } +} + +#[cfg(test)] +mod tests { + use crate::test::{call_service, init_service, TestRequest}; + use crate::{ + http::{header::HeaderValue, Method, StatusCode}, + middleware::earlydata::default_allow_early_data, + middleware::Earlydata, + web, App, HttpResponse, + }; + + #[actix_rt::test] + async fn early_data() { + let app = init_service( + App::new() + .wrap(Earlydata::default()) + .service(web::resource("/").to(HttpResponse::Ok)), + ) + .await; + + // No early data + let req = TestRequest::default().uri("/").to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + + // Early data (but trusted) + let req = TestRequest::default() + .uri("/") + .insert_header(("early-data", "1")) + .to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + + // Explicitly no early data + let req = TestRequest::default() + .uri("/") + .method(Method::PUT) + .insert_header(("early-data", "0")) + .to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + + // Early data but PUT + let req = TestRequest::default() + .uri("/") + .method(Method::PUT) + .insert_header(("early-data", "1")) + .to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::from_u16(425).unwrap()); + } + + #[actix_rt::test] + async fn early_data_custom_function() { + let app = init_service( + App::new() + .wrap(Earlydata::new(|_| true, |req| req.method() == Method::PUT)) + .service(web::resource("/").to(HttpResponse::Ok)), + ) + .await; + + // Should return `true` for PUT now + let req = TestRequest::default() + .uri("/") + .method(Method::PUT) + .insert_header(("early-data", "1")) + .to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + } + + #[actix_rt::test] + async fn early_data_proxy_trust() { + let app = init_service( + App::new() + .wrap(Earlydata::new( + |req| { + req.headers().get("please-trust-me") + == Some(&HeaderValue::from_static("1")) + }, + default_allow_early_data, + )) + .service(web::resource("/").to(HttpResponse::Ok)), + ) + .await; + + // Not trusted -> No 200 + let req = TestRequest::default() + .uri("/") + .method(Method::GET) + .insert_header(("early-data", "1")) + .to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::from_u16(425).unwrap()); + + // Trusted -> 200 + let req = TestRequest::default() + .uri("/") + .method(Method::GET) + .insert_header(("early-data", "1")) + .insert_header(("please-trust-me", "1")) + .to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + } +} diff --git a/actix-web/src/middleware/mod.rs b/actix-web/src/middleware/mod.rs index 0a61ad6cb..9d8eb5053 100644 --- a/actix-web/src/middleware/mod.rs +++ b/actix-web/src/middleware/mod.rs @@ -3,6 +3,7 @@ mod compat; mod condition; mod default_headers; +mod earlydata; mod err_handlers; mod logger; #[cfg(test)] @@ -12,6 +13,7 @@ mod normalize; pub use self::compat::Compat; pub use self::condition::Condition; pub use self::default_headers::DefaultHeaders; +pub use self::earlydata::Earlydata; pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; #[cfg(test)]