diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 045ae461f..3899ab643 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -4,12 +4,14 @@ ### Added - 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()`. [#????] ### Performance - Improve overall performance of operations on `Extensions`. [#2890] [#2868]: https://github.com/actix/actix-web/pull/2868 [#2890]: https://github.com/actix/actix-web/pull/2890 +[#????]: https://github.com/actix/actix-web/pull/???? ## 3.2.2 - 2022-09-11 diff --git a/actix-http/examples/h2c-detect.rs b/actix-http/examples/h2c-detect.rs index 550a03d2a..aa3dd5d31 100644 --- a/actix-http/examples/h2c-detect.rs +++ b/actix-http/examples/h2c-detect.rs @@ -8,38 +8,20 @@ use std::{convert::Infallible, io}; -use actix_http::{error::DispatchError, HttpService, Protocol, Request, Response, StatusCode}; -use actix_rt::net::TcpStream; +use actix_http::{HttpService, Request, Response, StatusCode}; use actix_server::Server; -use actix_service::{fn_service, ServiceFactoryExt}; -const H2_PREFACE: &[u8] = b"PRI * HTTP/2"; - -#[actix_rt::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); Server::build() .bind("h2c-detect", ("127.0.0.1", 8080), || { - fn_service(move |io: TcpStream| async move { - let mut buf = [0; 12]; - - io.peek(&mut buf).await.map_err(DispatchError::Io)?; - - let proto = if buf == H2_PREFACE { - tracing::info!("selecting h2c"); - Protocol::Http2 - } else { - tracing::info!("selecting h1"); - Protocol::Http1 - }; - - let peer_addr = io.peer_addr().ok(); - Ok((io, proto, peer_addr)) - }) - .and_then(HttpService::build().finish(|_req: Request| async move { - Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!")) - })) + HttpService::build() + .finish(|_req: Request| async move { + Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!")) + }) + .tcp_auto_h2c() })? .workers(2) .run() diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index 71b933835..e2693acaf 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -186,7 +186,7 @@ where self } - /// Finish service configuration and create a HTTP Service for HTTP/1 protocol. + /// Finish service configuration and create a service for the HTTP/1 protocol. pub fn h1(self, service: F) -> H1Service where B: MessageBody, @@ -209,7 +209,7 @@ where .on_connect_ext(self.on_connect_ext) } - /// Finish service configuration and create a HTTP service for HTTP/2 protocol. + /// Finish service configuration and create a service for the HTTP/2 protocol. #[cfg(feature = "http2")] #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub fn h2(self, service: F) -> crate::h2::H2Service diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index bcca5b188..315c3c6c9 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -24,7 +24,36 @@ use crate::{ h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig, }; -/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol. +/// A [`ServiceFactory`] for HTTP/1.1 and HTTP/2 connections. +/// +/// Use [`build`](Self::build) to begin constructing service. Also see [`HttpServiceBuilder`]. +/// +/// # Automatic HTTP Version Selection +/// There are two ways to select the HTTP version of an incoming connection: +/// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both +/// versions are supported automatically when using either of the `.rustls()` or `.openssl()` +/// finalizing methods. +/// - The other is to read the first few bytes of the TCP stream. This is the only viable approach +/// for supporting H2C, which allows the HTTP/2 protocol to work over plaintext connections. Use +/// the `.tcp_auto_h2c()` finalizing method to enable this behavior. +/// +/// # Examples +/// ``` +/// use actix_http::HttpService; +/// +/// HttpService::build() +/// // the builder finalizing method, other finalizers would not return an `HttpService` +/// .finish(|_req: Request| async move { +/// Ok::<_, Infallible>( +/// Response::build(StatusCode::OK).body("Hello!") +/// ) +/// }) +/// // the service finalizing method method +/// // you can use `.tcp_auto_h2c()`, `.rustls()`, or `.openssl()` instead of `.tcp()` +/// .tcp(); +/// +/// // this service would then be used in an actix_server::Server +/// ``` pub struct HttpService { srv: S, cfg: ServiceConfig, @@ -163,7 +192,9 @@ where U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create simple tcp stream service + /// Creates TCP stream service from HTTP service. + /// + /// The resulting service only supports HTTP/1.x. pub fn tcp( self, ) -> impl ServiceFactory< @@ -179,6 +210,42 @@ where }) .and_then(self) } + + /// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2 + /// on plaintext connections. + #[cfg(feature = "http2")] + #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] + pub fn tcp_auto_h2c( + self, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = DispatchError, + InitError = (), + > { + fn_service(move |io: TcpStream| async move { + // subset of HTTP/2 preface defined by RFC 9113 ยง3.4 + // this subset was chosen to maximize likelihood that peeking only once will allow us to + // reliably determine version or else it should fallback to h1 and fail quickly if data + // on the wire is junk + const H2_PREFACE: &[u8] = b"PRI * HTTP/2"; + + let mut buf = [0; 12]; + + io.peek(&mut buf).await?; + + let proto = if buf == H2_PREFACE { + Protocol::Http2 + } else { + Protocol::Http1 + }; + + let peer_addr = io.peer_addr().ok(); + Ok((io, proto, peer_addr)) + }) + .and_then(self) + } } /// Configuration options used when accepting TLS connection.