v0.34.0_1
This commit is contained in:
1 parent
9b3d6731cc
commit
00f29f92c8
68 files changed
+2599
-513
No files matched your search
@@ -0,0 +1,207 @@
|
||||
// HTTP/1 dispatcher and upgrade flow; split from TLS listener runtime.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn connection<I>(
|
||||
stream: I,
|
||||
router: Router,
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
transport: Transport,
|
||||
tls_sni: Option<String>,
|
||||
h2: bool,
|
||||
extended_connect: bool,
|
||||
h2c_upgrade: bool,
|
||||
frontend_state: Option<Arc<App>>,
|
||||
lease: Arc<ConnectionLease>,
|
||||
stop: CancellationToken,
|
||||
grace: Duration,
|
||||
http2_settings: Http2Connections,
|
||||
sendfile_slots: TransportSendfileSlots,
|
||||
) where
|
||||
I: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
if h2 {
|
||||
http2_connection(
|
||||
stream,
|
||||
router,
|
||||
peer,
|
||||
local,
|
||||
transport,
|
||||
tls_sni.clone(),
|
||||
extended_connect,
|
||||
frontend_state.clone(),
|
||||
lease,
|
||||
stop,
|
||||
grace,
|
||||
http2_settings,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
let request_stop = stop.clone();
|
||||
let service_frontend_state = frontend_state.clone();
|
||||
let service = service_fn(move |mut request: Request<Incoming>| {
|
||||
let router = router.clone();
|
||||
let request_stop = request_stop.clone();
|
||||
let request_lease = lease.clone();
|
||||
let upgrade_state = service_frontend_state.clone();
|
||||
#[cfg(target_os = "linux")]
|
||||
let sendfile_slots = sendfile_slots.clone();
|
||||
async move {
|
||||
request.extensions_mut().insert(PeerAddr(peer));
|
||||
request.extensions_mut().insert(LocalAddr(local));
|
||||
request.extensions_mut().insert(transport);
|
||||
request.extensions_mut().insert(request_lease.clone());
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(slots) = sendfile_slots {
|
||||
request.extensions_mut().insert(slots);
|
||||
}
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(ServerShutdown(request_stop.clone()));
|
||||
|
||||
if h2c_attempt(&request) {
|
||||
if !h2c_upgrade {
|
||||
let (authority, host) = request_log_identity(&request);
|
||||
log_protocol_error_with_identity(
|
||||
upgrade_state.as_ref(),
|
||||
"h2c_upgrade_rejected",
|
||||
"h2c upgrade is disabled on this listener",
|
||||
peer,
|
||||
local,
|
||||
"tcp",
|
||||
"1.1",
|
||||
400,
|
||||
authority,
|
||||
host,
|
||||
None,
|
||||
);
|
||||
return Ok::<_, Infallible>(
|
||||
Response::builder()
|
||||
.status(http::StatusCode::BAD_REQUEST)
|
||||
.header(http::header::CACHE_CONTROL, "no-store")
|
||||
.body(Body::from("h2c upgrade is disabled on this listener"))
|
||||
.expect("static h2c disabled response"),
|
||||
);
|
||||
}
|
||||
let injected_headers = match h2_headers_frame(&request) {
|
||||
Ok(frame) => frame,
|
||||
Err(message) => {
|
||||
let (authority, host) = request_log_identity(&request);
|
||||
log_protocol_error_with_identity(
|
||||
upgrade_state.as_ref(),
|
||||
"h2c_upgrade_invalid",
|
||||
message,
|
||||
peer,
|
||||
local,
|
||||
"tcp",
|
||||
"1.1",
|
||||
400,
|
||||
authority,
|
||||
host,
|
||||
None,
|
||||
);
|
||||
return Ok::<_, Infallible>(
|
||||
Response::builder()
|
||||
.status(http::StatusCode::BAD_REQUEST)
|
||||
.header(http::header::CACHE_CONTROL, "no-store")
|
||||
.body(Body::from(message))
|
||||
.expect("static h2c validation response"),
|
||||
);
|
||||
}
|
||||
};
|
||||
let on_upgrade = hyper::upgrade::on(&mut request);
|
||||
let upgrade_router = router.clone();
|
||||
let upgrade_stop = request_stop.clone();
|
||||
let upgrade_lease = request_lease.clone();
|
||||
let upgrade_state_for_task = upgrade_state.clone();
|
||||
let task = async move {
|
||||
match on_upgrade.await {
|
||||
Ok(upgraded) => {
|
||||
if let Some(state) = &upgrade_state_for_task {
|
||||
state
|
||||
.metrics
|
||||
.http2_connections
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
let extended_connect =
|
||||
upgrade_state_for_task.as_ref().is_some_and(|state| {
|
||||
state.config.load().websocket_rfc8441_on(local, false)
|
||||
});
|
||||
let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers);
|
||||
http2_connection(
|
||||
io,
|
||||
upgrade_router,
|
||||
peer,
|
||||
local,
|
||||
Transport { tls: false },
|
||||
None,
|
||||
extended_connect,
|
||||
upgrade_state_for_task.clone(),
|
||||
upgrade_lease,
|
||||
upgrade_stop,
|
||||
grace,
|
||||
http2_settings,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
log_protocol_error(
|
||||
upgrade_state_for_task.as_ref(),
|
||||
"h2c_transport_error",
|
||||
&error,
|
||||
peer,
|
||||
local,
|
||||
"tcp",
|
||||
"2",
|
||||
0,
|
||||
None,
|
||||
);
|
||||
tracing::debug!(%error, "h2c upgrade transport failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(state) = upgrade_state {
|
||||
state.tunnels.spawn(task);
|
||||
} else {
|
||||
tokio::spawn(task);
|
||||
}
|
||||
return Ok::<_, Infallible>(
|
||||
Response::builder()
|
||||
.status(http::StatusCode::SWITCHING_PROTOCOLS)
|
||||
.header(http::header::CONNECTION, "Upgrade")
|
||||
.header(http::header::UPGRADE, "h2c")
|
||||
.body(Body::empty())
|
||||
.expect("static h2c switching response"),
|
||||
);
|
||||
}
|
||||
|
||||
router.oneshot(request.map(Body::new)).await
|
||||
}
|
||||
});
|
||||
let mut builder = http1::Builder::new();
|
||||
builder
|
||||
.timer(TokioTimer::new())
|
||||
.keep_alive(true)
|
||||
.max_headers(64)
|
||||
.max_buf_size(65_536)
|
||||
.header_read_timeout(Duration::from_secs(10));
|
||||
let conn = builder
|
||||
.serve_connection(TokioIo::new(stream), service)
|
||||
.with_upgrades();
|
||||
tokio::pin!(conn);
|
||||
tokio::select! {
|
||||
result = &mut conn => {
|
||||
if let Err(error) = result {
|
||||
log_protocol_error(
|
||||
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
|
||||
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, tls_sni.clone(),
|
||||
);
|
||||
tracing::debug!(%error, "HTTP/1 connection closed");
|
||||
}
|
||||
},
|
||||
_ = stop.cancelled() => {
|
||||
conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
//! Linux HTTP/1 kernel sendfile bridge.
|
||||
//!
|
||||
//! Hyper's Body uses Bytes, not file descriptors. This opt-in bridge conveys a
|
||||
//! uniquely tagged, exact-length placeholder through the ordinary metering
|
||||
//! pipeline; a TCP-only AsyncWrite adapter replaces those bytes with the
|
||||
//! original, already-validated file via Linux sendfile(2). An unmatched marker
|
||||
//! or short file is a hard I/O error, never silently returned as user content.
|
||||
//! Never register a placeholder for cached, compressed, transformed, TLS, H2,
|
||||
//! H3, ranged or non-GET responses.
|
||||
|
||||
use bytes::Bytes;
|
||||
use http_body::{Body as HttpBody, Frame, SizeHint};
|
||||
use std::{
|
||||
fs::File,
|
||||
io,
|
||||
os::fd::{AsRawFd, RawFd},
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
sync::atomic::Ordering,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{io::{AsyncRead, AsyncWrite, Interest, ReadBuf}, net::TcpStream};
|
||||
|
||||
const TAG_LEN: usize = 32;
|
||||
const FRAME_LEN: usize = 64 * 1024;
|
||||
const MAX_PENDING: usize = 8;
|
||||
const MIN_SIZE: u64 = 64 * 1024;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SendfileCandidate {
|
||||
pub file: Arc<File>,
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
struct Slot {
|
||||
tag: [u8; TAG_LEN],
|
||||
file: Arc<File>,
|
||||
length: u64,
|
||||
written: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Registry {
|
||||
slots: Vec<Slot>,
|
||||
active: Option<[u8; TAG_LEN]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct SendfileSlots(Arc<Mutex<Registry>>);
|
||||
|
||||
pub(crate) struct SendfileBody {
|
||||
bytes: Bytes,
|
||||
remaining: u64,
|
||||
web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
|
||||
}
|
||||
|
||||
// Web Server counters count emitted body frames before any downstream encoder.
|
||||
// Atomic reference is supplied by the Web Server that originally opened the file.
|
||||
impl SendfileSlots {
|
||||
pub fn create_body(
|
||||
&self,
|
||||
candidate: SendfileCandidate,
|
||||
web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
|
||||
) -> Option<SendfileBody> {
|
||||
if !(MIN_SIZE..=i64::MAX as u64).contains(&candidate.length) {
|
||||
return None;
|
||||
}
|
||||
let mut registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if registry.slots.len() >= MAX_PENDING { return None; }
|
||||
let mut tag = [0u8; TAG_LEN];
|
||||
tag[0] = 0; // A valid HTTP/1 response header never contains NUL.
|
||||
tag[1..].copy_from_slice(&rand::random::<[u8; TAG_LEN - 1]>());
|
||||
if registry.slots.iter().any(|slot| slot.tag == tag) { return None; }
|
||||
let mut data = vec![0u8; FRAME_LEN];
|
||||
for chunk in data.chunks_exact_mut(TAG_LEN) { chunk.copy_from_slice(&tag); }
|
||||
registry.slots.push(Slot {
|
||||
tag, file: candidate.file, length: candidate.length, written: 0,
|
||||
});
|
||||
Some(SendfileBody {
|
||||
bytes: Bytes::from(data), remaining: candidate.length, web_metrics,
|
||||
})
|
||||
}
|
||||
|
||||
fn plan(&self, buf: &[u8]) -> WritePlan {
|
||||
let registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// Critical for mixed reverse-proxy traffic: almost all socket writes
|
||||
// have no pending static file; do not inspect their payload at all.
|
||||
if registry.slots.is_empty() { return WritePlan::Plain(buf.len()); }
|
||||
if let Some(tag) = registry.active {
|
||||
if let Some(slot) = registry.slots.iter().find(|s| s.tag == tag) {
|
||||
return plan_file(slot, buf);
|
||||
}
|
||||
return WritePlan::Invalid;
|
||||
}
|
||||
// A marker is an opaque 32-byte random tag, searched only on a
|
||||
// connection that has a registered response. Headers preceding a
|
||||
// marker are written intact; each subsequent body byte is validated.
|
||||
for start in 0..buf.len() {
|
||||
if buf[start] != 0 { continue; }
|
||||
for slot in ®istry.slots {
|
||||
let available = buf.len() - start;
|
||||
let compare = available.min(TAG_LEN);
|
||||
if buf[start..start + compare] == slot.tag[..compare] {
|
||||
if start > 0 { return WritePlan::Plain(start); }
|
||||
return plan_file(slot, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
WritePlan::Plain(buf.len())
|
||||
}
|
||||
|
||||
fn commit(&self, tag: [u8; TAG_LEN], count: usize) {
|
||||
let mut registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
|
||||
registry.active = Some(tag);
|
||||
if let Some(slot) = registry.slots.iter_mut().find(|s| s.tag == tag) {
|
||||
slot.written += count as u64;
|
||||
if slot.written == slot.length {
|
||||
registry.slots.retain(|s| s.tag != tag);
|
||||
registry.active = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum WritePlan {
|
||||
Plain(usize),
|
||||
File { tag: [u8; TAG_LEN], fd: RawFd, file: Arc<File>, offset: u64, count: usize },
|
||||
Invalid,
|
||||
}
|
||||
|
||||
fn plan_file(slot: &Slot, buf: &[u8]) -> WritePlan {
|
||||
let expected = (slot.length - slot.written).min(buf.len() as u64) as usize;
|
||||
let matched = buf[..expected].iter().enumerate()
|
||||
.take_while(|(n, byte)| **byte == slot.tag[(slot.written as usize + n) % TAG_LEN])
|
||||
.count();
|
||||
if matched == 0 { return WritePlan::Invalid; }
|
||||
WritePlan::File {
|
||||
tag: slot.tag, fd: slot.file.as_raw_fd(), file: slot.file.clone(),
|
||||
offset: slot.written, count: matched,
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpBody for SendfileBody {
|
||||
type Data = Bytes;
|
||||
type Error = io::Error;
|
||||
fn poll_frame(mut self: Pin<&mut Self>, _cx: &mut Context<'_>)
|
||||
-> Poll<Option<Result<Frame<Bytes>, io::Error>>>
|
||||
{
|
||||
if self.remaining == 0 { return Poll::Ready(None); }
|
||||
let len = self.remaining.min(FRAME_LEN as u64) as usize;
|
||||
self.remaining -= len as u64;
|
||||
self.web_metrics.response_bytes.fetch_add(len as u64, Ordering::Relaxed);
|
||||
Poll::Ready(Some(Ok(Frame::data(self.bytes.slice(0..len)))))
|
||||
}
|
||||
fn size_hint(&self) -> SizeHint { SizeHint::with_exact(self.remaining) }
|
||||
fn is_end_stream(&self) -> bool { self.remaining == 0 }
|
||||
}
|
||||
|
||||
pub(crate) struct SendfileIo {
|
||||
stream: TcpStream,
|
||||
slots: SendfileSlots,
|
||||
}
|
||||
impl SendfileIo {
|
||||
pub fn new(stream: TcpStream) -> (Self, SendfileSlots) {
|
||||
let slots = SendfileSlots::default();
|
||||
(Self { stream, slots: slots.clone() }, slots)
|
||||
}
|
||||
}
|
||||
impl AsyncRead for SendfileIo {
|
||||
fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.get_mut().stream).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
impl AsyncWrite for SendfileIo {
|
||||
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
|
||||
if buf.is_empty() { return Poll::Ready(Ok(0)); }
|
||||
match self.slots.plan(buf) {
|
||||
WritePlan::Plain(count) => Pin::new(&mut self.stream).poll_write(cx, &buf[..count]),
|
||||
WritePlan::Invalid => Poll::Ready(Err(io::Error::new(io::ErrorKind::InvalidData,
|
||||
"sendfile placeholder did not match its registered descriptor"))),
|
||||
WritePlan::File { tag, fd, file, offset, count } => {
|
||||
let _keep_open = file;
|
||||
let socket: RawFd = self.stream.as_raw_fd();
|
||||
let mut at: libc::off_t = offset as libc::off_t;
|
||||
match self.stream.poll_write_ready(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Ready(Ok(())) => {
|
||||
let result = self.stream.try_io(Interest::WRITABLE, || {
|
||||
let n = unsafe { libc::sendfile(socket, fd, &mut at, count) };
|
||||
if n < 0 { Err(io::Error::last_os_error()) }
|
||||
else if n == 0 { Err(io::Error::new(io::ErrorKind::UnexpectedEof,
|
||||
"static source truncated during sendfile")) }
|
||||
else { Ok(n as usize) }
|
||||
});
|
||||
match result {
|
||||
Ok(n) => { self.slots.commit(tag, n); Poll::Ready(Ok(n)) },
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending,
|
||||
Err(e) => Poll::Ready(Err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_flush(cx)
|
||||
}
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_shutdown(cx)
|
||||
}
|
||||
fn is_write_vectored(&self) -> bool { self.stream.is_write_vectored() }
|
||||
fn poll_write_vectored(mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[io::IoSlice<'_>]) -> Poll<io::Result<usize>> {
|
||||
// Preserve zero-extra-syscall vectored I/O whenever no sendfile body
|
||||
// has been prepared on this connection.
|
||||
if self.slots.0.lock().unwrap_or_else(|e| e.into_inner()).slots.is_empty() {
|
||||
return Pin::new(&mut self.stream).poll_write_vectored(cx, bufs);
|
||||
}
|
||||
match bufs.iter().find(|part| !part.is_empty()) {
|
||||
Some(part) => self.poll_write(cx, part),
|
||||
None => Poll::Ready(Ok(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http_body::Body;
|
||||
use tempfile::tempfile;
|
||||
use std::io::Write;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[test]
|
||||
fn placeholder_has_exact_frame_lengths_and_recovers_fd() {
|
||||
let mut file = tempfile().unwrap();
|
||||
file.write_all(&vec![42u8; 100_000]).unwrap();
|
||||
let slots = SendfileSlots::default();
|
||||
let counter = Arc::new(crate::web_server::metrics::PhpMetrics::default());
|
||||
let mut body = slots.create_body(SendfileCandidate { file: Arc::new(file), length: 100_000 }, counter.clone()).unwrap();
|
||||
let first = match Pin::new(&mut body).poll_frame(&mut Context::from_waker(std::task::Waker::noop())) {
|
||||
Poll::Ready(Some(Ok(frame))) => frame.into_data().unwrap(), _ => panic!("frame missing"),
|
||||
};
|
||||
assert_eq!(first.len(), FRAME_LEN);
|
||||
match slots.plan(&first) { WritePlan::File { count, .. } => assert_eq!(count, FRAME_LEN), _ => panic!("missing kernel file plan") }
|
||||
let tag = slots.0.lock().unwrap().slots[0].tag;
|
||||
slots.commit(tag, FRAME_LEN);
|
||||
let second = match Pin::new(&mut body).poll_frame(&mut Context::from_waker(std::task::Waker::noop())) {
|
||||
Poll::Ready(Some(Ok(frame))) => frame.into_data().unwrap(), _ => panic!("frame missing"),
|
||||
};
|
||||
assert_eq!(second.len(), 34_464);
|
||||
match slots.plan(&second) { WritePlan::File { count, offset, .. } => { assert_eq!(offset, 65_536); assert_eq!(count, 34_464); }, _ => panic!("second plan missing") }
|
||||
slots.commit(tag, second.len());
|
||||
assert!(slots.0.lock().unwrap().slots.is_empty());
|
||||
assert!(body.is_end_stream());
|
||||
assert_eq!(counter.response_bytes.load(Ordering::Relaxed), 100_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_sendfile_transfers_exact_file_and_keeps_socket_reusable() {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
let (tcp, _) = listener.accept().await.unwrap();
|
||||
let (mut socket, slots) = SendfileIo::new(tcp);
|
||||
for round in 0..2 {
|
||||
let expected = if round == 0 { vec![0, 1, 2, 3].repeat(25_000) }
|
||||
else { vec![9u8; 100_003] };
|
||||
let mut file = tempfile().unwrap();
|
||||
file.write_all(&expected).unwrap();
|
||||
let web = Arc::new(crate::web_server::metrics::PhpMetrics::default());
|
||||
let body = slots.create_body(SendfileCandidate {
|
||||
file: Arc::new(file), length: expected.len() as u64,
|
||||
}, web.clone()).unwrap();
|
||||
let first = format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", expected.len());
|
||||
// Combine header and first marker into one socket write: the
|
||||
// bridge must send the header before enabling kernel transfer.
|
||||
let mut body = body;
|
||||
let first_frame = http_body_util::BodyExt::frame(&mut body).await.unwrap().unwrap().into_data().unwrap();
|
||||
let mut joined = first.into_bytes();
|
||||
joined.extend_from_slice(&first_frame);
|
||||
socket.write_all(&joined).await.unwrap();
|
||||
while let Some(frame) = http_body_util::BodyExt::frame(&mut body).await {
|
||||
socket.write_all(&frame.unwrap().into_data().unwrap()).await.unwrap();
|
||||
}
|
||||
assert_eq!(web.response_bytes.load(Ordering::Relaxed), expected.len() as u64);
|
||||
}
|
||||
socket.shutdown().await.unwrap();
|
||||
});
|
||||
let mut client = tokio::net::TcpStream::connect(address).await.unwrap();
|
||||
let mut actual = Vec::new();
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), client.read_to_end(&mut actual)).await.unwrap().unwrap();
|
||||
server.await.unwrap();
|
||||
let mut wanted = Vec::new();
|
||||
for expected in [vec![0, 1, 2, 3].repeat(25_000), vec![9u8; 100_003]] {
|
||||
wanted.extend_from_slice(format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", expected.len()).as_bytes());
|
||||
wanted.extend_from_slice(&expected);
|
||||
}
|
||||
assert_eq!(actual, wanted, "kernel sendfile must never leak placeholder markers");
|
||||
}
|
||||
}
|
||||
+20
-199
@@ -134,6 +134,7 @@ async fn serve_inner(
|
||||
stop,
|
||||
grace,
|
||||
http2_settings,
|
||||
Default::default(),
|
||||
)
|
||||
.await;
|
||||
},
|
||||
@@ -178,6 +179,7 @@ async fn serve_inner(
|
||||
stop,
|
||||
grace,
|
||||
Http2Connections::default(),
|
||||
Default::default(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
@@ -208,6 +210,7 @@ async fn serve_inner(
|
||||
stop,
|
||||
grace,
|
||||
Http2Connections::default(),
|
||||
Default::default(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -242,6 +245,22 @@ async fn serve_inner(
|
||||
&& frontend_state
|
||||
.as_ref()
|
||||
.is_some_and(|state| state.config.load().websocket_rfc8441_on(local, false));
|
||||
// The existing Hyper transport remains untouched when the
|
||||
// option is disabled. Connections opened after a hot config
|
||||
// change acquire a new bridge automatically.
|
||||
#[cfg(target_os = "linux")]
|
||||
if !prior_h2 && frontend_state.as_ref().is_some_and(|state| {
|
||||
state.config.load().web_php_services.values()
|
||||
.any(|service| service.sendfile == crate::web_server::SendfileMode::Auto)
|
||||
}) {
|
||||
let (io, slots) = sendfile::SendfileIo::new(stream);
|
||||
connection(
|
||||
io, router, peer, local, Transport { tls: false }, None,
|
||||
false, false, h2c, frontend_state, lease, stop, grace,
|
||||
http2_settings, Some(slots),
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
connection(
|
||||
stream,
|
||||
router,
|
||||
@@ -257,6 +276,7 @@ async fn serve_inner(
|
||||
stop,
|
||||
grace,
|
||||
http2_settings,
|
||||
Default::default(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -273,205 +293,6 @@ async fn serve_inner(
|
||||
while tasks.join_next().await.is_some() {}
|
||||
Ok(())
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn connection<I>(
|
||||
stream: I,
|
||||
router: Router,
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
transport: Transport,
|
||||
tls_sni: Option<String>,
|
||||
h2: bool,
|
||||
extended_connect: bool,
|
||||
h2c_upgrade: bool,
|
||||
frontend_state: Option<Arc<App>>,
|
||||
lease: Arc<ConnectionLease>,
|
||||
stop: CancellationToken,
|
||||
grace: Duration,
|
||||
http2_settings: Http2Connections,
|
||||
) where
|
||||
I: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
if h2 {
|
||||
http2_connection(
|
||||
stream,
|
||||
router,
|
||||
peer,
|
||||
local,
|
||||
transport,
|
||||
tls_sni.clone(),
|
||||
extended_connect,
|
||||
frontend_state.clone(),
|
||||
lease,
|
||||
stop,
|
||||
grace,
|
||||
http2_settings,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
let request_stop = stop.clone();
|
||||
let service_frontend_state = frontend_state.clone();
|
||||
let service = service_fn(move |mut request: Request<Incoming>| {
|
||||
let router = router.clone();
|
||||
let request_stop = request_stop.clone();
|
||||
let request_lease = lease.clone();
|
||||
let upgrade_state = service_frontend_state.clone();
|
||||
async move {
|
||||
request.extensions_mut().insert(PeerAddr(peer));
|
||||
request.extensions_mut().insert(LocalAddr(local));
|
||||
request.extensions_mut().insert(transport);
|
||||
request.extensions_mut().insert(request_lease.clone());
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(ServerShutdown(request_stop.clone()));
|
||||
|
||||
if h2c_attempt(&request) {
|
||||
if !h2c_upgrade {
|
||||
let (authority, host) = request_log_identity(&request);
|
||||
log_protocol_error_with_identity(
|
||||
upgrade_state.as_ref(),
|
||||
"h2c_upgrade_rejected",
|
||||
"h2c upgrade is disabled on this listener",
|
||||
peer,
|
||||
local,
|
||||
"tcp",
|
||||
"1.1",
|
||||
400,
|
||||
authority,
|
||||
host,
|
||||
None,
|
||||
);
|
||||
return Ok::<_, Infallible>(
|
||||
Response::builder()
|
||||
.status(http::StatusCode::BAD_REQUEST)
|
||||
.header(http::header::CACHE_CONTROL, "no-store")
|
||||
.body(Body::from("h2c upgrade is disabled on this listener"))
|
||||
.expect("static h2c disabled response"),
|
||||
);
|
||||
}
|
||||
let injected_headers = match h2_headers_frame(&request) {
|
||||
Ok(frame) => frame,
|
||||
Err(message) => {
|
||||
let (authority, host) = request_log_identity(&request);
|
||||
log_protocol_error_with_identity(
|
||||
upgrade_state.as_ref(),
|
||||
"h2c_upgrade_invalid",
|
||||
message,
|
||||
peer,
|
||||
local,
|
||||
"tcp",
|
||||
"1.1",
|
||||
400,
|
||||
authority,
|
||||
host,
|
||||
None,
|
||||
);
|
||||
return Ok::<_, Infallible>(
|
||||
Response::builder()
|
||||
.status(http::StatusCode::BAD_REQUEST)
|
||||
.header(http::header::CACHE_CONTROL, "no-store")
|
||||
.body(Body::from(message))
|
||||
.expect("static h2c validation response"),
|
||||
);
|
||||
}
|
||||
};
|
||||
let on_upgrade = hyper::upgrade::on(&mut request);
|
||||
let upgrade_router = router.clone();
|
||||
let upgrade_stop = request_stop.clone();
|
||||
let upgrade_lease = request_lease.clone();
|
||||
let upgrade_state_for_task = upgrade_state.clone();
|
||||
let task = async move {
|
||||
match on_upgrade.await {
|
||||
Ok(upgraded) => {
|
||||
if let Some(state) = &upgrade_state_for_task {
|
||||
state
|
||||
.metrics
|
||||
.http2_connections
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
let extended_connect =
|
||||
upgrade_state_for_task.as_ref().is_some_and(|state| {
|
||||
state.config.load().websocket_rfc8441_on(local, false)
|
||||
});
|
||||
let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers);
|
||||
http2_connection(
|
||||
io,
|
||||
upgrade_router,
|
||||
peer,
|
||||
local,
|
||||
Transport { tls: false },
|
||||
None,
|
||||
extended_connect,
|
||||
upgrade_state_for_task.clone(),
|
||||
upgrade_lease,
|
||||
upgrade_stop,
|
||||
grace,
|
||||
http2_settings,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
log_protocol_error(
|
||||
upgrade_state_for_task.as_ref(),
|
||||
"h2c_transport_error",
|
||||
&error,
|
||||
peer,
|
||||
local,
|
||||
"tcp",
|
||||
"2",
|
||||
0,
|
||||
None,
|
||||
);
|
||||
tracing::debug!(%error, "h2c upgrade transport failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(state) = upgrade_state {
|
||||
state.tunnels.spawn(task);
|
||||
} else {
|
||||
tokio::spawn(task);
|
||||
}
|
||||
return Ok::<_, Infallible>(
|
||||
Response::builder()
|
||||
.status(http::StatusCode::SWITCHING_PROTOCOLS)
|
||||
.header(http::header::CONNECTION, "Upgrade")
|
||||
.header(http::header::UPGRADE, "h2c")
|
||||
.body(Body::empty())
|
||||
.expect("static h2c switching response"),
|
||||
);
|
||||
}
|
||||
|
||||
router.oneshot(request.map(Body::new)).await
|
||||
}
|
||||
});
|
||||
let mut builder = http1::Builder::new();
|
||||
builder
|
||||
.timer(TokioTimer::new())
|
||||
.keep_alive(true)
|
||||
.max_headers(64)
|
||||
.max_buf_size(65_536)
|
||||
.header_read_timeout(Duration::from_secs(10));
|
||||
let conn = builder
|
||||
.serve_connection(TokioIo::new(stream), service)
|
||||
.with_upgrades();
|
||||
tokio::pin!(conn);
|
||||
tokio::select! {
|
||||
result = &mut conn => {
|
||||
if let Err(error) = result {
|
||||
log_protocol_error(
|
||||
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
|
||||
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, tls_sni.clone(),
|
||||
);
|
||||
tracing::debug!(%error, "HTTP/1 connection closed");
|
||||
}
|
||||
},
|
||||
_ = stop.cancelled() => {
|
||||
conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum H2ShutdownCause {
|
||||
Idle,
|
||||
|
||||
Reference in new issue
Block a user