Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,83 @@
|
||||
// 正确性检查在计时外覆盖凭据、Provider、角色、路由及严格输入,不能用空 200 掩盖漏做。
|
||||
fn check_requests(connection: &mut HttpConnection, input: &AuthInput) {
|
||||
for request in &input.requests[..16] {
|
||||
check_reply(&connection.request(&request_bytes(request)), 200);
|
||||
}
|
||||
check_reply(
|
||||
&connection.request(&request_bytes(&input.requests[16])),
|
||||
401,
|
||||
);
|
||||
let valid = &input.requests[0];
|
||||
let mut missing = valid.clone();
|
||||
missing.headers_mut().remove("cookie");
|
||||
let mut invalid = valid.clone();
|
||||
invalid
|
||||
.headers_mut()
|
||||
.insert("cookie", "session=v1.invalid-jwt".parse().unwrap());
|
||||
let mut host = valid.clone();
|
||||
host.headers_mut()
|
||||
.insert("host", "other.test".parse().unwrap());
|
||||
let mut path = valid.clone();
|
||||
*path.uri_mut() = format!("{}-missing", path.uri()).parse().unwrap();
|
||||
let mut query = valid.clone();
|
||||
*query.uri_mut() = format!("{}?unexpected=1", query.uri()).parse().unwrap();
|
||||
for (request, status) in [
|
||||
(missing, 401),
|
||||
(invalid, 401),
|
||||
(host, 404),
|
||||
(path, 404),
|
||||
(query, 400),
|
||||
] {
|
||||
check_reply(&connection.request(&request_bytes(&request)), status);
|
||||
}
|
||||
}
|
||||
|
||||
fn membership(runtime: &Runtime, registry: &Arc<Registry>, input: &AuthInput, grant: bool) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[0], false, input.now)
|
||||
.unwrap();
|
||||
rust_verify(&mut context, claims.unwrap(), scope, registry)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(AUTH_BINDING, scope).await.unwrap();
|
||||
if grant {
|
||||
authorization::grant(executor, scope, &context, 16, "reader")
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
authorization::revoke(executor, scope, &context, 16, "reader")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn check(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
) {
|
||||
for generated in [true, false] {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
check_requests(&mut HttpConnection::connect(port), input);
|
||||
});
|
||||
}
|
||||
membership(runtime, registry, input, false);
|
||||
for generated in [true, false] {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
let mut connection = HttpConnection::connect(port);
|
||||
check_reply(
|
||||
&connection.request(&request_bytes(&input.requests[15])),
|
||||
403,
|
||||
);
|
||||
});
|
||||
}
|
||||
membership(runtime, registry, input, true);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// 客户端只连接本轮持有的回环监听器,使用同一 HTTP/1.1 长连接顺序发送请求。
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
net::TcpStream,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const ROUTE: &str = "/bench/account/admin/permissions";
|
||||
const CONTENT_TYPE: &str = "application/json; charset=utf-8";
|
||||
const SUCCESS: &str = "{\"code\":0,\"message\":\"ok\",\"data\":[{\"key\":\"bench.account.admin.permissions\",\"component\":\"bench\",\"domain\":\"account\",\"site\":\"admin\",\"action\":\"permissions\",\"method\":\"GET\",\"kind\":\"http\"}]}";
|
||||
|
||||
struct HttpReply {
|
||||
status: u16,
|
||||
content_type: String,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
struct HttpConnection(BufReader<TcpStream>);
|
||||
|
||||
impl HttpConnection {
|
||||
fn connect(port: u16) -> Self {
|
||||
let socket = TcpStream::connect(("127.0.0.1", port)).unwrap();
|
||||
socket.set_nodelay(true).unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
socket
|
||||
.set_write_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
Self(BufReader::new(socket))
|
||||
}
|
||||
|
||||
fn request(&mut self, request: &[u8]) -> HttpReply {
|
||||
self.0.get_mut().write_all(request).unwrap();
|
||||
let mut line = String::new();
|
||||
self.0.read_line(&mut line).unwrap();
|
||||
assert!(line.starts_with("HTTP/1.1 "));
|
||||
let status = line.split_whitespace().nth(1).unwrap().parse().unwrap();
|
||||
let mut length = None;
|
||||
let mut content_type = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
assert!(self.0.read_line(&mut line).unwrap() > 0);
|
||||
if line == "\r\n" {
|
||||
break;
|
||||
}
|
||||
let (name, value) = line.trim_end().split_once(':').unwrap();
|
||||
match name.to_ascii_lowercase().as_str() {
|
||||
"content-length" => {
|
||||
assert!(length.is_none());
|
||||
length = Some(value.trim().parse::<usize>().unwrap());
|
||||
}
|
||||
"content-type" => content_type = value.trim().into(),
|
||||
"connection" => assert_ne!(value.trim(), "close"),
|
||||
"transfer-encoding" | "set-cookie" => panic!("unexpected response header"),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
let mut body = vec![0; length.unwrap()];
|
||||
self.0.read_exact(&mut body).unwrap();
|
||||
HttpReply {
|
||||
status,
|
||||
content_type,
|
||||
body,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn request_bytes(request: &Request) -> Vec<u8> {
|
||||
assert!(request.body().is_empty());
|
||||
let mut bytes = format!("{} {} HTTP/1.1\r\n", request.method(), request.uri()).into_bytes();
|
||||
for (name, value) in request.headers() {
|
||||
bytes.extend_from_slice(name.as_str().as_bytes());
|
||||
bytes.extend_from_slice(b": ");
|
||||
bytes.extend_from_slice(value.as_bytes());
|
||||
bytes.extend_from_slice(b"\r\n");
|
||||
}
|
||||
bytes.extend_from_slice(b"Connection: keep-alive\r\nContent-Length: 0\r\n\r\n");
|
||||
bytes
|
||||
}
|
||||
|
||||
fn check_reply(reply: &HttpReply, status: u16) {
|
||||
assert_eq!(reply.status, status);
|
||||
assert_eq!(reply.content_type, CONTENT_TYPE);
|
||||
let expected = if status == 200 {
|
||||
SUCCESS.as_bytes().to_vec()
|
||||
} else {
|
||||
api::error(status).body().to_vec()
|
||||
};
|
||||
assert_eq!(reply.body, expected);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 两侧复用相同 HTTP 引擎、限额和调度策略;计时包含真实 JWT 校验、三次数据库读取及响应。
|
||||
fn with_server<Value>(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
generated: bool,
|
||||
client: impl FnOnce(u16) -> Value,
|
||||
) -> Value {
|
||||
let registry = registry.clone();
|
||||
let config = config.clone();
|
||||
let authentication = input.service.clone();
|
||||
std::thread::scope(|threads| {
|
||||
let (started, port) = std::sync::mpsc::sync_channel(0);
|
||||
let (finished, completion) = tokio::sync::oneshot::channel();
|
||||
let serving = threads.spawn(move || {
|
||||
runtime
|
||||
.run(async move |scope| {
|
||||
let listener = dever_runtime::net::listen("127.0.0.1", 0, scope)
|
||||
.await
|
||||
.unwrap();
|
||||
let port = listener.port().unwrap() as u16;
|
||||
let incoming = listener.clone();
|
||||
let server = scope
|
||||
.spawn(move |child| async move {
|
||||
let limits = dever_runtime::http::Limits {
|
||||
connections: 1,
|
||||
..Default::default()
|
||||
};
|
||||
api::serve::<_, _, true>(
|
||||
&incoming,
|
||||
&limits,
|
||||
&child,
|
||||
None,
|
||||
move |request, scope, peer| {
|
||||
let config = config.clone();
|
||||
let registry = registry.clone();
|
||||
let authentication = authentication.clone();
|
||||
async move {
|
||||
let path = request.uri().path();
|
||||
let response = match (request.method().as_str(), path) {
|
||||
("GET", ROUTE) if config.accepts("admin", &request) => {
|
||||
if generated {
|
||||
GENERATED_HTTP(
|
||||
&request,
|
||||
peer,
|
||||
&scope,
|
||||
®istry,
|
||||
&authentication,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
rust_http(
|
||||
&request,
|
||||
peer,
|
||||
&scope,
|
||||
®istry,
|
||||
&authentication,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
_ => api::error(404),
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
.unwrap();
|
||||
// 阻塞客户端独立于运行时线程,也不占用数据库的阻塞池;交接在计时外。
|
||||
started.send(port).unwrap();
|
||||
completion.await.unwrap();
|
||||
listener.close().unwrap();
|
||||
match server.timeout(5000).await.unwrap() {
|
||||
dever_runtime::task::Timed::Ready(result) => result.unwrap(),
|
||||
_ => panic!("owned HTTP benchmark server failed to drain"),
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
let result = client(port.recv().unwrap());
|
||||
finished.send(()).unwrap();
|
||||
serving.join().unwrap();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn run(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
) {
|
||||
check(runtime, registry, config, input);
|
||||
eprintln!(
|
||||
"HTTP/1.1 loopback; one persistent connection; rotating 16 JWT cookies; JWT + Provider + role gate + permission list + JSON response included; TCP connect and response assertions excluded; no TLS/proxy/concurrent-load claim"
|
||||
);
|
||||
let requests = input.requests[..16]
|
||||
.iter()
|
||||
.map(request_bytes)
|
||||
.collect::<Vec<_>>();
|
||||
samples("http_jwt_provider_roles", |generated, count| {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
let mut connection = HttpConnection::connect(port);
|
||||
check_reply(&connection.request(&requests[0]), 200);
|
||||
let mut replies = Vec::with_capacity(count);
|
||||
let start = Instant::now();
|
||||
for index in 0..count {
|
||||
replies.push(connection.request(black_box(&requests[index % requests.len()])));
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
let mut checksum = 0u64;
|
||||
for reply in &replies {
|
||||
check_reply(reply, 200);
|
||||
checksum += black_box(reply.body.iter().map(|byte| u64::from(*byte)).sum::<u64>());
|
||||
}
|
||||
(elapsed, checksum)
|
||||
})
|
||||
});
|
||||
check(runtime, registry, config, input);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 手写 Rust 完成同样的请求边界、Provider、权限门禁和业务返回,不调用生成 API。
|
||||
use dever_runtime::{api, authorization, wire};
|
||||
use wire::serde::{self, Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct RustPermission {
|
||||
key: String,
|
||||
component: String,
|
||||
domain: String,
|
||||
site: String,
|
||||
action: String,
|
||||
method: String,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct EmptyFields {}
|
||||
|
||||
struct EmptyInput;
|
||||
|
||||
impl<'input> Deserialize<'input> for EmptyInput {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
let _: EmptyFields = wire::object(decoder)?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
fn access_failure(error: authorization::Failure) -> ApiFailure {
|
||||
match error {
|
||||
authorization::Failure::Access(status) => ApiFailure(status),
|
||||
authorization::Failure::Database(_) => ApiFailure(500),
|
||||
}
|
||||
}
|
||||
|
||||
async fn rust_http(
|
||||
request: &Request,
|
||||
peer: std::net::SocketAddr,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
authentication: &Arc<auth::Service>,
|
||||
) -> Response {
|
||||
let now = match auth::now() {
|
||||
Ok(now) => now,
|
||||
Err(error) => return api::error(error.status()),
|
||||
};
|
||||
let (mut context, claims) = match authentication.prepare_at("admin", request, false, now, peer)
|
||||
{
|
||||
Ok(prepared) => prepared,
|
||||
Err(error) => return api::error(error.status()),
|
||||
};
|
||||
if let Some(claims) = claims {
|
||||
if let Err(error) = rust_verify(&mut context, claims, scope, registry).await {
|
||||
return error.response();
|
||||
}
|
||||
}
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let outcome = async {
|
||||
authorization::authorize(
|
||||
&mut database,
|
||||
AUTH_BINDING,
|
||||
scope,
|
||||
&context,
|
||||
"bench",
|
||||
"account",
|
||||
"permissions",
|
||||
)
|
||||
.await
|
||||
.map_err(access_failure)?;
|
||||
let input = api::input(request).map_err(ApiFailure)?;
|
||||
let _: EmptyInput = input
|
||||
.decode(dever_runtime::wire::decode)
|
||||
.map_err(|_| ApiFailure(400))?;
|
||||
let values = authorization::permissions(&mut database, AUTH_BINDING, scope, &context, true)
|
||||
.await
|
||||
.map_err(access_failure)?
|
||||
.into_iter()
|
||||
.map(|value| RustPermission {
|
||||
key: value.key,
|
||||
component: value.component,
|
||||
domain: value.domain,
|
||||
site: value.site,
|
||||
action: value.action,
|
||||
method: value.method,
|
||||
kind: value.kind,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
wire::response(&values).map_err(ApiFailure::from)
|
||||
}
|
||||
.await;
|
||||
match outcome {
|
||||
Ok(body) => {
|
||||
let mut response = api::response(body);
|
||||
context.publish(&mut response);
|
||||
response
|
||||
}
|
||||
Err(error) => error.response(),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user