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