v0.31.5
This commit is contained in:
1 parent
75104f87ba
commit
4cc994b3bc
86 files changed
+942
-223
No files matched your search
+3
-3
@@ -110,8 +110,8 @@ impl HttpBody for ReceiveBody {
|
||||
}
|
||||
impl Drop for ReceiveBody {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the request stream cancels the receive side at QUIC level.
|
||||
// Explicit STOP_SENDING is used only from poll_frame paths where the
|
||||
// stream handle is available and a protocol error was detected.
|
||||
// Do not call stop_sending here: h3/quinn can still have a pending
|
||||
// receive read and has previously panicked on that cancellation path.
|
||||
// Protocol framing violations explicitly stop only from poll_frame.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
struct Lease {
|
||||
_permit: OwnedSemaphorePermit,
|
||||
metrics: Arc<Metrics>,
|
||||
}
|
||||
impl Drop for Lease {
|
||||
fn drop(&mut self) {
|
||||
self.metrics
|
||||
.http3_active_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
self.metrics
|
||||
.active_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn log_protocol_error(
|
||||
state: &App,
|
||||
kind: &'static str,
|
||||
detail: impl std::fmt::Display,
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
status: u16,
|
||||
authority: Option<String>,
|
||||
host: Option<String>,
|
||||
sni: Option<String>,
|
||||
) {
|
||||
let resolved = state.config.load().resolve_logging_vhost(
|
||||
local,
|
||||
true,
|
||||
None,
|
||||
authority.as_deref(),
|
||||
host.as_deref(),
|
||||
sni.as_deref(),
|
||||
);
|
||||
let (vhost, matched_by) = resolved
|
||||
.map(|resolved| (Some(resolved.vhost), resolved.matched_by))
|
||||
.unwrap_or((None, "none"));
|
||||
state.logs.push_protocol_error_attributed(
|
||||
kind,
|
||||
detail,
|
||||
peer,
|
||||
local,
|
||||
"quic",
|
||||
"3",
|
||||
status,
|
||||
sni,
|
||||
authority.or(host),
|
||||
vhost,
|
||||
matched_by,
|
||||
);
|
||||
}
|
||||
|
||||
/// Service-level breadcrumbs for a full HTTP/3 GET / navigation. User-provided
|
||||
/// query strings, headers and error details must not reach this diagnostic.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn log_root_navigation(
|
||||
stage: &'static str,
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
sni: &Option<String>,
|
||||
stream_id: &str,
|
||||
phase: &'static str,
|
||||
response_status: u16,
|
||||
body_bytes_sent: u64,
|
||||
cancelled: bool,
|
||||
) {
|
||||
match stage {
|
||||
"started" => tracing::info!(
|
||||
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
|
||||
method="GET", path="/", "http3_navigation_started"
|
||||
),
|
||||
"complete" => tracing::info!(
|
||||
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
|
||||
method="GET", path="/", response_status, body_bytes_sent,
|
||||
"http3_navigation_complete"
|
||||
),
|
||||
"interrupted" => tracing::warn!(
|
||||
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
|
||||
method="GET", path="/", phase, response_status, body_bytes_sent,
|
||||
cancelled, "http3_navigation_interrupted"
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn log_h3_response_complete(peer: SocketAddr, stream_id: &str, response_status: u16, body_bytes_sent: u64) {
|
||||
tracing::debug!(
|
||||
peer=%peer, stream_id=%stream_id, response_status, body_bytes_sent,
|
||||
"HTTP/3 response completed"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// A client can cancel an obsolete in-flight request during navigation/F5.
|
||||
/// RFC 9114 section 4.1.1 gives this a stream reset code, not an HTTP 400.
|
||||
/// Treat only peer-originated cancellation as benign; local protocol errors
|
||||
/// and other remote reset codes remain visible to operators.
|
||||
fn client_cancelled_code(code: Code) -> bool {
|
||||
code == Code::H3_REQUEST_CANCELLED || code == Code::H3_NO_ERROR
|
||||
}
|
||||
fn client_cancelled_request(error: &h3::error::StreamError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
h3::error::StreamError::RemoteTerminate { code, .. } if client_cancelled_code(*code)
|
||||
)
|
||||
}
|
||||
fn request_identity<B>(request: &http::Request<B>) -> (Option<String>, Option<String>) {
|
||||
let authority = request
|
||||
.uri()
|
||||
.authority()
|
||||
.map(|value| value.as_str().to_owned());
|
||||
let host = request
|
||||
.headers()
|
||||
.get(http::header::HOST)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::to_owned);
|
||||
(authority, host)
|
||||
}
|
||||
fn connection_sni(connection: &quinn::Connection) -> Option<String> {
|
||||
connection
|
||||
.handshake_data()?
|
||||
.downcast::<quinn::crypto::rustls::HandshakeData>()
|
||||
.ok()?
|
||||
.server_name
|
||||
.and_then(|name| crate::config::request_host(&name).ok())
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
pub alt_svc_max_age: u64,
|
||||
pub max_concurrent_connections: u32,
|
||||
pub max_concurrent_bidistream: u32,
|
||||
pub max_concurrent_unistream: u32,
|
||||
/// QUIC idle timeout in seconds; zero disables the transport idle timeout.
|
||||
pub max_idle_timeout: u64,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
alt_svc_max_age: 3600,
|
||||
max_concurrent_connections: 512,
|
||||
max_concurrent_bidistream: 100,
|
||||
max_concurrent_unistream: 100,
|
||||
max_idle_timeout: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure!(
|
||||
self.alt_svc_max_age <= 604800,
|
||||
"HTTP/3 Alt-Svc lifetime must be at most seven days"
|
||||
);
|
||||
ensure!(
|
||||
(1..=4096).contains(&self.max_concurrent_connections),
|
||||
"HTTP/3 connections per listener must be 1..4096"
|
||||
);
|
||||
ensure!(
|
||||
(1..=1024).contains(&self.max_concurrent_bidistream),
|
||||
"HTTP/3 bidirectional streams must be 1..1024"
|
||||
);
|
||||
ensure!((3..=1024).contains(&self.max_concurrent_unistream),"HTTP/3 requires at least three peer unidirectional streams (control and QPACK), maximum 1024");
|
||||
ensure!(
|
||||
self.max_idle_timeout <= 86400,
|
||||
"HTTP/3 idle timeout must be 0..86400 seconds"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
pub fn transport(&self) -> Result<quinn::TransportConfig> {
|
||||
self.validate()?;
|
||||
let mut transport = quinn::TransportConfig::default();
|
||||
transport
|
||||
.max_concurrent_bidi_streams(self.max_concurrent_bidistream.into())
|
||||
.max_concurrent_uni_streams(self.max_concurrent_unistream.into())
|
||||
.max_idle_timeout(if self.max_idle_timeout == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(self.max_idle_timeout).try_into()?)
|
||||
})
|
||||
.stream_receive_window((256 * 1024u32).into())
|
||||
.receive_window((8 * 1024 * 1024u32).into())
|
||||
.send_window(8 * 1024 * 1024)
|
||||
.datagram_receive_buffer_size(None);
|
||||
Ok(transport)
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,15 @@ mod tests {
|
||||
assert!(!h.contains_key("alt-svc"));
|
||||
}
|
||||
#[test]
|
||||
fn client_cancellation_is_a_stream_event_not_an_http_400() {
|
||||
// Exercise the production classifier; RemoteTerminate is
|
||||
// non-exhaustive in hpx-h3 and cannot be constructed by this crate.
|
||||
assert!(client_cancelled_code(Code::H3_REQUEST_CANCELLED));
|
||||
assert!(client_cancelled_code(Code::H3_NO_ERROR));
|
||||
assert!(!client_cancelled_code(Code::H3_MESSAGE_ERROR));
|
||||
assert!(!client_cancelled_code(Code::H3_INTERNAL_ERROR));
|
||||
}
|
||||
#[test]
|
||||
fn h3_rejects_http1_connection_specific_headers() {
|
||||
for name in ["connection", "transfer-encoding", "upgrade"] {
|
||||
let mut h = HeaderMap::new();
|
||||
|
||||
Reference in new issue
Block a user