Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions actix-web/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
238 changes: 238 additions & 0 deletions actix-web/src/middleware/earlydata.rs
Original file line number Diff line number Diff line change
@@ -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<S, B> Transform<S, ServiceRequest> for Earlydata
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Transform = EarlydataMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;

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<S> {
service: Rc<S>,
is_proxy_trusted: fn(&ServiceRequest) -> bool,
allow_early_data: fn(&ServiceRequest) -> bool,
}

impl<S, B> Service<ServiceRequest> for EarlydataMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

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);
}
}
2 changes: 2 additions & 0 deletions actix-web/src/middleware/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod compat;
mod condition;
mod default_headers;
mod earlydata;
mod err_handlers;
mod logger;
#[cfg(test)]
Expand All @@ -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)]
Expand Down