feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,548 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use hyper::body::Incoming;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::sync::{Mutex, OwnedSemaphorePermit, mpsc};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use super::client_body::{RequestBody, download, hold_upload, upload};
|
||||
use super::client_driver::{self, Control, Driver, Entry, Http2Entry, Http2Lease, Http2Pool, Idle};
|
||||
use super::{Budget, Header, Limits, Request, Response, message};
|
||||
use crate::{async_stream::AsyncStream, bytes::Bytes, task, tls::ClientTls};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PoolLimits {
|
||||
pub idle_ms: i64,
|
||||
pub chunk_bytes: i64,
|
||||
pub read_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StreamRequest {
|
||||
pub method: String,
|
||||
pub target: String,
|
||||
pub headers: Vec<Header>,
|
||||
pub body: AsyncStream<Result<Bytes, String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StreamResponse {
|
||||
pub status: i64,
|
||||
pub headers: Vec<Header>,
|
||||
pub body: AsyncStream<Result<Bytes, String>>,
|
||||
}
|
||||
|
||||
struct State {
|
||||
authority: String,
|
||||
scheme: &'static str,
|
||||
host: String,
|
||||
port: i64,
|
||||
tls: Option<ClientTls>,
|
||||
budget: Budget,
|
||||
chunks: usize,
|
||||
read_timeout: Duration,
|
||||
control: Arc<Control>,
|
||||
idle: Arc<Idle>,
|
||||
http2: Option<Arc<Http2Pool>>,
|
||||
queue: mpsc::Sender<Driver>,
|
||||
driver: Mutex<Option<task::Task<()>>>,
|
||||
}
|
||||
|
||||
impl Drop for State {
|
||||
fn drop(&mut self) {
|
||||
self.control.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpClient(Arc<State>);
|
||||
|
||||
impl std::fmt::Debug for HttpClient {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("HttpClient(<resource>)")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn client(
|
||||
origin: &str,
|
||||
tls: ClientTls,
|
||||
limits: Limits,
|
||||
pool: PoolLimits,
|
||||
) -> Result<HttpClient, String> {
|
||||
let budget = limits.check()?;
|
||||
let idle_timeout = task::positive_duration(pool.idle_ms)?;
|
||||
let read_timeout = task::positive_duration(pool.read_ms)?;
|
||||
let chunks = usize::try_from(pool.chunk_bytes)
|
||||
.ok()
|
||||
.filter(|size| *size > 0)
|
||||
.ok_or("HTTP chunk_bytes must be positive")?;
|
||||
let uri: hyper::Uri = origin
|
||||
.parse()
|
||||
.map_err(|error| format!("invalid HTTP origin: {error}"))?;
|
||||
let scheme = match uri.scheme_str() {
|
||||
Some("http") => "http",
|
||||
Some("https") => "https",
|
||||
_ => return Err("HTTP origin must use http or https".into()),
|
||||
};
|
||||
let secure = scheme == "https";
|
||||
let authority = uri
|
||||
.authority()
|
||||
.ok_or("HTTP origin requires a host")?
|
||||
.as_str()
|
||||
.to_owned();
|
||||
if authority.contains('@')
|
||||
|| origin.contains('#')
|
||||
|| uri.query().is_some()
|
||||
|| !matches!(uri.path(), "" | "/")
|
||||
{
|
||||
return Err(
|
||||
"HTTP client origin cannot contain credentials, path, query or fragment".into(),
|
||||
);
|
||||
}
|
||||
let host = uri
|
||||
.host()
|
||||
.ok_or("HTTP origin requires a host")?
|
||||
.trim_matches(['[', ']'])
|
||||
.to_owned();
|
||||
let port = i64::from(uri.port_u16().unwrap_or(if secure { 443 } else { 80 }));
|
||||
let connections = limits.connections as usize;
|
||||
let request_capacity = budget
|
||||
.http2
|
||||
.map(|http2| connections * http2.streams as usize)
|
||||
.unwrap_or(connections);
|
||||
let control = Control::new(request_capacity, connections);
|
||||
let idle = Idle::new(idle_timeout);
|
||||
let http2 = budget
|
||||
.http2
|
||||
.map(|limits| Http2Pool::new(limits.streams as usize, idle_timeout));
|
||||
let (queue, driver) =
|
||||
client_driver::start(control.clone(), idle.clone(), http2.clone(), connections).await?;
|
||||
Ok(HttpClient(Arc::new(State {
|
||||
authority,
|
||||
scheme,
|
||||
host,
|
||||
port,
|
||||
tls: secure.then(|| super::protocol::client_tls(tls, budget.http2)),
|
||||
budget,
|
||||
chunks,
|
||||
read_timeout,
|
||||
control,
|
||||
idle,
|
||||
http2,
|
||||
queue,
|
||||
driver: Mutex::new(Some(driver)),
|
||||
})))
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
pub(super) fn control(&self) -> &Arc<Control> {
|
||||
&self.0.control
|
||||
}
|
||||
|
||||
pub(super) fn chunk_limit(&self) -> usize {
|
||||
self.0.chunks
|
||||
}
|
||||
|
||||
pub(super) fn read_timeout(&self) -> Duration {
|
||||
self.0.read_timeout
|
||||
}
|
||||
|
||||
fn encode(
|
||||
&self,
|
||||
mut request: Request,
|
||||
) -> Result<hyper::Request<http_body_util::Full<bytes::Bytes>>, String> {
|
||||
if !request
|
||||
.headers
|
||||
.iter()
|
||||
.any(|header| header.name.eq_ignore_ascii_case("host"))
|
||||
{
|
||||
request.headers.push(Header {
|
||||
name: "host".into(),
|
||||
value: Bytes::from_string(self.0.authority.clone()),
|
||||
});
|
||||
}
|
||||
let mut request = message::encode_request(request, self.0.budget)?;
|
||||
if self.0.http2.is_some() {
|
||||
message::http2_request(&mut request, self.0.scheme)?;
|
||||
}
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
async fn http1_connection(&self) -> Result<Entry, String> {
|
||||
while let Some(mut entry) = self.0.idle.take() {
|
||||
if entry.sender.ready().await.is_ok() {
|
||||
return Ok(entry);
|
||||
}
|
||||
// Discard a closed idle transport before sending any request bytes.
|
||||
}
|
||||
let permit = self
|
||||
.0
|
||||
.control
|
||||
.connections
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|_| "HTTP client is closed".to_owned())?;
|
||||
let transport =
|
||||
crate::transport::connect(&self.0.host, self.0.port, self.0.tls.as_ref()).await?;
|
||||
super::protocol::check_alpn(&transport, self.0.budget.http2)?;
|
||||
let (sender, connection) = hyper::client::conn::http1::Builder::new()
|
||||
.max_buf_size(self.0.budget.headers)
|
||||
.handshake(TokioIo::new(transport))
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let (abort, driver) = client_driver::driver(connection, permit);
|
||||
// The connection permit bounds queued plus running drivers together.
|
||||
self.0
|
||||
.queue
|
||||
.try_send(driver)
|
||||
.map_err(|_| "HTTP client driver is closed".to_owned())?;
|
||||
Ok(Entry { sender, abort })
|
||||
}
|
||||
|
||||
async fn new_http2_connection(
|
||||
&self,
|
||||
permit: OwnedSemaphorePermit,
|
||||
) -> Result<Arc<Http2Entry>, String> {
|
||||
let limits = self.0.budget.http2.expect("HTTP/2 client limits");
|
||||
let pool = self.0.http2.as_ref().expect("HTTP/2 client pool");
|
||||
let transport =
|
||||
crate::transport::connect(&self.0.host, self.0.port, self.0.tls.as_ref()).await?;
|
||||
super::protocol::check_alpn(&transport, self.0.budget.http2)?;
|
||||
let (driver, handshake) = client_driver::http2_handshake(
|
||||
transport,
|
||||
permit,
|
||||
limits,
|
||||
self.0.budget.headers,
|
||||
self.0.budget.timeout,
|
||||
Arc::clone(pool),
|
||||
);
|
||||
self.0
|
||||
.queue
|
||||
.try_send(driver)
|
||||
.map_err(|_| "HTTP client driver is closed".to_owned())?;
|
||||
handshake
|
||||
.await
|
||||
.map_err(|_| "HTTP client driver closed during HTTP/2 handshake".to_owned())?
|
||||
}
|
||||
|
||||
async fn http2_connection(&self) -> Result<(Arc<Http2Entry>, OwnedSemaphorePermit), String> {
|
||||
let pool = self.0.http2.as_ref().expect("HTTP/2 client pool");
|
||||
loop {
|
||||
let changed = pool.changed();
|
||||
tokio::pin!(changed);
|
||||
changed.as_mut().enable();
|
||||
if let Some(connection) = pool.take() {
|
||||
return Ok(connection);
|
||||
}
|
||||
let connection = self.0.control.connections.clone().acquire_owned();
|
||||
tokio::pin!(connection);
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = changed => continue,
|
||||
permit = &mut connection => {
|
||||
let permit = permit.map_err(|_| "HTTP client is closed".to_owned())?;
|
||||
let entry = self.new_http2_connection(permit).await?;
|
||||
if let Some(stream) = entry.acquire() {
|
||||
return Ok((entry, stream));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn exchange(
|
||||
&self,
|
||||
request: hyper::Request<RequestBody>,
|
||||
) -> Result<StreamResponse, String> {
|
||||
if self.0.http2.is_some() {
|
||||
self.exchange_http2(request).await
|
||||
} else {
|
||||
self.exchange_http1(request).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn exchange_http1(
|
||||
&self,
|
||||
request: hyper::Request<RequestBody>,
|
||||
) -> Result<StreamResponse, String> {
|
||||
self.0
|
||||
.control
|
||||
.while_open(async {
|
||||
tokio::time::timeout(self.0.budget.timeout, async {
|
||||
let permit = self
|
||||
.0
|
||||
.control
|
||||
.requests
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|_| "HTTP client is closed".to_owned())?;
|
||||
let mut entry = self.http1_connection().await?;
|
||||
let response = entry
|
||||
.sender
|
||||
.send_request(request)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let lease = Lease::Http1 {
|
||||
entry: Some(entry),
|
||||
client: self.clone(),
|
||||
_permit: permit,
|
||||
};
|
||||
self.response(response, lease)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "HTTP request timed out".to_owned())?
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn exchange_http2(
|
||||
&self,
|
||||
request: hyper::Request<RequestBody>,
|
||||
) -> Result<StreamResponse, String> {
|
||||
self.0
|
||||
.control
|
||||
.while_open(async {
|
||||
tokio::time::timeout(self.0.budget.timeout, async {
|
||||
let (head, body) = request.into_parts();
|
||||
let (body, upload_lease) = hold_upload(body);
|
||||
let mut pending = Some(hyper::Request::from_parts(head, body));
|
||||
loop {
|
||||
let request_permit = self
|
||||
.0
|
||||
.control
|
||||
.requests
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|_| "HTTP client is closed".to_owned())?;
|
||||
let (entry, stream_permit) = self.http2_connection().await?;
|
||||
let stream = Http2Lease::new(entry.clone(), stream_permit, request_permit);
|
||||
let mut sender = entry.sender.clone();
|
||||
if sender.ready().await.is_err() {
|
||||
entry.drain();
|
||||
continue;
|
||||
}
|
||||
upload_lease.replace(stream.clone());
|
||||
let request = pending.take().expect("pending HTTP/2 request");
|
||||
let response = match sender.try_send_request(request).await {
|
||||
Ok(response) => response,
|
||||
Err(mut error) => {
|
||||
if sender.is_closed() {
|
||||
entry.drain();
|
||||
}
|
||||
if let Some(request) = error.take_message() {
|
||||
upload_lease.clear();
|
||||
drop(stream);
|
||||
pending = Some(request);
|
||||
continue;
|
||||
}
|
||||
return Err(error.into_error().to_string());
|
||||
}
|
||||
};
|
||||
let lease = Lease::Http2 {
|
||||
client: self.clone(),
|
||||
_stream: stream,
|
||||
};
|
||||
return self.response(response, lease);
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "HTTP request timed out".to_owned())?
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn response(
|
||||
&self,
|
||||
response: hyper::Response<Incoming>,
|
||||
lease: Lease,
|
||||
) -> Result<StreamResponse, String> {
|
||||
if response.status() == hyper::StatusCode::SWITCHING_PROTOCOLS {
|
||||
return Err("HTTP client upgrades require the WebSocket API".into());
|
||||
}
|
||||
if message::header_size(response.headers()) + 32 > self.0.budget.headers {
|
||||
return Err("HTTP response headers exceed header_bytes".into());
|
||||
}
|
||||
let (head, body) = response.into_parts();
|
||||
Ok(StreamResponse {
|
||||
status: i64::from(head.status.as_u16()),
|
||||
headers: message::decode_headers(head.headers),
|
||||
body: download(body, lease),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum Lease {
|
||||
Http1 {
|
||||
entry: Option<Entry>,
|
||||
client: HttpClient,
|
||||
_permit: OwnedSemaphorePermit,
|
||||
},
|
||||
Http2 {
|
||||
client: HttpClient,
|
||||
_stream: Http2Lease,
|
||||
},
|
||||
}
|
||||
|
||||
impl Lease {
|
||||
pub fn client(&self) -> &HttpClient {
|
||||
match self {
|
||||
Self::Http1 { client, .. } | Self::Http2 { client, .. } => client,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn complete(&mut self) {
|
||||
let Self::Http1 { entry, client, .. } = self else {
|
||||
return;
|
||||
};
|
||||
let mut entry = entry.take().expect("active HTTP lease");
|
||||
if !client.control().closed.is_closed() && entry.sender.ready().await.is_ok() {
|
||||
client.0.idle.put(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn send_http2(
|
||||
address: &str,
|
||||
port: i64,
|
||||
mut request: hyper::Request<http_body_util::Full<bytes::Bytes>>,
|
||||
budget: Budget,
|
||||
) -> Result<Response, String> {
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
message::http2_request(&mut request, "http")?;
|
||||
let deadline = Instant::now() + budget.timeout;
|
||||
let timeout_ms = i64::try_from(budget.timeout.as_millis())
|
||||
.map_err(|_| "HTTP timeout is too large".to_owned())?;
|
||||
let transport = tokio::time::timeout_at(
|
||||
deadline,
|
||||
crate::net::connect_stream_timeout(address, port, timeout_ms),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "HTTP request timed out".to_owned())??;
|
||||
let (mut owner, spawner, fault) = task::scoped_owner(())?;
|
||||
let limits = budget.http2.expect("HTTP/2 send limits");
|
||||
let builder = limits.client_builder(super::executor::Http2Executor(spawner), budget.headers);
|
||||
let handshake = {
|
||||
let handshake = tokio::time::timeout_at(
|
||||
deadline,
|
||||
owner.enter(builder.handshake(TokioIo::new(transport))),
|
||||
);
|
||||
tokio::pin!(handshake);
|
||||
tokio::select! {
|
||||
result = &mut handshake => match result {
|
||||
Ok(result) => result.map_err(|error| error.to_string()),
|
||||
Err(_) => Err("HTTP request timed out".to_owned()),
|
||||
},
|
||||
error = fault.wait() => Err(error),
|
||||
}
|
||||
};
|
||||
let (mut sender, connection) = match handshake {
|
||||
Ok(connection) => connection,
|
||||
Err(error) => {
|
||||
let _ = owner.stop().await;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let request = request.map(|body| body.map_err(|never| match never {}).boxed_unsync());
|
||||
let exchange = async {
|
||||
let response = sender
|
||||
.send_request(request)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
if response.status() == hyper::StatusCode::SWITCHING_PROTOCOLS {
|
||||
return Err("HTTP upgrades are not supported by send".into());
|
||||
}
|
||||
if message::header_size(response.headers()) + 32 > budget.headers {
|
||||
return Err("HTTP response headers exceed header_bytes".into());
|
||||
}
|
||||
let (head, body) = response.into_parts();
|
||||
let body = message::collect_body(body, budget.body)
|
||||
.await
|
||||
.map_err(|status| format!("HTTP response body rejected: {status}"))?;
|
||||
Ok(Response {
|
||||
status: i64::from(head.status.as_u16()),
|
||||
headers: message::decode_headers(head.headers),
|
||||
body,
|
||||
})
|
||||
};
|
||||
let result = {
|
||||
let connection = owner.enter(connection);
|
||||
tokio::pin!(connection, exchange);
|
||||
match tokio::time::timeout_at(deadline, async {
|
||||
tokio::select! {
|
||||
result = &mut exchange => result,
|
||||
result = &mut connection => {
|
||||
result.map_err(|error| error.to_string())?;
|
||||
exchange.await
|
||||
}
|
||||
error = fault.wait() => Err(error),
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err("HTTP request timed out".to_owned()),
|
||||
}
|
||||
};
|
||||
let cleanup = owner.stop().await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
cleanup?;
|
||||
Ok(response)
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn open(client: &HttpClient, request: Request) -> Result<StreamResponse, String> {
|
||||
use http_body_util::BodyExt;
|
||||
let request = client
|
||||
.encode(request)?
|
||||
.map(|body| body.map_err(|never| match never {}).boxed_unsync());
|
||||
client.exchange(request).await
|
||||
}
|
||||
|
||||
pub async fn open_stream(
|
||||
client: &HttpClient,
|
||||
request: StreamRequest,
|
||||
) -> Result<StreamResponse, String> {
|
||||
let head = client.encode(Request {
|
||||
method: request.method,
|
||||
target: request.target,
|
||||
headers: request.headers,
|
||||
body: Bytes::new(Vec::new()),
|
||||
})?;
|
||||
let request = head.map(|_| upload(request.body, client.chunk_limit(), client.read_timeout()));
|
||||
client.exchange(request).await
|
||||
}
|
||||
|
||||
pub async fn request(client: &HttpClient, request: Request) -> Result<Response, String> {
|
||||
tokio::time::timeout(client.0.budget.timeout, async {
|
||||
let response = open(client, request).await?;
|
||||
let _owner = response.body.close_on_drop();
|
||||
let mut body = Bytes::new(Vec::new());
|
||||
while let Some(chunk) = response.body.pull().await {
|
||||
let chunk = chunk?;
|
||||
if body.values().len() + chunk.values().len() > client.0.budget.body {
|
||||
return Err("HTTP response body exceeds body_bytes".into());
|
||||
}
|
||||
body = body.concat(&chunk);
|
||||
}
|
||||
Ok(Response {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
body,
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "HTTP request timed out".to_owned())?
|
||||
}
|
||||
|
||||
pub async fn close_client(client: &HttpClient) -> Result<(), String> {
|
||||
client.0.control.close();
|
||||
task::stop_resource(&mut *client.0.driver.lock().await).await
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_util::stream;
|
||||
use http_body_util::{BodyExt, StreamBody, combinators::UnsyncBoxBody};
|
||||
use hyper::body::{Body, Frame, Incoming};
|
||||
|
||||
use super::client::Lease;
|
||||
use super::client_driver::Http2Lease;
|
||||
use crate::{async_stream::AsyncStream, bytes::Bytes, resource::Pull};
|
||||
|
||||
pub(super) type RequestBody = UnsyncBoxBody<bytes::Bytes, io::Error>;
|
||||
|
||||
struct LeasedRequestBody {
|
||||
body: RequestBody,
|
||||
_lease: UploadLease,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct UploadLease(Arc<Mutex<Option<Http2Lease>>>);
|
||||
|
||||
impl UploadLease {
|
||||
pub(super) fn replace(&self, lease: Http2Lease) {
|
||||
*self.0.lock().expect("HTTP/2 upload lease lock") = Some(lease);
|
||||
}
|
||||
|
||||
pub(super) fn clear(&self) {
|
||||
self.0.lock().expect("HTTP/2 upload lease lock").take();
|
||||
}
|
||||
}
|
||||
|
||||
impl Body for LeasedRequestBody {
|
||||
type Data = bytes::Bytes;
|
||||
type Error = io::Error;
|
||||
|
||||
fn poll_frame(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
Pin::new(&mut self.body).poll_frame(context)
|
||||
}
|
||||
|
||||
fn is_end_stream(&self) -> bool {
|
||||
self.body.is_end_stream()
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> hyper::body::SizeHint {
|
||||
self.body.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn hold_upload(body: RequestBody) -> (RequestBody, UploadLease) {
|
||||
let lease = UploadLease(Arc::new(Mutex::new(None)));
|
||||
let body = LeasedRequestBody {
|
||||
body,
|
||||
_lease: lease.clone(),
|
||||
}
|
||||
.boxed_unsync();
|
||||
(body, lease)
|
||||
}
|
||||
|
||||
pub(super) fn upload(
|
||||
source: AsyncStream<Result<Bytes, String>>,
|
||||
maximum: usize,
|
||||
timeout: Duration,
|
||||
) -> RequestBody {
|
||||
let owner = source.close_on_drop();
|
||||
StreamBody::new(stream::unfold(
|
||||
(source, owner),
|
||||
move |(source, owner)| async move {
|
||||
let chunk = tokio::time::timeout(timeout, source.pull()).await;
|
||||
let result = match chunk {
|
||||
Ok(Some(Ok(bytes))) if bytes.values().len() <= maximum => {
|
||||
Ok(Frame::data(bytes.into_http()))
|
||||
}
|
||||
Ok(Some(Ok(_))) => Err(io::Error::other("HTTP upload chunk exceeds chunk_bytes")),
|
||||
Ok(Some(Err(error))) => Err(io::Error::other(error)),
|
||||
Ok(None) => return None,
|
||||
Err(_) => Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"HTTP upload timed out",
|
||||
)),
|
||||
};
|
||||
if result.is_err() {
|
||||
source.close();
|
||||
}
|
||||
Some((result, (source, owner)))
|
||||
},
|
||||
))
|
||||
.boxed_unsync()
|
||||
}
|
||||
|
||||
struct Download {
|
||||
body: Incoming,
|
||||
pending: bytes::Bytes,
|
||||
lease: Lease,
|
||||
}
|
||||
|
||||
pub(super) fn download(body: Incoming, lease: Lease) -> AsyncStream<Result<Bytes, String>> {
|
||||
AsyncStream::new(stream::unfold(
|
||||
Download {
|
||||
body,
|
||||
pending: bytes::Bytes::new(),
|
||||
lease,
|
||||
},
|
||||
|mut state| async move {
|
||||
let client = state.lease.client().clone();
|
||||
let next = client
|
||||
.control()
|
||||
.while_open(async {
|
||||
tokio::time::timeout(client.read_timeout(), state.next())
|
||||
.await
|
||||
.map_err(|_| "HTTP response read timed out".to_owned())?
|
||||
})
|
||||
.await;
|
||||
match next {
|
||||
Ok(Some((chunk, last))) => {
|
||||
let item = if last {
|
||||
Pull::Last(Ok(chunk))
|
||||
} else {
|
||||
Pull::Item(Ok(chunk))
|
||||
};
|
||||
Some((item, state))
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(error) => Some((Pull::Last(Err(error)), state)),
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
impl Download {
|
||||
async fn next(&mut self) -> Result<Option<(Bytes, bool)>, String> {
|
||||
while self.pending.is_empty() {
|
||||
if self.body.is_end_stream() {
|
||||
self.lease.complete().await;
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(frame) = self.body.frame().await else {
|
||||
self.lease.complete().await;
|
||||
return Ok(None);
|
||||
};
|
||||
let frame = frame.map_err(|error| error.to_string())?;
|
||||
match frame.into_data() {
|
||||
Ok(bytes) => self.pending = bytes,
|
||||
Err(frame)
|
||||
if frame
|
||||
.trailers_ref()
|
||||
.is_some_and(|trailers| !trailers.is_empty()) =>
|
||||
{
|
||||
return Err("HTTP response trailers are not supported".into());
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
let length = self.pending.len().min(self.lease.client().chunk_limit());
|
||||
let chunk = Bytes::from_http(self.pending.split_to(length));
|
||||
let last = self.pending.is_empty() && self.body.is_end_stream();
|
||||
if last {
|
||||
self.lease.complete().await;
|
||||
}
|
||||
Ok(Some((chunk, last)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_util::future::{AbortHandle, Abortable};
|
||||
use futures_util::{StreamExt, stream::FuturesUnordered};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use super::client_body::RequestBody;
|
||||
use super::executor::Http2Executor;
|
||||
use super::protocol::Http2Limits;
|
||||
use crate::transport::Transport;
|
||||
|
||||
pub(super) type Driver = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
|
||||
pub(super) struct Entry {
|
||||
pub sender: hyper::client::conn::http1::SendRequest<RequestBody>,
|
||||
pub abort: AbortHandle,
|
||||
}
|
||||
|
||||
impl Drop for Entry {
|
||||
fn drop(&mut self) {
|
||||
self.abort.abort();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Idle {
|
||||
entries: Mutex<Vec<(Instant, Entry)>>,
|
||||
changed: Notify,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl Idle {
|
||||
pub fn new(timeout: Duration) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
entries: Mutex::new(Vec::new()),
|
||||
changed: Notify::new(),
|
||||
timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn take(&self) -> Option<Entry> {
|
||||
let mut entries = self.entries.lock().expect("pool state lock");
|
||||
entries.retain(|(deadline, entry)| *deadline > Instant::now() && !entry.sender.is_closed());
|
||||
entries.pop().map(|(_, entry)| entry)
|
||||
}
|
||||
|
||||
pub fn put(&self, entry: Entry) {
|
||||
self.entries
|
||||
.lock()
|
||||
.expect("pool state lock")
|
||||
.push((Instant::now() + self.timeout, entry));
|
||||
self.changed.notify_one();
|
||||
}
|
||||
|
||||
fn deadline(&self) -> Option<Instant> {
|
||||
let mut entries = self.entries.lock().expect("pool state lock");
|
||||
entries.retain(|(deadline, entry)| *deadline > Instant::now() && !entry.sender.is_closed());
|
||||
entries.iter().map(|(deadline, _)| *deadline).min()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Control {
|
||||
pub closed: Semaphore,
|
||||
pub requests: Arc<Semaphore>,
|
||||
pub connections: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl Control {
|
||||
pub fn new(requests: usize, connections: usize) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
closed: Semaphore::new(0),
|
||||
requests: Arc::new(Semaphore::new(requests)),
|
||||
connections: Arc::new(Semaphore::new(connections)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
self.closed.close();
|
||||
self.requests.close();
|
||||
self.connections.close();
|
||||
}
|
||||
|
||||
pub async fn while_open<T>(
|
||||
&self,
|
||||
future: impl Future<Output = Result<T, String>>,
|
||||
) -> Result<T, String> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.closed.acquire() => Err("HTTP client is closed".into()),
|
||||
result = future => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Http2Entry {
|
||||
pub sender: hyper::client::conn::http2::SendRequest<RequestBody>,
|
||||
streams: Arc<Semaphore>,
|
||||
accepting: AtomicBool,
|
||||
active: AtomicUsize,
|
||||
idle_deadline: Mutex<Option<Instant>>,
|
||||
abort: AbortHandle,
|
||||
changed: Arc<Notify>,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Http2Entry {
|
||||
fn new(
|
||||
sender: hyper::client::conn::http2::SendRequest<RequestBody>,
|
||||
streams: usize,
|
||||
abort: AbortHandle,
|
||||
changed: Arc<Notify>,
|
||||
idle_timeout: Duration,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
sender,
|
||||
streams: Arc::new(Semaphore::new(streams)),
|
||||
accepting: AtomicBool::new(true),
|
||||
active: AtomicUsize::new(0),
|
||||
idle_deadline: Mutex::new(Some(Instant::now() + idle_timeout)),
|
||||
abort,
|
||||
changed,
|
||||
idle_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn acquire(self: &Arc<Self>) -> Option<OwnedSemaphorePermit> {
|
||||
if !self.accepting.load(Ordering::Acquire) || self.sender.is_closed() {
|
||||
self.drain();
|
||||
return None;
|
||||
}
|
||||
let permit = self.streams.clone().try_acquire_owned().ok()?;
|
||||
if !self.accepting.load(Ordering::Acquire) || self.sender.is_closed() {
|
||||
self.drain();
|
||||
return None;
|
||||
}
|
||||
self.active.fetch_add(1, Ordering::AcqRel);
|
||||
*self.idle_deadline.lock().expect("HTTP/2 pool state lock") = None;
|
||||
Some(permit)
|
||||
}
|
||||
|
||||
fn release(&self) {
|
||||
let previous = self.active.fetch_sub(1, Ordering::AcqRel);
|
||||
debug_assert!(previous > 0, "active HTTP/2 stream count");
|
||||
if previous == 1 && self.accepting.load(Ordering::Acquire) {
|
||||
*self.idle_deadline.lock().expect("HTTP/2 pool state lock") =
|
||||
Some(Instant::now() + self.idle_timeout);
|
||||
}
|
||||
self.changed.notify_waiters();
|
||||
}
|
||||
|
||||
pub fn drain(&self) {
|
||||
if self.accepting.swap(false, Ordering::AcqRel) {
|
||||
self.streams.close();
|
||||
*self.idle_deadline.lock().expect("HTTP/2 pool state lock") = None;
|
||||
self.changed.notify_waiters();
|
||||
}
|
||||
}
|
||||
|
||||
fn retire(&self) {
|
||||
self.drain();
|
||||
self.abort.abort();
|
||||
}
|
||||
|
||||
fn deadline(&self) -> Option<Instant> {
|
||||
*self.idle_deadline.lock().expect("HTTP/2 pool state lock")
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Http2Pool {
|
||||
entries: Mutex<Vec<Arc<Http2Entry>>>,
|
||||
changed: Arc<Notify>,
|
||||
streams: usize,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Http2Pool {
|
||||
pub fn new(streams: usize, idle_timeout: Duration) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
entries: Mutex::new(Vec::new()),
|
||||
changed: Arc::new(Notify::new()),
|
||||
streams,
|
||||
idle_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn take(&self) -> Option<(Arc<Http2Entry>, OwnedSemaphorePermit)> {
|
||||
let mut entries = self.entries.lock().expect("HTTP/2 pool state lock");
|
||||
entries.retain(|entry| entry.accepting.load(Ordering::Acquire));
|
||||
entries
|
||||
.iter()
|
||||
.find_map(|entry| entry.acquire().map(|permit| (Arc::clone(entry), permit)))
|
||||
}
|
||||
|
||||
pub fn add(
|
||||
&self,
|
||||
sender: hyper::client::conn::http2::SendRequest<RequestBody>,
|
||||
abort: AbortHandle,
|
||||
) -> Arc<Http2Entry> {
|
||||
let entry = Http2Entry::new(
|
||||
sender,
|
||||
self.streams,
|
||||
abort,
|
||||
Arc::clone(&self.changed),
|
||||
self.idle_timeout,
|
||||
);
|
||||
self.entries
|
||||
.lock()
|
||||
.expect("HTTP/2 pool state lock")
|
||||
.push(Arc::clone(&entry));
|
||||
self.changed.notify_waiters();
|
||||
entry
|
||||
}
|
||||
|
||||
pub fn changed(&self) -> tokio::sync::futures::OwnedNotified {
|
||||
self.changed.clone().notified_owned()
|
||||
}
|
||||
|
||||
fn expire_and_deadline(&self) -> Option<Instant> {
|
||||
let now = Instant::now();
|
||||
let mut entries = self.entries.lock().expect("HTTP/2 pool state lock");
|
||||
entries.retain(|entry| {
|
||||
if !entry.accepting.load(Ordering::Acquire) || entry.sender.is_closed() {
|
||||
entry.drain();
|
||||
return false;
|
||||
}
|
||||
if entry.active.load(Ordering::Acquire) == 0
|
||||
&& entry.deadline().is_some_and(|deadline| deadline <= now)
|
||||
{
|
||||
entry.retire();
|
||||
return false;
|
||||
}
|
||||
true
|
||||
});
|
||||
entries.iter().filter_map(|entry| entry.deadline()).min()
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
let entries = std::mem::take(&mut *self.entries.lock().expect("HTTP/2 pool state lock"));
|
||||
for entry in entries {
|
||||
entry.retire();
|
||||
}
|
||||
self.changed.notify_waiters();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Http2Lease {
|
||||
_owner: Arc<Http2LeaseOwner>,
|
||||
}
|
||||
|
||||
struct Http2LeaseOwner {
|
||||
entry: Arc<Http2Entry>,
|
||||
stream: Option<OwnedSemaphorePermit>,
|
||||
request: Option<OwnedSemaphorePermit>,
|
||||
}
|
||||
|
||||
impl Http2Lease {
|
||||
pub fn new(
|
||||
entry: Arc<Http2Entry>,
|
||||
stream: OwnedSemaphorePermit,
|
||||
request: OwnedSemaphorePermit,
|
||||
) -> Self {
|
||||
Self {
|
||||
_owner: Arc::new(Http2LeaseOwner {
|
||||
entry,
|
||||
stream: Some(stream),
|
||||
request: Some(request),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Http2LeaseOwner {
|
||||
fn drop(&mut self) {
|
||||
self.stream.take();
|
||||
self.request.take();
|
||||
self.entry.release();
|
||||
}
|
||||
}
|
||||
|
||||
struct DriverOwner {
|
||||
control: Arc<Control>,
|
||||
idle: Arc<Idle>,
|
||||
http2: Option<Arc<Http2Pool>>,
|
||||
}
|
||||
|
||||
impl Drop for DriverOwner {
|
||||
fn drop(&mut self) {
|
||||
self.control.close();
|
||||
self.idle.entries.lock().expect("pool state lock").clear();
|
||||
if let Some(http2) = &self.http2 {
|
||||
http2.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn start(
|
||||
control: Arc<Control>,
|
||||
idle: Arc<Idle>,
|
||||
http2: Option<Arc<Http2Pool>>,
|
||||
capacity: usize,
|
||||
) -> Result<(mpsc::Sender<Driver>, crate::task::Task<()>), String> {
|
||||
let (queue, mut receiver) = mpsc::channel::<Driver>(capacity);
|
||||
// The parent may finish before this future is first polled.
|
||||
let owner = DriverOwner {
|
||||
control: control.clone(),
|
||||
idle: idle.clone(),
|
||||
http2: http2.clone(),
|
||||
};
|
||||
let task = crate::task::run(async move {
|
||||
let _owner = owner;
|
||||
let mut drivers = FuturesUnordered::<Driver>::new();
|
||||
loop {
|
||||
let deadline = [
|
||||
idle.deadline(),
|
||||
http2.as_ref().and_then(|pool| pool.expire_and_deadline()),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.min();
|
||||
let expire = async {
|
||||
match deadline {
|
||||
Some(deadline) => tokio::time::sleep_until(deadline).await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
};
|
||||
let http2_changed = async {
|
||||
match &http2 {
|
||||
Some(pool) => pool.changed().await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
};
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = control.closed.acquire() => return Ok(()),
|
||||
Some(()) = drivers.next(), if !drivers.is_empty() => {},
|
||||
_ = idle.changed.notified() => {},
|
||||
_ = http2_changed => {},
|
||||
_ = expire => {},
|
||||
next = receiver.recv() => match next {
|
||||
Some(driver) => drivers.push(driver),
|
||||
None => return Ok(()),
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok((queue, task))
|
||||
}
|
||||
|
||||
pub(super) fn driver(
|
||||
connection: impl Future<Output = Result<(), hyper::Error>> + Send + 'static,
|
||||
permit: tokio::sync::OwnedSemaphorePermit,
|
||||
) -> (AbortHandle, Driver) {
|
||||
let (abort, registration) = AbortHandle::new_pair();
|
||||
let driver = Box::pin(async move {
|
||||
let _permit = permit;
|
||||
// Protocol/I/O failures are observed through SendRequest/Incoming.
|
||||
let _completion = Abortable::new(connection, registration).await;
|
||||
});
|
||||
(abort, driver)
|
||||
}
|
||||
|
||||
async fn drive_http2<C>(
|
||||
connection: C,
|
||||
mut owner: crate::task::ScopedOwner,
|
||||
fault: crate::task::ScopeFault,
|
||||
entry: Arc<Http2Entry>,
|
||||
registration: futures_util::future::AbortRegistration,
|
||||
) where
|
||||
C: Future<Output = Result<(), hyper::Error>> + Send + 'static,
|
||||
{
|
||||
let completion = {
|
||||
let connection = Abortable::new(owner.enter(connection), registration);
|
||||
tokio::pin!(connection);
|
||||
tokio::select! {
|
||||
result = &mut connection => Some(result),
|
||||
_ = fault.wait() => None,
|
||||
}
|
||||
};
|
||||
entry.drain();
|
||||
match completion {
|
||||
Some(Ok(Ok(()))) => {
|
||||
let _ = owner.finish().await;
|
||||
}
|
||||
Some(Ok(Err(_))) | Some(Err(_)) | None => {
|
||||
let _ = owner.stop().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn http2_handshake(
|
||||
transport: Transport,
|
||||
permit: OwnedSemaphorePermit,
|
||||
limits: Http2Limits,
|
||||
headers: usize,
|
||||
timeout: Duration,
|
||||
pool: Arc<Http2Pool>,
|
||||
) -> (Driver, oneshot::Receiver<Result<Arc<Http2Entry>, String>>) {
|
||||
let (result, receiver) = oneshot::channel();
|
||||
let driver = Box::pin(async move {
|
||||
let (mut owner, spawner, fault) = match crate::task::scoped_owner(permit) {
|
||||
Ok(owner) => owner,
|
||||
Err(error) => {
|
||||
let _ = result.send(Err(error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let handshake = {
|
||||
let builder = limits.client_builder(Http2Executor(spawner), headers);
|
||||
let handshake = tokio::time::timeout(
|
||||
timeout,
|
||||
owner.enter(builder.handshake(TokioIo::new(transport))),
|
||||
);
|
||||
tokio::pin!(handshake);
|
||||
tokio::select! {
|
||||
result = &mut handshake => match result {
|
||||
Ok(result) => result.map_err(|error| error.to_string()),
|
||||
Err(_) => Err("HTTP/2 handshake timed out".to_owned()),
|
||||
},
|
||||
error = fault.wait() => Err(error),
|
||||
}
|
||||
};
|
||||
let (sender, connection) = match handshake {
|
||||
Ok(connection) => connection,
|
||||
Err(error) => {
|
||||
let _ = result.send(Err(error));
|
||||
let _ = owner.stop().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (abort, registration) = AbortHandle::new_pair();
|
||||
let entry = pool.add(sender, abort);
|
||||
let _ = result.send(Ok(Arc::clone(&entry)));
|
||||
drive_http2(connection, owner, fault, entry, registration).await;
|
||||
});
|
||||
(driver, receiver)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use std::future::Future;
|
||||
|
||||
use hyper::rt::Executor;
|
||||
|
||||
use crate::task::ScopedSpawner;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Http2Executor(pub(super) ScopedSpawner);
|
||||
|
||||
impl<F> Executor<F> for Http2Executor
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
fn execute(&self, future: F) {
|
||||
self.0.spawn(future);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
use std::future::Future;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use hyper::service::service_fn;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::reply::{HttpReply, LiveLimits, Outgoing, Session};
|
||||
use super::write_timeout::WriteTimeout;
|
||||
use super::{Budget, Limits, Request, RequestContext, read_request, reject};
|
||||
use crate::{net, task};
|
||||
|
||||
/// HTTP/1 每条连接最多一个活跃 handler;升级后仍占用同一个连接和任务名额。
|
||||
pub async fn serve_live<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
limits: Limits,
|
||||
live: LiveLimits,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(Request, HttpReply) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<(), String>> + Send + 'static,
|
||||
{
|
||||
serve_transport(route, listener, limits, live, None).await
|
||||
}
|
||||
|
||||
pub async fn serve_live_tls<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
limits: Limits,
|
||||
live: LiveLimits,
|
||||
tls: crate::tls::ServerTls,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(Request, HttpReply) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<(), String>> + Send + 'static,
|
||||
{
|
||||
serve_transport(route, listener, limits, live, Some(tls)).await
|
||||
}
|
||||
|
||||
async fn serve_transport<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
limits: Limits,
|
||||
live: LiveLimits,
|
||||
tls: Option<crate::tls::ServerTls>,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(Request, HttpReply) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<(), String>> + Send + 'static,
|
||||
{
|
||||
let budget = limits.check()?;
|
||||
live.check()?;
|
||||
let tls = tls.map(|tls| super::protocol::server_tls(tls, budget.http2));
|
||||
task::parallel_each_stream(
|
||||
net::incoming(listener.clone(), true),
|
||||
limits.connections,
|
||||
move |connection| {
|
||||
let route = route.clone();
|
||||
let tls = tls.clone();
|
||||
let listener = listener.clone();
|
||||
async move {
|
||||
let stream = connection?;
|
||||
let stream = match crate::tls::accept(stream, tls.as_ref(), limits.timeout_ms).await
|
||||
{
|
||||
Ok(stream) => stream,
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
if super::protocol::check_alpn(&stream, budget.http2).is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(http2) = budget.http2 {
|
||||
return serve_http2(route, listener, stream, budget, live, http2).await;
|
||||
}
|
||||
let handlers = Arc::new(Mutex::new(Some(task::Group::new(1)?)));
|
||||
let upgraded = Arc::new(AtomicBool::new(false));
|
||||
let service = {
|
||||
let handlers = handlers.clone();
|
||||
let upgraded = upgraded.clone();
|
||||
service_fn(move |request| {
|
||||
dispatch(
|
||||
request,
|
||||
route.clone(),
|
||||
budget,
|
||||
live,
|
||||
handlers.clone(),
|
||||
upgraded.clone(),
|
||||
)
|
||||
})
|
||||
};
|
||||
let connection = super::server_builder(budget)
|
||||
.serve_connection(
|
||||
TokioIo::new(WriteTimeout::new(stream, budget.timeout)),
|
||||
service,
|
||||
)
|
||||
.with_upgrades();
|
||||
tokio::pin!(connection);
|
||||
let result = tokio::select! {
|
||||
result = &mut connection => result,
|
||||
_ = listener.closed() => {
|
||||
connection.as_mut().graceful_shutdown();
|
||||
connection.await
|
||||
}
|
||||
};
|
||||
let group = handlers
|
||||
.lock()
|
||||
.await
|
||||
.take()
|
||||
.expect("connection owns handlers");
|
||||
if result.is_ok() && upgraded.load(Ordering::Acquire) {
|
||||
group.wait().await?;
|
||||
} else {
|
||||
group.stop().await?;
|
||||
}
|
||||
super::connection_result(result)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn serve_http2<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
stream: crate::transport::Transport,
|
||||
budget: Budget,
|
||||
live: LiveLimits,
|
||||
http2: super::Http2Limits,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(Request, HttpReply) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<(), String>> + Send + 'static,
|
||||
{
|
||||
let (mut owner, spawner, fault) = task::scoped_owner(())?;
|
||||
let first_request = super::server_h2::FirstRequest::new();
|
||||
let service = {
|
||||
let first_request = first_request.clone();
|
||||
service_fn(move |request| {
|
||||
first_request.mark();
|
||||
let route = route.clone();
|
||||
dispatch_http2(request, route, budget, live)
|
||||
})
|
||||
};
|
||||
let mut builder = http2.server_builder(super::executor::Http2Executor(spawner), budget.headers);
|
||||
super::server_h2::configure_liveness(&mut builder, budget.timeout);
|
||||
let connection = builder.serve_connection(
|
||||
hyper_util::rt::TokioIo::new(WriteTimeout::new(stream, budget.timeout)),
|
||||
service,
|
||||
);
|
||||
let result = owner
|
||||
.enter(super::server_h2::drive(
|
||||
connection,
|
||||
&listener,
|
||||
&first_request,
|
||||
budget.timeout,
|
||||
&fault,
|
||||
|connection| connection.graceful_shutdown(),
|
||||
))
|
||||
.await;
|
||||
super::server_h2::complete(owner, result).await
|
||||
}
|
||||
|
||||
async fn dispatch<F, Fut>(
|
||||
mut request: hyper::Request<hyper::body::Incoming>,
|
||||
route: F,
|
||||
budget: Budget,
|
||||
live: LiveLimits,
|
||||
handlers: Arc<Mutex<Option<task::Group>>>,
|
||||
upgraded: Arc<AtomicBool>,
|
||||
) -> Result<hyper::Response<Outgoing>, std::convert::Infallible>
|
||||
where
|
||||
F: Fn(Request, HttpReply) -> Fut + Send + 'static,
|
||||
Fut: Future<Output = Result<(), String>> + Send + 'static,
|
||||
{
|
||||
let upgrade = if request.headers().contains_key(hyper::header::UPGRADE) {
|
||||
let upgrade = hyper::upgrade::on(&mut request);
|
||||
let mut handshake = hyper::Request::new(());
|
||||
*handshake.method_mut() = request.method().clone();
|
||||
*handshake.uri_mut() = request.uri().clone();
|
||||
*handshake.version_mut() = request.version();
|
||||
*handshake.headers_mut() = request.headers().clone();
|
||||
Some((handshake, upgrade))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let request = match read_request(request, budget).await {
|
||||
Ok(request) => request,
|
||||
Err(status) => return Ok(reject(status, budget).map(Outgoing::Full)),
|
||||
};
|
||||
let context = RequestContext::new("http/1.1", request.target.clone(), None);
|
||||
let (session, head) = Session::new(budget, live, upgrade, upgraded);
|
||||
let reply = session.reply();
|
||||
let state = reply.clone();
|
||||
let handler_context = context.clone();
|
||||
let started = handlers
|
||||
.lock()
|
||||
.await
|
||||
.as_mut()
|
||||
.expect("active connection")
|
||||
.run(async move {
|
||||
let result = session.run(route(request, reply)).await;
|
||||
if let Err(error) = result {
|
||||
handler_context.report(error);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
if let Err(error) = started {
|
||||
context.report(error);
|
||||
return Ok(reject(hyper::StatusCode::INTERNAL_SERVER_ERROR, budget).map(Outgoing::Full));
|
||||
}
|
||||
match tokio::time::timeout(budget.timeout, head).await {
|
||||
Ok(Ok(response)) => Ok(response),
|
||||
Ok(Err(_)) => {
|
||||
if state.fault().is_none() {
|
||||
context.report("HTTP live handler ended without a response");
|
||||
}
|
||||
Ok(reject(hyper::StatusCode::INTERNAL_SERVER_ERROR, budget).map(Outgoing::Full))
|
||||
}
|
||||
Err(_) => Ok(reject(hyper::StatusCode::GATEWAY_TIMEOUT, budget).map(Outgoing::Full)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn dispatch_http2<F, Fut>(
|
||||
request: hyper::Request<hyper::body::Incoming>,
|
||||
route: F,
|
||||
budget: Budget,
|
||||
live: LiveLimits,
|
||||
) -> Result<hyper::Response<Outgoing>, std::convert::Infallible>
|
||||
where
|
||||
F: Fn(Request, HttpReply) -> Fut + Send + 'static,
|
||||
Fut: Future<Output = Result<(), String>> + Send + 'static,
|
||||
{
|
||||
if request.method() == hyper::Method::CONNECT
|
||||
|| request.headers().contains_key(hyper::header::UPGRADE)
|
||||
{
|
||||
return Ok(reject(hyper::StatusCode::NOT_IMPLEMENTED, budget).map(Outgoing::Full));
|
||||
}
|
||||
let request = match read_request(request, budget).await {
|
||||
Ok(request) => request,
|
||||
Err(status) => return Ok(reject(status, budget).map(Outgoing::Full)),
|
||||
};
|
||||
let context = RequestContext::new("http/2", request.target.clone(), None);
|
||||
let (session, head) = Session::new(budget, live, None, Arc::new(AtomicBool::new(false)));
|
||||
let reply = session.reply();
|
||||
let state = reply.clone();
|
||||
let mut handler = match task::Group::new(1) {
|
||||
Ok(handler) => handler,
|
||||
Err(error) => {
|
||||
context.report(error);
|
||||
return Ok(reject(hyper::StatusCode::INTERNAL_SERVER_ERROR, budget).map(Outgoing::Full));
|
||||
}
|
||||
};
|
||||
let handler_context = context.clone();
|
||||
let started = handler
|
||||
.run(async move {
|
||||
let result = session.run(route(request, reply)).await;
|
||||
if let Err(error) = result {
|
||||
handler_context.report(error);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
if let Err(error) = started {
|
||||
context.report(error);
|
||||
return Ok(reject(hyper::StatusCode::INTERNAL_SERVER_ERROR, budget).map(Outgoing::Full));
|
||||
}
|
||||
match tokio::time::timeout(budget.timeout, head).await {
|
||||
Ok(Ok(response)) => Ok(response.map(|body| body.with_handler(handler))),
|
||||
Ok(Err(_)) => {
|
||||
if let Err(error) = handler.wait().await {
|
||||
context.report(error);
|
||||
} else if state.fault().is_none() {
|
||||
context.report("HTTP live handler ended without a response");
|
||||
}
|
||||
Ok(reject(hyper::StatusCode::INTERNAL_SERVER_ERROR, budget).map(Outgoing::Full))
|
||||
}
|
||||
Err(_) => match handler.stop().await {
|
||||
Ok(()) => Ok(reject(hyper::StatusCode::GATEWAY_TIMEOUT, budget).map(Outgoing::Full)),
|
||||
Err(error) => {
|
||||
context.report(error);
|
||||
Ok(reject(hyper::StatusCode::GATEWAY_TIMEOUT, budget).map(Outgoing::Full))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use http_body_util::{BodyExt, Full, LengthLimitError, Limited};
|
||||
use hyper::body::{Body, Incoming};
|
||||
use hyper::header::{CONTENT_LENGTH, HOST, TRANSFER_ENCODING, UPGRADE};
|
||||
use hyper::{HeaderMap, Method, StatusCode, Uri};
|
||||
|
||||
use super::{Budget, Header, Request, Response};
|
||||
use crate::bytes::Bytes;
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
enum MessageKind {
|
||||
Request,
|
||||
Response,
|
||||
}
|
||||
|
||||
pub(super) fn header_size(headers: &HeaderMap) -> usize {
|
||||
headers
|
||||
.iter()
|
||||
.map(|(name, value)| name.as_str().len() + value.as_bytes().len() + 4)
|
||||
.sum::<usize>()
|
||||
+ 2
|
||||
}
|
||||
|
||||
pub(super) fn decode_headers(headers: HeaderMap) -> Vec<Header> {
|
||||
headers
|
||||
.iter()
|
||||
.map(|(name, value)| Header {
|
||||
name: name.to_string(),
|
||||
value: Bytes::new(value.as_bytes().to_vec()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn encode_headers(
|
||||
headers: Vec<Header>,
|
||||
budget: Budget,
|
||||
kind: MessageKind,
|
||||
) -> Result<HeaderMap, String> {
|
||||
let mut result = HeaderMap::new();
|
||||
let mut length = 2;
|
||||
for header in headers {
|
||||
length += header.name.len() + header.value.values().len() + 4;
|
||||
if length > budget.headers {
|
||||
return Err("HTTP headers exceed header_bytes".into());
|
||||
}
|
||||
let name = hyper::header::HeaderName::from_bytes(header.name.as_bytes())
|
||||
.map_err(|error| error.to_string())?;
|
||||
if name == CONTENT_LENGTH || name == TRANSFER_ENCODING {
|
||||
return Err("HTTP framing headers are owned by the engine".into());
|
||||
}
|
||||
if name == UPGRADE {
|
||||
return Err("HTTP upgrades are not supported yet".into());
|
||||
}
|
||||
let value = hyper::header::HeaderValue::from_maybe_shared(header.value.into_http())
|
||||
.map_err(|error| error.to_string())?;
|
||||
if budget.http2.is_some() {
|
||||
check_http2_header(name.as_str(), value.as_bytes(), kind)?;
|
||||
}
|
||||
result
|
||||
.try_append(name, value)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn check_http2_header(name: &str, value: &[u8], kind: MessageKind) -> Result<(), String> {
|
||||
if matches!(
|
||||
name,
|
||||
"connection" | "keep-alive" | "proxy-connection" | "transfer-encoding" | "upgrade"
|
||||
) || (name == "te"
|
||||
&& (kind == MessageKind::Response || !value.eq_ignore_ascii_case(b"trailers")))
|
||||
{
|
||||
return Err(format!("HTTP/2 forbids connection-specific header {name}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Source 继续使用 origin-form;scheme/authority 只在 HTTP/2 线协议边界补齐。
|
||||
pub(super) fn http2_request<B>(
|
||||
request: &mut hyper::Request<B>,
|
||||
scheme: &str,
|
||||
) -> Result<(), String> {
|
||||
let host = request
|
||||
.headers()
|
||||
.get(HOST)
|
||||
.ok_or("HTTP request requires Host")?
|
||||
.to_str()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let authority = host
|
||||
.parse::<hyper::http::uri::Authority>()
|
||||
.map_err(|error| error.to_string())?;
|
||||
if authority.as_str().contains('@') {
|
||||
return Err("HTTP/2 authority cannot contain credentials".into());
|
||||
}
|
||||
let mut uri = request.uri().clone().into_parts();
|
||||
uri.scheme = Some(
|
||||
scheme
|
||||
.parse::<hyper::http::uri::Scheme>()
|
||||
.map_err(|error| error.to_string())?,
|
||||
);
|
||||
uri.authority = Some(authority);
|
||||
*request.uri_mut() = Uri::from_parts(uri).map_err(|error| error.to_string())?;
|
||||
*request.version_mut() = hyper::Version::HTTP_2;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_message(
|
||||
headers: &HeaderMap,
|
||||
body: &Bytes,
|
||||
start_bytes: usize,
|
||||
budget: Budget,
|
||||
) -> Result<(), String> {
|
||||
// 预留引擎生成的 Content-Length 和 Date;不截断用户内容。
|
||||
if header_size(headers) + start_bytes + 80 > budget.headers {
|
||||
return Err("HTTP headers exceed header_bytes".into());
|
||||
}
|
||||
if body.values().len() > budget.body {
|
||||
return Err("HTTP body exceeds body_bytes".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn encode_request(
|
||||
request: Request,
|
||||
budget: Budget,
|
||||
) -> Result<hyper::Request<Full<bytes::Bytes>>, String> {
|
||||
let method =
|
||||
Method::from_bytes(request.method.as_bytes()).map_err(|error| error.to_string())?;
|
||||
let uri = request
|
||||
.target
|
||||
.parse::<Uri>()
|
||||
.map_err(|error| error.to_string())?;
|
||||
if method == Method::CONNECT
|
||||
|| uri.scheme().is_some()
|
||||
|| uri.authority().is_some()
|
||||
|| !(request.target.starts_with('/')
|
||||
|| (method == Method::OPTIONS && request.target == "*"))
|
||||
{
|
||||
return Err(
|
||||
"HTTP send requires an origin-form target or OPTIONS *; CONNECT is not supported"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let headers = encode_headers(request.headers, budget, MessageKind::Request)?;
|
||||
if headers.get_all(HOST).iter().count() != 1 {
|
||||
return Err("HTTP request requires exactly one Host header".into());
|
||||
}
|
||||
let start_bytes = request.method.len() + request.target.len() + 14;
|
||||
check_message(&headers, &request.body, start_bytes, budget)?;
|
||||
let mut result = hyper::Request::new(Full::new(request.body.into_http()));
|
||||
*result.method_mut() = method;
|
||||
*result.uri_mut() = uri;
|
||||
*result.headers_mut() = headers;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(super) fn encode_response(
|
||||
response: Response,
|
||||
budget: Budget,
|
||||
) -> Result<hyper::Response<Full<bytes::Bytes>>, String> {
|
||||
let status = u16::try_from(response.status)
|
||||
.ok()
|
||||
.filter(|status| (200..=599).contains(status))
|
||||
.ok_or("HTTP response status must be between 200 and 599")?;
|
||||
let status = StatusCode::from_u16(status).map_err(|error| error.to_string())?;
|
||||
let no_body = matches!(
|
||||
status,
|
||||
StatusCode::NO_CONTENT | StatusCode::RESET_CONTENT | StatusCode::NOT_MODIFIED
|
||||
);
|
||||
if no_body && !response.body.values().is_empty() {
|
||||
return Err("HTTP 204, 205 and 304 responses cannot contain a body".into());
|
||||
}
|
||||
let headers = encode_headers(response.headers, budget, MessageKind::Response)?;
|
||||
check_message(&headers, &response.body, 32, budget)?;
|
||||
let mut result = hyper::Response::new(Full::new(response.body.into_http()));
|
||||
*result.status_mut() = status;
|
||||
*result.headers_mut() = headers;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(super) async fn collect_body(body: Incoming, limit: usize) -> Result<Bytes, StatusCode> {
|
||||
if body.size_hint().lower() > limit as u64 {
|
||||
return Err(StatusCode::PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
let mut incoming = Limited::new(body, limit);
|
||||
let mut body = Bytes::new(Vec::new());
|
||||
// 单帧直接共享,多帧逐步扩容;不为大量微小帧保留一份元数据队列。
|
||||
while let Some(frame) = incoming.frame().await {
|
||||
let frame = frame.map_err(|error| {
|
||||
if error.is::<LengthLimitError>() {
|
||||
StatusCode::PAYLOAD_TOO_LARGE
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
})?;
|
||||
match frame.into_data() {
|
||||
Ok(bytes) => body = body.concat(&Bytes::from_http(bytes)),
|
||||
Err(frame)
|
||||
if frame
|
||||
.trailers_ref()
|
||||
.is_some_and(|trailers| !trailers.is_empty()) =>
|
||||
{
|
||||
return Err(StatusCode::NOT_IMPLEMENTED);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
mod client;
|
||||
mod client_body;
|
||||
mod client_driver;
|
||||
mod executor;
|
||||
mod live;
|
||||
mod message;
|
||||
mod protocol;
|
||||
pub(crate) mod reply;
|
||||
mod server_h2;
|
||||
mod write_timeout;
|
||||
|
||||
pub use protocol::Http2Limits;
|
||||
|
||||
pub use client::{
|
||||
HttpClient, PoolLimits, StreamRequest, StreamResponse, client, close_client, open, open_stream,
|
||||
request,
|
||||
};
|
||||
pub use live::{serve_live, serve_live_tls};
|
||||
pub use reply::{HttpReply, LiveLimits, finish, respond, start, write};
|
||||
|
||||
use std::convert::Infallible;
|
||||
use std::future::Future;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use http_body_util::Full;
|
||||
use hyper::body::Incoming;
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Method, StatusCode};
|
||||
use hyper_util::rt::{TokioIo, TokioTimer};
|
||||
|
||||
use crate::bytes::Bytes;
|
||||
use crate::{net, task};
|
||||
use message::{collect_body, decode_headers, encode_request, encode_response, header_size};
|
||||
use write_timeout::WriteTimeout;
|
||||
|
||||
static NEXT_REQUEST_ID: AtomicU64 = AtomicU64::new(1);
|
||||
tokio::task_local! { static CURRENT_REQUEST: RequestContext; }
|
||||
|
||||
#[cfg(feature = "wire")]
|
||||
pub(crate) fn request_id() -> u64 {
|
||||
CURRENT_REQUEST
|
||||
.try_with(|request| request.id)
|
||||
.unwrap_or_else(|_| NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
|
||||
#[cfg(feature = "wire")]
|
||||
pub(crate) fn client_address() -> Option<String> {
|
||||
CURRENT_REQUEST
|
||||
.try_with(|request| request.client_address.clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RequestContext {
|
||||
id: u64,
|
||||
protocol: &'static str,
|
||||
target: String,
|
||||
client_address: Option<String>,
|
||||
}
|
||||
|
||||
impl RequestContext {
|
||||
fn new(protocol: &'static str, target: String, client_address: Option<String>) -> Self {
|
||||
Self {
|
||||
id: NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed),
|
||||
protocol,
|
||||
target,
|
||||
client_address,
|
||||
}
|
||||
}
|
||||
|
||||
fn report(&self, error: impl Into<String>) {
|
||||
crate::log::error(
|
||||
error,
|
||||
vec![
|
||||
crate::log::Field {
|
||||
name: "boundary".into(),
|
||||
value: "http".into(),
|
||||
},
|
||||
crate::log::Field {
|
||||
name: "request_id".into(),
|
||||
value: self.id.to_string(),
|
||||
},
|
||||
crate::log::Field {
|
||||
name: "protocol".into(),
|
||||
value: self.protocol.into(),
|
||||
},
|
||||
crate::log::Field {
|
||||
name: "target".into(),
|
||||
value: self.target.clone(),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Header {
|
||||
pub name: String,
|
||||
pub value: Bytes,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Request {
|
||||
pub method: String,
|
||||
pub target: String,
|
||||
pub headers: Vec<Header>,
|
||||
pub body: Bytes,
|
||||
}
|
||||
|
||||
/// Server-only body ownership. Source-level HTTP values remain bounded Request values.
|
||||
pub struct IncomingRequest {
|
||||
pub head: Request,
|
||||
pub body: Incoming,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Response {
|
||||
pub status: i64,
|
||||
pub headers: Vec<Header>,
|
||||
pub body: Bytes,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Limits {
|
||||
pub header_bytes: i64,
|
||||
pub body_bytes: i64,
|
||||
pub timeout_ms: i64,
|
||||
pub connections: i64,
|
||||
pub http2: Option<Http2Limits>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Budget {
|
||||
headers: usize,
|
||||
body: usize,
|
||||
timeout: Duration,
|
||||
http2: Option<Http2Limits>,
|
||||
}
|
||||
|
||||
impl Limits {
|
||||
fn check(self) -> Result<Budget, String> {
|
||||
let headers = usize::try_from(self.header_bytes)
|
||||
.ok()
|
||||
.filter(|value| *value >= 8192)
|
||||
.ok_or("HTTP header_bytes must be at least 8192")?;
|
||||
let body =
|
||||
usize::try_from(self.body_bytes).map_err(|_| "HTTP body_bytes must be nonnegative")?;
|
||||
let millis = u64::try_from(self.timeout_ms)
|
||||
.ok()
|
||||
.filter(|value| *value > 0)
|
||||
.ok_or("HTTP timeout_ms must be positive")?;
|
||||
if !(1..=65536).contains(&self.connections) {
|
||||
return Err("HTTP connections must be between 1 and 65536".into());
|
||||
}
|
||||
if let Some(http2) = self.http2 {
|
||||
http2.check(self.connections, headers)?;
|
||||
}
|
||||
Ok(Budget {
|
||||
headers,
|
||||
body,
|
||||
timeout: Duration::from_millis(millis),
|
||||
http2: self.http2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 连接由现有任务组持有,取消 serve 会沿 Scope 排空连接和 handler 子任务。
|
||||
pub async fn serve<F, Fut>(route: F, listener: net::Listener, limits: Limits) -> Result<(), String>
|
||||
where
|
||||
F: Fn(Request) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<Response, String>> + Send + 'static,
|
||||
{
|
||||
serve_buffered(route, listener, limits, None).await
|
||||
}
|
||||
|
||||
pub async fn serve_tls<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
limits: Limits,
|
||||
tls: crate::tls::ServerTls,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(Request) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<Response, String>> + Send + 'static,
|
||||
{
|
||||
serve_buffered(route, listener, limits, Some(tls)).await
|
||||
}
|
||||
|
||||
async fn serve_buffered<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
limits: Limits,
|
||||
tls: Option<crate::tls::ServerTls>,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(Request) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<Response, String>> + Send + 'static,
|
||||
{
|
||||
let budget = limits.check()?;
|
||||
serve_transport(
|
||||
move |incoming: IncomingRequest| {
|
||||
let route = route.clone();
|
||||
async move {
|
||||
let mut request = incoming.head;
|
||||
request.body = match tokio::time::timeout(
|
||||
budget.timeout,
|
||||
collect_body(incoming.body, budget.body),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(body)) => body,
|
||||
Ok(Err(status)) => {
|
||||
return Ok(Response {
|
||||
status: status.as_u16().into(),
|
||||
headers: Vec::new(),
|
||||
body: Bytes::new(Vec::new()),
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(Response {
|
||||
status: 408,
|
||||
headers: Vec::new(),
|
||||
body: Bytes::new(Vec::new()),
|
||||
});
|
||||
}
|
||||
};
|
||||
route(request).await
|
||||
}
|
||||
},
|
||||
listener,
|
||||
limits,
|
||||
tls,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn serve_incoming<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
limits: Limits,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(IncomingRequest) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<Response, String>> + Send + 'static,
|
||||
{
|
||||
serve_transport(route, listener, limits, None).await
|
||||
}
|
||||
|
||||
async fn serve_transport<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
limits: Limits,
|
||||
tls: Option<crate::tls::ServerTls>,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(IncomingRequest) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<Response, String>> + Send + 'static,
|
||||
{
|
||||
let budget = limits.check()?;
|
||||
let tls = tls.map(|tls| protocol::server_tls(tls, budget.http2));
|
||||
task::parallel_each_stream(
|
||||
net::incoming(listener.clone(), true),
|
||||
limits.connections,
|
||||
move |connection| {
|
||||
let route = route.clone();
|
||||
let tls = tls.clone();
|
||||
let listener = listener.clone();
|
||||
async move {
|
||||
let stream = connection?;
|
||||
let client_address = stream
|
||||
.peer_addr()
|
||||
.ok()
|
||||
.map(|address| address.ip().to_string());
|
||||
let stream = match crate::tls::accept(stream, tls.as_ref(), limits.timeout_ms).await
|
||||
{
|
||||
Ok(stream) => stream,
|
||||
Err(_) => return Ok(()), // A peer's TLS failure does not terminate the listener.
|
||||
};
|
||||
if protocol::check_alpn(&stream, budget.http2).is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(http2) = budget.http2 {
|
||||
return serve_http2(route, listener, stream, budget, http2, client_address)
|
||||
.await;
|
||||
}
|
||||
let service = service_fn(move |request| {
|
||||
dispatch(request, route.clone(), budget, client_address.clone())
|
||||
});
|
||||
let connection = server_builder(budget).serve_connection(
|
||||
TokioIo::new(WriteTimeout::new(stream, budget.timeout)),
|
||||
service,
|
||||
);
|
||||
tokio::pin!(connection);
|
||||
let result = tokio::select! {
|
||||
result = &mut connection => result,
|
||||
_ = listener.closed() => {
|
||||
connection.as_mut().graceful_shutdown();
|
||||
connection.await
|
||||
}
|
||||
};
|
||||
connection_result(result)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn serve_http2<F, Fut>(
|
||||
route: F,
|
||||
listener: net::Listener,
|
||||
stream: crate::transport::Transport,
|
||||
budget: Budget,
|
||||
http2: Http2Limits,
|
||||
client_address: Option<String>,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(IncomingRequest) -> Fut + Clone + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<Response, String>> + Send + 'static,
|
||||
{
|
||||
let (mut owner, spawner, fault) = task::scoped_owner(())?;
|
||||
let first_request = server_h2::FirstRequest::new();
|
||||
let service = {
|
||||
let first_request = first_request.clone();
|
||||
service_fn(move |request| {
|
||||
first_request.mark();
|
||||
let route = route.clone();
|
||||
dispatch(request, route, budget, client_address.clone())
|
||||
})
|
||||
};
|
||||
let mut builder = http2.server_builder(executor::Http2Executor(spawner), budget.headers);
|
||||
server_h2::configure_liveness(&mut builder, budget.timeout);
|
||||
let connection = builder.serve_connection(
|
||||
TokioIo::new(WriteTimeout::new(stream, budget.timeout)),
|
||||
service,
|
||||
);
|
||||
let result = owner
|
||||
.enter(server_h2::drive(
|
||||
connection,
|
||||
&listener,
|
||||
&first_request,
|
||||
budget.timeout,
|
||||
&fault,
|
||||
|connection| connection.graceful_shutdown(),
|
||||
))
|
||||
.await;
|
||||
server_h2::complete(owner, result).await
|
||||
}
|
||||
|
||||
fn server_builder(budget: Budget) -> hyper::server::conn::http1::Builder {
|
||||
let mut builder = hyper::server::conn::http1::Builder::new();
|
||||
builder
|
||||
.timer(TokioTimer::new())
|
||||
.header_read_timeout(budget.timeout)
|
||||
.max_buf_size(budget.headers);
|
||||
builder
|
||||
}
|
||||
|
||||
fn connection_result(result: Result<(), hyper::Error>) -> Result<(), String> {
|
||||
// 对端断开、畸形报文和 I/O 超时只结束该连接;请求故障已在 dispatch 边界处理。
|
||||
let _ = result;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn dispatch<F, Fut>(
|
||||
request: hyper::Request<Incoming>,
|
||||
route: F,
|
||||
budget: Budget,
|
||||
client_address: Option<String>,
|
||||
) -> Result<hyper::Response<Full<bytes::Bytes>>, Infallible>
|
||||
where
|
||||
F: Fn(IncomingRequest) -> Fut,
|
||||
Fut: Future<Output = Result<Response, String>>,
|
||||
{
|
||||
if request.method() == Method::CONNECT || request.headers().contains_key(hyper::header::UPGRADE)
|
||||
{
|
||||
return Ok(reject(StatusCode::NOT_IMPLEMENTED, budget));
|
||||
}
|
||||
let request = match read_incoming(request, budget) {
|
||||
Ok(request) => request,
|
||||
Err(status) => return Ok(reject(status, budget)),
|
||||
};
|
||||
let context = RequestContext::new(
|
||||
if budget.http2.is_some() {
|
||||
"http/2"
|
||||
} else {
|
||||
"http/1.1"
|
||||
},
|
||||
request.head.target.clone(),
|
||||
client_address,
|
||||
);
|
||||
let response = match tokio::time::timeout(
|
||||
budget.timeout,
|
||||
CURRENT_REQUEST.scope(context.clone(), route(request)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(response)) => response,
|
||||
Ok(Err(error)) => {
|
||||
context.report(error);
|
||||
return Ok(reject(StatusCode::INTERNAL_SERVER_ERROR, budget));
|
||||
}
|
||||
Err(_) => return Ok(reject(StatusCode::GATEWAY_TIMEOUT, budget)),
|
||||
};
|
||||
match encode_response(response, budget) {
|
||||
Ok(response) => Ok(response),
|
||||
Err(error) => {
|
||||
context.report(error);
|
||||
Ok(reject(StatusCode::INTERNAL_SERVER_ERROR, budget))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_request(
|
||||
request: hyper::Request<Incoming>,
|
||||
budget: Budget,
|
||||
) -> Result<Request, StatusCode> {
|
||||
let incoming = read_incoming(request, budget)?;
|
||||
let mut request = incoming.head;
|
||||
request.body = tokio::time::timeout(budget.timeout, collect_body(incoming.body, budget.body))
|
||||
.await
|
||||
.map_err(|_| StatusCode::REQUEST_TIMEOUT)??;
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
fn read_incoming(
|
||||
mut request: hyper::Request<Incoming>,
|
||||
budget: Budget,
|
||||
) -> Result<IncomingRequest, StatusCode> {
|
||||
if request.method() == Method::CONNECT {
|
||||
return Err(StatusCode::NOT_IMPLEMENTED);
|
||||
}
|
||||
let host_count = request
|
||||
.headers()
|
||||
.get_all(hyper::header::HOST)
|
||||
.iter()
|
||||
.count();
|
||||
let target = if request.version() == hyper::Version::HTTP_2 {
|
||||
let authority = request
|
||||
.uri()
|
||||
.authority()
|
||||
.ok_or(StatusCode::BAD_REQUEST)?
|
||||
.as_str()
|
||||
.to_owned();
|
||||
if host_count > 1 {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
if let Some(host) = request.headers().get(hyper::header::HOST) {
|
||||
if !host.as_bytes().eq_ignore_ascii_case(authority.as_bytes()) {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
} else {
|
||||
let host = hyper::header::HeaderValue::from_str(&authority)
|
||||
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
request.headers_mut().insert(hyper::header::HOST, host);
|
||||
}
|
||||
request
|
||||
.uri()
|
||||
.path_and_query()
|
||||
.map(|value| value.as_str())
|
||||
.unwrap_or("/")
|
||||
.to_owned()
|
||||
} else {
|
||||
if request.version() == hyper::Version::HTTP_11 && host_count != 1 {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
request.uri().to_string()
|
||||
};
|
||||
let start_bytes = request.method().as_str().len() + target.len() + 14;
|
||||
if header_size(request.headers()) + start_bytes > budget.headers {
|
||||
return Err(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
|
||||
}
|
||||
let (head, body) = request.into_parts();
|
||||
Ok(IncomingRequest {
|
||||
head: Request {
|
||||
method: head.method.to_string(),
|
||||
target,
|
||||
headers: decode_headers(head.headers),
|
||||
body: Bytes::new(Vec::new()),
|
||||
},
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
fn reject(status: StatusCode, budget: Budget) -> hyper::Response<Full<bytes::Bytes>> {
|
||||
let mut response = hyper::Response::new(Full::new(bytes::Bytes::new()));
|
||||
*response.status_mut() = status;
|
||||
if budget.http2.is_none() {
|
||||
response.headers_mut().insert(
|
||||
hyper::header::CONNECTION,
|
||||
hyper::header::HeaderValue::from_static("close"),
|
||||
);
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
/// 单次请求独占驱动 future;完成、错误和取消都直接释放连接,不产生后台任务。
|
||||
pub async fn send(
|
||||
address: &str,
|
||||
port: i64,
|
||||
request: Request,
|
||||
limits: Limits,
|
||||
) -> Result<Response, String> {
|
||||
let budget = limits.check()?;
|
||||
let request = encode_request(request, budget)?;
|
||||
if budget.http2.is_some() {
|
||||
return client::send_http2(address, port, request, budget).await;
|
||||
}
|
||||
tokio::time::timeout(budget.timeout, async {
|
||||
let stream = net::connect_stream_timeout(address, port, limits.timeout_ms).await?;
|
||||
let (mut sender, connection) = hyper::client::conn::http1::Builder::new()
|
||||
.max_buf_size(budget.headers)
|
||||
.handshake(TokioIo::new(stream))
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let exchange = async {
|
||||
let response = sender
|
||||
.send_request(request)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
if response.status() == StatusCode::SWITCHING_PROTOCOLS {
|
||||
return Err("HTTP upgrades are not supported by send".into());
|
||||
}
|
||||
if header_size(response.headers()) + 32 > budget.headers {
|
||||
return Err("HTTP response headers exceed header_bytes".into());
|
||||
}
|
||||
let (head, body) = response.into_parts();
|
||||
let body = collect_body(body, budget.body)
|
||||
.await
|
||||
.map_err(|status| format!("HTTP response body rejected: {status}"))?;
|
||||
Ok(Response {
|
||||
status: i64::from(head.status.as_u16()),
|
||||
headers: decode_headers(head.headers),
|
||||
body,
|
||||
})
|
||||
};
|
||||
tokio::pin!(exchange, connection);
|
||||
tokio::select! {
|
||||
result = &mut exchange => result,
|
||||
result = &mut connection => {
|
||||
result.map_err(|error| error.to_string())?;
|
||||
// Connection: close 的最后一帧可能已入队,继续取完响应。
|
||||
exchange.await
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "HTTP request timed out".to_owned())?
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use crate::{
|
||||
tls::{ClientTls, ServerTls},
|
||||
transport::Transport,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Http2Limits {
|
||||
pub streams: i64,
|
||||
pub stream_window_bytes: i64,
|
||||
pub connection_window_bytes: i64,
|
||||
}
|
||||
|
||||
impl Http2Limits {
|
||||
pub(super) fn check(self, connections: i64, headers: usize) -> Result<(), String> {
|
||||
if !(1..=65536).contains(&self.streams) || connections * self.streams > 65536 {
|
||||
return Err("HTTP/2 connections * streams must be between 1 and 65536".into());
|
||||
}
|
||||
if !(1..=i32::MAX as i64).contains(&self.stream_window_bytes) {
|
||||
return Err("HTTP/2 stream_window_bytes must be between 1 and 2147483647".into());
|
||||
}
|
||||
if !(65535..=i32::MAX as i64).contains(&self.connection_window_bytes) {
|
||||
return Err(
|
||||
"HTTP/2 connection_window_bytes must be between 65535 and 2147483647".into(),
|
||||
);
|
||||
}
|
||||
if u32::try_from(headers).is_err() {
|
||||
return Err("HTTP/2 header_bytes must fit an unsigned 32-bit limit".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn client_builder<E: Clone>(
|
||||
self,
|
||||
executor: E,
|
||||
headers: usize,
|
||||
) -> hyper::client::conn::http2::Builder<E> {
|
||||
let mut builder = hyper::client::conn::http2::Builder::new(executor);
|
||||
builder
|
||||
.timer(hyper_util::rt::TokioTimer::new())
|
||||
.adaptive_window(false)
|
||||
.initial_stream_window_size(self.stream_window_bytes as u32)
|
||||
.initial_connection_window_size(self.connection_window_bytes as u32)
|
||||
.initial_max_send_streams(self.streams as usize)
|
||||
.max_concurrent_streams(0)
|
||||
.max_header_list_size(headers as u32)
|
||||
.max_frame_size(FRAME_BYTES)
|
||||
.max_send_buf_size(FRAME_BYTES as usize);
|
||||
builder
|
||||
}
|
||||
|
||||
pub(super) fn server_builder<E>(
|
||||
self,
|
||||
executor: E,
|
||||
headers: usize,
|
||||
) -> hyper::server::conn::http2::Builder<E> {
|
||||
let mut builder = hyper::server::conn::http2::Builder::new(executor);
|
||||
builder
|
||||
.timer(hyper_util::rt::TokioTimer::new())
|
||||
.adaptive_window(false)
|
||||
.initial_stream_window_size(self.stream_window_bytes as u32)
|
||||
.initial_connection_window_size(self.connection_window_bytes as u32)
|
||||
.max_concurrent_streams(self.streams as u32)
|
||||
.max_header_list_size(headers as u32)
|
||||
.max_frame_size(FRAME_BYTES)
|
||||
.max_send_buf_size(FRAME_BYTES as usize);
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
const FRAME_BYTES: u32 = 16384;
|
||||
|
||||
// HTTP 单独派生 ALPN;不能修改 WSS 和其他 TLS 使用者共享的配置。
|
||||
pub(super) fn client_tls(tls: ClientTls, http2: Option<Http2Limits>) -> ClientTls {
|
||||
let mut config = (*tls.0).clone();
|
||||
config.alpn_protocols = vec![alpn(http2).to_vec()];
|
||||
ClientTls(std::sync::Arc::new(config))
|
||||
}
|
||||
|
||||
pub(super) fn server_tls(tls: ServerTls, http2: Option<Http2Limits>) -> ServerTls {
|
||||
let mut config = (*tls.0).clone();
|
||||
config.alpn_protocols = vec![alpn(http2).to_vec()];
|
||||
ServerTls(std::sync::Arc::new(config))
|
||||
}
|
||||
|
||||
fn alpn(http2: Option<Http2Limits>) -> &'static [u8] {
|
||||
if http2.is_some() { b"h2" } else { b"http/1.1" }
|
||||
}
|
||||
|
||||
pub(super) fn check_alpn(stream: &Transport, http2: Option<Http2Limits>) -> Result<(), String> {
|
||||
if let Transport::Tls(stream) = stream
|
||||
&& http2.is_some()
|
||||
&& stream.get_ref().1.alpn_protocol() != Some(b"h2")
|
||||
{
|
||||
return Err("HTTP/2 TLS peer did not negotiate h2".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use http_body_util::Full;
|
||||
use hyper::body::{Body, Frame, SizeHint};
|
||||
use tokio::sync::{Mutex as AsyncMutex, Semaphore, mpsc, oneshot};
|
||||
use tokio::time::Sleep;
|
||||
|
||||
use super::{Budget, Header, Response, message::encode_response};
|
||||
use crate::{bytes::Bytes, task, websocket};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LiveLimits {
|
||||
pub chunk_bytes: i64,
|
||||
pub idle_ms: i64,
|
||||
pub heartbeat_ms: i64,
|
||||
}
|
||||
|
||||
impl LiveLimits {
|
||||
pub(super) fn check(self) -> Result<(), String> {
|
||||
if self.chunk_bytes <= 0 || usize::try_from(self.chunk_bytes).is_err() {
|
||||
return Err("HTTP chunk_bytes must be positive".into());
|
||||
}
|
||||
if self.idle_ms <= 0 || self.heartbeat_ms <= 0 || self.heartbeat_ms >= self.idle_ms {
|
||||
return Err("HTTP live limits require 0 < heartbeat_ms < idle_ms".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type Head = hyper::Response<Outgoing>;
|
||||
enum Mode {
|
||||
Pending,
|
||||
Streaming {
|
||||
sender: mpsc::Sender<bytes::Bytes>,
|
||||
sse: bool,
|
||||
},
|
||||
Finished,
|
||||
Upgraded(Option<websocket::WebSocket>),
|
||||
}
|
||||
|
||||
struct State {
|
||||
mode: Mode,
|
||||
head: Option<oneshot::Sender<Head>>,
|
||||
upgrade: Option<(hyper::Request<()>, hyper::upgrade::OnUpgrade)>,
|
||||
fault: Option<String>,
|
||||
body_cancelled: bool,
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
state: Mutex<State>,
|
||||
writer: AsyncMutex<()>,
|
||||
closed: Semaphore,
|
||||
budget: Budget,
|
||||
live: LiveLimits,
|
||||
upgraded: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
fn close_body(&self, cancelled: bool) {
|
||||
if cancelled {
|
||||
self.state
|
||||
.lock()
|
||||
.expect("reply lock poisoned")
|
||||
.body_cancelled = true;
|
||||
}
|
||||
self.closed.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpReply(Arc<Shared>);
|
||||
|
||||
impl std::fmt::Debug for HttpReply {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("HttpReply(<resource>)")
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpReply {
|
||||
pub(crate) fn chunk_limit(&self) -> usize {
|
||||
self.0.live.chunk_bytes as usize
|
||||
}
|
||||
|
||||
pub(crate) fn fault(&self) -> Option<String> {
|
||||
self.0
|
||||
.state
|
||||
.lock()
|
||||
.expect("reply lock poisoned")
|
||||
.fault
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn publish(&self, prepare: impl FnOnce() -> (Head, Mode)) -> Result<(), String> {
|
||||
let mut state = self.0.state.lock().expect("reply lock poisoned");
|
||||
if !matches!(state.mode, Mode::Pending) {
|
||||
return Err("HTTP response has already started".into());
|
||||
}
|
||||
let head = state.head.take().ok_or("HTTP response is closed")?;
|
||||
// 只有真正提交的正文才能拥有取消信号;拒绝重复提交时不能关闭已有响应。
|
||||
let (response, mode) = prepare();
|
||||
state.upgrade.take();
|
||||
state.mode = mode;
|
||||
// SendError 会析构响应正文;先释放状态锁,正文 Drop 才能登记传输取消。
|
||||
drop(state);
|
||||
if head.send(response).is_err() {
|
||||
// 空正文从创建起就是 end-stream,不能只依赖 BufferedBody::drop 判断取消。
|
||||
self.0.close_body(true);
|
||||
return Err("HTTP peer is closed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn upgrade(
|
||||
&self,
|
||||
limits: websocket::Limits,
|
||||
) -> Result<websocket::WebSocket, String> {
|
||||
limits.check()?;
|
||||
let (head, response, upgrade) = {
|
||||
let mut state = self.0.state.lock().expect("reply lock poisoned");
|
||||
if !matches!(state.mode, Mode::Pending) {
|
||||
return Err("HTTP response has already started".into());
|
||||
}
|
||||
let (request, upgrade) = state.upgrade.take().ok_or("HTTP upgrade is unavailable")?;
|
||||
let response =
|
||||
tokio_tungstenite::tungstenite::handshake::server::create_response(&request)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let response = response.map(|()| Outgoing::Full(Full::new(bytes::Bytes::new())));
|
||||
state.mode = Mode::Upgraded(None);
|
||||
self.0.upgraded.store(true, Ordering::Release);
|
||||
let head = state.head.take().ok_or("HTTP response is closed")?;
|
||||
(head, response, upgrade)
|
||||
};
|
||||
if head.send(response).is_err() {
|
||||
self.0.close_body(true);
|
||||
return Err("HTTP peer is closed".into());
|
||||
}
|
||||
let stream = tokio::time::timeout(self.0.budget.timeout, upgrade)
|
||||
.await
|
||||
.map_err(|_| "WebSocket upgrade timed out")?
|
||||
.map_err(|error| error.to_string())?;
|
||||
// 取回已知的底层 I/O 和预读帧;升级后不再继承 HTTP 写入期限。
|
||||
let parts = stream
|
||||
.downcast::<hyper_util::rt::TokioIo<super::write_timeout::WriteTimeout>>()
|
||||
.map_err(|_| "WebSocket upgrade transport does not match the HTTP server")?;
|
||||
let socket = websocket::from_server(
|
||||
parts.io.into_inner().into_inner(),
|
||||
parts.read_buf.to_vec(),
|
||||
limits,
|
||||
)
|
||||
.await?;
|
||||
let mut state = self.0.state.lock().expect("reply lock poisoned");
|
||||
state.mode = Mode::Upgraded(Some(socket.clone()));
|
||||
Ok(socket)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Session(HttpReply);
|
||||
|
||||
impl Session {
|
||||
pub(super) fn new(
|
||||
budget: Budget,
|
||||
live: LiveLimits,
|
||||
upgrade: Option<(hyper::Request<()>, hyper::upgrade::OnUpgrade)>,
|
||||
upgraded: Arc<AtomicBool>,
|
||||
) -> (Self, oneshot::Receiver<Head>) {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
(
|
||||
Self(HttpReply(Arc::new(Shared {
|
||||
state: Mutex::new(State {
|
||||
mode: Mode::Pending,
|
||||
head: Some(sender),
|
||||
upgrade,
|
||||
fault: None,
|
||||
body_cancelled: false,
|
||||
}),
|
||||
writer: AsyncMutex::new(()),
|
||||
closed: Semaphore::new(0),
|
||||
budget,
|
||||
live,
|
||||
upgraded,
|
||||
}))),
|
||||
receiver,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn reply(&self) -> HttpReply {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
pub(super) async fn run(
|
||||
self,
|
||||
route: impl Future<Output = Result<(), String>>,
|
||||
) -> Result<(), String> {
|
||||
let result = tokio::select! {
|
||||
biased;
|
||||
result = route => result,
|
||||
_ = self.0.0.closed.acquire() => Ok(()),
|
||||
};
|
||||
if let Err(error) = &result {
|
||||
let mut state = self.0.0.state.lock().expect("reply lock poisoned");
|
||||
// 传输侧先取消正文时,随后被唤醒的 write 失败属于该流,不是程序故障。
|
||||
if state.body_cancelled {
|
||||
return Ok(());
|
||||
}
|
||||
state.fault = Some(error.clone());
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Session {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.0.0.state.lock().expect("reply lock poisoned");
|
||||
// 函数返回或取消都释放正文发送端/升级连接;外部别名不能延长会话。
|
||||
if let Mode::Upgraded(Some(socket)) = &state.mode {
|
||||
socket.abort();
|
||||
}
|
||||
state.mode = Mode::Finished;
|
||||
state.upgrade.take();
|
||||
state.head.take();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn respond(reply: &HttpReply, response: Response) -> Result<(), String> {
|
||||
let _writer = reply.0.writer.lock().await;
|
||||
let response = encode_response(response, reply.0.budget)?;
|
||||
reply.publish(|| {
|
||||
(
|
||||
response.map(|body| {
|
||||
Outgoing::Buffered(BufferedBody {
|
||||
body,
|
||||
reply: reply.clone(),
|
||||
})
|
||||
}),
|
||||
Mode::Finished,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start(reply: &HttpReply, status: i64, headers: Vec<Header>) -> Result<(), String> {
|
||||
start_stream(reply, status, headers, false).await
|
||||
}
|
||||
|
||||
pub(crate) async fn start_stream(
|
||||
reply: &HttpReply,
|
||||
status: i64,
|
||||
headers: Vec<Header>,
|
||||
sse: bool,
|
||||
) -> Result<(), String> {
|
||||
if matches!(status, 204 | 205 | 304) {
|
||||
return Err("bodyless status requires http.respond".into());
|
||||
}
|
||||
let _writer = reply.0.writer.lock().await;
|
||||
let response = encode_response(
|
||||
Response {
|
||||
status,
|
||||
headers,
|
||||
body: Bytes::new(Vec::new()),
|
||||
},
|
||||
reply.0.budget,
|
||||
)?;
|
||||
reply.publish(|| {
|
||||
let (sender, receiver) = mpsc::channel(1);
|
||||
let millis = if sse {
|
||||
reply.0.live.heartbeat_ms
|
||||
} else {
|
||||
reply.0.live.idle_ms
|
||||
};
|
||||
let interval = Duration::from_millis(millis as u64);
|
||||
let body = StreamBody {
|
||||
receiver,
|
||||
reply: reply.clone(),
|
||||
interval,
|
||||
deadline: Box::pin(tokio::time::sleep(interval)),
|
||||
sse,
|
||||
ended: false,
|
||||
};
|
||||
(
|
||||
response.map(|_| Outgoing::Stream(body)),
|
||||
Mode::Streaming { sender, sse },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn write(reply: &HttpReply, bytes: Bytes) -> Result<(), String> {
|
||||
write_chunk(reply, bytes, false).await
|
||||
}
|
||||
|
||||
pub(crate) async fn write_chunk(reply: &HttpReply, bytes: Bytes, sse: bool) -> Result<(), String> {
|
||||
if bytes.values().len() > reply.chunk_limit() {
|
||||
return Err("HTTP chunk exceeds chunk_bytes".into());
|
||||
}
|
||||
tokio::time::timeout(reply.0.budget.timeout, async {
|
||||
let _writer = reply.0.writer.lock().await;
|
||||
let sender = {
|
||||
let state = reply.0.state.lock().expect("reply lock poisoned");
|
||||
match &state.mode {
|
||||
Mode::Streaming { sender, sse: mode } if *mode == sse => sender.clone(),
|
||||
_ => return Err("HTTP reply is not an open stream of this kind".into()),
|
||||
}
|
||||
};
|
||||
sender
|
||||
.send(bytes.into_http())
|
||||
.await
|
||||
.map_err(|_| "HTTP peer is closed".into())
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "HTTP stream write timed out".to_owned())?
|
||||
}
|
||||
|
||||
pub async fn finish(reply: &HttpReply) -> Result<(), String> {
|
||||
let _writer = reply.0.writer.lock().await;
|
||||
let mut state = reply.0.state.lock().expect("reply lock poisoned");
|
||||
if !matches!(state.mode, Mode::Streaming { .. }) {
|
||||
return Err("HTTP reply is not an open stream".into());
|
||||
}
|
||||
state.mode = Mode::Finished;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) enum Outgoing {
|
||||
Full(Full<bytes::Bytes>),
|
||||
Buffered(BufferedBody),
|
||||
Stream(StreamBody),
|
||||
Handler(HandlerBody),
|
||||
}
|
||||
|
||||
impl Outgoing {
|
||||
pub(super) fn with_handler(self, handler: task::Group) -> Self {
|
||||
Self::Handler(HandlerBody {
|
||||
body: Box::new(self),
|
||||
_handler: handler,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct HandlerBody {
|
||||
body: Box<Outgoing>,
|
||||
_handler: task::Group,
|
||||
}
|
||||
|
||||
pub(super) struct BufferedBody {
|
||||
body: Full<bytes::Bytes>,
|
||||
reply: HttpReply,
|
||||
}
|
||||
|
||||
impl Drop for BufferedBody {
|
||||
fn drop(&mut self) {
|
||||
self.reply.0.close_body(!self.body.is_end_stream());
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct StreamBody {
|
||||
receiver: mpsc::Receiver<bytes::Bytes>,
|
||||
reply: HttpReply,
|
||||
interval: Duration,
|
||||
deadline: Pin<Box<Sleep>>,
|
||||
sse: bool,
|
||||
ended: bool,
|
||||
}
|
||||
|
||||
impl Drop for StreamBody {
|
||||
fn drop(&mut self) {
|
||||
self.reply.0.close_body(!self.ended);
|
||||
}
|
||||
}
|
||||
|
||||
impl Body for Outgoing {
|
||||
type Data = bytes::Bytes;
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn poll_frame(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
let body = match self.get_mut() {
|
||||
Self::Stream(body) => body,
|
||||
Self::Full(body) => {
|
||||
return Pin::new(body)
|
||||
.poll_frame(cx)
|
||||
.map(|frame| frame.map(|frame| frame.map_err(|never| match never {})));
|
||||
}
|
||||
Self::Buffered(body) => {
|
||||
return Pin::new(&mut body.body)
|
||||
.poll_frame(cx)
|
||||
.map(|frame| frame.map(|frame| frame.map_err(|never| match never {})));
|
||||
}
|
||||
Self::Handler(body) => return Pin::new(body.body.as_mut()).poll_frame(cx),
|
||||
};
|
||||
if body.ended {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
match body.receiver.poll_recv(cx) {
|
||||
Poll::Ready(Some(bytes)) => {
|
||||
body.deadline
|
||||
.as_mut()
|
||||
.reset(tokio::time::Instant::now() + body.interval);
|
||||
return Poll::Ready(Some(Ok(Frame::data(bytes))));
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
body.ended = true;
|
||||
let fault = body
|
||||
.reply
|
||||
.0
|
||||
.state
|
||||
.lock()
|
||||
.expect("reply lock poisoned")
|
||||
.fault
|
||||
.clone();
|
||||
return Poll::Ready(fault.map(|fault| Err(std::io::Error::other(fault))));
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
if body.deadline.as_mut().poll(cx).is_pending() {
|
||||
return Poll::Pending;
|
||||
}
|
||||
body.deadline
|
||||
.as_mut()
|
||||
.reset(tokio::time::Instant::now() + body.interval);
|
||||
if body.sse {
|
||||
Poll::Ready(Some(Ok(Frame::data(bytes::Bytes::from_static(b":\n\n")))))
|
||||
} else {
|
||||
body.ended = true;
|
||||
body.reply.0.close_body(true);
|
||||
Poll::Ready(Some(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"HTTP stream idle timeout",
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_end_stream(&self) -> bool {
|
||||
match self {
|
||||
Self::Full(body) => body.is_end_stream(),
|
||||
Self::Buffered(body) => body.body.is_end_stream(),
|
||||
Self::Stream(body) => body.ended,
|
||||
Self::Handler(body) => body.body.is_end_stream(),
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> SizeHint {
|
||||
match self {
|
||||
Self::Full(body) => body.size_hint(),
|
||||
Self::Buffered(body) => body.body.size_hint(),
|
||||
Self::Stream(_) => SizeHint::default(),
|
||||
Self::Handler(body) => body.body.size_hint(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::{
|
||||
net,
|
||||
task::{ScopeFault, ScopedOwner},
|
||||
};
|
||||
|
||||
pub(super) fn configure_liveness<E>(
|
||||
builder: &mut hyper::server::conn::http2::Builder<E>,
|
||||
timeout: Duration,
|
||||
) {
|
||||
builder
|
||||
.keep_alive_interval(Some(timeout))
|
||||
.keep_alive_timeout(timeout);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct FirstRequest(Arc<FirstRequestState>);
|
||||
|
||||
struct FirstRequestState {
|
||||
seen: AtomicBool,
|
||||
changed: Notify,
|
||||
}
|
||||
|
||||
impl FirstRequest {
|
||||
pub(super) fn new() -> Self {
|
||||
Self(Arc::new(FirstRequestState {
|
||||
seen: AtomicBool::new(false),
|
||||
changed: Notify::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn mark(&self) {
|
||||
if !self.0.seen.swap(true, Ordering::AcqRel) {
|
||||
self.0.changed.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait(&self) {
|
||||
loop {
|
||||
let changed = self.0.changed.notified();
|
||||
if self.0.seen.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
changed.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn drive<C, Shutdown>(
|
||||
connection: C,
|
||||
listener: &net::Listener,
|
||||
first_request: &FirstRequest,
|
||||
timeout: Duration,
|
||||
fault: &ScopeFault,
|
||||
mut shutdown: Shutdown,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
C: Future<Output = Result<(), hyper::Error>>,
|
||||
Shutdown: for<'a> FnMut(Pin<&'a mut C>),
|
||||
{
|
||||
tokio::pin!(connection);
|
||||
tokio::select! {
|
||||
biased;
|
||||
error = fault.wait() => return Err(error),
|
||||
result = &mut connection => return super::connection_result(result),
|
||||
_ = listener.closed() => {
|
||||
shutdown(connection.as_mut());
|
||||
return drain(connection.as_mut(), fault).await;
|
||||
}
|
||||
_ = first_request.wait() => {}
|
||||
_ = tokio::time::sleep(timeout) => return Ok(()),
|
||||
}
|
||||
tokio::select! {
|
||||
biased;
|
||||
error = fault.wait() => Err(error),
|
||||
result = &mut connection => super::connection_result(result),
|
||||
_ = listener.closed() => {
|
||||
shutdown(connection.as_mut());
|
||||
drain(connection.as_mut(), fault).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn drain<C>(mut connection: Pin<&mut C>, fault: &ScopeFault) -> Result<(), String>
|
||||
where
|
||||
C: Future<Output = Result<(), hyper::Error>>,
|
||||
{
|
||||
tokio::select! {
|
||||
biased;
|
||||
error = fault.wait() => Err(error),
|
||||
result = connection.as_mut() => super::connection_result(result),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn complete(owner: ScopedOwner, result: Result<(), String>) -> Result<(), String> {
|
||||
match result {
|
||||
Ok(()) => owner.finish().await,
|
||||
Err(error) => {
|
||||
let _ = owner.stop().await;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use std::future::Future;
|
||||
use std::io::{self, IoSlice};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::transport::Transport;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio::time::Sleep;
|
||||
|
||||
// Header/body 的期限由协议层持有;这里仅限制无法取得进展的写操作。
|
||||
pub(super) struct WriteTimeout {
|
||||
stream: Transport,
|
||||
timeout: Duration,
|
||||
deadline: Option<Pin<Box<Sleep>>>,
|
||||
}
|
||||
|
||||
impl WriteTimeout {
|
||||
pub(super) fn into_inner(self) -> Transport {
|
||||
self.stream
|
||||
}
|
||||
pub fn new(stream: Transport, timeout: Duration) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
timeout,
|
||||
deadline: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn output<T>(
|
||||
&mut self,
|
||||
context: &mut Context<'_>,
|
||||
write: impl FnOnce(Pin<&mut Transport>, &mut Context<'_>) -> Poll<io::Result<T>>,
|
||||
) -> Poll<io::Result<T>> {
|
||||
match write(Pin::new(&mut self.stream), context) {
|
||||
Poll::Ready(result) => {
|
||||
self.deadline = None;
|
||||
Poll::Ready(result)
|
||||
}
|
||||
Poll::Pending => {
|
||||
let deadline = self
|
||||
.deadline
|
||||
.get_or_insert_with(|| Box::pin(tokio::time::sleep(self.timeout)));
|
||||
match deadline.as_mut().poll(context) {
|
||||
Poll::Ready(()) => Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"HTTP write timed out",
|
||||
))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for WriteTimeout {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut Context<'_>,
|
||||
buffer: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_read(context, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for WriteTimeout {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut Context<'_>,
|
||||
bytes: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
self.output(context, |stream, context| stream.poll_write(context, bytes))
|
||||
}
|
||||
|
||||
fn poll_write_vectored(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut Context<'_>,
|
||||
buffers: &[IoSlice<'_>],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
self.output(context, |stream, context| {
|
||||
stream.poll_write_vectored(context, buffers)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
self.stream.is_write_vectored()
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.output(context, |stream, context| stream.poll_flush(context))
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.output(context, |stream, context| stream.poll_shutdown(context))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user