From 63635b35c1b630516106f9ea00d5d85c4fbe5eb6 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Sun, 17 May 2026 20:01:39 +0300 Subject: [PATCH 1/7] chore(primp-hyper): merge upstream hyper 1.8.1 -> 1.9.0 - Apply upstream source diff (15 files changed) - Update Cargo.toml version to 1.9.0 - Remove deprecated pin-utils dependency - Add max_local_error_reset_streams and cancel_rx support - Add current_max_send_streams/current_max_recv_streams accessors - Various HTTP1/H2 protocol improvements and fixes --- crates/primp-hyper/Cargo.toml | 7 +- crates/primp-hyper/src/body/incoming.rs | 4 + crates/primp-hyper/src/client/conn/http2.rs | 33 +++++- crates/primp-hyper/src/client/dispatch.rs | 3 + crates/primp-hyper/src/common/io/rewind.rs | 12 ++- crates/primp-hyper/src/common/task.rs | 4 +- crates/primp-hyper/src/error.rs | 23 +++++ crates/primp-hyper/src/ffi/http_types.rs | 9 +- crates/primp-hyper/src/proto/h1/conn.rs | 3 +- crates/primp-hyper/src/proto/h1/decode.rs | 92 ++++++++++------- crates/primp-hyper/src/proto/h2/client.rs | 48 +++++++++ crates/primp-hyper/src/proto/h2/mod.rs | 15 ++- crates/primp-hyper/src/rt/io.rs | 105 +++++++++++++++++++- crates/primp-hyper/src/server/conn/http1.rs | 6 ++ 14 files changed, 304 insertions(+), 60 deletions(-) diff --git a/crates/primp-hyper/Cargo.toml b/crates/primp-hyper/Cargo.toml index 2c404f1..3b359b9 100644 --- a/crates/primp-hyper/Cargo.toml +++ b/crates/primp-hyper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "primp-hyper" -version = "1.8.1" +version = "1.9.0" description = "Fork of the original hyper HTTP library." readme = "README.md" license = "MIT" @@ -76,7 +76,6 @@ http1 = [ "dep:futures-core", "dep:httparse", "dep:itoa", - "dep:pin-utils", ] http2 = [ "dep:futures-channel", @@ -149,10 +148,6 @@ optional = true version = "0.2.4" optional = true -[dependencies.pin-utils] -version = "0.1" -optional = true - [dependencies.smallvec] version = "1.12" features = [ diff --git a/crates/primp-hyper/src/body/incoming.rs b/crates/primp-hyper/src/body/incoming.rs index 64ee500..8c7c6ff 100644 --- a/crates/primp-hyper/src/body/incoming.rs +++ b/crates/primp-hyper/src/body/incoming.rs @@ -458,12 +458,16 @@ impl fmt::Debug for Sender { #[cfg(test)] mod tests { + #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))] use std::mem; + #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))] use std::task::Poll; + #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))] use super::{Body, Incoming, SizeHint}; #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))] use super::{DecodedLength, Sender}; + #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))] use http_body_util::BodyExt; #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))] diff --git a/crates/primp-hyper/src/client/conn/http2.rs b/crates/primp-hyper/src/client/conn/http2.rs index abe506b..9cc468b 100644 --- a/crates/primp-hyper/src/client/conn/http2.rs +++ b/crates/primp-hyper/src/client/conn/http2.rs @@ -216,6 +216,26 @@ where pub fn is_extended_connect_protocol_enabled(&self) -> bool { self.inner.1.is_extended_connect_protocol_enabled() } + + /// Returns the current maximum send stream count. + /// + /// This setting is configured in a [`SETTINGS_MAX_CONCURRENT_STREAMS` parameter][1] in a `SETTINGS` frame, + /// and may change throughout the connection lifetime. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc7540#section-5.1.2 + pub fn current_max_send_streams(&self) -> usize { + self.inner.1.current_max_send_streams() + } + + /// Returns the current maximum receive stream count. + /// + /// This setting is configured in a [`SETTINGS_MAX_CONCURRENT_STREAMS` parameter][1] in a `SETTINGS` frame, + /// and may change throughout the connection lifetime. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc7540#section-5.1.2 + pub fn current_max_recv_streams(&self) -> usize { + self.inner.1.current_max_recv_streams() + } } impl fmt::Debug for Connection @@ -399,7 +419,7 @@ where /// /// See [Section 5.1.2] in the HTTP/2 spec for more details. /// - /// [Section 5.1.2]: https://http2.github.io/http2-spec/#rfc.section.5.1.2 + /// [Section 5.1.2]: https://httpwg.org/specs/rfc7540.html#rfc.section.5.1.2 pub fn max_concurrent_streams(&mut self, max: impl Into>) -> &mut Self { self.h2_builder.max_concurrent_streams = max.into(); self @@ -544,6 +564,17 @@ where self } + /// Configures the maximum number of local resets due to protocol errors made by the remote end. + /// + /// See the documentation of [`h2::client::Builder::max_local_error_reset_streams`] for more + /// details. + /// + /// The default value is 1024. + pub fn max_local_error_reset_streams(&mut self, max: impl Into>) -> &mut Self { + self.h2_builder.max_local_error_reset_streams = max.into(); + self + } + /// Constructs a connection with the configured options and IO. /// See [`client::conn`](crate::client::conn) for more. /// diff --git a/crates/primp-hyper/src/client/dispatch.rs b/crates/primp-hyper/src/client/dispatch.rs index ab22cd3..4af96f4 100644 --- a/crates/primp-hyper/src/client/dispatch.rs +++ b/crates/primp-hyper/src/client/dispatch.rs @@ -366,6 +366,9 @@ where } }; trace!("send_when canceled"); + // Tell pipe_task to reset the h2 stream so that + // RST_STREAM is sent and flow-control capacity freed. + this.when.as_mut().cancel(); Poll::Ready(()) } Poll::Ready(Err((error, message))) => { diff --git a/crates/primp-hyper/src/common/io/rewind.rs b/crates/primp-hyper/src/common/io/rewind.rs index c1f7a4f..b692f43 100644 --- a/crates/primp-hyper/src/common/io/rewind.rs +++ b/crates/primp-hyper/src/common/io/rewind.rs @@ -14,7 +14,11 @@ pub(crate) struct Rewind { } impl Rewind { - #[cfg(test)] + #[cfg(all( + test, + any(feature = "client", feature = "server"), + any(feature = "http1", feature = "http2") + ))] pub(crate) fn new(io: T) -> Self { Rewind { pre: None, @@ -29,7 +33,11 @@ impl Rewind { } } - #[cfg(test)] + #[cfg(all( + test, + any(feature = "client", feature = "server"), + any(feature = "http1", feature = "http2") + ))] pub(crate) fn rewind(&mut self, bs: Bytes) { debug_assert!(self.pre.is_none()); self.pre = Some(bs); diff --git a/crates/primp-hyper/src/common/task.rs b/crates/primp-hyper/src/common/task.rs index 90a0883..7e60245 100644 --- a/crates/primp-hyper/src/common/task.rs +++ b/crates/primp-hyper/src/common/task.rs @@ -36,8 +36,8 @@ fn noop_waker() -> Waker { pub(crate) fn now_or_never(fut: F) -> Option { let waker = noop_waker(); let mut cx = Context::from_waker(&waker); - // TODO: replace with std::pin::pin! and drop pin-utils once MSRV >= 1.68 - pin_utils::pin_mut!(fut); + // TODO: replace with std::pin::pin! once MSRV >= 1.68 + tokio::pin!(fut); match fut.poll(&mut cx) { Poll::Ready(res) => Some(res), Poll::Pending => None, diff --git a/crates/primp-hyper/src/error.rs b/crates/primp-hyper/src/error.rs index 9015eee..90c6400 100644 --- a/crates/primp-hyper/src/error.rs +++ b/crates/primp-hyper/src/error.rs @@ -192,6 +192,13 @@ impl Error { matches!(self.inner.kind, Kind::Parse(Parse::Status)) } + /// Returns true if this was an HTTP parse error caused by HTTP2 preface sent over an HTTP1 + /// connection. + #[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))] + pub fn is_parse_version_h2(&self) -> bool { + matches!(self.inner.kind, Kind::Parse(Parse::VersionH2)) + } + /// Returns true if this error was caused by user code. pub fn is_user(&self) -> bool { matches!(self.inner.kind, Kind::User(_)) @@ -218,6 +225,22 @@ impl Error { } /// Returns true if the connection closed before a message could complete. + /// + /// This means that the supplied IO connection reported EOF (closed) while + /// hyper's HTTP state indicates more of the message (either request or + /// response) needed to be transmitted. + /// + /// Some cases this could happen (not exhaustive): + /// + /// - A request is written on a connection, and the next `read` reports + /// EOF (perhaps a server just closed an "idle" connection). + /// - A message body is only partially receive before the connection + /// reports EOF. + /// - A client writes a request to your server, and then closes the write + /// half while waiting for your response. If you need to support this, + /// consider enabling [`half_close`]. + /// + /// [`half_close`]: crate::server::conn::http1::Builder::half_close() pub fn is_incomplete_message(&self) -> bool { #[cfg(not(all(any(feature = "client", feature = "server"), feature = "http1")))] return false; diff --git a/crates/primp-hyper/src/ffi/http_types.rs b/crates/primp-hyper/src/ffi/http_types.rs index 3dc4a25..14c3d24 100644 --- a/crates/primp-hyper/src/ffi/http_types.rs +++ b/crates/primp-hyper/src/ffi/http_types.rs @@ -189,9 +189,10 @@ ffi_fn! { }; builder = builder.path_and_query(path_and_query_bytes); } + let req = non_null!(&mut *req ?= hyper_code::HYPERE_INVALID_ARG); match builder.build() { Ok(u) => { - *unsafe { &mut *req }.0.uri_mut() = u; + *req.0.uri_mut() = u; hyper_code::HYPERE_OK }, Err(_) => { @@ -232,7 +233,8 @@ ffi_fn! { /// This is not an owned reference, so it should not be accessed after the /// `hyper_request` has been consumed. fn hyper_request_headers(req: *mut hyper_request) -> *mut hyper_headers { - hyper_headers::get_or_default(unsafe { &mut *req }.0.extensions_mut()) + let req = non_null!(&mut *req ?= std::ptr::null_mut()); + hyper_headers::get_or_default(req.0.extensions_mut()) } ?= std::ptr::null_mut() } @@ -367,7 +369,8 @@ ffi_fn! { /// This is not an owned reference, so it should not be accessed after the /// `hyper_response` has been freed. fn hyper_response_headers(resp: *mut hyper_response) -> *mut hyper_headers { - hyper_headers::get_or_default(unsafe { &mut *resp }.0.extensions_mut()) + let resp = non_null!(&mut *resp ?= std::ptr::null_mut()); + hyper_headers::get_or_default(resp.0.extensions_mut()) } ?= std::ptr::null_mut() } diff --git a/crates/primp-hyper/src/proto/h1/conn.rs b/crates/primp-hyper/src/proto/h1/conn.rs index 503b026..3c37622 100644 --- a/crates/primp-hyper/src/proto/h1/conn.rs +++ b/crates/primp-hyper/src/proto/h1/conn.rs @@ -394,7 +394,8 @@ where }; (reading, Poll::Ready(maybe_frame)) } else if frame.is_trailers() { - (Reading::Closed, Poll::Ready(Some(Ok(frame)))) + debug!("incoming body completed with trailers"); + (Reading::KeepAlive, Poll::Ready(Some(Ok(frame)))) } else { trace!("discarding unknown frame"); (Reading::Closed, Poll::Ready(None)) diff --git a/crates/primp-hyper/src/proto/h1/decode.rs b/crates/primp-hyper/src/proto/h1/decode.rs index 91fc4a7..102e54e 100644 --- a/crates/primp-hyper/src/proto/h1/decode.rs +++ b/crates/primp-hyper/src/proto/h1/decode.rs @@ -185,13 +185,15 @@ impl Decoder { *state = ready!(state.step( cx, body, - chunk_len, - extensions_cnt, - &mut buf, - trailers_buf, - trailers_cnt, - h1_max_headers, - h1_max_header_size + StepArgs { + chunk_size: chunk_len, + extensions_cnt, + chunk_buf: &mut buf, + trailers_buf, + trailers_cnt, + max_headers_cnt: h1_max_headers, + max_headers_bytes: h1_max_header_size, + } ))?; if *state == ChunkedState::End { trace!("end of chunked"); @@ -291,6 +293,16 @@ macro_rules! put_u8 { }; } +struct StepArgs<'a> { + chunk_size: &'a mut u64, + chunk_buf: &'a mut Option, + extensions_cnt: &'a mut u64, + trailers_buf: &'a mut Option, + trailers_cnt: &'a mut usize, + max_headers_cnt: usize, + max_headers_bytes: usize, +} + impl ChunkedState { fn new() -> ChunkedState { ChunkedState::Start @@ -300,35 +312,37 @@ impl ChunkedState { &self, cx: &mut Context<'_>, body: &mut R, - size: &mut u64, - extensions_cnt: &mut u64, - buf: &mut Option, - trailers_buf: &mut Option, - trailers_cnt: &mut usize, - h1_max_headers: usize, - h1_max_header_size: usize, + StepArgs { + chunk_size, + chunk_buf, + extensions_cnt, + trailers_buf, + trailers_cnt, + max_headers_cnt, + max_headers_bytes, + }: StepArgs<'_>, ) -> Poll> { use self::ChunkedState::*; match *self { - Start => ChunkedState::read_start(cx, body, size), - Size => ChunkedState::read_size(cx, body, size), + Start => ChunkedState::read_start(cx, body, chunk_size), + Size => ChunkedState::read_size(cx, body, chunk_size), SizeLws => ChunkedState::read_size_lws(cx, body), Extension => ChunkedState::read_extension(cx, body, extensions_cnt), - SizeLf => ChunkedState::read_size_lf(cx, body, *size), - Body => ChunkedState::read_body(cx, body, size, buf), + SizeLf => ChunkedState::read_size_lf(cx, body, *chunk_size), + Body => ChunkedState::read_body(cx, body, chunk_size, chunk_buf), BodyCr => ChunkedState::read_body_cr(cx, body), BodyLf => ChunkedState::read_body_lf(cx, body), - Trailer => ChunkedState::read_trailer(cx, body, trailers_buf, h1_max_header_size), + Trailer => ChunkedState::read_trailer(cx, body, trailers_buf, max_headers_bytes), TrailerLf => ChunkedState::read_trailer_lf( cx, body, trailers_buf, trailers_cnt, - h1_max_headers, - h1_max_header_size, + max_headers_cnt, + max_headers_bytes, ), - EndCr => ChunkedState::read_end_cr(cx, body, trailers_buf, h1_max_header_size), - EndLf => ChunkedState::read_end_lf(cx, body, trailers_buf, h1_max_header_size), + EndCr => ChunkedState::read_end_cr(cx, body, trailers_buf, max_headers_bytes), + EndLf => ChunkedState::read_end_lf(cx, body, trailers_buf, max_headers_bytes), End => Poll::Ready(Ok(ChunkedState::End)), } } @@ -751,13 +765,15 @@ mod tests { state.step( cx, rdr, - &mut size, - &mut ext_cnt, - &mut None, - &mut None, - &mut trailers_cnt, - DEFAULT_MAX_HEADERS, - TRAILER_LIMIT, + StepArgs { + chunk_size: &mut size, + extensions_cnt: &mut ext_cnt, + chunk_buf: &mut None, + trailers_buf: &mut None, + trailers_cnt: &mut trailers_cnt, + max_headers_cnt: DEFAULT_MAX_HEADERS, + max_headers_bytes: TRAILER_LIMIT, + }, ) }) .await; @@ -781,13 +797,15 @@ mod tests { state.step( cx, rdr, - &mut size, - &mut ext_cnt, - &mut None, - &mut None, - &mut trailers_cnt, - DEFAULT_MAX_HEADERS, - TRAILER_LIMIT, + StepArgs { + chunk_size: &mut size, + extensions_cnt: &mut ext_cnt, + chunk_buf: &mut None, + trailers_buf: &mut None, + trailers_cnt: &mut trailers_cnt, + max_headers_cnt: DEFAULT_MAX_HEADERS, + max_headers_bytes: TRAILER_LIMIT, + }, ) }) .await; diff --git a/crates/primp-hyper/src/proto/h2/client.rs b/crates/primp-hyper/src/proto/h2/client.rs index f97bf0a..7da38c8 100644 --- a/crates/primp-hyper/src/proto/h2/client.rs +++ b/crates/primp-hyper/src/proto/h2/client.rs @@ -73,6 +73,7 @@ pub(crate) struct Config { pub(crate) max_concurrent_reset_streams: Option, pub(crate) max_send_buffer_size: usize, pub(crate) max_pending_accept_reset_streams: Option, + pub(crate) max_local_error_reset_streams: Option, pub(crate) header_table_size: Option, pub(crate) max_concurrent_streams: Option, pub(crate) enable_push: bool, @@ -100,6 +101,7 @@ impl Default for Config { max_concurrent_reset_streams: None, max_send_buffer_size: DEFAULT_MAX_SEND_BUF_SIZE, max_pending_accept_reset_streams: None, + max_local_error_reset_streams: Some(1024), header_table_size: None, max_concurrent_streams: None, enable_push: false, @@ -134,6 +136,9 @@ fn new_builder(config: &Config) -> Builder { if let Some(max) = config.max_pending_accept_reset_streams { builder.max_pending_accept_reset_streams(max); } + if let Some(max) = config.max_local_error_reset_streams { + builder.max_local_error_reset_streams(max); + } if let Some(size) = config.header_table_size { builder.header_table_size(size); } @@ -477,6 +482,12 @@ where pub(crate) fn is_extended_connect_protocol_enabled(&self) -> bool { self.h2_tx.is_extended_connect_protocol_enabled() } + pub(crate) fn current_max_send_streams(&self) -> usize { + self.h2_tx.current_max_send_streams() + } + pub(crate) fn current_max_recv_streams(&self) -> usize { + self.h2_tx.current_max_recv_streams() + } } pin_project! { @@ -490,6 +501,7 @@ pin_project! { conn_drop_ref: Option>, #[pin] ping: Option, + cancel_rx: Option>, } } @@ -503,6 +515,26 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll { let mut this = self.project(); + // Check if the client cancelled the request (e.g. dropped the + // response future due to a timeout). If so, reset the h2 stream + // so that a RST_STREAM is sent and flow-control capacity is freed. + let cancel_result = this.cancel_rx.as_mut().map(|rx| Pin::new(rx).poll(cx)); + match cancel_result { + Some(Poll::Ready(Ok(()))) => { + debug!("client request body send cancelled, resetting stream"); + this.pipe.as_mut().send_reset(h2::Reason::CANCEL); + drop(this.conn_drop_ref.take().expect("Future polled twice")); + drop(this.ping.take().expect("Future polled twice")); + return Poll::Ready(()); + } + Some(Poll::Ready(Err(_))) => { + // Sender dropped without cancelling (normal response or error). + // Stop polling the receiver. + *this.cancel_rx = None; + } + Some(Poll::Pending) | None => {} + } + match Pin::new(&mut this.pipe).poll(cx) { Poll::Ready(result) => { if let Err(_e) = result { @@ -529,6 +561,10 @@ where fn poll_pipe(&mut self, f: FutCtx, cx: &mut Context<'_>) { let ping = self.ping.clone(); + // A one-shot channel so that send_task can tell pipe_task to + // reset the stream when the client cancels the request. + let (cancel_tx, cancel_rx) = oneshot::channel::<()>(); + let send_stream = if !f.is_connect { if !f.eos { let mut pipe = PipeToSendStream::new(f.body, f.body_tx); @@ -548,6 +584,7 @@ where pipe, conn_drop_ref: Some(conn_drop_ref), ping: Some(ping), + cancel_rx: Some(cancel_rx), }; // Clear send task self.executor @@ -568,6 +605,7 @@ where ping: Some(ping), send_stream: Some(send_stream), exec: self.executor.clone(), + cancel_tx: Some(cancel_tx), }, call_back: Some(f.cb), }, @@ -587,6 +625,16 @@ pin_project! { #[pin] send_stream: Option::Data>>>>, exec: E, + cancel_tx: Option>, + } +} + +impl ResponseFutMap { + /// Signal the pipe_task to reset the stream (e.g. on client cancellation). + pub(crate) fn cancel(self: Pin<&mut Self>) { + if let Some(cancel_tx) = self.project().cancel_tx.take() { + let _ = cancel_tx.send(()); + } } } diff --git a/crates/primp-hyper/src/proto/h2/mod.rs b/crates/primp-hyper/src/proto/h2/mod.rs index bb61c40..22dff28 100644 --- a/crates/primp-hyper/src/proto/h2/mod.rs +++ b/crates/primp-hyper/src/proto/h2/mod.rs @@ -64,17 +64,17 @@ fn strip_connection_headers(headers: &mut HeaderMap, is_request: bool) { "Connection header illegal in HTTP/2: {}", CONNECTION.as_str() ); - let header_contents = header.to_str().unwrap(); - // A `Connection` header may have a comma-separated list of names of other headers that // are meant for only this specific connection. // // Iterate these names and remove them as headers. Connection-specific headers are // forbidden in HTTP2, as that information has been moved into frame types of the h2 // protocol. - for name in header_contents.split(',') { - let name = name.trim(); - headers.remove(name); + if let Ok(header_contents) = header.to_str() { + for name in header_contents.split(',') { + let name = name.trim(); + headers.remove(name); + } } } } @@ -104,6 +104,11 @@ where stream, } } + + #[cfg(feature = "client")] + fn send_reset(self: Pin<&mut Self>, reason: h2::Reason) { + self.project().body_tx.send_reset(reason); + } } impl Future for PipeToSendStream diff --git a/crates/primp-hyper/src/rt/io.rs b/crates/primp-hyper/src/rt/io.rs index ed4af09..fef98b1 100644 --- a/crates/primp-hyper/src/rt/io.rs +++ b/crates/primp-hyper/src/rt/io.rs @@ -25,6 +25,52 @@ use std::task::{Context, Poll}; /// Reads bytes from a source. /// /// This trait is similar to `std::io::Read`, but supports asynchronous reads. +/// +/// # Implementing `Read` +/// +/// Implementations should read data into the provided [`ReadBufCursor`] and +/// advance the cursor to indicate how many bytes were written. The simplest +/// and safest approach is to use [`ReadBufCursor::put_slice`]: +/// +/// ``` +/// use hyper::rt::{Read, ReadBufCursor}; +/// use std::pin::Pin; +/// use std::task::{Context, Poll}; +/// use std::io; +/// +/// struct MyReader { +/// data: Vec, +/// position: usize, +/// } +/// +/// impl Read for MyReader { +/// fn poll_read( +/// mut self: Pin<&mut Self>, +/// _cx: &mut Context<'_>, +/// mut buf: ReadBufCursor<'_>, +/// ) -> Poll> { +/// let remaining_data = &self.data[self.position..]; +/// if remaining_data.is_empty() { +/// // No more data to read, signal EOF by returning Ok without +/// // advancing the buffer +/// return Poll::Ready(Ok(())); +/// } +/// +/// // Calculate how many bytes we can write +/// let to_copy = remaining_data.len().min(buf.remaining()); +/// // Use put_slice to safely copy data and advance the cursor +/// buf.put_slice(&remaining_data[..to_copy]); +/// +/// self.position += to_copy; +/// Poll::Ready(Ok(())) +/// } +/// } +/// ``` +/// +/// For more advanced use cases where you need direct access to the buffer +/// (e.g., when interfacing with APIs that write directly to a pointer), +/// you can use the unsafe [`ReadBufCursor::as_mut`] and [`ReadBufCursor::advance`] +/// methods. See their documentation for safety requirements. pub trait Read { /// Attempts to read bytes into the `buf`. /// @@ -124,9 +170,62 @@ pub struct ReadBuf<'a> { init: usize, } -/// The cursor part of a [`ReadBuf`]. +/// The cursor part of a [`ReadBuf`], representing the unfilled portion. +/// +/// This is created by calling [`ReadBuf::unfilled()`]. +/// +/// `ReadBufCursor` provides safe and unsafe methods for writing data into the +/// buffer: +/// +/// - **Safe approach**: Use [`put_slice`](Self::put_slice) to copy data from +/// a slice. This handles initialization tracking and cursor advancement +/// automatically. +/// +/// - **Unsafe approach**: For zero-copy scenarios or when interfacing with +/// low-level APIs, use [`as_mut`](Self::as_mut) to get a mutable slice +/// of `MaybeUninit`, then call [`advance`](Self::advance) after writing. +/// This is more efficient but requires careful attention to safety invariants. /// -/// This is created by calling `ReadBuf::unfilled()`. +/// # Example using safe methods +/// +/// ``` +/// use hyper::rt::ReadBuf; +/// +/// let mut backing = [0u8; 64]; +/// let mut read_buf = ReadBuf::new(&mut backing); +/// +/// { +/// let mut cursor = read_buf.unfilled(); +/// // put_slice handles everything safely +/// cursor.put_slice(b"hello"); +/// } +/// +/// assert_eq!(read_buf.filled(), b"hello"); +/// ``` +/// +/// # Example using unsafe methods +/// +/// ``` +/// use hyper::rt::ReadBuf; +/// +/// let mut backing = [0u8; 64]; +/// let mut read_buf = ReadBuf::new(&mut backing); +/// +/// { +/// let mut cursor = read_buf.unfilled(); +/// // SAFETY: we will initialize exactly 5 bytes +/// let slice = unsafe { cursor.as_mut() }; +/// slice[0].write(b'h'); +/// slice[1].write(b'e'); +/// slice[2].write(b'l'); +/// slice[3].write(b'l'); +/// slice[4].write(b'o'); +/// // SAFETY: we have initialized 5 bytes +/// unsafe { cursor.advance(5) }; +/// } +/// +/// assert_eq!(read_buf.filled(), b"hello"); +/// ``` #[derive(Debug)] pub struct ReadBufCursor<'a> { buf: &'a mut ReadBuf<'a>, @@ -253,7 +352,7 @@ impl ReadBufCursor<'_> { self.buf.remaining() } - /// Transfer bytes into `self`` from `src` and advance the cursor + /// Transfer bytes into `self` from `src` and advance the cursor /// by the number of bytes written. /// /// # Panics diff --git a/crates/primp-hyper/src/server/conn/http1.rs b/crates/primp-hyper/src/server/conn/http1.rs index 3fdbbc1..f7305a2 100644 --- a/crates/primp-hyper/src/server/conn/http1.rs +++ b/crates/primp-hyper/src/server/conn/http1.rs @@ -528,6 +528,12 @@ where Pin::new(conn).graceful_shutdown() } } + + /// Return the inner IO object, and additional information provided the connection + /// has not yet been upgraded. + pub fn into_parts(self) -> Option> { + self.inner.map(|conn| conn.into_parts()) + } } impl Future for UpgradeableConnection From 79ecd4c35dda13b750c573fa8c6f86b3308fea59 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Sun, 17 May 2026 20:01:39 +0300 Subject: [PATCH 2/7] chore(primp-hyper-rustls): merge upstream hyper-rustls 0.27.7 -> 0.27.9 - Update Cargo.toml version to 0.27.9 - Remove pki-types dependency, use rustls::pki_types::ServerName instead - Update rustls-platform-verifier 0.6 -> 0.7 - Add HttpsConnector::new() constructor method - Use inline format strings in log macros - Simplify error handling with io::Error::other() --- crates/primp-hyper-rustls/Cargo.toml | 6 ++---- crates/primp-hyper-rustls/src/config.rs | 6 ++---- crates/primp-hyper-rustls/src/connector.rs | 19 ++++++++++++++++++- .../src/connector/builder.rs | 4 ++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/crates/primp-hyper-rustls/Cargo.toml b/crates/primp-hyper-rustls/Cargo.toml index de2dd47..bf8f6a8 100644 --- a/crates/primp-hyper-rustls/Cargo.toml +++ b/crates/primp-hyper-rustls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "primp-hyper-rustls" -version = "0.27.7" +version = "0.27.9" edition = "2021" rust-version = "1.84" license = "Apache-2.0 OR ISC OR MIT" @@ -28,9 +28,8 @@ http = "1" hyper = { version = "1.8", path = "../primp-hyper", package = "primp-hyper", default-features = false } hyper-util = { version = "0.1", path = "../primp-hyper-util", package = "primp-hyper-util", default-features = false, features = ["client-legacy", "tokio"] } log = { version = "0.4.4", optional = true } -pki-types = { package = "rustls-pki-types", version = "1" } rustls-native-certs = { version = "0.8", optional = true } -rustls-platform-verifier = { version = "0.6", optional = true } +rustls-platform-verifier = { version = "0.7", optional = true } rustls = { version = "0.23", path = "../primp-rustls/rustls", package = "primp-rustls", default-features = false } tokio = "1.0" tokio-rustls = { version = "0.26", path = "../primp-tokio-rustls", package = "primp-tokio-rustls", default-features = false } @@ -42,7 +41,6 @@ cfg-if = "1" http-body-util = "0.1" hyper-util = { version = "0.1", path = "../primp-hyper-util", package = "primp-hyper-util", default-features = false, features = ["server-auto"] } rustls = { version = "0.23", path = "../primp-rustls/rustls", package = "primp-rustls", default-features = false, features = ["tls12"] } -rustls-pemfile = "2" tokio = { version = "1.0", features = ["io-std", "macros", "net", "rt-multi-thread"] } [package.metadata.docs.rs] diff --git a/crates/primp-hyper-rustls/src/config.rs b/crates/primp-hyper-rustls/src/config.rs index da89855..4b11713 100644 --- a/crates/primp-hyper-rustls/src/config.rs +++ b/crates/primp-hyper-rustls/src/config.rs @@ -93,16 +93,14 @@ impl ConfigBuilderExt for ConfigBuilder { match roots.add(cert) { Ok(_) => valid_count += 1, Err(err) => { - crate::log::debug!("certificate parsing failed: {:?}", err); + crate::log::debug!("certificate parsing failed: {err:?}"); invalid_count += 1 } } } crate::log::debug!( - "with_native_roots processed {} valid and {} invalid certs", - valid_count, - invalid_count + "with_native_roots processed {valid_count} valid and {invalid_count} invalid certs" ); if roots.is_empty() { crate::log::debug!("no valid native root CA certificates found"); diff --git a/crates/primp-hyper-rustls/src/connector.rs b/crates/primp-hyper-rustls/src/connector.rs index 08fd550..1f57c80 100644 --- a/crates/primp-hyper-rustls/src/connector.rs +++ b/crates/primp-hyper-rustls/src/connector.rs @@ -8,7 +8,7 @@ use http::Uri; use hyper::rt; use hyper_util::client::legacy::connect::Connection; use hyper_util::rt::TokioIo; -use pki_types::ServerName; +use rustls::pki_types::ServerName; use tokio_rustls::TlsConnector; use tower_service::Service; @@ -35,6 +35,23 @@ impl HttpsConnector { builder::ConnectorBuilder::new() } + /// Creates a new `HttpsConnector`. + /// + /// The recommended way to create a `HttpsConnector` is to use a [`crate::HttpsConnectorBuilder`]. See [`HttpsConnector::builder()`]. + pub fn new( + http: T, + tls_config: impl Into>, + force_https: bool, + server_name_resolver: Arc, + ) -> Self { + Self { + http, + tls_config: tls_config.into(), + force_https, + server_name_resolver, + } + } + /// Force the use of HTTPS when connecting. /// /// If a URL is not `https` when connecting, an error is returned. diff --git a/crates/primp-hyper-rustls/src/connector/builder.rs b/crates/primp-hyper-rustls/src/connector/builder.rs index fbac229..0887fc8 100644 --- a/crates/primp-hyper-rustls/src/connector/builder.rs +++ b/crates/primp-hyper-rustls/src/connector/builder.rs @@ -16,7 +16,7 @@ use super::{DefaultServerNameResolver, HttpsConnector, ResolveServerName}; feature = "rustls-platform-verifier" ))] use crate::config::ConfigBuilderExt; -use pki_types::ServerName; +use rustls::pki_types::ServerName; /// A builder for an [`HttpsConnector`] /// @@ -108,7 +108,7 @@ impl ConnectorBuilder { ClientConfig::builder_with_provider(provider.into()) .with_safe_default_protocol_versions() .and_then(|builder| builder.try_with_platform_verifier()) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? + .map_err(std::io::Error::other)? .with_no_client_auth(), )) } From f44385b9c65fd781c4968d38d370cc2e0aa4f8a1 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Sun, 17 May 2026 20:01:39 +0300 Subject: [PATCH 3/7] fix(primp-hyper): wrap max_local_error_reset_streams in Some() The h2 Builder::max_local_error_reset_streams takes Option, not usize. Wrap the extracted value to match the expected type. --- crates/primp-hyper/src/proto/h2/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/primp-hyper/src/proto/h2/client.rs b/crates/primp-hyper/src/proto/h2/client.rs index 7da38c8..bfb185f 100644 --- a/crates/primp-hyper/src/proto/h2/client.rs +++ b/crates/primp-hyper/src/proto/h2/client.rs @@ -137,7 +137,7 @@ fn new_builder(config: &Config) -> Builder { builder.max_pending_accept_reset_streams(max); } if let Some(max) = config.max_local_error_reset_streams { - builder.max_local_error_reset_streams(max); + builder.max_local_error_reset_streams(Some(max)); } if let Some(size) = config.header_table_size { builder.header_table_size(size); From 599043db3e2b279d200eb89ae0d47d54cf045723 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Sun, 17 May 2026 20:01:39 +0300 Subject: [PATCH 4/7] chore(primp-reqwest): merge upstream reqwest 0.13.2 -> 0.13.3 - Update Cargo.toml version to 0.13.3 - Fix H3 connector to use bound_addr for address filtering - Fix H3 pool idle connection cleanup and stop-sending handling - Fix CRL from_pem to properly parse PEM (was treating as raw DER) - Update doc comments for HTTP2 flow control methods - Fix debug logging to only log host (not full URI with query params) - Update hickory DNS resolver imports - Minor style fixes (fmt::Display import) --- crates/primp-reqwest/Cargo.toml | 2 +- crates/primp-reqwest/src/async_impl/client.rs | 4 +- .../src/async_impl/h3_client/connect.rs | 93 +++++++++++++++---- .../src/async_impl/h3_client/pool.rs | 41 +++++++- crates/primp-reqwest/src/blocking/client.rs | 4 +- crates/primp-reqwest/src/connect.rs | 4 +- crates/primp-reqwest/src/dns/hickory.rs | 43 ++++----- crates/primp-reqwest/src/lib.rs | 6 +- crates/primp-reqwest/src/retry.rs | 14 +++ crates/primp-reqwest/src/tls.rs | 3 +- crates/primp-reqwest/src/util.rs | 4 +- 11 files changed, 163 insertions(+), 55 deletions(-) diff --git a/crates/primp-reqwest/Cargo.toml b/crates/primp-reqwest/Cargo.toml index 20ba5c9..cdae672 100644 --- a/crates/primp-reqwest/Cargo.toml +++ b/crates/primp-reqwest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "primp-reqwest" -version = "0.13.2" +version = "0.13.3" description = "Fork of the original reqwest library" keywords = ["http", "request", "client"] categories = ["web-programming::http-client", "wasm"] diff --git a/crates/primp-reqwest/src/async_impl/client.rs b/crates/primp-reqwest/src/async_impl/client.rs index ff87402..16aa393 100644 --- a/crates/primp-reqwest/src/async_impl/client.rs +++ b/crates/primp-reqwest/src/async_impl/client.rs @@ -1615,7 +1615,7 @@ impl ClientBuilder { /// Sets the `SETTINGS_INITIAL_WINDOW_SIZE` option for HTTP2 stream-level flow control. /// - /// Default is currently 65,535 but may change internally to optimize for common uses. + /// Default may change internally to optimize for common uses. #[cfg(feature = "http2")] #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub fn http2_initial_stream_window_size(mut self, sz: impl Into>) -> ClientBuilder { @@ -1625,7 +1625,7 @@ impl ClientBuilder { /// Sets the max connection-level flow control for HTTP2 /// - /// Default is currently 65,535 but may change internally to optimize for common uses. + /// Default may change internally to optimize for common uses. #[cfg(feature = "http2")] #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub fn http2_initial_connection_window_size( diff --git a/crates/primp-reqwest/src/async_impl/h3_client/connect.rs b/crates/primp-reqwest/src/async_impl/h3_client/connect.rs index d542070..dfe5610 100644 --- a/crates/primp-reqwest/src/async_impl/h3_client/connect.rs +++ b/crates/primp-reqwest/src/async_impl/h3_client/connect.rs @@ -59,6 +59,7 @@ pub(crate) struct H3Connector { resolver: DynResolver, endpoint: Endpoint, client_config: H3ClientConfig, + bound_addr: IpAddr, } impl H3Connector { @@ -74,6 +75,7 @@ impl H3Connector { // FIXME: Replace this when there is a setter. config.transport_config(Arc::new(transport_config)); + // Pipe the local address through to the endpoint creation let socket_addr = match local_addr { Some(ip) => SocketAddr::new(ip, 0), None => "[::]:0".parse::().unwrap(), @@ -82,10 +84,14 @@ impl H3Connector { let mut endpoint = Endpoint::client(socket_addr)?; endpoint.set_default_client_config(config); + // Get the actual bound address from the endpoint + let bound_addr = endpoint.local_addr()?.ip(); + Ok(Self { resolver, endpoint, client_config, + bound_addr, }) } @@ -117,28 +123,83 @@ impl H3Connector { addrs: Vec, server_name: &str, ) -> Result { - let mut err = None; + if addrs.is_empty() { + return Err("no addresses to connect to".into()); + } + + let (mut ipv6_addrs, mut ipv4_addrs): (Vec, Vec) = + addrs.into_iter().partition(|addr| addr.is_ipv6()); + + if self.bound_addr.is_ipv6() { + ipv4_addrs.clear(); + } else { + ipv6_addrs.clear(); + } + + if ipv6_addrs.is_empty() { + return Self::try_addresses_static( + &self.endpoint, + &ipv4_addrs, + server_name, + &self.client_config, + ) + .await; + } + if ipv4_addrs.is_empty() { + return Self::try_addresses_static( + &self.endpoint, + &ipv6_addrs, + server_name, + &self.client_config, + ) + .await; + } + + let endpoint = self.endpoint.clone(); + let client_config = self.client_config.clone(); + + match Self::try_addresses_static(&endpoint, &ipv6_addrs, server_name, &client_config).await + { + Ok(conn) => Ok(conn), + Err(_) => { + Self::try_addresses_static(&endpoint, &ipv4_addrs, server_name, &client_config) + .await + } + } + } + + async fn try_addresses_static( + endpoint: &Endpoint, + addrs: &[SocketAddr], + server_name: &str, + client_config: &H3ClientConfig, + ) -> Result { + let mut last_err: Option = None; + for addr in addrs { - match self.endpoint.connect(addr, server_name)?.await { - Ok(new_conn) => { - let quinn_conn = Connection::new(new_conn); - let mut h3_client_builder = h3::client::builder(); - if let Some(max_field_section_size) = self.client_config.max_field_section_size - { - h3_client_builder.max_field_section_size(max_field_section_size); + match endpoint.connect(*addr, server_name) { + Ok(connecting) => match connecting.await { + Ok(new_conn) => { + let quinn_conn = Connection::new(new_conn); + let mut h3_client_builder = h3::client::builder(); + if let Some(max_field_section_size) = client_config.max_field_section_size { + h3_client_builder.max_field_section_size(max_field_section_size); + } + if let Some(send_grease) = client_config.send_grease { + h3_client_builder.send_grease(send_grease); + } + return Ok(h3_client_builder.build(quinn_conn).await?); } - if let Some(send_grease) = self.client_config.send_grease { - h3_client_builder.send_grease(send_grease); + Err(e) => { + last_err = Some(Box::new(e) as BoxError); } - return Ok(h3_client_builder.build(quinn_conn).await?); + }, + Err(e) => { + last_err = Some(Box::new(e) as BoxError); } - Err(e) => err = Some(e), } } - match err { - Some(e) => Err(Box::new(e) as BoxError), - None => Err("failed to establish connection for HTTP/3 request".into()), - } + Err(last_err.unwrap_or_else(|| "no addresses available".into())) } } diff --git a/crates/primp-reqwest/src/async_impl/h3_client/pool.rs b/crates/primp-reqwest/src/async_impl/h3_client/pool.rs index 8d730f3..bfa9aca 100644 --- a/crates/primp-reqwest/src/async_impl/h3_client/pool.rs +++ b/crates/primp-reqwest/src/async_impl/h3_client/pool.rs @@ -126,6 +126,7 @@ impl Pool { if let Some(duration) = timeout { if Instant::now().saturating_duration_since(conn.idle_timeout) > duration { trace!("pooled connection expired"); + inner.idle_conns.remove(&key); return None; } } @@ -229,6 +230,10 @@ impl PoolClient { Some(Ok(frame)) => { if let Ok(b) = frame.into_data() { if let Err(e) = send.send_data(Bytes::copy_from_slice(&b)).await { + if is_stop_sending(&e) { + let _ = tx.send(Ok(())); + return; + } if let Err(e) = tx.send(Err(e.into())) { error!("Failed to communicate send.send_data() error: {e:?}"); } @@ -248,10 +253,12 @@ impl PoolClient { } if let Err(e) = send.finish().await { - if let Err(e) = tx.send(Err(e.into())) { - error!("Failed to communicate send.finish read error: {e:?}"); + if !is_stop_sending(&e) { + if let Err(e) = tx.send(Err(e.into())) { + error!("Failed to communicate send.finish read error: {e:?}"); + } + return; } - return; } let _ = tx.send(Ok(())); @@ -357,7 +364,22 @@ where pub(crate) fn extract_domain(uri: &mut Uri) -> Result { let uri_clone = uri.clone(); match (uri_clone.scheme(), uri_clone.authority()) { - (Some(scheme), Some(auth)) => Ok((scheme.clone(), auth.clone())), + (Some(scheme), Some(auth)) => { + let scheme_str = scheme.as_str(); + if scheme_str != "https" && scheme_str != "h3" { + return Err(Error::new( + Kind::Request, + Some(Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "HTTP/3 only supports 'https' or 'h3' schemes, got: {}", + scheme_str + ), + ))), + )); + } + Ok((scheme.clone(), auth.clone())) + } _ => Err(Error::new(Kind::Request, None::)), } } @@ -370,3 +392,14 @@ pub(crate) fn domain_as_uri((scheme, auth): Key) -> Uri { .build() .expect("domain is valid Uri") } + +/// Indicates the remote requested the peer to stop sending data without error. +fn is_stop_sending(e: &h3::error::StreamError) -> bool { + matches!( + e, + h3::error::StreamError::RemoteTerminate { + code: h3::error::Code::H3_NO_ERROR, + .. + } + ) +} diff --git a/crates/primp-reqwest/src/blocking/client.rs b/crates/primp-reqwest/src/blocking/client.rs index 3f93f05..fbd2d71 100644 --- a/crates/primp-reqwest/src/blocking/client.rs +++ b/crates/primp-reqwest/src/blocking/client.rs @@ -484,7 +484,7 @@ impl ClientBuilder { /// Sets the `SETTINGS_INITIAL_WINDOW_SIZE` option for HTTP2 stream-level flow control. /// - /// Default is currently 65,535 but may change internally to optimize for common uses. + /// Default may change internally to optimize for common uses. #[cfg(feature = "http2")] #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub fn http2_initial_stream_window_size(self, sz: impl Into>) -> ClientBuilder { @@ -493,7 +493,7 @@ impl ClientBuilder { /// Sets the max connection-level flow control for HTTP2 /// - /// Default is currently 65,535 but may change internally to optimize for common uses. + /// Default may change internally to optimize for common uses. #[cfg(feature = "http2")] #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub fn http2_initial_connection_window_size(self, sz: impl Into>) -> ClientBuilder { diff --git a/crates/primp-reqwest/src/connect.rs b/crates/primp-reqwest/src/connect.rs index af4acdc..1432f69 100644 --- a/crates/primp-reqwest/src/connect.rs +++ b/crates/primp-reqwest/src/connect.rs @@ -782,7 +782,7 @@ impl ConnectorService { } async fn connect_via_proxy(self, dst: Uri, proxy: Intercepted) -> Result { - log::debug!("proxy({proxy:?}) intercepts '{dst:?}'"); + log::debug!("proxy({proxy:?}) intercepts '{:?}'", dst.host()); #[cfg(feature = "socks")] match proxy.uri().scheme_str().ok_or("proxy scheme expected")? { @@ -927,7 +927,7 @@ impl Service for ConnectorService { } fn call(&mut self, dst: Uri) -> Self::Future { - log::debug!("starting new connection: {dst:?}"); + log::debug!("starting new connection '{:?}'", dst.host()); let timeout = self.simple_timeout; // Local transports (UDS, Windows Named Pipes) skip proxies diff --git a/crates/primp-reqwest/src/dns/hickory.rs b/crates/primp-reqwest/src/dns/hickory.rs index f720e36..7fd670d 100644 --- a/crates/primp-reqwest/src/dns/hickory.rs +++ b/crates/primp-reqwest/src/dns/hickory.rs @@ -1,11 +1,13 @@ //! DNS resolution via the [hickory-resolver](https://github.com/hickory-dns/hickory-dns) crate use hickory_resolver::{ - config::LookupIpStrategy, lookup_ip::LookupIpIntoIter, ResolveError, TokioResolver, + config::{LookupIpStrategy, ResolverConfig}, + lookup_ip::LookupIpIntoIter, + name_server::TokioConnectionProvider, + TokioResolver, }; use once_cell::sync::OnceCell; -use std::fmt; use std::net::SocketAddr; use std::sync::Arc; @@ -24,14 +26,11 @@ struct SocketAddrs { iter: LookupIpIntoIter, } -#[derive(Debug)] -struct HickoryDnsSystemConfError(ResolveError); - impl Resolve for HickoryDnsResolver { fn resolve(&self, name: Name) -> Resolving { let resolver = self.clone(); Box::pin(async move { - let resolver = resolver.state.get_or_try_init(new_resolver)?; + let resolver = resolver.state.get_or_init(new_resolver); let lookup = resolver.lookup_ip(name.as_str()).await?; let addrs: Addrs = Box::new(SocketAddrs { @@ -51,23 +50,21 @@ impl Iterator for SocketAddrs { } /// Create a new resolver with the default configuration, -/// which reads from `/etc/resolve.conf`. The options are -/// overridden to look up for both IPv4 and IPv6 addresses +/// which reads from `/etc/resolve.conf`. If reading `/etc/resolv.conf` fails, +/// it fallbacks to hickory_resolver's default config. +/// The options are overridden to look up for both IPv4 and IPv6 addresses /// to work with "happy eyeballs" algorithm. -fn new_resolver() -> Result { - let mut builder = TokioResolver::builder_tokio().map_err(HickoryDnsSystemConfError)?; +fn new_resolver() -> TokioResolver { + let mut builder = TokioResolver::builder_tokio().unwrap_or_else(|err| { + log::debug!( + "hickory-dns: failed to load system DNS configuration; falling back to hickory_resolver defaults: {:?}", + err + ); + TokioResolver::builder_with_config( + ResolverConfig::default(), + TokioConnectionProvider::default(), + ) + }); builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6; - Ok(builder.build()) -} - -impl fmt::Display for HickoryDnsSystemConfError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("error reading DNS system conf for hickory-dns") - } -} - -impl std::error::Error for HickoryDnsSystemConfError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.0) - } + builder.build() } diff --git a/crates/primp-reqwest/src/lib.rs b/crates/primp-reqwest/src/lib.rs index 4086b27..e6584ce 100644 --- a/crates/primp-reqwest/src/lib.rs +++ b/crates/primp-reqwest/src/lib.rs @@ -193,9 +193,11 @@ //! - **default-tls** *(enabled by default)*: Provides TLS support to connect //! over HTTPS. //! - **rustls**: Enables TLS functionality provided by `rustls`. +//! - **rustls-no-provider**: Enables TLS provided by `rustls` without specifying a crypto provider. //! - **native-tls**: Enables TLS functionality provided by `native-tls`. //! - **native-tls-vendored**: Enables the `vendored` feature of `native-tls`. //! - **native-tls-no-alpn**: Enables `native-tls` without its `alpn` feature. +//! - **native-tls-vendored-no-alpn**: Enables `native-tls-vendored` without its `alpn` feature. //! - **blocking**: Provides the [blocking][] client API. //! - **charset** *(enabled by default)*: Improved support for decoding text. //! - **cookies**: Provides cookie session support. @@ -262,14 +264,14 @@ use sync_wrapper as _; macro_rules! if_wasm { ($($item:item)*) => {$( - #[cfg(target_arch = "wasm32")] + #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))] $item )*} } macro_rules! if_hyper { ($($item:item)*) => {$( - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))] $item )*} } diff --git a/crates/primp-reqwest/src/retry.rs b/crates/primp-reqwest/src/retry.rs index a3992e8..de580e9 100644 --- a/crates/primp-reqwest/src/retry.rs +++ b/crates/primp-reqwest/src/retry.rs @@ -41,6 +41,9 @@ use std::time::Duration; use tower::retry::budget::{Budget as _, TpsBudget as Budget}; +#[cfg(docsrs)] +pub use classify::ReqRep; + /// Builder to configure retries /// /// Construct with [`for_host()`]. @@ -393,30 +396,41 @@ mod classify { } } + /// A request/response result to inspect for possible retries. + /// + /// This is passed to a `classify` function. #[derive(Debug)] pub struct ReqRep<'a>(&'a super::Req, Result); impl ReqRep<'_> { + /// Access the request method. pub fn method(&self) -> &http::Method { self.0.method() } + /// Access the request URI. pub fn uri(&self) -> &http::Uri { self.0.uri() } + /// Access the response status, if it did not error. pub fn status(&self) -> Option { self.1.ok() } + /// Access the error, if a response was not received. pub fn error(&self) -> Option<&(dyn std::error::Error + 'static)> { self.1.as_ref().err().map(|e| &**e as _) } + /// Classify this attempt as retryable. pub fn retryable(self) -> Action { Action::Retryable } + /// Classify this attempt as success. + /// + /// Even if it was a domain error, a "success" means it will not retry. pub fn success(self) -> Action { Action::Success } diff --git a/crates/primp-reqwest/src/tls.rs b/crates/primp-reqwest/src/tls.rs index 6f444de..6db5618 100644 --- a/crates/primp-reqwest/src/tls.rs +++ b/crates/primp-reqwest/src/tls.rs @@ -453,7 +453,8 @@ impl CertificateRevocationList { pub fn from_pem(pem: &[u8]) -> crate::Result { Ok(CertificateRevocationList { #[cfg(feature = "__rustls")] - inner: rustls_pki_types::CertificateRevocationListDer::from(pem.to_vec()), + inner: rustls_pki_types::CertificateRevocationListDer::from_pem_slice(pem) + .map_err(|_| crate::error::builder("invalid crl encoding"))?, }) } diff --git a/crates/primp-reqwest/src/util.rs b/crates/primp-reqwest/src/util.rs index 2237f5b..60c48e9 100644 --- a/crates/primp-reqwest/src/util.rs +++ b/crates/primp-reqwest/src/util.rs @@ -3,8 +3,8 @@ use std::fmt; pub fn basic_auth(username: U, password: Option

) -> HeaderValue where - U: std::fmt::Display, - P: std::fmt::Display, + U: fmt::Display, + P: fmt::Display, { use base64::prelude::BASE64_STANDARD; use base64::write::EncoderWriter; From 81a3c120d4a4a814608b5e78f54c876440702d4e Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Sun, 17 May 2026 20:01:39 +0300 Subject: [PATCH 5/7] chore(primp-rustls): merge upstream rustls 0.23.38 -> 0.23.40 - Update Cargo.toml version to 0.23.40 - require_ems now uses self.provider.fips() instead of cfg!(feature = "fips") for runtime FIPS detection based on the crypto provider - Update FIPS documentation to reflect provider-based detection - Fix ECH padding calculation: use iter::repeat instead of vec![0; N], fix name padding calculation with usize conversions - Add comprehensive ECH padding tests for inner name length invariance - Add feature(core_io) for read_buf support - Fix ECH HRR acceptance match guard syntax --- crates/primp-rustls/rustls/Cargo.toml | 2 +- .../primp-rustls/rustls/src/client/builder.rs | 5 +- .../rustls/src/client/client_conn.rs | 3 +- crates/primp-rustls/rustls/src/client/ech.rs | 222 ++++++++++++++++-- crates/primp-rustls/rustls/src/lib.rs | 1 + crates/primp-rustls/rustls/src/manual/fips.rs | 5 +- .../primp-rustls/rustls/src/server/builder.rs | 5 +- .../rustls/src/server/server_conn.rs | 3 +- 8 files changed, 220 insertions(+), 26 deletions(-) diff --git a/crates/primp-rustls/rustls/Cargo.toml b/crates/primp-rustls/rustls/Cargo.toml index 50b73a7..332c88e 100644 --- a/crates/primp-rustls/rustls/Cargo.toml +++ b/crates/primp-rustls/rustls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "primp-rustls" -version = "0.23.38" +version = "0.23.40" edition = "2021" rust-version = "1.84" license = "Apache-2.0 OR ISC OR MIT" diff --git a/crates/primp-rustls/rustls/src/client/builder.rs b/crates/primp-rustls/rustls/src/client/builder.rs index d1543f0..123ec0f 100644 --- a/crates/primp-rustls/rustls/src/client/builder.rs +++ b/crates/primp-rustls/rustls/src/client/builder.rs @@ -160,6 +160,9 @@ impl ConfigBuilder { self, client_auth_cert_resolver: Arc, ) -> ClientConfig { + #[cfg(feature = "tls12")] + let require_ems = self.provider.fips(); + ClientConfig { provider: self.provider, alpn_protocols: Vec::new(), @@ -173,7 +176,7 @@ impl ConfigBuilder { enable_secret_extraction: false, enable_early_data: false, #[cfg(feature = "tls12")] - require_ems: cfg!(feature = "fips"), + require_ems, time_provider: self.time_provider, cert_compressors: compress::default_cert_compressors().to_vec(), cert_compression_cache: Arc::new(compress::CompressionCache::default()), diff --git a/crates/primp-rustls/rustls/src/client/client_conn.rs b/crates/primp-rustls/rustls/src/client/client_conn.rs index 9b3cf09..8d6d499 100644 --- a/crates/primp-rustls/rustls/src/client/client_conn.rs +++ b/crates/primp-rustls/rustls/src/client/client_conn.rs @@ -224,7 +224,8 @@ pub struct ClientConfig { /// If set to `true`, requires the server to support the extended /// master secret extraction method defined in [RFC 7627]. /// - /// The default is `true` if the `fips` crate feature is enabled, + /// The default is `true` if the configured [`CryptoProvider`] is + /// FIPS-compliant (i.e., [`CryptoProvider::fips()`] returns `true`), /// `false` otherwise. /// /// It must be set to `true` to meet FIPS requirement mentioned in section diff --git a/crates/primp-rustls/rustls/src/client/ech.rs b/crates/primp-rustls/rustls/src/client/ech.rs index 55ce88b..97733dd 100644 --- a/crates/primp-rustls/rustls/src/client/ech.rs +++ b/crates/primp-rustls/rustls/src/client/ech.rs @@ -1,6 +1,7 @@ use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; +use core::iter; use pki_types::{DnsName, EchConfigListBytes, ServerName}; use subtle::ConstantTimeEq; @@ -13,7 +14,7 @@ use crate::hash_hs::{HandshakeHash, HandshakeHashBuffer}; use crate::log::{debug, trace, warn}; use crate::msgs::base::{Payload, PayloadU16}; use crate::msgs::codec::{Codec, Reader}; -use crate::msgs::enums::ExtensionType; +use crate::msgs::enums::{ExtensionType, HpkeKem}; use crate::msgs::handshake::{ ClientExtensions, ClientHelloPayload, EchConfigContents, EchConfigPayload, Encoding, EncryptedClientHello, EncryptedClientHelloOuter, HandshakeMessagePayload, HandshakePayload, @@ -669,30 +670,25 @@ impl EchState { // Calculate padding // max_name_len = L - let max_name_len = self.maximum_name_length; + let max_name_len = usize::from(self.maximum_name_length); let max_name_len = if max_name_len > 0 { max_name_len } else { 255 }; - let padding_len = match &self.inner_name { - ServerName::DnsName(name) => { + let name_padding_len = match &inner_hello.server_name { + Some(ServerNamePayload::SingleDnsName(name)) => { // name.len() = D // max(0, L - D) - core::cmp::max( - 0, - max_name_len.saturating_sub(name.as_ref().len() as u8) as usize, - ) - } - _ => { - // L + 9 - // "This is the length of a "server_name" extension with an L-byte name." - // We widen to usize here to avoid overflowing u8 + u8. - max_name_len as usize + 9 + Ord::max(0, max_name_len.saturating_sub(name.as_ref().len())) } + // L + 9 + // "This is the length of a "server_name" extension with an L-byte name." + _ => max_name_len + 9, }; + encoded_hello.extend(iter::repeat(0).take(name_padding_len)); // Let L be the length of the EncodedClientHelloInner with all the padding computed so far // Let N = 31 - ((L - 1) % 32) and add N bytes of padding. - let padding_len = 31 - ((encoded_hello.len() + padding_len - 1) % 32); - encoded_hello.extend(vec![0; padding_len]); + let padding_len = 31 - ((encoded_hello.len() - 1) % 32); + encoded_hello.extend(iter::repeat(0).take(padding_len)); // Construct the inner hello message that will be used for the transcript. let inner_hello_msg = Message { @@ -823,10 +819,12 @@ pub(crate) fn fatal_alert_required( #[cfg(test)] mod tests { - use crate::enums::CipherSuite; - use crate::msgs::handshake::{Random, ServerExtensions, SessionId}; + use std::string::String; use super::*; + use crate::enums::CipherSuite; + use crate::msgs::enums::{Compression, HpkeAead, HpkeKdf, HpkeKem}; + use crate::msgs::handshake::{Random, ServerExtensions, SessionId}; #[test] fn server_hello_conf_alters_server_hello_random() { @@ -835,7 +833,7 @@ mod tests { random: Random([0xffu8; 32]), session_id: SessionId::empty(), cipher_suite: CipherSuite::TLS13_AES_256_GCM_SHA384, - compression_method: crate::msgs::enums::Compression::Null, + compression_method: Compression::Null, extensions: Box::new(ServerExtensions::default()), }; let message = Message { @@ -881,4 +879,190 @@ mod tests { " afterwards those bytes are zeroed ^^^^^^^^^^^^^^^^ " ); } + + #[test] + fn inner_client_hello_length_conceals_inner_name_length() { + let base_inner_len = inner_hello_encoding_for_name(dns_name_of_len(1), true).len(); + assert!( + base_inner_len % 32 == 0, + "inner hello length must be 32-byte padded" + ); + assert!( + base_inner_len >= 256, + "inner hello must include inner name and its padding" + ); + + for inner_name_len in 1..251 { + assert_eq!( + inner_hello_encoding_for_name(dns_name_of_len(inner_name_len), true).len(), + base_inner_len, + "all inner hello lengths must be invariant wrt inner name length" + ); + } + } + + #[test] + fn inner_client_hello_length_does_not_leak_length_of_omitted_inner_name() { + let base_inner_len = inner_hello_encoding_for_name(dns_name_of_len(1), false).len(); + assert!( + base_inner_len % 32 == 0, + "inner hello length must be 32-byte padded" + ); + assert!( + base_inner_len >= 256, + "inner hello must include maximum_name_length bytes of padding" + ); + + for inner_name_len in 1..251 { + assert_eq!( + inner_hello_encoding_for_name(dns_name_of_len(inner_name_len), false).len(), + base_inner_len, + "all inner hello lengths must be invariant wrt inner name length" + ); + } + } + + fn inner_hello_encoding_for_name(name: DnsName<'static>, enable_sni: bool) -> Vec { + let config = EchConfig { + config: EchConfigPayload::V18(EchConfigContents { + key_config: HpkeKeyConfig { + config_id: 0, + kem_id: MockHpke::SUITE.kem, + public_key: PayloadU16::new(vec![0; 32]), + symmetric_cipher_suites: vec![], + }, + maximum_name_length: 255, + public_name: DnsName::try_from("public").unwrap(), + extensions: vec![], + }), + suite: &MockHpke, + }; + + EchState::new( + &config, + ServerName::from(name.clone()), + false, + &FixedRandom, + enable_sni, + ) + .unwrap() + .encode_inner_hello( + &ClientHelloPayload { + client_version: ProtocolVersion::TLSv1_3, + random: Random([0u8; 32]), + session_id: SessionId::empty(), + cipher_suites: vec![], + compression_methods: vec![Compression::Null], + extensions: Box::new(ClientExtensions { + server_name: Some(ServerNamePayload::from(&name)), + ..Default::default() + }), + }, + None, + &None, + ) + } + + fn dns_name_of_len(mut len: usize) -> DnsName<'static> { + let mut s = String::new(); + let labels = len.div_ceil(63); + for _ in 0..labels { + let chars = Ord::min(len, 63); + len -= chars; + for _ in 0..chars { + s.push('a'); + } + if len != 0 { + s.push('.'); + } + } + DnsName::try_from(s).unwrap() + } + + #[derive(Debug)] + struct MockHpke; + + impl MockHpke { + const SUITE: HpkeSuite = HpkeSuite { + kem: HpkeKem::DHKEM_P256_HKDF_SHA256, + sym: HpkeSymmetricCipherSuite { + kdf_id: HpkeKdf::HKDF_SHA256, + aead_id: HpkeAead::AES_128_GCM, + }, + }; + } + + impl Hpke for MockHpke { + #[cfg_attr(coverage_nightly, coverage(off))] + fn seal( + &self, + _info: &[u8], + _aad: &[u8], + _plaintext: &[u8], + _pub_key: &HpkePublicKey, + ) -> Result<(EncapsulatedSecret, Vec), Error> { + todo!() + } + + fn setup_sealer( + &self, + _info: &[u8], + _pub_key: &HpkePublicKey, + ) -> Result<(EncapsulatedSecret, Box), Error> { + Ok((EncapsulatedSecret(vec![]), Box::new(MockHpkeSealer))) + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn open( + &self, + _enc: &EncapsulatedSecret, + _info: &[u8], + _aad: &[u8], + _ciphertext: &[u8], + _secret_key: &crate::crypto::hpke::HpkePrivateKey, + ) -> Result, Error> { + todo!() + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn setup_opener( + &self, + _enc: &EncapsulatedSecret, + _info: &[u8], + _secret_key: &crate::crypto::hpke::HpkePrivateKey, + ) -> Result, Error> { + todo!() + } + + #[cfg_attr(coverage_nightly, coverage(off))] + fn generate_key_pair( + &self, + ) -> Result<(HpkePublicKey, crate::crypto::hpke::HpkePrivateKey), Error> { + todo!() + } + + fn suite(&self) -> HpkeSuite { + Self::SUITE + } + } + + #[derive(Debug)] + struct MockHpkeSealer; + + impl HpkeSealer for MockHpkeSealer { + #[cfg_attr(coverage_nightly, coverage(off))] + fn seal(&mut self, _aad: &[u8], _plaintext: &[u8]) -> Result, Error> { + todo!() + } + } + + #[derive(Debug)] + struct FixedRandom; + + impl SecureRandom for FixedRandom { + fn fill(&self, buf: &mut [u8]) -> Result<(), crate::rand::GetRandomFailed> { + buf.fill(0x55); + Ok(()) + } + } } diff --git a/crates/primp-rustls/rustls/src/lib.rs b/crates/primp-rustls/rustls/src/lib.rs index 1956e15..38652fe 100644 --- a/crates/primp-rustls/rustls/src/lib.rs +++ b/crates/primp-rustls/rustls/src/lib.rs @@ -378,6 +378,7 @@ // is used to avoid needing `rustversion` to be compiled twice during // cross-compiling. #![cfg_attr(read_buf, feature(read_buf))] +#![cfg_attr(read_buf, feature(core_io))] #![cfg_attr(read_buf, feature(core_io_borrowed_buf))] #![cfg_attr(bench, feature(test))] #![no_std] diff --git a/crates/primp-rustls/rustls/src/manual/fips.rs b/crates/primp-rustls/rustls/src/manual/fips.rs index 203a759..04a2428 100644 --- a/crates/primp-rustls/rustls/src/manual/fips.rs +++ b/crates/primp-rustls/rustls/src/manual/fips.rs @@ -1,7 +1,8 @@ /*! # Using rustls with FIPS-approved cryptography -To use FIPS-approved cryptography with rustls, you should take -these actions: +To use FIPS-approved cryptography with rustls, you should +utilize a FIPS-approved `CryptoProvider`. +rustls ships with one using `aws-lc-rs`, take these actions to make use of it: ## 1. Enable the `fips` crate feature for rustls. diff --git a/crates/primp-rustls/rustls/src/server/builder.rs b/crates/primp-rustls/rustls/src/server/builder.rs index ee591fd..c450ea1 100644 --- a/crates/primp-rustls/rustls/src/server/builder.rs +++ b/crates/primp-rustls/rustls/src/server/builder.rs @@ -100,6 +100,9 @@ impl ConfigBuilder { /// Sets a custom [`ResolvesServerCert`]. pub fn with_cert_resolver(self, cert_resolver: Arc) -> ServerConfig { + #[cfg(feature = "tls12")] + let require_ems = self.provider.fips(); + ServerConfig { provider: self.provider, verifier: self.state.verifier, @@ -119,7 +122,7 @@ impl ConfigBuilder { send_half_rtt_data: false, send_tls13_tickets: 2, #[cfg(feature = "tls12")] - require_ems: cfg!(feature = "fips"), + require_ems, time_provider: self.time_provider, cert_compressors: compress::default_cert_compressors().to_vec(), cert_compression_cache: Arc::new(compress::CompressionCache::default()), diff --git a/crates/primp-rustls/rustls/src/server/server_conn.rs b/crates/primp-rustls/rustls/src/server/server_conn.rs index acdf7ac..cdf80ac 100644 --- a/crates/primp-rustls/rustls/src/server/server_conn.rs +++ b/crates/primp-rustls/rustls/src/server/server_conn.rs @@ -398,7 +398,8 @@ pub struct ServerConfig { /// If set to `true`, requires the client to support the extended /// master secret extraction method defined in [RFC 7627]. /// - /// The default is `true` if the "fips" crate feature is enabled, + /// The default is `true` if the configured [`CryptoProvider`] is + /// FIPS-compliant (i.e., [`CryptoProvider::fips()`] returns `true`), /// `false` otherwise. /// /// It must be set to `true` to meet FIPS requirement mentioned in section From d243d1daf804dcc1db6fa2e28f97b2529f64ced7 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Sun, 17 May 2026 20:25:16 +0300 Subject: [PATCH 6/7] refactor(crates): cargo clippy --fix --- crates/primp-hyper-util/src/client/pool/cache.rs | 11 ++++------- crates/primp-hyper/src/proto/h1/conn.rs | 2 +- crates/primp-hyper/src/proto/h1/role.rs | 2 +- crates/primp-hyper/src/proto/h2/server.rs | 4 ++-- crates/primp-rustls/rustls/src/client/ech.rs | 7 +++---- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/crates/primp-hyper-util/src/client/pool/cache.rs b/crates/primp-hyper-util/src/client/pool/cache.rs index a3d086d..13f1b73 100644 --- a/crates/primp-hyper-util/src/client/pool/cache.rs +++ b/crates/primp-hyper-util/src/client/pool/cache.rs @@ -264,25 +264,22 @@ mod internal { future::Either::Left((Ok(pool_got), connecting)) => { events.on_race_lost(BackgroundConnect { future: connecting, - shared: Arc::downgrade(&shared), + shared: Arc::downgrade(shared), }); return Poll::Ready(Ok(Cached::new( pool_got, - Arc::downgrade(&shared), + Arc::downgrade(shared), ))); } future::Either::Right((connected, _waiter)) => { let inner = connected?; - return Poll::Ready(Ok(Cached::new( - inner, - Arc::downgrade(&shared), - ))); + return Poll::Ready(Ok(Cached::new(inner, Arc::downgrade(shared)))); } } } CacheFuture::Connecting { shared, future } => { let inner = ready!(Pin::new(future).poll(cx))?; - return Poll::Ready(Ok(Cached::new(inner, Arc::downgrade(&shared)))); + return Poll::Ready(Ok(Cached::new(inner, Arc::downgrade(shared)))); } CacheFuture::Cached { svc } => { return Poll::Ready(Ok(svc.take().unwrap())); diff --git a/crates/primp-hyper/src/proto/h1/conn.rs b/crates/primp-hyper/src/proto/h1/conn.rs index 3c37622..7eca348 100644 --- a/crates/primp-hyper/src/proto/h1/conn.rs +++ b/crates/primp-hyper/src/proto/h1/conn.rs @@ -513,7 +513,7 @@ where let result = ready!(self.io.poll_read_from_io(cx)); Poll::Ready(result.inspect_err(|_e| { - trace!(error = %e, "force_io_read; io error"); + trace!(error = %_e, "force_io_read; io error"); self.state.close(); })) } diff --git a/crates/primp-hyper/src/proto/h1/role.rs b/crates/primp-hyper/src/proto/h1/role.rs index 457c964..31879fb 100644 --- a/crates/primp-hyper/src/proto/h1/role.rs +++ b/crates/primp-hyper/src/proto/h1/role.rs @@ -607,7 +607,7 @@ impl Server { ref mut current, title_case_headers, } = *self; - if current.as_ref().map_or(true, |(last, _)| last != name) { + if current.as_ref().is_none_or(|(last, _)| last != name) { *current = None; } let (_, values) = diff --git a/crates/primp-hyper/src/proto/h2/server.rs b/crates/primp-hyper/src/proto/h2/server.rs index e077c1d..570e008 100644 --- a/crates/primp-hyper/src/proto/h2/server.rs +++ b/crates/primp-hyper/src/proto/h2/server.rs @@ -280,7 +280,7 @@ where None, ) } else { - if content_length.map_or(false, |len| len != 0) { + if content_length.is_some_and(|len| len != 0) { warn!("h2 connect request with non-zero body not supported"); respond.send_reset(h2::Reason::INTERNAL_ERROR); return Poll::Ready(Ok(())); @@ -478,7 +478,7 @@ where if let Some(connect_parts) = connect_parts.take() { if res.status().is_success() { if headers::content_length_parse_all(res.headers()) - .map_or(false, |len| len != 0) + .is_some_and(|len| len != 0) { warn!("h2 successful response to CONNECT request with body not supported"); me.reply.send_reset(h2::Reason::INTERNAL_ERROR); diff --git a/crates/primp-rustls/rustls/src/client/ech.rs b/crates/primp-rustls/rustls/src/client/ech.rs index 97733dd..7588476 100644 --- a/crates/primp-rustls/rustls/src/client/ech.rs +++ b/crates/primp-rustls/rustls/src/client/ech.rs @@ -1,7 +1,6 @@ use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; -use core::iter; use pki_types::{DnsName, EchConfigListBytes, ServerName}; use subtle::ConstantTimeEq; @@ -14,7 +13,7 @@ use crate::hash_hs::{HandshakeHash, HandshakeHashBuffer}; use crate::log::{debug, trace, warn}; use crate::msgs::base::{Payload, PayloadU16}; use crate::msgs::codec::{Codec, Reader}; -use crate::msgs::enums::{ExtensionType, HpkeKem}; +use crate::msgs::enums::ExtensionType; use crate::msgs::handshake::{ ClientExtensions, ClientHelloPayload, EchConfigContents, EchConfigPayload, Encoding, EncryptedClientHello, EncryptedClientHelloOuter, HandshakeMessagePayload, HandshakePayload, @@ -683,12 +682,12 @@ impl EchState { // "This is the length of a "server_name" extension with an L-byte name." _ => max_name_len + 9, }; - encoded_hello.extend(iter::repeat(0).take(name_padding_len)); + encoded_hello.extend(core::iter::repeat_n(0, name_padding_len)); // Let L be the length of the EncodedClientHelloInner with all the padding computed so far // Let N = 31 - ((L - 1) % 32) and add N bytes of padding. let padding_len = 31 - ((encoded_hello.len() - 1) % 32); - encoded_hello.extend(iter::repeat(0).take(padding_len)); + encoded_hello.extend(core::iter::repeat_n(0, padding_len)); // Construct the inner hello message that will be used for the transcript. let inner_hello_msg = Message { From 6823594ffdf61382a69c90eb825554f2eac9e0a7 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Sun, 17 May 2026 20:50:30 +0300 Subject: [PATCH 7/7] fix(ci): remove pinned maturin version, update attest action --- .github/workflows/ci.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0385db..07a45e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,14 +63,12 @@ jobs: target: ${{ matrix.platform.target }} args: --release --out dist --manifest-path crates/primp-python/Cargo.toml manylinux: auto - maturin-version: 1.12.4 - name: Build free-threaded wheels uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} args: --release --out dist -i python3.14t --manifest-path crates/primp-python/Cargo.toml manylinux: auto - maturin-version: 1.12.4 - name: Upload wheels uses: actions/upload-artifact@v6 with: @@ -144,14 +142,12 @@ jobs: target: ${{ matrix.platform.target }} args: --release --out dist --manifest-path crates/primp-python/Cargo.toml manylinux: musllinux_1_2 - maturin-version: 1.12.4 - name: Build free-threaded wheels uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} args: --release --out dist -i python3.14t --manifest-path crates/primp-python/Cargo.toml manylinux: musllinux_1_2 - maturin-version: 1.12.4 - name: Upload wheels uses: actions/upload-artifact@v6 with: @@ -218,7 +214,6 @@ jobs: with: target: ${{ matrix.platform.target }} args: --release --out dist --manifest-path crates/primp-python/Cargo.toml - maturin-version: 1.12.4 - uses: actions/setup-python@v6 with: python-version: 3.14t @@ -228,7 +223,6 @@ jobs: with: target: ${{ matrix.platform.target }} args: --release --out dist -i python3.14t --manifest-path crates/primp-python/Cargo.toml - maturin-version: 1.12.4 - name: Upload wheels uses: actions/upload-artifact@v6 with: @@ -270,13 +264,11 @@ jobs: with: target: ${{ matrix.platform.target }} args: --release --out dist --manifest-path crates/primp-python/Cargo.toml - maturin-version: 1.12.4 - name: Build free-threaded wheels uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} args: --release --out dist -i python3.14t --manifest-path crates/primp-python/Cargo.toml - maturin-version: 1.12.4 - name: Upload wheels uses: actions/upload-artifact@v6 with: @@ -306,7 +298,6 @@ jobs: with: command: sdist args: --out dist --manifest-path crates/primp-python/Cargo.toml - maturin-version: 1.12.4 - name: Upload sdist uses: actions/upload-artifact@v6 with: @@ -325,7 +316,7 @@ jobs: steps: - uses: actions/download-artifact@v7 - name: Generate artifact attestation - uses: actions/attest-build-provenance@v4 + uses: actions/attest@v4 with: subject-path: 'wheels-*/*' - name: Install uv