From 0b6ca89be9636472b25f3677dc957fe098f72fab Mon Sep 17 00:00:00 2001 From: Sunli Date: Sat, 18 Nov 2023 13:48:37 +0800 Subject: [PATCH 01/11] upgrade to `hyper1` --- Cargo.toml | 6 +- examples/Cargo.toml | 2 +- examples/grpc/helloworld/src/client.rs | 2 +- examples/grpc/middleware/src/client.rs | 2 +- poem-grpc/Cargo.toml | 17 +- poem-grpc/src/client.rs | 38 +++-- poem-grpc/src/connector.rs | 155 +++++++++++++++++++ poem-grpc/src/encoding.rs | 82 +++++----- poem-grpc/src/lib.rs | 1 + poem-grpc/src/metadata.rs | 2 +- poem-grpc/src/request.rs | 4 +- poem-grpc/src/status.rs | 2 +- poem-lambda/Cargo.toml | 2 +- poem-lambda/src/lib.rs | 1 + poem-openapi-derive/Cargo.toml | 2 +- poem-openapi/src/base.rs | 1 + poem/Cargo.toml | 29 ++-- poem/src/body.rs | 133 ++++++++-------- poem/src/endpoint/tower_compat.rs | 73 ++------- poem/src/error.rs | 2 +- poem/src/listener/acme/auto_cert.rs | 4 +- poem/src/listener/acme/client.rs | 93 +++++------ poem/src/listener/acme/jose.rs | 51 +++--- poem/src/listener/acme/listener.rs | 31 ++-- poem/src/listener/acme/mod.rs | 1 - poem/src/listener/acme/protocol.rs | 16 +- poem/src/listener/acme/resolver.rs | 2 +- poem/src/listener/acme/serde.rs | 35 ----- poem/src/listener/rustls.rs | 54 ++++--- poem/src/middleware/opentelemetry_tracing.rs | 17 +- poem/src/request.rs | 21 +-- poem/src/response.rs | 31 ++-- poem/src/route/internal/radix_tree.rs | 11 ++ poem/src/route/router.rs | 2 +- poem/src/server.rs | 20 +-- poem/src/test/request_builder.rs | 2 +- poem/src/web/csrf.rs | 1 + poem/src/web/static_file.rs | 10 +- 38 files changed, 538 insertions(+), 420 deletions(-) create mode 100644 poem-grpc/src/connector.rs delete mode 100644 poem/src/listener/acme/serde.rs diff --git a/Cargo.toml b/Cargo.toml index 48db655337..a39bbe0452 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,8 +45,8 @@ quick-xml = { version = "0.30.0", features = ["serialize"] } base64 = "0.21.0" serde_urlencoded = "0.7.1" indexmap = "2.0.0" +reqwest = { version = "0.11.23", default-features = false } # rustls, update together -hyper-rustls = { version = "0.24.0", default-features = false } -rustls = "0.21.0" -tokio-rustls = "0.24.0" +rustls = "0.22.0" +tokio-rustls = "0.25.0" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 662163f802..c25fd954e0 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -21,4 +21,4 @@ serde = { version = "1.0.140", features = ["derive"] } mime = "0.3.16" futures-util = "0.3.21" tokio-stream = "0.1.8" -prost = "0.11.0" +prost = "0.12.0" diff --git a/examples/grpc/helloworld/src/client.rs b/examples/grpc/helloworld/src/client.rs index c4ad25c496..e3872e213f 100644 --- a/examples/grpc/helloworld/src/client.rs +++ b/examples/grpc/helloworld/src/client.rs @@ -11,7 +11,7 @@ async fn main() -> Result<(), Box .unwrap(), ); let request = Request::new(HelloRequest { - name: "Tonic".into(), + name: "Poem".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); diff --git a/examples/grpc/middleware/src/client.rs b/examples/grpc/middleware/src/client.rs index 5f8ac908ca..dc4343255f 100644 --- a/examples/grpc/middleware/src/client.rs +++ b/examples/grpc/middleware/src/client.rs @@ -13,7 +13,7 @@ async fn main() -> Result<(), Box .unwrap(), ); let request = Request::new(HelloRequest { - name: "Tonic".into(), + name: "Poem".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); diff --git a/poem-grpc/Cargo.toml b/poem-grpc/Cargo.toml index 639f520b02..ea0116fdfb 100644 --- a/poem-grpc/Cargo.toml +++ b/poem-grpc/Cargo.toml @@ -21,9 +21,8 @@ json-codec = ["serde", "serde_json"] poem = { workspace = true, default-features = true } futures-util.workspace = true -hyper = { version = "0.14.20", features = ["client"] } async-stream = "0.3.3" -tokio = { workspace = true, features = ["io-util", "rt", "sync"] } +tokio = { workspace = true, features = ["io-util", "rt", "sync", "net"] } flate2 = "1.0.24" itoa = "1.0.2" percent-encoding = "2.1.0" @@ -32,16 +31,18 @@ prost = "0.12.0" base64 = "0.21.0" prost-types = "0.12.0" tokio-stream = { workspace = true, features = ["sync"] } -hyper-rustls = { workspace = true, features = [ - "webpki-roots", - "http2", - "native-tokio", -] } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } -rustls.workspace = true +rustls = { workspace = true } thiserror.workspace = true fastrand = "2.0.0" +http = "1.0.0" +hyper = { version = "1.0.0", features = ["http1", "http2"] } +hyper-util = { version = "0.1.1", features = ["client-legacy", "tokio"] } +http-body-util = "0.1.0" +tokio-rustls.workspace = true +tower-service = "0.3.2" +webpki-roots = "0.26" [build-dependencies] poem-grpc-build.workspace = true diff --git a/poem-grpc/src/client.rs b/poem-grpc/src/client.rs index d72e821e7b..1f146b29ad 100644 --- a/poem-grpc/src/client.rs +++ b/poem-grpc/src/client.rs @@ -1,7 +1,9 @@ -use std::sync::Arc; +use std::{io::Error as IoError, sync::Arc}; +use bytes::Bytes; use futures_util::TryStreamExt; -use hyper_rustls::HttpsConnectorBuilder; +use http_body_util::BodyExt; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; use poem::{ http::{ header, header::InvalidHeaderValue, uri::InvalidUri, Extensions, HeaderValue, Method, @@ -14,10 +16,13 @@ use rustls::ClientConfig as TlsClientConfig; use crate::{ codec::Codec, + connector::HttpsConnector, encoding::{create_decode_response_body, create_encode_request_body}, Code, Metadata, Request, Response, Status, Streaming, }; +pub(crate) type BoxBody = http_body_util::combinators::BoxBody; + /// A configuration for GRPC client #[derive(Default)] pub struct ClientConfig { @@ -392,29 +397,17 @@ fn create_client_endpoint( config: ClientConfig, ) -> Arc + 'static> { let mut config = config; - let cli = match config.tls_config.take() { - Some(tls_config) => hyper::Client::builder().http2_only(true).build( - HttpsConnectorBuilder::new() - .with_tls_config(tls_config) - .https_or_http() - .enable_http2() - .build(), - ), - None => hyper::Client::builder().http2_only(true).build( - HttpsConnectorBuilder::new() - .with_webpki_roots() - .https_or_http() - .enable_http2() - .build(), - ), - }; + let cli = Client::builder(TokioExecutor::new()) + .http2_only(true) + .build(HttpsConnector::new(config.tls_config.take())); + let config = Arc::new(config); Arc::new(poem::endpoint::make(move |request| { let config = config.clone(); let cli = cli.clone(); async move { - let mut request: hyper::Request = request.into(); + let mut request: hyper::Request = request.into(); if config.uris.is_empty() { return Err(poem::Error::from_string( @@ -443,7 +436,12 @@ fn create_client_endpoint( } let resp = cli.request(request).await.map_err(to_boxed_error)?; - Ok::<_, poem::Error>(HttpResponse::from(resp)) + let (parts, body) = resp.into_parts(); + + Ok::<_, poem::Error>(HttpResponse::from(hyper::Response::from_parts( + parts, + body.map_err(|err| IoError::other(err)), + ))) } })) } diff --git a/poem-grpc/src/connector.rs b/poem-grpc/src/connector.rs new file mode 100644 index 0000000000..618347a0f1 --- /dev/null +++ b/poem-grpc/src/connector.rs @@ -0,0 +1,155 @@ +use std::{ + io::{Error as IoError, Result as IoResult}, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use futures_util::{future::BoxFuture, FutureExt}; +use http::{uri::Scheme, Uri}; +use hyper::rt::{Read, ReadBufCursor, Write}; +use hyper_util::{ + client::legacy::connect::{Connected, Connection}, + rt::TokioIo, +}; +use rustls::{ClientConfig, RootCertStore}; +use tokio::net::TcpStream; +use tokio_rustls::{client::TlsStream, TlsConnector}; +use tower_service::Service; + +pub(crate) enum MaybeHttpsStream { + TcpStream(TokioIo), + TlsStream { + stream: TokioIo>, + is_http2: bool, + }, +} + +impl Read for MaybeHttpsStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: ReadBufCursor<'_>, + ) -> Poll> { + match self.get_mut() { + MaybeHttpsStream::TcpStream(stream) => Pin::new(stream).poll_read(cx, buf), + MaybeHttpsStream::TlsStream { stream, .. } => Pin::new(stream).poll_read(cx, buf), + } + } +} + +impl Write for MaybeHttpsStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.get_mut() { + MaybeHttpsStream::TcpStream(stream) => Pin::new(stream).poll_write(cx, buf), + MaybeHttpsStream::TlsStream { stream, .. } => Pin::new(stream).poll_write(cx, buf), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + MaybeHttpsStream::TcpStream(stream) => Pin::new(stream).poll_flush(cx), + MaybeHttpsStream::TlsStream { stream, .. } => Pin::new(stream).poll_flush(cx), + } + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + MaybeHttpsStream::TcpStream(stream) => Pin::new(stream).poll_shutdown(cx), + MaybeHttpsStream::TlsStream { stream, .. } => Pin::new(stream).poll_shutdown(cx), + } + } +} + +impl Connection for MaybeHttpsStream { + fn connected(&self) -> Connected { + match self { + MaybeHttpsStream::TcpStream(_) => Connected::new(), + MaybeHttpsStream::TlsStream { is_http2, .. } => { + let mut connected = Connected::new(); + if *is_http2 { + connected = connected.negotiated_h2(); + } + connected + } + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct HttpsConnector { + tls_config: Option, +} + +impl HttpsConnector { + #[inline] + pub(crate) fn new(tls_config: Option) -> Self { + HttpsConnector { tls_config } + } +} + +impl Service for HttpsConnector { + type Response = MaybeHttpsStream; + type Error = IoError; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, uri: Uri) -> Self::Future { + do_connect(uri, self.tls_config.clone()).boxed() + } +} + +fn default_tls_config() -> ClientConfig { + let mut root_cert_store = RootCertStore::empty(); + root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + ClientConfig::builder() + .with_root_certificates(root_cert_store) + .with_no_client_auth() +} + +async fn do_connect( + uri: Uri, + tls_config: Option, +) -> Result { + let scheme = uri + .scheme() + .ok_or_else(|| IoError::other("missing scheme"))? + .clone(); + let host = uri + .host() + .ok_or_else(|| IoError::other("missing host"))? + .to_string(); + let port = uri + .port_u16() + .unwrap_or_else(|| if scheme == Scheme::HTTPS { 443 } else { 80 }); + + if scheme == Scheme::HTTP { + let stream = TcpStream::connect(format!("{}:{}", host, port)).await?; + Ok(MaybeHttpsStream::TcpStream(TokioIo::new(stream))) + } else if scheme == Scheme::HTTPS { + let mut tls_config = tls_config.unwrap_or_else(default_tls_config); + tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + let connector = TlsConnector::from(Arc::new(tls_config)); + let stream = TcpStream::connect(format!("{}:{}", host, port)).await?; + let domain = host.try_into().map_err(IoError::other)?; + let mut is_http2 = false; + let stream = connector + .connect_with(domain, stream, |conn| { + is_http2 = conn.alpn_protocol() == Some(b"h2"); + }) + .await?; + Ok(MaybeHttpsStream::TlsStream { + stream: TokioIo::new(stream), + is_http2, + }) + } else { + Err(IoError::other("invalid scheme")) + } +} diff --git a/poem-grpc/src/encoding.rs b/poem-grpc/src/encoding.rs index cfed4997b5..e9d3e33e77 100644 --- a/poem-grpc/src/encoding.rs +++ b/poem-grpc/src/encoding.rs @@ -1,12 +1,16 @@ -use std::io::Result as IoResult; +use std::io::{Error as IoError, Result as IoResult}; use bytes::{Buf, BufMut, Bytes, BytesMut}; use flate2::read::GzDecoder; use futures_util::StreamExt; -use hyper::{body::HttpBody, HeaderMap}; +use http_body_util::{BodyExt, StreamBody}; +use hyper::{body::Frame, HeaderMap}; use poem::Body; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; use crate::{ + client::BoxBody, codec::{Decoder, Encoder}, Code, Status, Streaming, }; @@ -76,18 +80,20 @@ pub(crate) fn create_decode_request_body( mut decoder: T, body: Body, ) -> Streaming { - let mut body: hyper::Body = body.into(); + let mut body: BoxBody = body.into(); Streaming::new(async_stream::try_stream! { let mut frame_decoder = DataFrameDecoder::default(); loop { - match body.data().await.transpose().map_err(Status::from_std_error)? { - Some(data) => { - frame_decoder.put_slice(data); - while let Some(data) = frame_decoder.next()? { - let message = decoder.decode(&data).map_err(Status::from_std_error)?; - yield message; + match body.frame().await.transpose().map_err(Status::from_std_error)? { + Some(frame) => { + if let Ok(data) = frame.into_data() { + frame_decoder.put_slice(data); + while let Some(data) = frame_decoder.next()? { + let message = decoder.decode(&data).map_err(Status::from_std_error)?; + yield message; + } } } None => { @@ -103,7 +109,7 @@ pub(crate) fn create_encode_response_body( mut encoder: T, mut stream: Streaming, ) -> Body { - let (mut sender, body) = hyper::Body::channel(); + let (tx, rx) = mpsc::channel(16); tokio::spawn(async move { let mut buf = BytesMut::new(); @@ -112,45 +118,51 @@ pub(crate) fn create_encode_response_body( match item { Ok(message) => { if let Ok(data) = encode_data_frame(&mut encoder, &mut buf, message) { - if sender.send_data(data).await.is_err() { + if tx.send(Frame::data(data)).await.is_err() { return; } } } Err(status) => { - let _ = sender.send_trailers(status.to_headers()).await; + _ = tx.send(Frame::trailers(status.to_headers())).await; return; } } } - let _ = sender - .send_trailers(Status::new(Code::Ok).to_headers()) + _ = tx + .send(Frame::trailers(Status::new(Code::Ok).to_headers())) .await; }); - body.into() + BodyExt::boxed(StreamBody::new( + ReceiverStream::new(rx).map(|frame| Ok::<_, IoError>(frame)), + )) + .into() } pub(crate) fn create_encode_request_body( mut encoder: T, mut stream: Streaming, ) -> Body { - let (mut sender, body) = hyper::Body::channel(); + let (tx, rx) = mpsc::channel(16); tokio::spawn(async move { let mut buf = BytesMut::new(); while let Some(Ok(message)) = stream.next().await { if let Ok(data) = encode_data_frame(&mut encoder, &mut buf, message) { - if sender.send_data(data).await.is_err() { + if tx.send(Frame::data(data)).await.is_err() { return; } } } }); - body.into() + BodyExt::boxed(StreamBody::new( + ReceiverStream::new(rx).map(|frame| Ok::<_, IoError>(frame)), + )) + .into() } pub(crate) fn create_decode_response_body( @@ -167,35 +179,33 @@ pub(crate) fn create_decode_response_body( }; } - let mut body: hyper::Body = body.into(); + let mut body: BoxBody = body.into(); Ok(Streaming::new(async_stream::try_stream! { let mut frame_decoder = DataFrameDecoder::default(); + let mut status = None; - loop { - if let Some(data) = body.data().await.transpose().map_err(Status::from_std_error)? { + while let Some(frame) = body.frame().await.transpose().map_err(Status::from_std_error)? { + if frame.is_data() { + let data = frame.into_data().unwrap(); frame_decoder.put_slice(data); while let Some(data) = frame_decoder.next()? { let message = decoder.decode(&data).map_err(Status::from_std_error)?; yield message; } - continue; - } - - frame_decoder.check_incomplete()?; - - match body.trailers().await.map_err(Status::from_std_error)? { - Some(trailers) => { - let status = Status::from_headers(&trailers)? - .ok_or_else(|| Status::new(Code::Internal).with_message("missing grpc-status"))?; - if !status.is_ok() { - Err(status)?; - } - } - None => Err(Status::new(Code::Internal).with_message("missing trailers"))?, + frame_decoder.check_incomplete()?; + } else if frame.is_trailers() { + let headers = frame.into_trailers().unwrap(); + status = Some(Status::from_headers(&headers)? + .ok_or_else(|| Status::new(Code::Internal) + .with_message("missing grpc-status"))?); + break; } + } - break; + let status = status.ok_or_else(|| Status::new(Code::Internal).with_message("missing trailers"))?; + if !status.is_ok() { + Err(status)?; } })) } diff --git a/poem-grpc/src/lib.rs b/poem-grpc/src/lib.rs index 75ba9ce9cc..67cb9e13bf 100644 --- a/poem-grpc/src/lib.rs +++ b/poem-grpc/src/lib.rs @@ -20,6 +20,7 @@ pub mod service; pub mod codec; pub mod metadata; +mod connector; mod encoding; mod health; mod reflection; diff --git a/poem-grpc/src/metadata.rs b/poem-grpc/src/metadata.rs index db9d158b5f..54e34a6a8b 100644 --- a/poem-grpc/src/metadata.rs +++ b/poem-grpc/src/metadata.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use base64::engine::{general_purpose::STANDARD_NO_PAD, Engine}; -use hyper::header::HeaderName; +use http::HeaderName; use poem::http::{HeaderMap, HeaderValue}; /// A metadata map diff --git a/poem-grpc/src/request.rs b/poem-grpc/src/request.rs index 5b3f81a592..8488412846 100644 --- a/poem-grpc/src/request.rs +++ b/poem-grpc/src/request.rs @@ -4,7 +4,7 @@ use std::{ }; use futures_util::Stream; -use hyper::http::Extensions; +use http::Extensions; use crate::{Metadata, Status, Streaming}; @@ -90,7 +90,7 @@ impl Request { /// Inserts a value to extensions, similar to /// `self.extensions().insert(data)`. #[inline] - pub fn set_data(&mut self, data: impl Send + Sync + 'static) { + pub fn set_data(&mut self, data: impl Send + Sync + Clone + 'static) { self.extensions.insert(data); } } diff --git a/poem-grpc/src/status.rs b/poem-grpc/src/status.rs index 796856b304..e4119738bc 100644 --- a/poem-grpc/src/status.rs +++ b/poem-grpc/src/status.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use hyper::{header::HeaderValue, HeaderMap}; +use http::{header::HeaderValue, HeaderMap}; use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS}; use crate::Metadata; diff --git a/poem-lambda/Cargo.toml b/poem-lambda/Cargo.toml index c83ae633ac..63655cf801 100644 --- a/poem-lambda/Cargo.toml +++ b/poem-lambda/Cargo.toml @@ -21,7 +21,7 @@ categories = [ [dependencies] poem = { workspace = true, default-features = false } -lambda_http = { version = "0.8.0" } +lambda_http = { version = "0.9.0" } [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/poem-lambda/src/lib.rs b/poem-lambda/src/lib.rs index 2b0478766b..4262d78a29 100644 --- a/poem-lambda/src/lib.rs +++ b/poem-lambda/src/lib.rs @@ -30,6 +30,7 @@ use poem::{Body, Endpoint, EndpointExt, FromRequest, IntoEndpoint, Request, Requ /// println!("request_id: {}", ctx.request_id); /// } /// ``` +#[derive(Debug, Clone)] pub struct Context(pub lambda_runtime::Context); impl Deref for Context { diff --git a/poem-openapi-derive/Cargo.toml b/poem-openapi-derive/Cargo.toml index 7641bad9d7..422ec54545 100644 --- a/poem-openapi-derive/Cargo.toml +++ b/poem-openapi-derive/Cargo.toml @@ -24,7 +24,7 @@ syn = { workspace = true, features = ["full", "visit-mut"] } thiserror.workspace = true indexmap.workspace = true regex.workspace = true -http = "0.2.5" +http = "1.0.0" mime.workspace = true [package.metadata.workspaces] diff --git a/poem-openapi/src/base.rs b/poem-openapi/src/base.rs index 0142098f09..fb468f0eae 100644 --- a/poem-openapi/src/base.rs +++ b/poem-openapi/src/base.rs @@ -31,6 +31,7 @@ pub enum ApiExtractorType { } #[doc(hidden)] +#[derive(Clone)] pub struct UrlQuery(pub Vec<(String, String)>); impl Deref for UrlQuery { diff --git a/poem/Cargo.toml b/poem/Cargo.toml index 4277d84313..a7bc8b59e6 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -21,7 +21,7 @@ categories = [ [features] default = ["server"] -server = ["tokio/rt", "tokio/net", "hyper/server", "hyper/runtime"] +server = ["tokio/rt", "tokio/net", "hyper/server"] websocket = ["tokio/rt", "tokio-tungstenite", "base64"] multipart = ["multer"] rustls = ["server", "tokio-rustls", "rustls-pemfile"] @@ -51,14 +51,13 @@ i18n = [ "intl-memoizer", ] acme = ["acme-native-roots"] -acme-native-roots = ["acme-base", "hyper-rustls/native-tokio"] -acme-webpki-roots = ["acme-base", "hyper-rustls/webpki-tokio"] +acme-native-roots = ["acme-base", "reqwest/rustls-tls-native-roots"] +acme-webpki-roots = ["acme-base", "reqwest/rustls-tls-webpki-roots"] acme-base = [ "server", - "hyper/client", + "reqwest", "rustls", "ring", - "hyper-rustls", "base64", "rcgen", "x509-parser", @@ -74,8 +73,10 @@ poem-derive.workspace = true async-trait = "0.1.51" bytes.workspace = true futures-util = { workspace = true, features = ["sink"] } -http = "0.2.5" -hyper = { version = "0.14.17", features = ["http1", "http2", "stream"] } +http = "1.0.0" +hyper = { version = "1.0.0", features = ["http1", "http2"] } +hyper-util = { version = "0.1.1", features = ["server-auto", "tokio"] } +http-body-util = "0.1.0" tokio = { workspace = true, features = ["sync", "time", "macros", "net"] } tokio-util = { version = "0.7.0", features = ["io"] } serde.workspace = true @@ -87,15 +88,16 @@ percent-encoding = "2.1.0" regex.workspace = true smallvec = "1.6.1" tracing.workspace = true -headers = "0.3.7" +headers = "0.4.0" thiserror.workspace = true rfc7239 = "0.1.0" mime.workspace = true wildmatch = "2" +sync_wrapper = { version = "0.1.2", features = ["futures"] } # Non-feature optional dependencies -multer = { version = "2.1.0", features = ["tokio"], optional = true } -tokio-tungstenite = { version = "0.20.0", optional = true } +multer = { version = "3.0.0", features = ["tokio"], optional = true } +tokio-tungstenite = { version = "0.21.0", optional = true } tokio-rustls = { workspace = true, optional = true } rustls-pemfile = { version = "1.0.0", optional = true } async-compression = { version = "0.4.0", optional = true, features = [ @@ -148,12 +150,7 @@ fluent-syntax = { version = "0.11.0", optional = true } unic-langid = { version = "0.9.0", optional = true, features = ["macros"] } intl-memoizer = { version = "0.5.1", optional = true } ring = { version = "0.16.20", optional = true } -hyper-rustls = { workspace = true, optional = true, features = [ - "http1", - "http2", - "tls12", - "logging", -] } +reqwest = { workspace = true, features = ["json"], optional = true } rcgen = { version = "0.11.1", optional = true } x509-parser = { version = "0.15.0", optional = true } tokio-metrics = { version = "0.3.0", optional = true } diff --git a/poem/src/body.rs b/poem/src/body.rs index 17225b6b04..85eebeddd5 100644 --- a/poem/src/body.rs +++ b/poem/src/body.rs @@ -1,14 +1,16 @@ use std::{ - fmt::{Debug, Display, Formatter}, + fmt::{Debug, Formatter}, io::{Error as IoError, ErrorKind}, pin::Pin, - task::{Context, Poll}, + task::Poll, }; use bytes::{Bytes, BytesMut}; use futures_util::{Stream, TryStreamExt}; -use hyper::body::HttpBody; +use http_body_util::BodyExt; +use hyper::body::{Body as _, Frame}; use serde::{de::DeserializeOwned, Serialize}; +use sync_wrapper::SyncStream; use tokio::io::{AsyncRead, AsyncReadExt}; use crate::{ @@ -16,60 +18,76 @@ use crate::{ Result, }; +pub(crate) type BoxBody = http_body_util::combinators::BoxBody; + /// A body object for requests and responses. #[derive(Default)] -pub struct Body(pub(crate) hyper::Body); +pub struct Body(pub(crate) BoxBody); -impl Debug for Body { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Body").finish() +impl From for BoxBody { + #[inline] + fn from(body: Body) -> Self { + body.0 } } -impl From for Body { - fn from(body: hyper::Body) -> Self { +impl From for Body { + #[inline] + fn from(body: BoxBody) -> Self { Body(body) } } -impl From for hyper::Body { - fn from(body: Body) -> Self { - body.0 +impl Debug for Body { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Body").finish() } } impl From<&'static [u8]> for Body { #[inline] fn from(data: &'static [u8]) -> Self { - Self(data.into()) + Self(BoxBody::new( + http_body_util::Full::new(data.into()).map_err::<_, IoError>(|_| unreachable!()), + )) } } impl From<&'static str> for Body { #[inline] fn from(data: &'static str) -> Self { - Self(data.into()) + Self(BoxBody::new( + http_body_util::Full::new(data.into()).map_err::<_, IoError>(|_| unreachable!()), + )) } } impl From for Body { #[inline] fn from(data: Bytes) -> Self { - Self(data.into()) + Self( + http_body_util::Full::new(data) + .map_err::<_, IoError>(|_| unreachable!()) + .boxed(), + ) } } impl From> for Body { #[inline] fn from(data: Vec) -> Self { - Self(data.into()) + Self( + http_body_util::Full::new(data.into()) + .map_err::<_, IoError>(|_| unreachable!()) + .boxed(), + ) } } impl From for Body { #[inline] fn from(data: String) -> Self { - Self(data.into()) + data.into_bytes().into() } } @@ -102,8 +120,8 @@ impl Body { /// Create a body object from reader. #[inline] pub fn from_async_read(reader: impl AsyncRead + Send + 'static) -> Self { - Self(hyper::Body::wrap_stream(tokio_util::io::ReaderStream::new( - reader, + Self(BoxBody::new(http_body_util::StreamBody::new( + SyncStream::new(tokio_util::io::ReaderStream::new(reader).map_ok(Frame::data)), ))) } @@ -112,9 +130,15 @@ impl Body { where S: Stream> + Send + 'static, O: Into + 'static, - E: std::error::Error + Send + Sync + 'static, + E: Into + 'static, { - Self(hyper::Body::wrap_stream(stream)) + Self(BoxBody::new(http_body_util::StreamBody::new( + SyncStream::new( + stream + .map_ok(|data| Frame::data(data.into())) + .map_err(Into::into), + ), + ))) } /// Create a body object from JSON. @@ -125,29 +149,33 @@ impl Body { /// Create an empty body. #[inline] pub fn empty() -> Self { - Self(hyper::Body::empty()) + Self( + http_body_util::Empty::new() + .map_err::<_, IoError>(|_| unreachable!()) + .boxed(), + ) } /// Returns `true` if this body is empty. pub fn is_empty(&self) -> bool { - let size_hint = hyper::body::HttpBody::size_hint(&self.0); + let size_hint = hyper::body::Body::size_hint(&self.0); size_hint.lower() == 0 && size_hint.upper() == Some(0) } /// Consumes this body object to return a [`Bytes`] that contains all data. pub async fn into_bytes(self) -> Result { - hyper::body::to_bytes(self.0) + Ok(self + .0 + .collect() .await - .map_err(|err| ReadBodyError::Io(IoError::new(ErrorKind::Other, err))) + .map_err(|err| ReadBodyError::Io(IoError::new(ErrorKind::Other, err)))? + .to_bytes()) } /// Consumes this body object to return a [`Vec`] that contains all /// data. pub async fn into_vec(self) -> Result, ReadBodyError> { - Ok(hyper::body::to_bytes(self.0) - .await - .map_err(|err| ReadBodyError::Io(IoError::new(ErrorKind::Other, err)))? - .to_vec()) + self.into_bytes().await.map(|data| data.to_vec()) } /// Consumes this body object to return a [`Bytes`] that contains all @@ -223,41 +251,23 @@ impl Body { /// Consumes this body object to return a reader. pub fn into_async_read(self) -> impl AsyncRead + Unpin + Send + 'static { - tokio_util::io::StreamReader::new(BodyStream::new(self.0)) + tokio_util::io::StreamReader::new(self.into_bytes_stream()) } /// Consumes this body object to return a bytes stream. pub fn into_bytes_stream(self) -> impl Stream> + Send + 'static { - TryStreamExt::map_err(self.0, |err| IoError::new(ErrorKind::Other, err)) - } -} - -pin_project_lite::pin_project! { - pub(crate) struct BodyStream { - #[pin] inner: T, - } -} - -impl BodyStream { - #[inline] - pub(crate) fn new(inner: T) -> Self { - Self { inner } - } -} - -impl Stream for BodyStream -where - T: HttpBody, - T::Error: Display, -{ - type Item = Result; - - #[inline] - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project() - .inner - .poll_data(cx) - .map_err(|err| IoError::new(ErrorKind::Other, err.to_string())) + let mut body = self.0; + futures_util::stream::poll_fn(move |ctx| loop { + match Pin::new(&mut body).poll_frame(ctx) { + Poll::Ready(Some(Ok(frame))) => match frame.into_data() { + Ok(data) => return Poll::Ready(Some(Ok(data))), + Err(_) => continue, + }, + Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), + Poll::Ready(None) => return Poll::Ready(None), + Poll::Pending => return Poll::Pending, + } + }) } } @@ -267,9 +277,6 @@ mod tests { #[tokio::test] async fn create() { - let body = Body::from(hyper::Body::from("abc")); - assert_eq!(body.into_string().await.unwrap(), "abc"); - let body = Body::from(b"abc".as_ref()); assert_eq!(body.into_vec().await.unwrap(), b"abc"); diff --git a/poem/src/endpoint/tower_compat.rs b/poem/src/endpoint/tower_compat.rs index 1956900dab..c1519dc2bb 100644 --- a/poem/src/endpoint/tower_compat.rs +++ b/poem/src/endpoint/tower_compat.rs @@ -1,10 +1,10 @@ use std::{error::Error as StdError, future::Future}; use bytes::Bytes; -use hyper::body::{HttpBody, Sender}; +use http_body_util::BodyExt; use tower::{Service, ServiceExt}; -use crate::{Endpoint, Error, Request, Response, Result}; +use crate::{body::BoxBody, Endpoint, Error, Request, Response, Result}; /// Extension trait for tower service compat. #[cfg_attr(docsrs, doc(cfg(feature = "tower-compat")))] @@ -12,12 +12,12 @@ pub trait TowerCompatExt { /// Converts a tower service to a poem endpoint. fn compat(self) -> TowerCompatEndpoint where - ResBody: HttpBody + Send + 'static, + ResBody: hyper::body::Body + Send + Sync + 'static, ResBody::Data: Into + Send + 'static, ResBody::Error: StdError + Send + Sync + 'static, Err: Into, Self: Service< - http::Request, + http::Request, Response = hyper::Response, Error = Err, Future = Fut, @@ -41,12 +41,12 @@ pub struct TowerCompatEndpoint(Svc); #[async_trait::async_trait] impl Endpoint for TowerCompatEndpoint where - ResBody: HttpBody + Send + 'static, + ResBody: hyper::body::Body + Send + Sync + 'static, ResBody::Data: Into + Send + 'static, ResBody::Error: StdError + Send + Sync + 'static, Err: Into, Svc: Service< - http::Request, + http::Request, Response = hyper::Response, Error = Err, Future = Fut, @@ -62,59 +62,14 @@ where let mut svc = self.0.clone(); svc.ready().await.map_err(Into::into)?; - - let hyper_req: http::Request = req.into(); - let hyper_resp = svc - .call(hyper_req.map(Into::into)) - .await - .map_err(Into::into)?; - - if !hyper_resp.body().is_end_stream() { - Ok(hyper_resp - .map(|body| { - let (sender, new_body) = hyper::Body::channel(); - tokio::spawn(copy_body(body, sender)); - new_body - }) - .into()) - } else { - Ok(hyper_resp.map(|_| hyper::Body::empty()).into()) - } - } -} - -async fn copy_body(body: T, mut sender: Sender) -where - T: HttpBody + Send + 'static, - T::Data: Into + Send + 'static, - T::Error: StdError + Send + Sync + 'static, -{ - tokio::pin!(body); - - loop { - match body.data().await { - Some(Ok(data)) => { - if sender.send_data(data.into()).await.is_err() { - break; - } - } - Some(Err(_)) => break, - None => {} - } - - match body.trailers().await { - Ok(Some(trailers)) => { - if sender.send_trailers(trailers).await.is_err() { - break; - } - } - Ok(None) => {} - Err(_) => break, - } - - if body.is_end_stream() { - break; - } + svc.call(req.into()).await.map_err(Into::into).map(|resp| { + let (parts, body) = resp.into_parts(); + let body = body + .map_frame(|frame| frame.map_data(Into::into)) + .map_err(std::io::Error::other) + .boxed(); + hyper::Response::from_parts(parts, body).into() + }) } } diff --git a/poem/src/error.rs b/poem/src/error.rs index f57d37ab4d..d3f11cb08c 100644 --- a/poem/src/error.rs +++ b/poem/src/error.rs @@ -463,7 +463,7 @@ impl Error { /// assert_eq!(resp.data::(), Some(&100)); /// ``` #[inline] - pub fn set_data(&mut self, data: impl Send + Sync + 'static) { + pub fn set_data(&mut self, data: impl Clone + Send + Sync + 'static) { self.extensions.insert(data); } diff --git a/poem/src/listener/acme/auto_cert.rs b/poem/src/listener/acme/auto_cert.rs index fb24e0f528..5beb7f8b38 100644 --- a/poem/src/listener/acme/auto_cert.rs +++ b/poem/src/listener/acme/auto_cert.rs @@ -3,15 +3,13 @@ use std::{ path::PathBuf, }; -use http::Uri; - use crate::listener::acme::{ builder::AutoCertBuilder, endpoint::Http01Endpoint, ChallengeType, Http01TokensMap, }; /// ACME configuration pub struct AutoCert { - pub(crate) directory_url: Uri, + pub(crate) directory_url: String, pub(crate) domains: Vec, pub(crate) contacts: Vec, pub(crate) challenge_type: ChallengeType, diff --git a/poem/src/listener/acme/client.rs b/poem/src/listener/acme/client.rs index c566ade51f..14f2e77c1a 100644 --- a/poem/src/listener/acme/client.rs +++ b/poem/src/listener/acme/client.rs @@ -4,26 +4,21 @@ use std::{ }; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use http::{header, Uri}; -use hyper::{client::HttpConnector, Client}; -use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; - -use crate::{ - listener::acme::{ - jose, - keypair::KeyPair, - protocol::{ - CsrRequest, Directory, FetchAuthorizationResponse, Identifier, NewAccountRequest, - NewOrderRequest, NewOrderResponse, - }, - ChallengeType, +use reqwest::Client; + +use crate::listener::acme::{ + jose, + keypair::KeyPair, + protocol::{ + CsrRequest, Directory, FetchAuthorizationResponse, Identifier, NewAccountRequest, + NewOrderRequest, NewOrderResponse, }, - Body, + ChallengeType, }; /// A client for ACME-supporting TLS certificate services. pub struct AcmeClient { - client: Client>, + client: Client, directory: Directory, pub(crate) key_pair: Arc, contacts: Vec, @@ -33,14 +28,8 @@ pub struct AcmeClient { impl AcmeClient { /// Create a new client. `directory_url` is the url for the ACME provider. `contacts` is a list /// of URLS (ex: `mailto:`) the ACME service can use to reach you if there's issues with your certificates. - pub async fn try_new(directory_url: &Uri, contacts: Vec) -> IoResult { - let client_builder = HttpsConnectorBuilder::new(); - #[cfg(feature = "acme-native-roots")] - let client_builder1 = client_builder.with_native_roots(); - #[cfg(all(feature = "acme-webpki-roots", not(feature = "acme-native-roots")))] - let client_builder1 = client_builder.with_webpki_roots(); - let client = - Client::builder().build(client_builder1.https_or_http().enable_http1().build()); + pub(crate) async fn try_new(directory_url: &str, contacts: Vec) -> IoResult { + let client = Client::new(); let directory = get_directory(&client, directory_url).await?; Ok(Self { client, @@ -98,7 +87,7 @@ impl AcmeClient { pub(crate) async fn fetch_authorization( &self, - auth_url: &Uri, + auth_url: &str, ) -> IoResult { tracing::debug!(auth_uri = %auth_url, "fetch authorization"); @@ -126,7 +115,7 @@ impl AcmeClient { &self, domain: &str, challenge_type: ChallengeType, - url: &Uri, + url: &str, ) -> IoResult<()> { tracing::debug!( auth_uri = %url, @@ -149,7 +138,7 @@ impl AcmeClient { Ok(()) } - pub(crate) async fn send_csr(&self, url: &Uri, csr: &[u8]) -> IoResult { + pub(crate) async fn send_csr(&self, url: &str, csr: &[u8]) -> IoResult { tracing::debug!(url = %url, "send certificate request"); let nonce = get_nonce(&self.client, &self.directory).await?; @@ -166,7 +155,7 @@ impl AcmeClient { .await } - pub(crate) async fn obtain_certificate(&self, url: &Uri) -> IoResult> { + pub(crate) async fn obtain_certificate(&self, url: &str) -> IoResult> { tracing::debug!(url = %url, "send certificate request"); let nonce = get_nonce(&self.client, &self.directory).await?; @@ -180,22 +169,23 @@ impl AcmeClient { ) .await?; - resp.into_body().into_vec().await.map_err(|err| { - IoError::new( - ErrorKind::Other, - format!("failed to download certificate: {err}"), - ) - }) + Ok(resp + .bytes() + .await + .map_err(|err| { + IoError::new( + ErrorKind::Other, + format!("failed to download certificate: {err}"), + ) + })? + .to_vec()) } } -async fn get_directory( - client: &Client>, - directory_url: &Uri, -) -> IoResult { +async fn get_directory(client: &Client, directory_url: &str) -> IoResult { tracing::debug!("loading directory"); - let resp = client.get(directory_url.clone()).await.map_err(|err| { + let resp = client.get(directory_url).send().await.map_err(|err| { IoError::new(ErrorKind::Other, format!("failed to load directory: {err}")) })?; @@ -206,12 +196,9 @@ async fn get_directory( )); } - let directory = Body(resp.into_body()) - .into_json::() - .await - .map_err(|err| { - IoError::new(ErrorKind::Other, format!("failed to load directory: {err}")) - })?; + let directory = resp.json::().await.map_err(|err| { + IoError::new(ErrorKind::Other, format!("failed to load directory: {err}")) + })?; tracing::debug!( new_nonce = ?directory.new_nonce, @@ -222,14 +209,12 @@ async fn get_directory( Ok(directory) } -async fn get_nonce( - client: &Client>, - directory: &Directory, -) -> IoResult { +async fn get_nonce(client: &Client, directory: &Directory) -> IoResult { tracing::debug!("creating nonce"); let resp = client - .get(directory.new_nonce.clone()) + .get(&directory.new_nonce) + .send() .await .map_err(|err| IoError::new(ErrorKind::Other, format!("failed to get nonce: {err}")))?; @@ -252,7 +237,7 @@ async fn get_nonce( } async fn create_acme_account( - client: &Client>, + client: &Client, directory: &Directory, key_pair: &KeyPair, contacts: Vec, @@ -274,9 +259,11 @@ async fn create_acme_account( ) .await?; let kid = resp - .header(header::LOCATION) - .ok_or_else(|| IoError::new(ErrorKind::Other, "unable to get account id"))? - .to_string(); + .headers() + .get("location") + .and_then(|value| value.to_str().ok()) + .map(ToString::to_string) + .ok_or_else(|| IoError::new(ErrorKind::Other, "unable to get account id"))?; tracing::debug!(kid = kid.as_str(), "account created"); Ok(kid) diff --git a/poem/src/listener/acme/jose.rs b/poem/src/listener/acme/jose.rs index 635d4968e9..17564ad010 100644 --- a/poem/src/listener/acme/jose.rs +++ b/poem/src/listener/acme/jose.rs @@ -1,13 +1,11 @@ use std::io::{Error as IoError, ErrorKind, Result as IoResult}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use http::{Method, Uri}; -use hyper::{client::HttpConnector, Client}; -use hyper_rustls::HttpsConnector; +use reqwest::{Client, Response}; use ring::digest::{digest, Digest, SHA256}; use serde::{de::DeserializeOwned, Serialize}; -use crate::{listener::acme::keypair::KeyPair, Request, Response}; +use crate::listener::acme::keypair::KeyPair; #[derive(Serialize)] struct Protected<'a> { @@ -100,11 +98,11 @@ struct Body { } pub(crate) async fn request( - cli: &Client>, + cli: &Client, key_pair: &KeyPair, kid: Option<&str>, nonce: &str, - uri: &Uri, + uri: &str, payload: Option, ) -> IoResult { let jwk = match kid { @@ -121,26 +119,26 @@ pub(crate) async fn request( let payload = URL_SAFE_NO_PAD.encode(payload); let combined = format!("{}.{}", &protected, &payload); let signature = URL_SAFE_NO_PAD.encode(key_pair.sign(combined.as_bytes())?); - let body = serde_json::to_vec(&Body { - protected, - payload, - signature, - }) - .unwrap(); - let req = Request::builder() - .method(Method::POST) - .uri(uri.clone()) - .content_type("application/jose+json") - .body(body); tracing::debug!(uri = %uri, "http request"); - let resp = cli.request(req.into()).await.map_err(|err| { - IoError::new( - ErrorKind::Other, - format!("failed to send http request: {err}"), - ) - })?; + let resp = cli + .post(uri) + .json(&Body { + protected, + payload, + signature, + }) + .header("content-type", "application/jose+json") + .send() + .await + .map_err(|err| { + IoError::new( + ErrorKind::Other, + format!("failed to send http request: {err}"), + ) + })?; + if !resp.status().is_success() { return Err(IoError::new( ErrorKind::Other, @@ -151,11 +149,11 @@ pub(crate) async fn request( } pub(crate) async fn request_json( - cli: &Client>, + cli: &Client, key_pair: &KeyPair, kid: Option<&str>, nonce: &str, - uri: &Uri, + uri: &str, payload: Option, ) -> IoResult where @@ -165,8 +163,7 @@ where let resp = request(cli, key_pair, kid, nonce, uri, payload).await?; let data = resp - .into_body() - .into_string() + .text() .await .map_err(|_| IoError::new(ErrorKind::Other, "failed to read response"))?; serde_json::from_str(&data) diff --git a/poem/src/listener/acme/listener.rs b/poem/src/listener/acme/listener.rs index 179cd7ba5e..fea4810ef5 100644 --- a/poem/src/listener/acme/listener.rs +++ b/poem/src/listener/acme/listener.rs @@ -10,8 +10,10 @@ use rcgen::{ }; use tokio_rustls::{ rustls::{ - sign::{any_ecdsa_type, CertifiedKey}, - PrivateKey, ServerConfig, + crypto::ring::sign::any_ecdsa_type, + pki_types::{CertificateDer, PrivateKeyDer}, + sign::CertifiedKey, + ServerConfig, }, server::TlsStream, TlsAcceptor, @@ -37,7 +39,6 @@ pub(crate) async fn auto_cert_acceptor( challenge_type: ChallengeType, ) -> IoResult> { let mut server_config = ServerConfig::builder() - .with_safe_defaults() .with_no_client_auth() .with_cert_resolver(cert_resolver); server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; @@ -137,7 +138,7 @@ impl Listener for AutoCertListener { if let (Some(certs), Some(key)) = (cache_certs, cert_key) { let certs = certs .into_iter() - .map(tokio_rustls::rustls::Certificate) + .map(CertificateDer::from) .collect::>(); let expires_at = match certs @@ -156,7 +157,7 @@ impl Listener for AutoCertListener { ); *cert_resolver.cert.write() = Some(Arc::new(CertifiedKey::new( certs, - any_ecdsa_type(&PrivateKey(key)).unwrap(), + any_ecdsa_type(&PrivateKeyDer::Pkcs8(key.into())).unwrap(), ))); } @@ -231,13 +232,14 @@ fn gen_acme_cert(domain: &str, acme_hash: &[u8]) -> IoResult { params.custom_extensions = vec![CustomExtension::new_acme_identifier(acme_hash)]; let cert = Certificate::from_params(params) .map_err(|_| IoError::new(ErrorKind::Other, "failed to generate acme certificate"))?; - let key = any_ecdsa_type(&PrivateKey(cert.serialize_private_key_der())).unwrap(); + let key = any_ecdsa_type(&PrivateKeyDer::Pkcs8( + cert.serialize_private_key_der().into(), + )) + .unwrap(); Ok(CertifiedKey::new( - vec![tokio_rustls::rustls::Certificate( - cert.serialize_der().map_err(|_| { - IoError::new(ErrorKind::Other, "failed to serialize acme certificate") - })?, - )], + vec![CertificateDer::from(cert.serialize_der().map_err( + |_| IoError::new(ErrorKind::Other, "failed to serialize acme certificate"), + )?)], key, )) } @@ -353,7 +355,10 @@ pub async fn issue_cert>( format!("failed create certificate request: {err}"), ) })?; - let pk = any_ecdsa_type(&PrivateKey(cert.serialize_private_key_der())).unwrap(); + let pk = any_ecdsa_type(&PrivateKeyDer::Pkcs8( + cert.serialize_private_key_der().into(), + )) + .unwrap(); let csr = cert.serialize_request_der().map_err(|err| { IoError::new( ErrorKind::Other, @@ -400,7 +405,7 @@ pub async fn issue_cert>( let cert_chain = rustls_pemfile::certs(&mut acme_cert_pem.as_slice()) .map_err(|err| IoError::new(ErrorKind::Other, format!("invalid pem: {err}")))? .into_iter() - .map(tokio_rustls::rustls::Certificate) + .map(CertificateDer::from) .collect(); let cert_key = CertifiedKey::new(cert_chain, pk); diff --git a/poem/src/listener/acme/mod.rs b/poem/src/listener/acme/mod.rs index 4d898aa145..5bab9b9a07 100644 --- a/poem/src/listener/acme/mod.rs +++ b/poem/src/listener/acme/mod.rs @@ -12,7 +12,6 @@ mod keypair; mod listener; mod protocol; mod resolver; -mod serde; pub use auto_cert::AutoCert; pub use builder::AutoCertBuilder; diff --git a/poem/src/listener/acme/protocol.rs b/poem/src/listener/acme/protocol.rs index c570338501..f2ebd1d58c 100644 --- a/poem/src/listener/acme/protocol.rs +++ b/poem/src/listener/acme/protocol.rs @@ -5,8 +5,6 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::listener::acme::serde::SerdeUri; - /// HTTP-01 challenge const CHALLENGE_TYPE_HTTP_01: &str = "http-01"; @@ -38,9 +36,9 @@ impl Display for ChallengeType { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct Directory { - pub(crate) new_nonce: SerdeUri, - pub(crate) new_account: SerdeUri, - pub(crate) new_order: SerdeUri, + pub(crate) new_nonce: String, + pub(crate) new_account: String, + pub(crate) new_order: String, } #[derive(Serialize)] @@ -75,17 +73,17 @@ pub(crate) struct Problem { #[serde(rename_all = "camelCase")] pub(crate) struct NewOrderResponse { pub(crate) status: String, - pub(crate) authorizations: Vec, + pub(crate) authorizations: Vec, pub(crate) error: Option, - pub(crate) finalize: SerdeUri, - pub(crate) certificate: Option, + pub(crate) finalize: String, + pub(crate) certificate: Option, } #[derive(Debug, Deserialize)] pub(crate) struct Challenge { #[serde(rename = "type")] pub(crate) ty: String, - pub(crate) url: SerdeUri, + pub(crate) url: String, pub(crate) token: String, } diff --git a/poem/src/listener/acme/resolver.rs b/poem/src/listener/acme/resolver.rs index 9cdde0e587..85ba58d169 100644 --- a/poem/src/listener/acme/resolver.rs +++ b/poem/src/listener/acme/resolver.rs @@ -30,7 +30,7 @@ pub fn seconds_until_expiry(cert: &CertifiedKey) -> i64 { } /// Shared ACME key state. -#[derive(Default)] +#[derive(Default, Debug)] pub struct ResolveServerCert { /// The current TLS certificate. Swap it with `Arc::write`. pub cert: RwLock>>, diff --git a/poem/src/listener/acme/serde.rs b/poem/src/listener/acme/serde.rs deleted file mode 100644 index c3b57beeb0..0000000000 --- a/poem/src/listener/acme/serde.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::{ - fmt::{self, Debug, Formatter}, - ops::Deref, -}; - -use http::Uri; -use serde::{de::Error, Deserialize, Deserializer}; - -pub(crate) struct SerdeUri(pub(crate) Uri); - -impl<'de> Deserialize<'de> for SerdeUri { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - String::deserialize(deserializer)? - .parse::() - .map(SerdeUri) - .map_err(|err| D::Error::custom(err.to_string())) - } -} - -impl Debug for SerdeUri { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Debug::fmt(&self.0, f) - } -} - -impl Deref for SerdeUri { - type Target = Uri; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/poem/src/listener/rustls.rs b/poem/src/listener/rustls.rs index 7eeba2a3de..b38e6c83f7 100644 --- a/poem/src/listener/rustls.rs +++ b/poem/src/listener/rustls.rs @@ -9,12 +9,11 @@ use rustls_pemfile::Item; use tokio::io::{Error as IoError, ErrorKind, Result as IoResult}; use tokio_rustls::{ rustls::{ - server::{ - AllowAnyAnonymousOrAuthenticatedClient, AllowAnyAuthenticatedClient, ClientHello, - NoClientAuth, ResolvesServerCert, - }, - sign::{self, CertifiedKey}, - Certificate, PrivateKey, RootCertStore, ServerConfig, + crypto::ring::sign::any_supported_type, + pki_types::{CertificateDer, PrivateKeyDer}, + server::{ClientHello, ResolvesServerCert, WebPkiClientVerifier}, + sign::CertifiedKey, + RootCertStore, ServerConfig, }, server::TlsStream, }; @@ -72,7 +71,7 @@ impl RustlsCertificate { impl RustlsCertificate { fn create_certificate_key(&self) -> IoResult { let cert = rustls_pemfile::certs(&mut self.cert.as_slice()) - .map(|mut certs| certs.drain(..).map(Certificate).collect()) + .map(|mut certs| certs.drain(..).map(CertificateDer::from).collect()) .map_err(|_| IoError::new(ErrorKind::Other, "failed to parse tls certificates"))?; let priv_key = { @@ -90,12 +89,12 @@ impl RustlsCertificate { _ => continue, }; if !key.is_empty() { - break PrivateKey(key); + break PrivateKeyDer::Pkcs8(key.into()); } } }; - let key = sign::any_supported_type(&priv_key) + let key = any_supported_type(&priv_key) .map_err(|_| IoError::new(ErrorKind::Other, "invalid private key"))?; Ok(CertifiedKey { @@ -106,7 +105,6 @@ impl RustlsCertificate { } else { None }, - sct_list: None, }) } } @@ -239,24 +237,30 @@ impl RustlsConfig { ); } - let client_auth = match &self.client_auth { - TlsClientAuth::Off => NoClientAuth::boxed(), + let builder = ServerConfig::builder(); + let builder = match &self.client_auth { + TlsClientAuth::Off => builder.with_no_client_auth(), TlsClientAuth::Optional(trust_anchor) => { - AllowAnyAnonymousOrAuthenticatedClient::new(read_trust_anchor(trust_anchor)?) - .boxed() + let verifier = + WebPkiClientVerifier::builder(read_trust_anchor(trust_anchor)?.into()) + .allow_unauthenticated() + .build() + .map_err(|err| IoError::other(err))?; + builder.with_client_cert_verifier(verifier) } TlsClientAuth::Required(trust_anchor) => { - AllowAnyAuthenticatedClient::new(read_trust_anchor(trust_anchor)?).boxed() + let verifier = + WebPkiClientVerifier::builder(read_trust_anchor(trust_anchor)?.into()) + .build() + .map_err(|err| IoError::other(err))?; + builder.with_client_cert_verifier(verifier) } }; - let mut server_config = ServerConfig::builder() - .with_safe_defaults() - .with_client_cert_verifier(client_auth) - .with_cert_resolver(Arc::new(ResolveServerCert { - certifcate_keys, - fallback, - })); + let mut server_config = builder.with_cert_resolver(Arc::new(ResolveServerCert { + certifcate_keys, + fallback, + })); server_config.alpn_protocols = vec!["h2".into(), "http/1.1".into()]; Ok(server_config) @@ -268,7 +272,7 @@ fn read_trust_anchor(mut trust_anchor: &[u8]) -> IoResult { let ders = rustls_pemfile::certs(&mut trust_anchor)?; for der in ders { store - .add(&Certificate(der)) + .add(CertificateDer::from(der)) .map_err(|err| IoError::new(ErrorKind::Other, err.to_string()))?; } Ok(store) @@ -402,6 +406,7 @@ where } } +#[derive(Debug)] struct ResolveServerCert { certifcate_keys: HashMap>, fallback: Option>, @@ -422,7 +427,7 @@ mod tests { io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, }; - use tokio_rustls::rustls::{ClientConfig, ServerName}; + use tokio_rustls::rustls::{pki_types::ServerName, ClientConfig}; use super::*; use crate::listener::TcpListener; @@ -441,7 +446,6 @@ mod tests { tokio::spawn(async move { let config = ClientConfig::builder() - .with_safe_defaults() .with_root_certificates( read_trust_anchor(include_bytes!("certs/chain1.pem")).unwrap(), ) diff --git a/poem/src/middleware/opentelemetry_tracing.rs b/poem/src/middleware/opentelemetry_tracing.rs index 6e6b89f50b..aaedde9024 100644 --- a/poem/src/middleware/opentelemetry_tracing.rs +++ b/poem/src/middleware/opentelemetry_tracing.rs @@ -2,10 +2,10 @@ use std::sync::Arc; use libopentelemetry::{ global, + propagation::Extractor, trace::{FutureExt, Span, SpanKind, TraceContextExt, Tracer}, Context, Key, }; -use opentelemetry_http::HeaderExtractor; use opentelemetry_semantic_conventions::{resource, trace}; use crate::{ @@ -52,6 +52,21 @@ pub struct OpenTelemetryTracingEndpoint { inner: E, } +struct HeaderExtractor<'a>(&'a http::HeaderMap); + +impl<'a> Extractor for HeaderExtractor<'a> { + fn get(&self, key: &str) -> Option<&str> { + self.0.get(key).and_then(|value| value.to_str().ok()) + } + + fn keys(&self) -> Vec<&str> { + self.0 + .keys() + .map(|value| value.as_str()) + .collect::>() + } +} + #[async_trait::async_trait] impl Endpoint for OpenTelemetryTracingEndpoint where diff --git a/poem/src/request.rs b/poem/src/request.rs index f83dc7f279..e8bbd5e018 100644 --- a/poem/src/request.rs +++ b/poem/src/request.rs @@ -1,5 +1,4 @@ use std::{ - any::Any, fmt::{self, Debug, Formatter}, future::Future, io::Error, @@ -9,6 +8,8 @@ use std::{ }; use http::uri::Scheme; +use http_body_util::BodyExt; +use hyper::{body::Incoming, rt::Write as _}; use parking_lot::Mutex; use serde::de::DeserializeOwned; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; @@ -16,7 +17,7 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; #[cfg(feature = "cookie")] use crate::web::cookie::CookieJar; use crate::{ - body::Body, + body::{Body, BoxBody}, error::{ParsePathError, ParseQueryError, UpgradeError}, http::{ header::{self, HeaderMap, HeaderName, HeaderValue}, @@ -108,10 +109,10 @@ impl Debug for Request { } } -impl From<(http::Request, LocalAddr, RemoteAddr, Scheme)> for Request { +impl From<(http::Request, LocalAddr, RemoteAddr, Scheme)> for Request { fn from( (req, local_addr, remote_addr, scheme): ( - http::Request, + http::Request, LocalAddr, RemoteAddr, Scheme, @@ -131,7 +132,7 @@ impl From<(http::Request, LocalAddr, RemoteAddr, Scheme)> for Reque version: parts.version, headers: parts.headers, extensions: parts.extensions, - body: Body(body), + body: Body(body.map_err(|err| Error::other(err)).boxed()), state: RequestState { local_addr, remote_addr, @@ -146,13 +147,13 @@ impl From<(http::Request, LocalAddr, RemoteAddr, Scheme)> for Reque } } -impl From for hyper::Request { +impl From for hyper::Request { fn from(req: Request) -> Self { let mut hyper_req = http::Request::builder() .method(req.method) .uri(req.uri) .version(req.version) - .body(req.body.into()) + .body(req.body.0) .unwrap(); *hyper_req.headers_mut() = req.headers; *hyper_req.extensions_mut() = req.extensions; @@ -372,7 +373,7 @@ impl Request { /// Inserts a value to extensions, similar to /// `self.extensions().insert(data)`. #[inline] - pub fn set_data(&mut self, data: impl Send + Sync + 'static) { + pub fn set_data(&mut self, data: impl Clone + Send + Sync + 'static) { self.extensions.insert(data); } @@ -490,7 +491,7 @@ impl AsyncRead for Upgraded { cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { - self.project().stream.poll_read(cx, buf) + Pin::new(&mut hyper_util::rt::TokioIo::new(self.project().stream)).poll_read(cx, buf) } } @@ -597,7 +598,7 @@ impl RequestBuilder { #[must_use] pub fn extension(mut self, extension: T) -> Self where - T: Any + Send + Sync + 'static, + T: Clone + Send + Sync + 'static, { self.extensions.insert(extension); self diff --git a/poem/src/response.rs b/poem/src/response.rs index 4eb0562244..02512acaf5 100644 --- a/poem/src/response.rs +++ b/poem/src/response.rs @@ -1,11 +1,11 @@ -use std::{ - any::Any, - fmt::{self, Debug, Formatter}, -}; +use std::fmt::{self, Debug, Formatter}; +use bytes::Bytes; use headers::HeaderMapExt; +use http_body_util::BodyExt; use crate::{ + body::BoxBody, http::{ header::{self, HeaderMap, HeaderName, HeaderValue}, Extensions, StatusCode, Version, @@ -77,9 +77,9 @@ impl> From<(StatusCode, T)> for Response { } } -impl From for hyper::Response { +impl From for hyper::Response { fn from(resp: Response) -> Self { - let mut hyper_resp = hyper::Response::new(resp.body.into()); + let mut hyper_resp = hyper::Response::new(resp.body.0); *hyper_resp.status_mut() = resp.status; *hyper_resp.version_mut() = resp.version; *hyper_resp.headers_mut() = resp.headers; @@ -88,15 +88,24 @@ impl From for hyper::Response { } } -impl From> for Response { - fn from(hyper_resp: hyper::Response) -> Self { +impl From> for Response +where + T: hyper::body::Body + Send + Sync + 'static, + T::Data: Into, + T::Error: Into, +{ + fn from(hyper_resp: hyper::Response) -> Self { let (parts, body) = hyper_resp.into_parts(); Response { status: parts.status, version: parts.version, headers: parts.headers, extensions: parts.extensions, - body: body.into(), + body: Body( + body.map_frame(|frame| frame.map_data(Into::into)) + .map_err(Into::into) + .boxed(), + ), } } } @@ -217,7 +226,7 @@ impl Response { /// Inserts a value to extensions, similar to /// `self.extensions().insert(data)`. #[inline] - pub fn set_data(&mut self, data: impl Send + Sync + 'static) { + pub fn set_data(&mut self, data: impl Clone + Send + Sync + 'static) { self.extensions.insert(data); } @@ -312,7 +321,7 @@ impl ResponseBuilder { #[must_use] pub fn extension(mut self, extension: T) -> Self where - T: Any + Send + Sync + 'static, + T: Clone + Send + Sync + 'static, { self.extensions.insert(extension); self diff --git a/poem/src/route/internal/radix_tree.rs b/poem/src/route/internal/radix_tree.rs index 64e2cdb760..1e32afb8b1 100644 --- a/poem/src/route/internal/radix_tree.rs +++ b/poem/src/route/internal/radix_tree.rs @@ -1085,6 +1085,7 @@ mod tests { ("/*p1", 9), ("/abc/<\\d+>/def", 10), ("/kcd/:p1<\\d+>", 11), + ("/:package/-/:package_tgz<.*tgz$>", 12), ]; for (path, id) in paths { @@ -1154,6 +1155,16 @@ mod tests { NodeData::new(11, "/kcd/:p1<\\d+>"), )), ), + ( + "/is-number/-/is-number-7.0.0.tgz", + Some(( + create_url_params(vec![ + ("package", "is-number"), + ("package_tgz", "is-number-7.0.0.tgz"), + ]), + NodeData::new(12, "/:package/-/:package_tgz<.*tgz$>"), + )), + ), ]; for (path, mut res) in matches { diff --git a/poem/src/route/router.rs b/poem/src/route/router.rs index 4b4817f563..fd6980f6e1 100644 --- a/poem/src/route/router.rs +++ b/poem/src/route/router.rs @@ -10,7 +10,7 @@ use crate::{ Endpoint, EndpointExt, IntoEndpoint, IntoResponse, Request, Response, Result, }; -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] struct PathPrefix(usize); /// Routing object diff --git a/poem/src/server.rs b/poem/src/server.rs index b5fab2a71a..5f3b9c60a3 100644 --- a/poem/src/server.rs +++ b/poem/src/server.rs @@ -12,7 +12,8 @@ use std::{ }; use http::uri::Scheme; -use hyper::server::conn::Http; +use hyper::body::Incoming; +use hyper_util::server::conn::auto; use pin_project_lite::pin_project; use tokio::{ io::{AsyncRead, AsyncWrite, ReadBuf, Result as IoResult}, @@ -321,7 +322,7 @@ async fn serve_connection( let service = hyper::service::service_fn({ let remote_addr = remote_addr.clone(); - move |req: hyper::Request| { + move |req: http::Request| { let ep = ep.clone(); let local_addr = local_addr.clone(); let remote_addr = remote_addr.clone(); @@ -352,12 +353,13 @@ async fn serve_connection( None => tokio_util::either::Either::Right(socket), }; - let mut conn = Http::new() - .serve_connection(socket, service) - .with_upgrades(); + let builder = auto::Builder::new(hyper_util::rt::TokioExecutor::new()); + let conn = + builder.serve_connection_with_upgrades(hyper_util::rt::TokioIo::new(socket), service); + futures_util::pin_mut!(conn); tokio::select! { - _ = &mut conn => { + _ = conn => { // Connection completed successfully. return; }, @@ -366,10 +368,4 @@ async fn serve_connection( } _ = server_graceful_shutdown_token.cancelled() => {} } - - // Init graceful shutdown for connection (`GOAWAY` for `HTTP/2` or disabling `keep-alive` for `HTTP/1`) - Pin::new(&mut conn).graceful_shutdown(); - - // Continue awaiting after graceful-shutdown is initiated to handle existed requests. - let _ = conn.await; } diff --git a/poem/src/test/request_builder.rs b/poem/src/test/request_builder.rs index 42d6d77f57..e27909c82d 100644 --- a/poem/src/test/request_builder.rs +++ b/poem/src/test/request_builder.rs @@ -200,7 +200,7 @@ impl<'a, E> TestRequestBuilder<'a, E> { #[must_use] pub fn data(mut self, data: T) -> Self where - T: Send + Sync + 'static, + T: Clone + Send + Sync + 'static, { self.extensions.insert(data); self diff --git a/poem/src/web/csrf.rs b/poem/src/web/csrf.rs index 927c184e36..10e618fc78 100644 --- a/poem/src/web/csrf.rs +++ b/poem/src/web/csrf.rs @@ -33,6 +33,7 @@ impl<'a> FromRequest<'a> for &'a CsrfToken { /// A verifier for CSRF Token. /// /// See also [`Csrf`](crate::middleware::Csrf) +#[derive(Clone)] #[cfg_attr(docsrs, doc(cfg(feature = "csrf")))] pub struct CsrfVerifier { cookie: Option, diff --git a/poem/src/web/static_file.rs b/poem/src/web/static_file.rs index 7b121d9f63..047abdee11 100644 --- a/poem/src/web/static_file.rs +++ b/poem/src/web/static_file.rs @@ -129,7 +129,10 @@ impl StaticFileRequest { let mut content_length = data.len() as u64; let mut content_range = None; - let body = if let Some((start, end)) = self.range.and_then(|range| range.iter().next()) { + let body = if let Some((start, end)) = self + .range + .and_then(|range| range.satisfiable_ranges(data.len() as u64).next()) + { let start = match start { Bound::Included(n) => n, Bound::Excluded(n) => n + 1, @@ -232,7 +235,10 @@ impl StaticFileRequest { let mut content_range = None; - let body = if let Some((start, end)) = self.range.and_then(|range| range.iter().next()) { + let body = if let Some((start, end)) = self + .range + .and_then(|range| range.satisfiable_ranges(metadata.len()).next()) + { let start = match start { Bound::Included(n) => n, Bound::Excluded(n) => n + 1, From 4c06798b728bc4540fe2054c726d0b453ed286db Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 5 Jan 2024 22:18:52 +0800 Subject: [PATCH 02/11] clippy clean --- poem-grpc/src/client.rs | 2 +- poem-grpc/src/encoding.rs | 4 ++-- poem/src/listener/acme/jose.rs | 4 ++-- poem/src/listener/rustls.rs | 4 ++-- poem/src/request.rs | 2 +- poem/src/server.rs | 1 - 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/poem-grpc/src/client.rs b/poem-grpc/src/client.rs index 1f146b29ad..dbef21c1b2 100644 --- a/poem-grpc/src/client.rs +++ b/poem-grpc/src/client.rs @@ -440,7 +440,7 @@ fn create_client_endpoint( Ok::<_, poem::Error>(HttpResponse::from(hyper::Response::from_parts( parts, - body.map_err(|err| IoError::other(err)), + body.map_err(IoError::other), ))) } })) diff --git a/poem-grpc/src/encoding.rs b/poem-grpc/src/encoding.rs index e9d3e33e77..39ffec5766 100644 --- a/poem-grpc/src/encoding.rs +++ b/poem-grpc/src/encoding.rs @@ -136,7 +136,7 @@ pub(crate) fn create_encode_response_body( }); BodyExt::boxed(StreamBody::new( - ReceiverStream::new(rx).map(|frame| Ok::<_, IoError>(frame)), + ReceiverStream::new(rx).map(Ok::<_, IoError>), )) .into() } @@ -160,7 +160,7 @@ pub(crate) fn create_encode_request_body( }); BodyExt::boxed(StreamBody::new( - ReceiverStream::new(rx).map(|frame| Ok::<_, IoError>(frame)), + ReceiverStream::new(rx).map(Ok::<_, IoError>), )) .into() } diff --git a/poem/src/listener/acme/jose.rs b/poem/src/listener/acme/jose.rs index 17564ad010..58767b2289 100644 --- a/poem/src/listener/acme/jose.rs +++ b/poem/src/listener/acme/jose.rs @@ -109,7 +109,7 @@ pub(crate) async fn request( None => Some(Jwk::new(key_pair)), Some(_) => None, }; - let protected = Protected::base64(jwk, kid, nonce, &uri.to_string())?; + let protected = Protected::base64(jwk, kid, nonce, uri)?; let payload = match payload { Some(payload) => serde_json::to_vec(&payload).map_err(|err| { IoError::new(ErrorKind::Other, format!("failed to encode payload: {err}")) @@ -145,7 +145,7 @@ pub(crate) async fn request( format!("unexpected status code: status = {}", resp.status()), )); } - Ok(resp.into()) + Ok(resp) } pub(crate) async fn request_json( diff --git a/poem/src/listener/rustls.rs b/poem/src/listener/rustls.rs index b38e6c83f7..445123dc3c 100644 --- a/poem/src/listener/rustls.rs +++ b/poem/src/listener/rustls.rs @@ -245,14 +245,14 @@ impl RustlsConfig { WebPkiClientVerifier::builder(read_trust_anchor(trust_anchor)?.into()) .allow_unauthenticated() .build() - .map_err(|err| IoError::other(err))?; + .map_err(IoError::other)?; builder.with_client_cert_verifier(verifier) } TlsClientAuth::Required(trust_anchor) => { let verifier = WebPkiClientVerifier::builder(read_trust_anchor(trust_anchor)?.into()) .build() - .map_err(|err| IoError::other(err))?; + .map_err(IoError::other)?; builder.with_client_cert_verifier(verifier) } }; diff --git a/poem/src/request.rs b/poem/src/request.rs index e8bbd5e018..e0416d6d66 100644 --- a/poem/src/request.rs +++ b/poem/src/request.rs @@ -132,7 +132,7 @@ impl From<(http::Request, LocalAddr, RemoteAddr, Scheme)> for Request version: parts.version, headers: parts.headers, extensions: parts.extensions, - body: Body(body.map_err(|err| Error::other(err)).boxed()), + body: Body(body.map_err(Error::other).boxed()), state: RequestState { local_addr, remote_addr, diff --git a/poem/src/server.rs b/poem/src/server.rs index 5f3b9c60a3..824340fd0b 100644 --- a/poem/src/server.rs +++ b/poem/src/server.rs @@ -361,7 +361,6 @@ async fn serve_connection( tokio::select! { _ = conn => { // Connection completed successfully. - return; }, _ = connection_shutdown_token.cancelled() => { tracing::info!(remote_addr=%remote_addr, "closing connection due to inactivity"); From e8cdf5ced2c399626e119a367bb77ac2da47aaba Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 5 Jan 2024 22:23:45 +0800 Subject: [PATCH 03/11] update MSRV to `1.74.0` --- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- poem-grpc/README.md | 2 +- poem-lambda/README.md | 2 +- poem-openapi/README.md | 2 +- poem/README.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b537553f5..427a838e48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: # Switch to stable Rust - uses: actions-rs/toolchain@v1 with: - toolchain: 1.67.0 + toolchain: 1.74.0 components: rustfmt, clippy override: true - name: Cache Rust @@ -91,7 +91,7 @@ jobs: # Switch to stable Rust - uses: actions-rs/toolchain@v1 with: - toolchain: 1.67.0 + toolchain: 1.74.0 components: rustfmt, clippy - name: Cache Rust uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 9b6a895949..da5e6cf62a 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -21,7 +21,7 @@ jobs: - name: Install Stable Toolchain uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.67.0 + toolchain: 1.74.0 components: rustfmt - name: Cache Rust uses: Swatinem/rust-cache@v2 diff --git a/poem-grpc/README.md b/poem-grpc/README.md index 56caecd9d4..b5a38a8c35 100644 --- a/poem-grpc/README.md +++ b/poem-grpc/README.md @@ -64,7 +64,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in ## MSRV -The minimum supported Rust version for this crate is `1.67.0`. +The minimum supported Rust version for this crate is `1.74.0`. ## Contributing diff --git a/poem-lambda/README.md b/poem-lambda/README.md index b9855a7d4b..c15cc8bae9 100644 --- a/poem-lambda/README.md +++ b/poem-lambda/README.md @@ -49,7 +49,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in ## MSRV -The minimum supported Rust version for this crate is `1.67.0`. +The minimum supported Rust version for this crate is `1.74.0`. ## Contributing diff --git a/poem-openapi/README.md b/poem-openapi/README.md index 294df521c8..c189cb1500 100644 --- a/poem-openapi/README.md +++ b/poem-openapi/README.md @@ -130,7 +130,7 @@ hello, sunli! ## MSRV -The minimum supported Rust version for this crate is `1.67.0`. +The minimum supported Rust version for this crate is `1.74.0`. ## Contributing diff --git a/poem/README.md b/poem/README.md index ffd27a88e7..754a02039f 100644 --- a/poem/README.md +++ b/poem/README.md @@ -107,7 +107,7 @@ More examples can be found [here][examples]. ## MSRV -The minimum supported Rust version for this crate is `1.67.0`. +The minimum supported Rust version for this crate is `1.74.0`. ## Contributing From 21c7176b1abbcf3b36a19e0355d451a5773f8610 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 5 Jan 2024 22:40:54 +0800 Subject: [PATCH 04/11] update examples --- examples/{openapi => disabled}/auth-github/Cargo.toml | 0 examples/{openapi => disabled}/auth-github/src/main.rs | 0 examples/{poem => disabled}/tonic/Cargo.toml | 0 examples/{poem => disabled}/tonic/build.rs | 0 examples/{poem => disabled}/tonic/proto/helloworld.proto | 0 examples/{poem => disabled}/tonic/src/client.rs | 0 examples/{poem => disabled}/tonic/src/main.rs | 0 examples/poem/acme-expanded-http-01/src/main.rs | 3 +-- examples/poem/opentelemetry-jaeger/src/server1.rs | 3 +-- poem/src/listener/acme/client.rs | 2 +- 10 files changed, 3 insertions(+), 5 deletions(-) rename examples/{openapi => disabled}/auth-github/Cargo.toml (100%) rename examples/{openapi => disabled}/auth-github/src/main.rs (100%) rename examples/{poem => disabled}/tonic/Cargo.toml (100%) rename examples/{poem => disabled}/tonic/build.rs (100%) rename examples/{poem => disabled}/tonic/proto/helloworld.proto (100%) rename examples/{poem => disabled}/tonic/src/client.rs (100%) rename examples/{poem => disabled}/tonic/src/main.rs (100%) diff --git a/examples/openapi/auth-github/Cargo.toml b/examples/disabled/auth-github/Cargo.toml similarity index 100% rename from examples/openapi/auth-github/Cargo.toml rename to examples/disabled/auth-github/Cargo.toml diff --git a/examples/openapi/auth-github/src/main.rs b/examples/disabled/auth-github/src/main.rs similarity index 100% rename from examples/openapi/auth-github/src/main.rs rename to examples/disabled/auth-github/src/main.rs diff --git a/examples/poem/tonic/Cargo.toml b/examples/disabled/tonic/Cargo.toml similarity index 100% rename from examples/poem/tonic/Cargo.toml rename to examples/disabled/tonic/Cargo.toml diff --git a/examples/poem/tonic/build.rs b/examples/disabled/tonic/build.rs similarity index 100% rename from examples/poem/tonic/build.rs rename to examples/disabled/tonic/build.rs diff --git a/examples/poem/tonic/proto/helloworld.proto b/examples/disabled/tonic/proto/helloworld.proto similarity index 100% rename from examples/poem/tonic/proto/helloworld.proto rename to examples/disabled/tonic/proto/helloworld.proto diff --git a/examples/poem/tonic/src/client.rs b/examples/disabled/tonic/src/client.rs similarity index 100% rename from examples/poem/tonic/src/client.rs rename to examples/disabled/tonic/src/client.rs diff --git a/examples/poem/tonic/src/main.rs b/examples/disabled/tonic/src/main.rs similarity index 100% rename from examples/poem/tonic/src/main.rs rename to examples/disabled/tonic/src/main.rs diff --git a/examples/poem/acme-expanded-http-01/src/main.rs b/examples/poem/acme-expanded-http-01/src/main.rs index bbfc845102..5c4750b777 100644 --- a/examples/poem/acme-expanded-http-01/src/main.rs +++ b/examples/poem/acme-expanded-http-01/src/main.rs @@ -31,8 +31,7 @@ async fn main() -> Result<(), std::io::Error> { } tracing_subscriber::fmt::init(); - let mut acme_client = - AcmeClient::try_new(&LETS_ENCRYPT_PRODUCTION.parse().unwrap(), vec![]).await?; + let mut acme_client = AcmeClient::try_new(LETS_ENCRYPT_PRODUCTION, vec![]).await?; let cert_resolver = Arc::new(ResolveServerCert::default()); let challenge = ChallengeType::Http01; let keys_for_http_challenge = Http01TokensMap::new(); diff --git a/examples/poem/opentelemetry-jaeger/src/server1.rs b/examples/poem/opentelemetry-jaeger/src/server1.rs index c04d04ae98..8978245853 100644 --- a/examples/poem/opentelemetry-jaeger/src/server1.rs +++ b/examples/poem/opentelemetry-jaeger/src/server1.rs @@ -9,7 +9,6 @@ use opentelemetry_http::HeaderInjector; use opentelemetry_sdk::{propagation::TraceContextPropagator, trace::Tracer}; use poem::{ get, handler, - http::Method, listener::TcpListener, middleware::{OpenTelemetryMetrics, OpenTelemetryTracing}, web::Data, @@ -38,7 +37,7 @@ async fn index(tracer: Data<&Tracer>, body: String) -> String { let req = { let mut req = reqwest::Request::new( - Method::GET, + reqwest::Method::GET, Url::from_str("http://localhost:3002/api2").unwrap(), ); global::get_text_map_propagator(|propagator| { diff --git a/poem/src/listener/acme/client.rs b/poem/src/listener/acme/client.rs index 14f2e77c1a..b3c4b55fea 100644 --- a/poem/src/listener/acme/client.rs +++ b/poem/src/listener/acme/client.rs @@ -28,7 +28,7 @@ pub struct AcmeClient { impl AcmeClient { /// Create a new client. `directory_url` is the url for the ACME provider. `contacts` is a list /// of URLS (ex: `mailto:`) the ACME service can use to reach you if there's issues with your certificates. - pub(crate) async fn try_new(directory_url: &str, contacts: Vec) -> IoResult { + pub async fn try_new(directory_url: &str, contacts: Vec) -> IoResult { let client = Client::new(); let directory = get_directory(&client, directory_url).await?; Ok(Self { From 51aa490890424321fb9bba3077feceb823f5fe2c Mon Sep 17 00:00:00 2001 From: Prabel <120583804+HiPrabel@users.noreply.github.com> Date: Fri, 5 Jan 2024 20:16:32 +0530 Subject: [PATCH 05/11] added documetation on how to merge API specs (#716) --- examples/openapi/combined-apis/Readme.md | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 examples/openapi/combined-apis/Readme.md diff --git a/examples/openapi/combined-apis/Readme.md b/examples/openapi/combined-apis/Readme.md new file mode 100644 index 0000000000..c12ed41b8e --- /dev/null +++ b/examples/openapi/combined-apis/Readme.md @@ -0,0 +1,84 @@ +## Merging API Specifications + +To merge API specifications for multiple services and expose them on a single page, follow these steps: + +1. Generate OpenAPI specifications for each service. +2. Create a function to merge the specifications. ... +3. Integrate the merged specification when creating the OpenApiService. +4. Test the merged API specification to ensure it works as expected. + +Example code snippet: + +```rust +// Merge OpenAPI specifications +let merged_spec = merge_openapi_specs(auth_spec, test_spec); + +// Create an OpenApiService for the merged specification +let api_service = OpenApiService::new_with_spec(merged_spec, "Merged API", version).server(api_doc_url_info); + +___________________________________________________________________________ + + +## Merging API Specifications [Explained]: + +If you have two API services and wish to merge their OpenAPI specifications to be accessed on a single page, follow these steps: + +1.Generate OpenAPI Specifications: + + Ensure that you have the OpenAPI specifications for each service. You can use tools like Swagger or OpenAPI Generator to automatically generate these specifications from your API code. + +2.Merge Specifications: + + Create a function to merge the OpenAPI specifications. Below is an example code snippet. Customize the function based on the structure of your OpenAPI specifications. Ensure that you handle conflicts appropriately.: + + +lang:'Rust' + +use openapiv3::OpenAPI; + +fn merge_openapi_specs(auth_spec: OpenAPI, test_spec: OpenAPI) -> OpenAPI { + let mut merged_spec = auth_spec.clone(); // Start with one of the specs + + // Merge paths + if let Some(test_paths) = test_spec.paths { + if let Some(merged_paths) = &mut merged_spec.paths { + merged_paths.extend(test_paths); + } else { + merged_spec.paths = Some(test_paths); + } + } + + // Merge components, etc. + + merged_spec +} + + +3. Integrate Merged Specification: + + Use the merged specification when creating the OpenApiService. Update your application code as following example: + +lang:'Rust' + +use poem_openapi::{OpenApiService, SwaggerUIConfig}; + +// Assuming you have your OpenAPI specs for AuthApi and TestApi in variables auth_spec and test_spec. + +// Merge OpenAPI specifications +let merged_spec = merge_openapi_specs(auth_spec, test_spec); + +// Create an OpenApiService for the merged specification +let api_service = OpenApiService::new_with_spec(merged_spec, "Merged API", version).server(api_doc_url_info); + +// Configure Swagger UI for the merged API +let ui = api_service.swagger_ui(SwaggerUIConfig::default().url("/panel/openapi.json")); + +let app = Route::new() + .at("/status", get(server_status)) + .nest("/api/auth", get_auth_api()) + .nest("/api/test", get_test_api()) + .nest("/panel", ui); + +4. Testing: + + Make sure to thoroughly test the merged API specification to ensure that it works as expected. Verify that the paths, components, and other relevant information are correctly combined. \ No newline at end of file From 0bf7f167678927b1b4933d60ba30596d07e2aded Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 5 Jan 2024 22:49:53 +0800 Subject: [PATCH 06/11] update docs --- poem/src/middleware/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/poem/src/middleware/mod.rs b/poem/src/middleware/mod.rs index f24a4d97b0..2885952b86 100644 --- a/poem/src/middleware/mod.rs +++ b/poem/src/middleware/mod.rs @@ -81,6 +81,7 @@ use crate::endpoint::Endpoint; /// const TOKEN_HEADER: &str = "X-Token"; /// /// /// Token data +/// #[derive(Clone)] /// struct Token(String); /// /// #[poem::async_trait] @@ -138,6 +139,7 @@ use crate::endpoint::Endpoint; /// } /// /// /// Token data +/// #[derive(Clone)] /// struct Token(String); /// /// async fn token_middleware(next: E, mut req: Request) -> Result { From 4367c63d89ce4de11ccfdc8780a919e225f94e9e Mon Sep 17 00:00:00 2001 From: Tom Linton Date: Sat, 6 Jan 2024 13:39:16 +1300 Subject: [PATCH 07/11] allow WebSocket casing for upgrade header (#709) --- poem/src/web/websocket/extractor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/poem/src/web/websocket/extractor.rs b/poem/src/web/websocket/extractor.rs index 8d712e2755..13f9ab7fde 100644 --- a/poem/src/web/websocket/extractor.rs +++ b/poem/src/web/websocket/extractor.rs @@ -28,8 +28,12 @@ pub struct WebSocket { impl WebSocket { async fn internal_from_request(req: &Request) -> Result { + let is_valid_upgrade_header = req.headers().get(header::UPGRADE) + == Some(&HeaderValue::from_static("websocket")) + || req.headers().get(header::UPGRADE) == Some(&HeaderValue::from_static("WebSocket")); + if req.method() != Method::GET - || req.headers().get(header::UPGRADE) != Some(&HeaderValue::from_static("websocket")) + || !is_valid_upgrade_header || req.headers().get(header::SEC_WEBSOCKET_VERSION) != Some(&HeaderValue::from_static("13")) { From 8185ecff9ca71a1c04b1d6ed18afa7a667f5f92a Mon Sep 17 00:00:00 2001 From: kik4444 <7779637+kik4444@users.noreply.github.com> Date: Sat, 6 Jan 2024 02:39:41 +0200 Subject: [PATCH 08/11] impl `Type` for `std::time::Duration` instead of only `humantime::Duration` (#713) * impl Type for std::time::Duration instead of humantime:Duration * Return support for the humantime::Duration wrapper --- poem-openapi/src/types/external/humantime.rs | 13 ++-- .../src/types/external/humantime_wrapper.rs | 78 +++++++++++++++++++ poem-openapi/src/types/external/mod.rs | 2 + 3 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 poem-openapi/src/types/external/humantime_wrapper.rs diff --git a/poem-openapi/src/types/external/humantime.rs b/poem-openapi/src/types/external/humantime.rs index 82df997c9b..be80ec5c08 100644 --- a/poem-openapi/src/types/external/humantime.rs +++ b/poem-openapi/src/types/external/humantime.rs @@ -1,6 +1,5 @@ -use std::borrow::Cow; +use std::{borrow::Cow, time::Duration}; -use humantime::Duration; use poem::{http::HeaderValue, web::Field}; use serde_json::Value; @@ -42,7 +41,7 @@ impl ParseFromJSON for Duration { fn parse_from_json(value: Option) -> ParseResult { let value = value.unwrap_or_default(); if let Value::String(value) = value { - Ok(value.parse()?) + Ok(humantime::parse_duration(&value)?) } else { Err(ParseError::expected_type(value)) } @@ -51,7 +50,7 @@ impl ParseFromJSON for Duration { impl ParseFromParameter for Duration { fn parse_from_parameter(value: &str) -> ParseResult { - value.parse().map_err(ParseError::custom) + humantime::parse_duration(value).map_err(ParseError::custom) } } @@ -59,7 +58,7 @@ impl ParseFromParameter for Duration { impl ParseFromMultipartField for Duration { async fn parse_from_multipart(field: Option) -> ParseResult { match field { - Some(field) => Ok(field.text().await?.parse()?), + Some(field) => Ok(humantime::parse_duration(&field.text().await?)?), None => Err(ParseError::expected_input()), } } @@ -67,12 +66,12 @@ impl ParseFromMultipartField for Duration { impl ToJSON for Duration { fn to_json(&self) -> Option { - Some(Value::String(self.to_string())) + Some(Value::String(humantime::format_duration(*self).to_string())) } } impl ToHeader for Duration { fn to_header(&self) -> Option { - HeaderValue::from_str(&self.to_string()).ok() + HeaderValue::from_str(&humantime::format_duration(*self).to_string()).ok() } } diff --git a/poem-openapi/src/types/external/humantime_wrapper.rs b/poem-openapi/src/types/external/humantime_wrapper.rs new file mode 100644 index 0000000000..82df997c9b --- /dev/null +++ b/poem-openapi/src/types/external/humantime_wrapper.rs @@ -0,0 +1,78 @@ +use std::borrow::Cow; + +use humantime::Duration; +use poem::{http::HeaderValue, web::Field}; +use serde_json::Value; + +use crate::{ + registry::{MetaSchema, MetaSchemaRef}, + types::{ + ParseError, ParseFromJSON, ParseFromMultipartField, ParseFromParameter, ParseResult, + ToHeader, ToJSON, Type, + }, +}; + +impl Type for Duration { + const IS_REQUIRED: bool = true; + + type RawValueType = Self; + + type RawElementValueType = Self; + + fn name() -> Cow<'static, str> { + "string(duration)".into() + } + + fn schema_ref() -> MetaSchemaRef { + MetaSchemaRef::Inline(Box::new(MetaSchema::new_with_format("string", "duration"))) + } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } + + fn raw_element_iter<'a>( + &'a self, + ) -> Box + 'a> { + Box::new(self.as_raw_value().into_iter()) + } +} + +impl ParseFromJSON for Duration { + fn parse_from_json(value: Option) -> ParseResult { + let value = value.unwrap_or_default(); + if let Value::String(value) = value { + Ok(value.parse()?) + } else { + Err(ParseError::expected_type(value)) + } + } +} + +impl ParseFromParameter for Duration { + fn parse_from_parameter(value: &str) -> ParseResult { + value.parse().map_err(ParseError::custom) + } +} + +#[poem::async_trait] +impl ParseFromMultipartField for Duration { + async fn parse_from_multipart(field: Option) -> ParseResult { + match field { + Some(field) => Ok(field.text().await?.parse()?), + None => Err(ParseError::expected_input()), + } + } +} + +impl ToJSON for Duration { + fn to_json(&self) -> Option { + Some(Value::String(self.to_string())) + } +} + +impl ToHeader for Duration { + fn to_header(&self) -> Option { + HeaderValue::from_str(&self.to_string()).ok() + } +} diff --git a/poem-openapi/src/types/external/mod.rs b/poem-openapi/src/types/external/mod.rs index ce4af05f60..b385f08fa5 100644 --- a/poem-openapi/src/types/external/mod.rs +++ b/poem-openapi/src/types/external/mod.rs @@ -16,6 +16,8 @@ mod hashmap; mod hashset; #[cfg(feature = "humantime")] mod humantime; +#[cfg(feature = "humantime")] +mod humantime_wrapper; mod integers; mod ip; mod optional; From 40b0d07f23369e8d7196ac6b68ba29e769dd7186 Mon Sep 17 00:00:00 2001 From: Artem Medvedev Date: Sat, 6 Jan 2024 01:40:04 +0100 Subject: [PATCH 09/11] docs(readme): mention `poem-grants` (#700) --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bb3c0a182e..80cd7456ea 100644 --- a/README.md +++ b/README.md @@ -40,16 +40,17 @@ This repo contains the following main components: The following are cases of community use: -| Repo | Description | Documentation | -|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| -| [delicate](https://github.com/BinChengZhao/delicate) | A distributed task scheduling platform written in rust. | [(README)](https://delicate-rs.github.io/Roadmap.html) | -| [databend](https://github.com/datafuselabs/databend) | A cloud-native data warehouse written in rust. | [(ROADMAP)](https://github.com/datafuselabs/databend/issues/746) | -| [muse](https://leihuo.163.com/) | A NetEase Leihuo's internal art resource sharing platform, backend in rust. | | -| [hik-proconnect](https://www.hikvision.com/en/products/software/hik-proconnect/) | A front-end automated deployment platform based on continuous integration of aws. Hik-ProConnect project for Hikvision | | -| [warpgate](https://github.com/eugeny/warpgate) | A smart SSH bastion host that works with any SSH clients. | [(README)](https://github.com/warp-tech/warpgate/blob/main/README.md) | -| [lust](https://github.com/ChillFish8/lust) | A fast, auto-optimizing image server designed for high throughput and caching. | [(README)](https://github.com/ChillFish8/lust/blob/master/README.md) | -| [aptos](https://github.com/aptos-labs/aptos-core) | Building the safest and most scalable Layer 1 blockchain. | [(WEBSITE)](https://aptoslabs.com/) | -| [poem-casbin](https://github.com/casbin-rs/poem-casbin) | Casbin access control middleware for poem framework. | [(WEBSITE)](https://casbin.org/) | +| Repo | Description | Documentation | +|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| [delicate](https://github.com/BinChengZhao/delicate) | A distributed task scheduling platform written in rust. | [(README)](https://delicate-rs.github.io/Roadmap.html) | +| [databend](https://github.com/datafuselabs/databend) | A cloud-native data warehouse written in rust. | [(ROADMAP)](https://github.com/datafuselabs/databend/issues/746) | +| [muse](https://leihuo.163.com/) | A NetEase Leihuo's internal art resource sharing platform, backend in rust. | | +| [hik-proconnect](https://www.hikvision.com/en/products/software/hik-proconnect/) | A front-end automated deployment platform based on continuous integration of aws. Hik-ProConnect project for Hikvision | | +| [warpgate](https://github.com/eugeny/warpgate) | A smart SSH bastion host that works with any SSH clients. | [(README)](https://github.com/warp-tech/warpgate/blob/main/README.md) | +| [lust](https://github.com/ChillFish8/lust) | A fast, auto-optimizing image server designed for high throughput and caching. | [(README)](https://github.com/ChillFish8/lust/blob/master/README.md) | +| [aptos](https://github.com/aptos-labs/aptos-core) | Building the safest and most scalable Layer 1 blockchain. | [(WEBSITE)](https://aptoslabs.com/) | +| [poem-casbin](https://github.com/casbin-rs/poem-casbin) | Casbin access control middleware for poem framework. | [(WEBSITE)](https://casbin.org/) | +| [poem-grants](https://github.com/DDtKey/protect-endpoints/tree/main/poem-grants) | Authorization extension to protect endpoints. | [(README)](https://github.com/DDtKey/protect-endpoints/blob/main/poem-grants/README.md) | ### Startups From 184ed6cf0ec3e1bc997feedd684c9d9ac4cbaeca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 08:46:46 +0800 Subject: [PATCH 10/11] build(deps): update quick-xml requirement from 0.29.0 to 0.30.0 (#608) Updates the requirements on [quick-xml](https://github.com/tafia/quick-xml) to permit the latest version. - [Release notes](https://github.com/tafia/quick-xml/releases) - [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md) - [Commits](https://github.com/tafia/quick-xml/compare/v0.29.0...v0.29.0) --- updated-dependencies: - dependency-name: quick-xml dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 18c267d04075ab1d1e19ad5f548f5fd2dfa820b3 Mon Sep 17 00:00:00 2001 From: Sunli Date: Sat, 6 Jan 2024 08:55:55 +0800 Subject: [PATCH 11/11] poem 2.0.0 --- Cargo.toml | 8 ++++---- poem-derive/Cargo.toml | 2 +- poem-grpc-build/Cargo.toml | 2 +- poem-grpc/Cargo.toml | 2 +- poem-lambda/Cargo.toml | 2 +- poem-openapi-derive/Cargo.toml | 2 +- poem-openapi/CHANGELOG.md | 6 ++++++ poem-openapi/Cargo.toml | 4 ++-- poem-openapi/README.md | 1 + poem-openapi/src/lib.rs | 1 + poem/CHANGELOG.md | 5 +++++ poem/Cargo.toml | 2 +- 12 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b13ca68af5..5540445e8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,10 +20,10 @@ repository = "https://github.com/poem-web/poem" rust-version = "1.64" [workspace.dependencies] -poem = { path = "poem", version = "1.3.59", default-features = false } -poem-derive = { path = "poem-derive", version = "1.3.59" } -poem-openapi-derive = { path = "poem-openapi-derive", version = "3.0.6" } -poem-grpc-build = { path = "poem-grpc-build", version = "0.2.23" } +poem = { path = "poem", version = "2.0.0", default-features = false } +poem-derive = { path = "poem-derive", version = "2.0.0" } +poem-openapi-derive = { path = "poem-openapi-derive", version = "4.0.0" } +poem-grpc-build = { path = "poem-grpc-build", version = "0.3.0" } proc-macro-crate = "2.0.0" proc-macro2 = "1.0.29" diff --git a/poem-derive/Cargo.toml b/poem-derive/Cargo.toml index 23b449fe3f..6c0f019b3d 100644 --- a/poem-derive/Cargo.toml +++ b/poem-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-derive" -version = "1.3.59" +version = "2.0.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-grpc-build/Cargo.toml b/poem-grpc-build/Cargo.toml index b335ec65ee..c0f107a382 100644 --- a/poem-grpc-build/Cargo.toml +++ b/poem-grpc-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-grpc-build" -version = "0.2.23" +version = "0.3.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-grpc/Cargo.toml b/poem-grpc/Cargo.toml index ec3160de43..dab8b02c75 100644 --- a/poem-grpc/Cargo.toml +++ b/poem-grpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-grpc" -version = "0.2.25" +version = "0.3.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-lambda/Cargo.toml b/poem-lambda/Cargo.toml index ff0b4bdf9a..aea09499b9 100644 --- a/poem-lambda/Cargo.toml +++ b/poem-lambda/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-lambda" -version = "1.3.59" +version = "4.0.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-openapi-derive/Cargo.toml b/poem-openapi-derive/Cargo.toml index 1eca7cb034..9ba46ca316 100644 --- a/poem-openapi-derive/Cargo.toml +++ b/poem-openapi-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi-derive" -version = "3.0.6" +version = "4.0.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-openapi/CHANGELOG.md b/poem-openapi/CHANGELOG.md index eecd275ce6..5ee723b148 100644 --- a/poem-openapi/CHANGELOG.md +++ b/poem-openapi/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [4.0.0] 2023-01-06 + +- upgrade to `hyper1` +- added documetation on how to merge API specs [#716](https://github.com/poem-web/poem/pull/716) +- impl Type for std::time::Duration instead of only humantime::Duration [#713](https://github.com/poem-web/poem/pull/713) + # [3.0.6] 2023-11-19 - add [`prost-wkt-types` crate](https://crates.io/crates/prost-wkt-types) support [#689](https://github.com/poem-web/poem/pull/689) diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index 7e85c64bb2..52277920c0 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi" -version = "3.0.6" +version = "4.0.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -69,7 +69,7 @@ bson = { version = "2.0.0", optional = true } rust_decimal = { version = "1.22.0", optional = true } humantime = { version = "2.1.0", optional = true } ipnet = { version = "2.7.1", optional = true } -prost-wkt-types = { version = "0.5.0", optional = true} +prost-wkt-types = { version = "0.5.0", optional = true } geo-types = { version = "0.7.12", optional = true } geojson = { version = "0.24.1", features = ["geo-types"], optional = true } diff --git a/poem-openapi/README.md b/poem-openapi/README.md index c189cb1500..17e6337a83 100644 --- a/poem-openapi/README.md +++ b/poem-openapi/README.md @@ -61,6 +61,7 @@ To avoid compiling unused dependencies, Poem gates certain features, some of whi | redoc | Add Redoc UI support | | email | Support for email address string | | hostname | Support for hostname string | +| humantime | Integrate with the [`humantime` crate](https://crates.io/crates/humantime) | | uuid | Integrate with the [`uuid` crate](https://crates.io/crates/uuid) | | url | Integrate with the [`url` crate](https://crates.io/crates/url) | | geo | Integrate with the [`geo-types` crate](https://crates.io/crates/geo-types) | diff --git a/poem-openapi/src/lib.rs b/poem-openapi/src/lib.rs index b2bd86e6c5..5ee4707919 100644 --- a/poem-openapi/src/lib.rs +++ b/poem-openapi/src/lib.rs @@ -103,6 +103,7 @@ //! | redoc | Add Redoc UI support | //! | email | Support for email address string | //! | hostname | Support for hostname string | +//! | humantime | Integrate with the [`humantime` crate](https://crates.io/crates/humantime) | //! | uuid | Integrate with the [`uuid` crate](https://crates.io/crates/uuid) | //! | url | Integrate with the [`url` crate](https://crates.io/crates/url) | //! | geo | Integrate with the [`geo-types` crate](https://crates.io/crates/geo-types) | diff --git a/poem/CHANGELOG.md b/poem/CHANGELOG.md index 862b3a2612..1345bf9a0d 100644 --- a/poem/CHANGELOG.md +++ b/poem/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [2.0.0] 2024-01-06 + +- upgrade to `hyper1` +- allow WebSocket casing for upgrade header [#709](https://github.com/poem-web/poem/pull/709) + # [1.3.59] 2023-11-19 - added permissions and owner to UnixListener [#668](https://github.com/poem-web/poem/pull/668) diff --git a/poem/Cargo.toml b/poem/Cargo.toml index d84d20f1d2..882ed91e30 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem" -version = "1.3.59" +version = "2.0.0" authors.workspace = true edition.workspace = true license.workspace = true