diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 4b8b5fd42..ec9f52b10 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -4,6 +4,7 @@ ### 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()`. [#2957] - `HeaderMap::retain()` [#2955]. - Header name constants in `header` module. [#2956] - `CROSS_ORIGIN_EMBEDDER_POLICY` @@ -18,6 +19,7 @@ [#2868]: https://github.com/actix/actix-web/pull/2868 [#2890]: https://github.com/actix/actix-web/pull/2890 +[#2957]: https://github.com/actix/actix-web/pull/2957 [#2955]: https://github.com/actix/actix-web/pull/2955 [#2956]: https://github.com/actix/actix-web/pull/2956 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..62128f3ec 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -24,7 +24,39 @@ 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 std::convert::Infallible; +/// use actix_http::{HttpService, Request, Response, StatusCode}; +/// +/// // this service would constructed in an actix_server::Server +/// +/// # actix_rt::System::new().block_on(async { +/// 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(); +/// # }) +/// ``` pub struct HttpService { srv: S, cfg: ServiceConfig, @@ -163,7 +195,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 +213,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. diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 0816ab221..2efb336ae 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -9,10 +9,10 @@ use std::{ use actix_http::{ body::{self, BodyStream, BoxBody, SizedStream}, - header, Error, HttpService, KeepAlive, Request, Response, StatusCode, + header, Error, HttpService, KeepAlive, Request, Response, StatusCode, Version, }; use actix_http_test::test_server; -use actix_rt::time::sleep; +use actix_rt::{net::TcpStream, time::sleep}; use actix_service::fn_service; use actix_utils::future::{err, ok, ready}; use bytes::Bytes; @@ -857,3 +857,44 @@ async fn not_modified_spec_h1() { srv.stop().await; } + +#[actix_rt::test] +async fn h2c_auto() { + let mut srv = test_server(|| { + HttpService::build() + .keep_alive(KeepAlive::Disabled) + .finish(|req: Request| { + let body = match req.version() { + Version::HTTP_11 => "h1", + Version::HTTP_2 => "h2", + _ => unreachable!(), + }; + ok::<_, Infallible>(Response::ok().set_body(body)) + }) + .tcp_auto_h2c() + }) + .await; + + let req = srv.get("/"); + assert_eq!(req.get_version(), &Version::HTTP_11); + let mut res = req.send().await.unwrap(); + assert!(res.status().is_success()); + assert_eq!(res.body().await.unwrap(), &b"h1"[..]); + + // awc doesn't support forcing the version to http/2 so use h2 manually + + let tcp = TcpStream::connect(srv.addr()).await.unwrap(); + let (h2, connection) = h2::client::handshake(tcp).await.unwrap(); + tokio::spawn(async move { connection.await.unwrap() }); + let mut h2 = h2.ready().await.unwrap(); + + let request = ::http::Request::new(()); + let (response, _) = h2.send_request(request, true).unwrap(); + let (head, mut body) = response.await.unwrap().into_parts(); + let body = body.data().await.unwrap().unwrap(); + + assert!(head.status.is_success()); + assert_eq!(body, &b"h2"[..]); + + srv.stop().await; +}