From e81dc768dcbb0c2f4147439a4249a8d97352302d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 6 Apr 2023 03:11:28 +0100 Subject: [PATCH] expose h2c methods on HttpServer (#2999 * expose h2c methods on HttpServer * update h2c docs --- actix-web/CHANGES.md | 1 + actix-web/examples/on-connect.rs | 9 ++-- actix-web/src/server.rs | 80 +++++++++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 757e31eeb..d5989982a 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -4,6 +4,7 @@ ### Added +- Add `HttpServer::{bind,listen}_auto_h2c()` method. - Add `Resource::{get, post, etc...}` methods for more concisely adding routes that don't need additional guards. ### Changed diff --git a/actix-web/examples/on-connect.rs b/actix-web/examples/on-connect.rs index 57017fcd6..0d56a8f25 100644 --- a/actix-web/examples/on-connect.rs +++ b/actix-web/examples/on-connect.rs @@ -4,8 +4,6 @@ //! For an example of extracting a client TLS certificate, see: //! -#![allow(clippy::uninlined_format_args)] - use std::{any::Any, io, net::SocketAddr}; use actix_web::{ @@ -24,8 +22,7 @@ struct ConnectionInfo { async fn route_whoami(req: HttpRequest) -> impl Responder { match req.conn_data::() { Some(info) => HttpResponse::Ok().body(format!( - "Here is some info about your connection:\n\n{:#?}", - info + "Here is some info about your connection:\n\n{info:#?}", )), None => { HttpResponse::InternalServerError().body("Missing expected request extension data") @@ -54,8 +51,8 @@ async fn main() -> io::Result<()> { HttpServer::new(|| App::new().default_service(web::to(route_whoami))) .on_connect(get_conn_info) - .bind(bind)? - .workers(1) + .bind_auto_h2c(bind)? + .workers(2) .run() .await } diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index 1fa279a65..c11d0ef53 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -41,10 +41,19 @@ struct Config { /// /// Create new HTTP server with application factory. /// -/// # HTTP/2 -/// Currently, HTTP/2 is only supported when using TLS (HTTPS). See `bind_rustls` or `bind_openssl`. +/// # 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 `.bind_rustls()` or +/// `.bind_openssl()` 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 `.bind_auto_h2c()` method to enable this behavior. /// /// # Examples +/// /// ```no_run /// use actix_web::{web, App, HttpResponse, HttpServer}; /// @@ -347,6 +356,18 @@ where Ok(self) } + /// Resolves socket address(es) and binds server to created listener(s) for plaintext HTTP/1.x + /// or HTTP/2 connections. + pub fn bind_auto_h2c(mut self, addrs: A) -> io::Result { + let sockets = bind_addrs(addrs, self.backlog)?; + + for lst in sockets { + self = self.listen_auto_h2c(lst)?; + } + + Ok(self) + } + /// Resolves socket address(es) and binds server to created listener(s) for TLS connections /// using Rustls. /// @@ -406,13 +427,13 @@ where self.builder = self.builder .listen(format!("actix-web-service-{}", addr), lst, move || { - let c = cfg.lock().unwrap(); - let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + let cfg = cfg.lock().unwrap(); + let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); let mut svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_request_timeout(c.client_request_timeout) - .client_disconnect_timeout(c.client_disconnect_timeout) + .keep_alive(cfg.keep_alive) + .client_request_timeout(cfg.client_request_timeout) + .client_disconnect_timeout(cfg.client_disconnect_timeout) .local_addr(addr); if let Some(handler) = on_connect_fn.clone() { @@ -430,6 +451,51 @@ where })) .tcp() })?; + + Ok(self) + } + + /// Binds to existing listener for accepting incoming plaintext HTTP/1.x or HTTP/2 connections. + pub fn listen_auto_h2c(mut self, lst: net::TcpListener) -> io::Result { + let cfg = self.config.clone(); + let factory = self.factory.clone(); + let addr = lst.local_addr().unwrap(); + + self.sockets.push(Socket { + addr, + scheme: "http", + }); + + let on_connect_fn = self.on_connect_fn.clone(); + + self.builder = + self.builder + .listen(format!("actix-web-service-{}", addr), lst, move || { + let cfg = cfg.lock().unwrap(); + let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); + + let mut svc = HttpService::build() + .keep_alive(cfg.keep_alive) + .client_request_timeout(cfg.client_request_timeout) + .client_disconnect_timeout(cfg.client_disconnect_timeout) + .local_addr(addr); + + if let Some(handler) = on_connect_fn.clone() { + svc = svc.on_connect_ext(move |io: &_, ext: _| { + (handler)(io as &dyn Any, ext) + }) + }; + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| { + AppConfig::new(false, host.clone(), addr) + })) + .tcp_auto_h2c() + })?; + Ok(self) }