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,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())?
|
||||
}
|
||||
Reference in new issue
Block a user