mirror of
https://github.com/fafhrd91/actix-web
synced 2024-11-27 09:42:57 +01:00
http service finalizer for automatic h2c detection (#2957)
* http service finalizer for automatic h2c detection * update changelog * add h2c auto test
This commit is contained in:
parent
fbd0e5dd0a
commit
08c2cdf641
@ -4,6 +4,7 @@
|
||||
### Added
|
||||
- Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868]
|
||||
- Implement `MessageBody` for `Pin<B>` 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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
||||
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<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||
|
@ -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<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
|
||||
srv: S,
|
||||
cfg: ServiceConfig,
|
||||
@ -163,7 +195,9 @@ where
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user