Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
use super::{Header, headers};
use crate::http::Limits;
use std::{hint::black_box, time::Instant};
#[test]
fn request_and_response_preserve_shared_body_storage() {
let shared = bytes::Bytes::from(vec![0x80; 65536]);
let request = hyper::Request::builder()
.method("POST")
.uri("/echo")
.header("host", "localhost")
.body(shared.clone())
.unwrap();
let request = super::request_value(request);
assert_eq!(request.body.as_ptr(), shared.as_ptr());
let response = super::response_value(super::Response {
status: 200,
headers: Vec::new(),
body: request.body,
})
.unwrap();
assert_eq!(response.body().as_ptr(), shared.as_ptr());
assert_eq!(response.body(), &shared);
}
#[test]
#[ignore = "explicit release performance probe"]
fn header_buffer_cost() {
let limits = Limits {
header_bytes: 131072,
..Limits::default()
}
.check()
.unwrap();
println!("\ncase,sample,iterations,elapsed_ns,checksum");
for (name, count, bytes) in [("short", 8, 32), ("wide", 32, 32), ("large", 4, 16384)] {
let input = (0..count)
.map(|index| Header {
name: format!("x-field-{index}"),
value: vec![0x80; bytes].into(),
})
.collect::<Vec<_>>();
let converted = headers(input.clone(), &limits, false, 80).unwrap();
for field in &input {
assert_eq!(
converted.get(field.name.as_str()).unwrap().as_bytes(),
field.value
);
}
let convert = || {
let output = headers(black_box(input.clone()), &limits, false, 80).unwrap();
black_box(output.values().map(|value| value.len()).sum::<usize>())
};
for _ in 0..100 {
assert_eq!(convert(), count * bytes);
}
for sample in 0..15 {
let started = Instant::now();
let mut checksum = 0;
for _ in 0..1000 {
checksum += convert();
}
let elapsed = started.elapsed().as_nanos();
assert_eq!(checksum, count * bytes * 1000);
println!("http_headers_{name},{sample},1000,{elapsed},{checksum}");
}
}
}
+206
View File
@@ -0,0 +1,206 @@
use super::*;
use http_body_util::{BodyExt, Full};
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::sync::Notify;
fn limits() -> Limits {
Limits {
http2: Some(http::Http2Limits::default()),
..Limits::default()
}
}
#[test]
fn idle_http2_connection_releases_capacity_after_response() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let limits = Limits {
connections: 1,
timeout_ms: 50,
..limits()
};
let serving = limits.clone();
let server = scope
.spawn(move |child| async move {
http::serve::<_, _, false>(&incoming, &serving, &child, route).await
})
.unwrap();
let stream = tokio::net::TcpStream::connect(("127.0.0.1", port as u16))
.await
.unwrap();
let (mut sender, connection) =
hyper::client::conn::http2::Builder::new(TokioExecutor::new())
.handshake::<_, Full<bytes::Bytes>>(TokioIo::new(stream))
.await
.unwrap();
let driver = scope.spawn_leaf(move || connection).unwrap();
let response = sender
.send_request(
hyper::Request::builder()
.uri("http://fixture.test/echo")
.body(Full::new(bytes::Bytes::from_static(b"first")))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.into_body().collect().await.unwrap().to_bytes(),
b"first"[..]
);
tokio::time::timeout(Duration::from_secs(2), driver.wait())
.await
.unwrap()
.unwrap()
.unwrap();
assert_eq!(
http::send("127.0.0.1", port, request("/echo", b"second"), &limits)
.await
.unwrap()
.body
.as_ref(),
b"second"
);
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn http2_single_request_preserves_body_headers_and_validates_protocol_options() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let server = scope
.spawn(move |child| async move {
http::serve::<_, _, false>(&incoming, &limits(), &child, route).await
})
.unwrap();
let response = http::send("127.0.0.1", port, request("/echo", b"http2"), &limits())
.await
.unwrap();
assert_eq!(response.body.as_ref(), b"http2");
let headers = response
.headers
.iter()
.filter(|header| header.name == "x-data")
.collect::<Vec<_>>();
assert_eq!(headers.len(), 2);
assert_eq!(headers[0].value.as_ref(), [0x80]);
for header in ["Connection", "Keep-Alive", "Proxy-Connection", "TE"] {
let mut message = request("/", b"");
message.headers.push(Header {
name: header.into(),
value: b"invalid".to_vec().into(),
});
assert!(
http::send("127.0.0.1", port, message, &limits())
.await
.unwrap_err()
.contains("connection header")
);
}
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn http2_multiplexes_independent_streams_normalizes_authority_and_drains_goaway() {
for threads in [1, 2] {
let runtime = Runtime::new(dever_runtime::task::Limits {
threads,
workers: 2,
tasks: 32,
})
.unwrap();
runtime.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let started = Arc::new(Notify::new());
let ready = started.clone();
let release = Arc::new(Notify::new());
let released = release.clone();
let server = scope.spawn(move |child| async move {
http::serve::<_, _, false>(&incoming, &limits(), &child, move |request, _| {
let ready = ready.clone();
let released = released.clone();
async move {
assert!(request.headers.iter().any(|header| header.name == "host" && header.value.as_ref() == b"fixture.test"));
if request.target == "/slow" {
ready.notify_one();
released.notified().await;
}
Ok(Response { status: 200, headers: vec![], body: request.target.into_bytes().into() })
}
}).await
}).unwrap();
let stream = tokio::net::TcpStream::connect(("127.0.0.1", port as u16)).await.unwrap();
let (mut sender, connection) = hyper::client::conn::http2::Builder::new(TokioExecutor::new())
.handshake::<_, Full<bytes::Bytes>>(TokioIo::new(stream)).await.unwrap();
let driver = scope.spawn_leaf(move || connection).unwrap();
let message = |path: &str| hyper::Request::builder().uri(format!("http://fixture.test{path}"))
.body(Full::new(bytes::Bytes::new())).unwrap();
let slow = sender.send_request(message("/slow"));
tokio::pin!(slow);
tokio::select! {
result = &mut slow => panic!("slow stream finished before release: {result:?}"),
() = started.notified() => (),
() = tokio::time::sleep(Duration::from_secs(3)) => panic!("slow stream did not start"),
}
let fast = tokio::time::timeout(Duration::from_secs(3), sender.send_request(message("/fast"))).await.unwrap().unwrap();
assert_eq!(fast.into_body().collect().await.unwrap().to_bytes(), b"/fast"[..]);
listener.close().unwrap();
release.notify_one();
let slow = slow.await.unwrap();
assert_eq!(slow.into_body().collect().await.unwrap().to_bytes(), b"/slow"[..]);
drop(sender);
tokio::time::timeout(Duration::from_secs(3), server.wait()).await.unwrap().unwrap().unwrap();
driver.stop().await.unwrap();
}).unwrap();
}
}
#[test]
fn http2_limits_reject_invalid_windows_and_total_stream_capacity() {
runtime()
.run(async |_| {
for http2 in [
http::Http2Limits {
streams: 2049,
..http::Http2Limits::default()
},
http::Http2Limits {
streams: 0,
..http::Http2Limits::default()
},
http::Http2Limits {
stream_window_bytes: 0,
..http::Http2Limits::default()
},
http::Http2Limits {
connection_window_bytes: 65534,
..http::Http2Limits::default()
},
] {
let limits = Limits {
http2: Some(http2),
..Limits::default()
};
assert!(
http::send("127.0.0.1", 1, request("/", b""), &limits)
.await
.unwrap_err()
.contains("HTTP/2 limits")
);
}
})
.unwrap();
}
+304
View File
@@ -0,0 +1,304 @@
use super::*;
use crate::wire::document::{MAX_BYTES, MAX_VALUES};
#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
struct Arguments {
name: String,
count: i64,
enabled: bool,
}
#[test]
#[ignore = "explicit release performance probe"]
fn form_decode_cost() {
use std::{hint::black_box, time::Instant};
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
for bytes in [64, 4096, 65536] {
let text = "a".repeat(bytes);
for sample in 0..15 {
for offset in 0..2 {
let native = (sample + offset) % 2 == 1;
let started = Instant::now();
let mut checksum = 0;
for _ in 0..1000 {
let fields = [
("name".into(), black_box(&text).clone()),
("count".into(), "-7".into()),
("enabled".into(), "true".into()),
]
.into_iter()
.collect();
let result: Arguments = if native {
native_form(fields).unwrap()
} else {
Input::fields(fields).decode(crate::wire::decode).unwrap()
};
assert_eq!(result.count, -7);
assert!(result.enabled);
checksum += black_box(result.name.len());
}
let elapsed = started.elapsed().as_nanos();
let implementation = if native { "rust" } else { "dever" };
println!("form_{bytes},{sample},{implementation},1000,{elapsed},{checksum}");
}
}
}
}
fn native_form(mut fields: BTreeMap<String, String>) -> Option<Arguments> {
if fields.len() != 3 {
return None;
}
let result = Arguments {
name: fields.remove("name")?,
count: fields.remove("count")?.parse().ok()?,
enabled: fields.remove("enabled")?.parse().ok()?,
};
// 字段名固定;Int、Bool 和标点合计不超过 60 字节。
if result.name.len().saturating_mul(6).saturating_add(60) > MAX_BYTES
&& serde_json::to_vec(&result).ok()?.len() > MAX_BYTES
{
return None;
}
Some(result)
}
#[test]
fn form_fields_use_target_types_without_url_decoding() {
let fields = BTreeMap::from([
("name".into(), "%FF+雪".into()),
("count".into(), "-7".into()),
("enabled".into(), "true".into()),
]);
let parsed: Arguments = Input::fields(fields.clone())
.decode(crate::wire::decode)
.unwrap();
assert_eq!(parsed, native_form(fields.clone()).unwrap());
assert_eq!(parsed.name, "%FF+雪");
for (name, value) in [("count", "1.5"), ("enabled", "1"), ("extra", "x")] {
let mut invalid = fields.clone();
invalid.insert(name.into(), value.into());
assert!(
Input::fields(invalid.clone())
.decode::<Arguments>(crate::wire::decode)
.is_err()
);
assert!(native_form(invalid).is_none());
}
let mut missing = fields;
missing.remove("enabled");
assert!(
Input::fields(missing)
.decode::<Arguments>(crate::wire::decode)
.is_err()
);
}
fn query_request(query: &str) -> Request {
hyper::Request::builder()
.uri(format!("/test?{query}"))
.body(Default::default())
.unwrap()
}
#[test]
fn scalar_fields_decode_without_changing_argument_rules() {
let request = query_request("name=%E9%9B%AA+test&count=-7&enabled=true");
assert_eq!(
input(&request)
.unwrap()
.decode::<Arguments>(crate::wire::decode)
.unwrap(),
Arguments {
name: "雪 test".into(),
count: -7,
enabled: true
}
);
for query in [
"name=x&name=y",
"unknown=x",
"count=1.5",
"enabled=1",
"name=%FF",
"name=%",
] {
assert!(
input(&query_request(query))
.unwrap()
.decode::<Arguments>(crate::wire::decode)
.is_err(),
"{query}"
);
}
assert!(
input(&query_request("name=x"))
.unwrap()
.decode::<Arguments>(crate::wire::decode)
.is_err()
);
}
#[test]
fn form_budgets_count_json_escaping_and_root_value() {
let mut fields = BTreeMap::new();
fields.insert("text".into(), "\0".repeat(MAX_BYTES / 6));
assert!(
Input::fields(fields)
.decode::<BTreeMap<String, String>>(crate::wire::decode)
.is_err()
);
let fields = (0..MAX_VALUES - 1)
.map(|index| (index.to_string(), "false".to_owned()))
.collect::<BTreeMap<_, _>>();
assert!(
Input::fields(fields.clone())
.decode::<BTreeMap<String, String>>(crate::wire::decode)
.is_ok()
);
let mut fields = fields;
fields.insert("extra".into(), "true".to_owned());
assert!(
Input::fields(fields)
.decode::<BTreeMap<String, String>>(crate::wire::decode)
.is_err()
);
}
#[test]
fn query_rejects_bad_trailing_optional_fields_and_non_finite_numbers() {
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Scalars {
ratio: f64,
tag: Option<String>,
}
for query in [
"ratio=1&tag=%",
"ratio=1&tag=%FF",
"ratio=1&tag",
"ratio=1&tag=a&tag=b",
"ratio=1&extra=x",
"ratio=NaN",
"ratio=inf",
"ratio=1e999",
] {
assert!(
input(&query_request(query))
.unwrap()
.decode::<Scalars>(crate::wire::decode)
.is_err(),
"{query}"
);
}
let result: Scalars = input(&query_request("ratio=-0&tag=%E9%9B%AA+test"))
.unwrap()
.decode(crate::wire::decode)
.unwrap();
assert_eq!(result.ratio.to_bits(), (-0.0f64).to_bits());
assert_eq!(result.tag.as_deref(), Some("雪 test"));
}
#[test]
fn large_query_uses_exact_existing_json_byte_budget() {
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct TextArgument {
text: String,
}
let overhead = serde_json::to_vec(&serde_json::json!({"text":""}))
.unwrap()
.len();
for (plain, count) in [
(true, MAX_BYTES / 6 + 1),
(true, MAX_BYTES - overhead),
(true, MAX_BYTES - overhead + 1),
(false, (MAX_BYTES - overhead) / 6),
(false, (MAX_BYTES - overhead) / 6 + 1),
] {
let text = if plain { "a" } else { "\0" }.repeat(count);
let encoded = if plain { "a" } else { "%00" }.repeat(count);
let source = serde_json::to_string(&serde_json::json!({"text": text})).unwrap();
let expected = crate::wire::decode::<TextArgument>(&source);
let fields = BTreeMap::from([("text".into(), text)]);
let form = Input::fields(fields).decode::<TextArgument>(crate::wire::decode);
assert_eq!(form.is_ok(), expected.is_ok());
if let (Ok(form), Ok(expected)) = (&form, &expected) {
assert_eq!(form.text, expected.text);
}
// 此处独立验证字段 codec 的 JSON 预算;超长 URI 会先被 HTTP 库拒绝。
let query = format!("text={encoded}");
let actual = Input(Data::Query(&query)).decode::<TextArgument>(crate::wire::decode);
assert_eq!(
actual.is_ok(),
expected.is_ok(),
"plain={plain}, count={count}"
);
if let (Ok(actual), Ok(expected)) = (actual, expected) {
assert_eq!(actual.text, expected.text);
}
}
}
#[cfg(all(feature = "calendar", feature = "uuid"))]
#[test]
fn query_uses_existing_scalar_codecs_and_optional_types() {
use crate::{
time::calendar::{Date, DateTime, Duration, Time},
value::{decimal::Decimal, id::Id, uuid::Uuid},
};
#[derive(Debug, PartialEq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Scalars {
decimal: Decimal,
id: Id,
uuid: Uuid,
date: Date,
time: Time,
datetime: DateTime,
duration: Duration,
count: Option<i64>,
enabled: Option<bool>,
ratio: Option<f64>,
}
let fields = serde_json::json!({
"decimal": "123456789.0123456789",
"id": "雪",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"date": "2026-10-26",
"time": "12:34:56.789",
"datetime": "2026-10-26T12:34:56Z",
"duration": -1500,
"count": 7,
"enabled": true,
"ratio": 1.25
});
let expected: Scalars = crate::wire::decode(&fields.to_string()).unwrap();
let query = "decimal=123456789.0123456789&id=%E9%9B%AA&uuid=550e8400-e29b-41d4-a716-446655440000&date=2026-10-26&time=12:34:56.789&datetime=2026-10-26T12:34:56Z&duration=-1500&count=7&enabled=true&ratio=1.25";
let actual: Scalars = input(&query_request(query))
.unwrap()
.decode(crate::wire::decode)
.unwrap();
assert_eq!(actual, expected);
for (valid, invalid) in [
("123456789.0123456789", "NaN"),
("550e8400-e29b-41d4-a716-446655440000", "bad-uuid"),
("date=2026-10-26", "date=2026-02-30"),
("time=12:34:56.789", "time=25:34:56.789"),
("duration=-1500", "duration=1.5"),
("count=7", "count=9223372036854775808"),
("enabled=true", "enabled=yes"),
("ratio=1.25", "ratio=NaN"),
] {
assert!(
input(&query_request(&query.replace(valid, invalid)))
.unwrap()
.decode::<Scalars>(crate::wire::decode)
.is_err(),
"{invalid}"
);
}
}
+311
View File
@@ -0,0 +1,311 @@
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Notify;
struct Running(Arc<AtomicUsize>);
impl Drop for Running {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::SeqCst);
}
}
#[test]
fn timeout_and_stop_drain_handler_descendants() {
runtime()
.run(async |scope| {
for cancel in [false, true] {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let running = Arc::new(AtomicUsize::new(0));
let live = running.clone();
let started = Arc::new(Notify::new());
let ready = started.clone();
let server = scope
.spawn(move |child| async move {
let limits = Limits {
timeout_ms: if cancel { 5000 } else { 100 },
..Limits::default()
};
http::serve::<_, _, true>(
&incoming,
&limits,
&child,
move |request, scope| {
let live = live.clone();
let ready = ready.clone();
async move {
let pending = scope
.spawn_leaf(move || async move {
live.fetch_add(1, Ordering::SeqCst);
let _running = Running(live);
ready.notify_one();
std::future::pending::<()>().await;
})
.unwrap();
pending.wait().await.unwrap();
route(request, scope).await
}
},
)
.await
})
.unwrap();
let client = scope
.spawn_leaf(move || async move {
http::send("127.0.0.1", port, request("/", b""), &Limits::default()).await
})
.unwrap();
tokio::time::timeout(Duration::from_secs(3), started.notified())
.await
.unwrap();
assert_eq!(running.load(Ordering::SeqCst), 1);
if cancel {
server.stop().await.unwrap();
assert!(client.wait().await.unwrap().is_err());
listener.close().unwrap();
} else {
assert_eq!(client.wait().await.unwrap().unwrap().status, 504);
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
}
assert_eq!(running.load(Ordering::SeqCst), 0);
}
})
.unwrap();
}
#[test]
fn closing_listener_drains_an_inflight_response() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let started = Arc::new(Notify::new());
let ready = started.clone();
let release = Arc::new(Notify::new());
let released = release.clone();
let server = scope
.spawn(move |child| async move {
http::serve::<_, _, false>(
&incoming,
&Limits::default(),
&child,
move |request, scope| {
let ready = ready.clone();
let released = released.clone();
async move {
ready.notify_one();
released.notified().await;
route(request, scope).await
}
},
)
.await
})
.unwrap();
let client = scope
.spawn_leaf(move || async move {
http::send(
"127.0.0.1",
port,
request("/", b"drained"),
&Limits::default(),
)
.await
})
.unwrap();
tokio::time::timeout(Duration::from_secs(3), started.notified())
.await
.unwrap();
listener.close().unwrap();
release.notify_one();
assert_eq!(
client.wait().await.unwrap().unwrap().body.as_ref(),
b"drained"
);
tokio::time::timeout(Duration::from_secs(3), server.wait())
.await
.unwrap()
.unwrap()
.unwrap();
})
.unwrap();
}
#[test]
fn client_deadline_drops_its_connection_driver() {
runtime()
.run(async |scope| {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = i64::from(listener.local_addr().unwrap().port());
let peer = scope
.spawn(move |child| async move {
let (stream, _) = listener.accept().await.unwrap();
let mut stream = stream.into_std().unwrap();
stream.set_nonblocking(false).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
child
.blocking(move || {
let mut data = Vec::new();
stream.read_to_end(&mut data).unwrap();
assert!(data.starts_with(b"POST / HTTP/1.1"));
})
.await
.unwrap();
})
.unwrap();
let limits = Limits {
timeout_ms: 100,
..Limits::default()
};
let error = http::send("127.0.0.1", port, request("/", b""), &limits)
.await
.unwrap_err();
assert_eq!(error, "HTTP request timed out");
peer.wait().await.unwrap();
})
.unwrap();
}
#[test]
fn invalid_service_configuration_keeps_listener_available() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let limits = Limits {
connections: 0,
..Limits::default()
};
assert_eq!(
http::serve::<_, _, false>(&listener, &limits, scope, route).await,
Err(http::ServeError::Limits)
);
assert!(listener.port().unwrap() > 0);
listener.close().unwrap();
assert_eq!(
http::serve::<_, _, false>(&listener, &Limits::default(), scope, route).await,
Err(http::ServeError::Listener)
);
})
.unwrap();
}
#[test]
fn exhausted_task_pool_preserves_connections_and_resumes_accepting() {
capacity_recovery(CapacityAction::Resume);
}
#[test]
fn closing_listener_interrupts_waiting_for_task_capacity() {
capacity_recovery(CapacityAction::Close);
}
#[test]
fn cancelling_server_interrupts_waiting_for_task_capacity() {
capacity_recovery(CapacityAction::Cancel);
}
enum CapacityAction {
Resume,
Close,
Cancel,
}
fn capacity_recovery(action: CapacityAction) {
use http_body_util::{BodyExt, Empty};
use hyper_util::rt::TokioIo;
Runtime::new(dever_runtime::task::Limits {
threads: 2,
workers: 1,
tasks: 3,
})
.unwrap()
.run(async |scope| {
let listener = 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 {
http::serve::<_, _, false>(&incoming, &Limits::default(), &child, route).await
})
.unwrap();
let mut clients = Vec::new();
let mut drivers = Vec::new();
for _ in 0..3 {
let socket = tokio::net::TcpStream::connect(("127.0.0.1", port))
.await
.unwrap();
let (sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(socket))
.await
.unwrap();
clients.push(sender);
drivers.push(tokio::spawn(connection));
}
let request = || {
hyper::Request::builder()
.uri("/echo")
.header("host", "fixture.test")
.body(Empty::<bytes::Bytes>::new())
.unwrap()
};
for client in &mut clients[..2] {
let response = client.send_request(request()).await.unwrap();
assert_eq!(response.status(), 200);
response.into_body().collect().await.unwrap();
}
let mut waiting = Box::pin(clients[2].send_request(request()));
assert!(
tokio::time::timeout(Duration::from_millis(100), &mut waiting)
.await
.is_err()
);
if matches!(action, CapacityAction::Resume) {
let response = clients[0].send_request(request()).await.unwrap();
assert_eq!(response.status(), 200);
response.into_body().collect().await.unwrap();
drivers[0].abort();
let response = tokio::time::timeout(Duration::from_secs(3), &mut waiting)
.await
.unwrap()
.unwrap();
assert_eq!(response.status(), 200);
response.into_body().collect().await.unwrap();
}
if matches!(action, CapacityAction::Cancel) {
tokio::time::timeout(Duration::from_secs(3), server.stop())
.await
.unwrap()
.unwrap();
listener.close().unwrap();
} else {
listener.close().unwrap();
tokio::time::timeout(Duration::from_secs(3), server.wait())
.await
.unwrap()
.unwrap()
.unwrap();
}
drop(waiting);
drop(clients);
for driver in drivers {
driver.abort();
let _ = driver.await;
}
assert_eq!(
scope
.spawn_leaf(|| async { 42 })
.unwrap()
.wait()
.await
.unwrap(),
42
);
})
.unwrap();
}
+103
View File
@@ -0,0 +1,103 @@
use super::*;
#[test]
fn body_budgets_timeouts_invalid_messages_and_recovery_are_bounded() {
runtime().run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let server = scope.spawn(move |child| async move {
let limits = Limits { body_bytes: 4, timeout_ms: 100, ..Limits::default() };
http::serve::<_, _, false>(&incoming, &limits, &child, route).await
}).unwrap();
scope.blocking(move || {
let query_only = format!("GET ?{} HTTP/1.1\r\nHost: t\r\n\r\n", "a".repeat(8163));
for (message, expected) in [
("POST /echo HTTP/1.1\r\nHost: fixture.test\r\nContent-Length: 5\r\n\r\n12345", 413),
("POST /echo HTTP/1.1\r\nHost: fixture.test\r\nContent-Length: 4\r\n\r\n1", 408),
("GET /wait HTTP/1.1\r\nHost: fixture.test\r\n\r\n", 504),
("GET /echo HTTP/1.1\r\nHost: one\r\nHost: two\r\n\r\n", 400),
(query_only.as_str(), 400),
] {
let mut stream = connect(port);
stream.get_mut().write_all(message.as_bytes()).unwrap();
assert_eq!(read_response(&mut stream, false).0, expected);
assert_eq!(stream.read(&mut [0]).unwrap(), 0);
}
let mut stream = connect(port);
stream.get_mut().write_all(b"GET / HTTP/1.1\r\nHost:").unwrap();
assert_eq!(stream.read(&mut [0]).unwrap(), 0);
}).await.unwrap();
let response = http::send("127.0.0.1", port, request("/echo", b"ok"), &Limits::default()).await.unwrap();
assert_eq!(response.body.as_ref(), b"ok");
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
}).unwrap();
}
#[test]
fn outgoing_contracts_reject_framing_injection_and_bad_limits() {
runtime()
.run(async |_| {
for header in [
Header {
name: "Content-Length".into(),
value: b"0".to_vec().into(),
},
Header {
name: "Transfer-Encoding".into(),
value: b"chunked".to_vec().into(),
},
Header {
name: "x-test".into(),
value: b"a\r\nb".to_vec().into(),
},
] {
let mut message = request("/", b"");
message.headers.push(header);
let error = http::send("127.0.0.1", 1, message, &Limits::default())
.await
.unwrap_err();
assert!(!error.contains("connection failed"), "{error}");
}
for limits in [
Limits {
header_bytes: 0,
..Limits::default()
},
Limits {
body_bytes: -1,
..Limits::default()
},
Limits {
timeout_ms: 0,
..Limits::default()
},
Limits {
connections: 65537,
..Limits::default()
},
] {
assert!(
http::send("127.0.0.1", 1, request("/", b""), &limits)
.await
.is_err()
);
}
let long_target = format!("/{}", "x".repeat(8192));
for (method, target) in [
("CONNECT", "/"),
("GET", "http://example.test/"),
("GET", "/#fragment"),
("GET", long_target.as_str()),
] {
let mut message = request(target, b"");
message.method = method.into();
let error = http::send("127.0.0.1", 1, message, &Limits::default())
.await
.unwrap_err();
assert!(!error.contains("connection failed"), "{error}");
}
})
.unwrap();
}
+233
View File
@@ -0,0 +1,233 @@
use super::*;
use tokio::sync::Notify;
#[test]
fn head_and_disconnect_cancel_requests_before_reusing_the_connection() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let incoming = listener.clone();
let active = Arc::new(AtomicUsize::new(0));
let running = active.clone();
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&Limits::default(),
&http::LiveLimits::default(),
&child,
move |request, reply, _| {
let active = active.clone();
async move {
assert_eq!(active.fetch_add(1, Ordering::SeqCst), 0);
let _running = Running(active);
reply.start(200, vec![]).await.unwrap();
if request.method == "HEAD" {
std::future::pending::<()>().await;
}
reply.write(b"data").await.unwrap();
std::future::pending::<Result<(), String>>().await
}
},
)
.await
})
.unwrap();
let (mut sender, driver) = client(listener.port().unwrap(), scope).await;
let response = sender.send_request(message("HEAD")).await.unwrap();
assert!(
response
.into_body()
.collect()
.await
.unwrap()
.to_bytes()
.is_empty()
);
assert_eq!(running.load(Ordering::SeqCst), 0);
let response = sender.send_request(message("GET")).await.unwrap();
let mut body = response.into_body();
assert_eq!(
body.frame().await.unwrap().unwrap().into_data().unwrap(),
b"data"[..]
);
drop(body);
drop(sender);
driver.wait().await.unwrap();
tokio::time::timeout(Duration::from_secs(2), async {
while running.load(Ordering::SeqCst) != 0 {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn slow_peer_bounds_writer_and_parent_stop_drains_blocking_descendants() {
for sse in [false, true] {
slow_peer_case(sse);
}
}
fn slow_peer_case(sse: bool) {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let incoming = listener.clone();
let count = Arc::new(AtomicUsize::new(0));
let written = count.clone();
let active = Arc::new(AtomicUsize::new(0));
let running = active.clone();
let ready = Arc::new(Notify::new());
let started = ready.clone();
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&Limits {
timeout_ms: 5000,
..Limits::default()
},
&http::LiveLimits::default(),
&child,
move |_, reply, scope| {
let written = written.clone();
let active = active.clone();
let ready = ready.clone();
async move {
let worker = scope
.spawn(move |child| async move {
child
.blocking(move || {
active.fetch_add(1, Ordering::SeqCst);
let _running = Running(active);
ready.notify_one();
std::thread::sleep(Duration::from_millis(400));
})
.await
})
.unwrap();
if sse {
reply.sse_start(vec![]).await.unwrap();
} else {
reply.start(200, vec![]).await.unwrap();
}
let bytes = vec![0; 65536];
let event = http::Event {
event: String::new(),
data: "a".repeat(65528),
id: None,
retry_ms: None,
};
for _ in 0..10000 {
let result = if sse {
reply.sse_send(&event).await
} else {
reply.write(&bytes).await
};
result.map_err(|error| error.to_string())?;
written.fetch_add(1, Ordering::SeqCst);
}
worker.wait().await.unwrap().unwrap();
Ok(())
}
},
)
.await
})
.unwrap();
let mut socket =
tokio::net::TcpStream::connect(("127.0.0.1", listener.port().unwrap() as u16))
.await
.unwrap();
use tokio::io::AsyncWriteExt;
socket
.write_all(b"GET / HTTP/1.1\r\nHost: fixture.test\r\n\r\n")
.await
.unwrap();
started.notified().await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(count.load(Ordering::SeqCst) < 10000);
server.stop().await.unwrap();
assert_eq!(running.load(Ordering::SeqCst), 0);
listener.close().unwrap();
})
.unwrap();
}
#[test]
fn live_header_and_idle_deadlines_are_distinct_and_faults_truncate() {
runtime()
.run(async |scope| {
for mode in ["headers", "idle", "fault", "missing"] {
let gate = Arc::new(Semaphore::new(0));
let failed = gate.clone();
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let incoming = listener.clone();
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&Limits {
timeout_ms: 100,
..Limits::default()
},
&http::LiveLimits {
idle_ms: 60,
heartbeat_ms: 20,
chunk_bytes: 64,
},
&child,
move |_, reply, _| {
let gate = failed.clone();
async move {
if mode == "missing" {
return Ok(());
}
if mode == "headers" {
std::future::pending::<()>().await;
}
reply.start(200, vec![]).await.unwrap();
if mode == "idle" {
std::future::pending::<()>().await;
}
reply.write(b"partial").await.unwrap();
let _permit = gate.acquire().await.unwrap();
Err("located handler failure".into())
}
},
)
.await
})
.unwrap();
let (mut sender, driver) = client(listener.port().unwrap(), scope).await;
let response = sender.send_request(message("GET")).await.unwrap();
match mode {
"headers" => assert_eq!(response.status(), 504),
"missing" => assert_eq!(response.status(), 500),
_ => {
assert_eq!(response.status(), 200);
let mut body = response.into_body();
if mode == "fault" {
assert_eq!(
body.frame().await.unwrap().unwrap().into_data().unwrap(),
b"partial"[..]
);
gate.add_permits(1);
}
assert!(body.collect().await.is_err());
}
}
drop(sender);
driver.wait().await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
}
})
.unwrap();
}
+279
View File
@@ -0,0 +1,279 @@
use super::*;
use bytes::Bytes;
use http_body_util::{BodyExt, Empty};
use hyper_util::rt::TokioIo;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
mod lifecycle;
mod performance;
mod transport;
async fn client(
port: i64,
scope: &Scope<'_>,
) -> (
hyper::client::conn::http1::SendRequest<Empty<Bytes>>,
dever_runtime::task::Task<()>,
) {
let socket = tokio::net::TcpStream::connect(("127.0.0.1", port as u16))
.await
.unwrap();
let (sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(socket))
.await
.unwrap();
let driver = scope
.spawn_leaf(move || async move {
let _ = connection.await;
})
.unwrap();
(sender, driver)
}
fn message(method: &str) -> hyper::Request<Empty<Bytes>> {
hyper::Request::builder()
.method(method)
.uri("/")
.header("Host", "fixture.test")
.body(Empty::new())
.unwrap()
}
struct Running(Arc<AtomicUsize>);
impl Drop for Running {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::SeqCst);
}
}
#[test]
fn sse_encoding_preserves_lines_ids_and_exact_limit() {
let event = http::Event {
event: "change".into(),
data: "一\r\n二\r\n\r".into(),
id: Some("".into()),
retry_ms: Some(0),
};
let expected = "event: change\nid: \nretry: 0\ndata: 一\ndata: 二\ndata: \ndata: \n\n";
assert_eq!(event.encode(expected.len()).unwrap(), expected);
assert!(event.encode(expected.len() - 1).is_err());
for event in [
http::Event {
event: "bad\n".into(),
..event.clone()
},
http::Event {
id: Some("bad\0".into()),
..event.clone()
},
http::Event {
retry_ms: Some(-1),
..event.clone()
},
] {
assert!(event.encode(1024).is_err());
}
}
#[test]
fn first_chunk_arrives_before_handler_finishes_and_outlives_head_deadline() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let gate = Arc::new(Semaphore::new(0));
let waiting = gate.clone();
let server = scope
.spawn(move |child| async move {
let limits = Limits {
timeout_ms: 100,
..Limits::default()
};
http::serve_live(
&incoming,
&limits,
&http::LiveLimits::default(),
&child,
move |_, reply, _| {
let gate = waiting.clone();
async move {
reply
.start(200, vec![])
.await
.map_err(|error| error.to_string())?;
reply
.write(b"first")
.await
.map_err(|error| error.to_string())?;
let _permit = gate.acquire().await.unwrap();
reply
.write(b"last")
.await
.map_err(|error| error.to_string())
}
},
)
.await
})
.unwrap();
let (mut sender, driver) = client(port, scope).await;
let response = sender.send_request(message("GET")).await.unwrap();
assert_eq!(response.status(), 200);
let mut body = response.into_body();
assert_eq!(
body.frame().await.unwrap().unwrap().into_data().unwrap(),
b"first"[..]
);
tokio::time::sleep(Duration::from_millis(180)).await;
gate.add_permits(1);
assert_eq!(body.collect().await.unwrap().to_bytes(), b"last"[..]);
drop(sender);
driver.wait().await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn sse_heartbeats_keep_session_alive_and_stop_drains_handler() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let incoming = listener.clone();
let running = Arc::new(AtomicUsize::new(0));
let active = running.clone();
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&Limits {
timeout_ms: 80,
..Limits::default()
},
&http::LiveLimits {
chunk_bytes: 64,
idle_ms: 100,
heartbeat_ms: 20,
},
&child,
move |_, reply, _| {
let active = active.clone();
async move {
active.fetch_add(1, Ordering::SeqCst);
let _running = Running(active);
reply
.sse_start(vec![])
.await
.map_err(|error| error.to_string())?;
std::future::pending::<Result<(), String>>().await
}
},
)
.await
})
.unwrap();
let (mut sender, driver) = client(listener.port().unwrap(), scope).await;
let response = sender.send_request(message("GET")).await.unwrap();
assert_eq!(response.headers()["content-type"], "text/event-stream");
assert_eq!(response.headers()["cache-control"], "no-cache");
let mut body = response.into_body();
for _ in 0..8 {
assert_eq!(
body.frame().await.unwrap().unwrap().into_data().unwrap(),
b":\n\n"[..]
);
}
assert_eq!(running.load(Ordering::SeqCst), 1);
server.stop().await.unwrap();
assert_eq!(running.load(Ordering::SeqCst), 0);
drop(body);
drop(sender);
driver.wait().await.unwrap();
listener.close().unwrap();
})
.unwrap();
}
#[test]
fn invalid_reply_operations_preserve_started_response_and_close_aliases() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let incoming = listener.clone();
let (sent, mut saved) = tokio::sync::mpsc::channel(1);
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&Limits::default(),
&http::LiveLimits {
chunk_bytes: 32,
..http::LiveLimits::default()
},
&child,
move |_, reply, _| {
let sent = sent.clone();
async move {
assert!(reply.write(b"before").await.is_err());
assert!(reply.start(204, vec![]).await.is_err());
reply.sse_start(vec![]).await.unwrap();
assert!(reply.start(200, vec![]).await.is_err());
assert!(reply.write(b"ordinary").await.is_err());
assert!(
reply
.sse_send(&http::Event {
event: "".into(),
data: "x".repeat(33),
id: None,
retry_ms: None
})
.await
.is_err()
);
reply
.sse_send(&http::Event {
event: "".into(),
data: "ok".into(),
id: None,
retry_ms: None,
})
.await
.unwrap();
reply.finish().await.unwrap();
assert!(reply.finish().await.is_err());
assert!(
reply
.sse_send(&http::Event {
event: "".into(),
data: "late".into(),
id: None,
retry_ms: None,
})
.await
.is_err()
);
sent.send(reply).await.unwrap();
Ok(())
}
},
)
.await
})
.unwrap();
let (mut sender, driver) = client(listener.port().unwrap(), scope).await;
let response = sender.send_request(message("GET")).await.unwrap();
assert_eq!(
response.into_body().collect().await.unwrap().to_bytes(),
b"data: ok\n\n"[..]
);
let alias = saved.recv().await.unwrap();
assert!(alias.start(200, vec![]).await.is_err());
drop(sender);
driver.wait().await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
+205
View File
@@ -0,0 +1,205 @@
use super::*;
use futures_util::stream;
use http_body_util::StreamBody;
use hyper::{body::Frame, service::service_fn};
use std::{convert::Infallible, time::Instant};
use tokio::sync::mpsc;
async fn independent_server(
data: Arc<str>,
iterations: usize,
limit: usize,
) -> (i64, tokio::task::JoinHandle<()>) {
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
.await
.unwrap();
let port = i64::from(listener.local_addr().unwrap().port());
let server = tokio::spawn(async move {
let (socket, _) = listener.accept().await.unwrap();
socket.set_nodelay(true).unwrap();
let service = service_fn(move |_: hyper::Request<hyper::body::Incoming>| {
let data = data.clone();
async move {
let (sender, receiver) = mpsc::channel::<Bytes>(1);
let producer = tokio::spawn(async move {
for _ in 0..iterations {
assert!(!data.contains(['\r', '\n']));
assert!(data.len().checked_add(8).unwrap() <= limit);
let encoded = format!("data: {data}\n\n");
tokio::time::timeout(
Duration::from_secs(5),
sender.send(Bytes::from(encoded)),
)
.await
.unwrap()
.unwrap();
}
});
let frames = stream::unfold(
(receiver, Some(producer)),
|(mut receiver, mut producer)| async move {
match receiver.recv().await {
Some(bytes) => Some((
Ok::<_, Infallible>(Frame::data(bytes)),
(receiver, producer),
)),
None => {
producer.take().unwrap().await.unwrap();
None
}
}
},
);
Ok::<_, Infallible>(
hyper::Response::builder()
.status(200)
.header("content-type", "text/event-stream")
.header("cache-control", "no-cache")
.body(StreamBody::new(frames))
.unwrap(),
)
}
});
hyper::server::conn::http1::Builder::new()
.serve_connection(TokioIo::new(socket), service)
.await
.unwrap();
});
(port, server)
}
async fn sample(
sender: &mut hyper::client::conn::http1::SendRequest<Empty<Bytes>>,
expected: &[u8],
iterations: usize,
) -> (u128, usize) {
let started = Instant::now();
let (response, chunks) = tokio::time::timeout(Duration::from_secs(10), async {
let response = sender.send_request(message("GET")).await.unwrap();
let (response, mut body) = response.into_parts();
let mut chunks = Vec::with_capacity(iterations);
while let Some(frame) = body.frame().await {
chunks.push(frame.unwrap().into_data().unwrap());
}
(response, chunks)
})
.await
.unwrap();
let elapsed = started.elapsed().as_nanos();
assert_eq!(response.status, 200);
assert_eq!(response.headers["content-type"], "text/event-stream");
let total = chunks.iter().map(Bytes::len).sum::<usize>();
assert_eq!(total, expected.len() * iterations);
assert!(
chunks
.iter()
.flat_map(|bytes| bytes.iter().copied())
.eq(expected.iter().copied().cycle().take(total))
);
(elapsed, total)
}
#[test]
#[ignore = "explicit release performance probe"]
fn sse_send_cost() {
let runtime = Runtime::new(dever_runtime::task::Limits {
threads: 1,
workers: 1,
tasks: 32,
})
.unwrap();
runtime
.run(async |scope| {
eprintln!(
"sse_send: 15 fixed rounds; one async worker; persistent HTTP/1 loopback; \
both implementations encode each single-line data event and use a one-chunk queue; \
rust_independent uses format!, Bytes and Hyper/Tokio directly, without Dever's SSE encoder/send; \
timed request through complete body includes network and bounded send; \
setup, exact byte verification and teardown excluded; \
successful-stream workload only, not lifecycle/heartbeat parity"
);
println!("\ncase,sample,implementation,iterations,elapsed_ns,bytes");
for (size, iterations) in [(64, 4096), (65_536, 128), (1_048_576, 16)] {
let event = Arc::new(http::Event {
event: String::new(),
data: "a".repeat(size),
id: None,
retry_ms: None,
});
let expected = format!("data: {}\n\n", event.data);
let limit = expected.len();
let encoded = event.encode(limit).unwrap();
println!(
"sse_capacity,{size},{},{}",
encoded.len(),
encoded.capacity()
);
assert_eq!(encoded, expected);
drop(encoded);
let (independent_port, independent) = independent_server(
Arc::from(event.data.as_str()),
iterations,
limit,
)
.await;
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let incoming = listener.clone();
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&Limits {
timeout_ms: 5000,
..Limits::default()
},
&http::LiveLimits {
chunk_bytes: limit as i64,
heartbeat_ms: 30000,
idle_ms: 60000,
},
&child,
move |_, reply, _| {
let event = event.clone();
async move {
reply.sse_start(vec![]).await.unwrap();
for _ in 0..iterations {
reply.sse_send(&event).await.unwrap();
}
reply.finish().await.unwrap();
Ok(())
}
},
)
.await
})
.unwrap();
let (mut dever_client, dever_driver) =
client(listener.port().unwrap(), scope).await;
let (mut independent_client, independent_driver) =
client(independent_port, scope).await;
sample(&mut dever_client, expected.as_bytes(), iterations).await;
sample(&mut independent_client, expected.as_bytes(), iterations).await;
for round in 0..15 {
let mut order = ["dever", "rust_independent", "control"];
order.rotate_left(round % 3);
for label in order {
let sender = if label == "rust_independent" {
&mut independent_client
} else {
&mut dever_client
};
let (elapsed, total) = sample(sender, expected.as_bytes(), iterations).await;
println!("sse_send_{size},{round},{label},{iterations},{elapsed},{total}");
}
}
drop(dever_client);
drop(independent_client);
dever_driver.wait().await.unwrap();
independent_driver.wait().await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
independent.await.unwrap();
}
})
.unwrap();
}
+149
View File
@@ -0,0 +1,149 @@
use super::*;
use hyper_util::rt::TokioExecutor;
#[test]
fn http2_live_reset_isolates_sse_neighbor_and_tls_uses_same_body_contract() {
runtime()
.run(async |scope| {
for secure in [false, true] {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let running = Arc::new(AtomicUsize::new(0));
let active = running.clone();
let server = scope
.spawn(move |child| async move {
let limits = Limits {
timeout_ms: 200,
http2: Some(http::Http2Limits::default()),
..Limits::default()
};
let live = http::LiveLimits {
heartbeat_ms: 20,
idle_ms: 1000,
..http::LiveLimits::default()
};
let route =
move |request: Request,
reply: http::HttpReply,
_: Arc<Scope<'static>>| {
let running = active.clone();
async move {
if request.target == "/ordinary" {
return reply
.respond(Response {
status: 200,
headers: vec![],
body: b"ok".to_vec().into(),
})
.await
.map_err(|error| error.to_string());
}
running.fetch_add(1, Ordering::SeqCst);
let _running = Running(running);
reply.sse_start(vec![]).await.unwrap();
std::future::pending::<Result<(), String>>().await
}
};
if secure {
let identity = dever_runtime::tls::server(
super::super::tls::CERTIFICATE,
super::super::tls::KEY,
)
.unwrap();
http::serve_live_tls(
&incoming, &limits, &live, &child, &identity, route,
)
.await
} else {
http::serve_live(&incoming, &limits, &live, &child, route).await
}
})
.unwrap();
let socket = tokio::net::TcpStream::connect(("127.0.0.1", port as u16))
.await
.unwrap();
let (mut sender, driver) = if secure {
let trust = dever_runtime::tls::client(super::super::tls::ROOT).unwrap();
let socket = trust.connect("localhost", socket, true).await.unwrap();
let (sender, connection) =
hyper::client::conn::http2::Builder::new(TokioExecutor::new())
.handshake::<_, Empty<Bytes>>(TokioIo::new(socket))
.await
.unwrap();
(
sender,
scope
.spawn_leaf(move || async move {
let _ = connection.await;
})
.unwrap(),
)
} else {
let (sender, connection) =
hyper::client::conn::http2::Builder::new(TokioExecutor::new())
.handshake::<_, Empty<Bytes>>(TokioIo::new(socket))
.await
.unwrap();
(
sender,
scope
.spawn_leaf(move || async move {
let _ = connection.await;
})
.unwrap(),
)
};
let request = |path| {
hyper::Request::builder()
.uri(format!(
"{}://localhost:{port}/{path}",
if secure { "https" } else { "http" }
))
.body(Empty::new())
.unwrap()
};
let mut first = sender
.send_request(request("first"))
.await
.unwrap()
.into_body();
let mut second = sender
.send_request(request("second"))
.await
.unwrap()
.into_body();
assert_eq!(
first.frame().await.unwrap().unwrap().into_data().unwrap(),
b":\n\n"[..]
);
assert_eq!(running.load(Ordering::SeqCst), 2);
drop(first);
tokio::time::timeout(Duration::from_secs(2), async {
while running.load(Ordering::SeqCst) != 1 {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
assert_eq!(
second.frame().await.unwrap().unwrap().into_data().unwrap(),
b":\n\n"[..]
);
// 活动 SSE 响应超过连接空闲期限仍可继续传输。
tokio::time::sleep(Duration::from_millis(300)).await;
let ordinary = sender.send_request(request("ordinary")).await.unwrap();
assert_eq!(
ordinary.into_body().collect().await.unwrap().to_bytes(),
b"ok"[..]
);
drop(second);
drop(sender);
driver.wait().await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
assert_eq!(running.load(Ordering::SeqCst), 0);
}
})
.unwrap();
}
+145
View File
@@ -0,0 +1,145 @@
use dever_runtime::{
http::{self, Header, Limits, Request, Response},
net,
task::{Runtime, Scope},
};
use std::{
io::{BufRead, BufReader, Read, Write},
net::TcpStream,
sync::Arc,
time::Duration,
};
mod http2;
mod lifecycle;
mod limits;
mod live;
mod pool;
mod streaming;
mod tls;
mod websocket;
fn runtime() -> Runtime {
Runtime::new(dever_runtime::task::Limits {
threads: 2,
workers: 2,
tasks: 32,
})
.unwrap()
}
fn request(target: &str, body: &[u8]) -> Request {
Request {
method: "POST".into(),
target: target.into(),
headers: vec![Header {
name: "Host".into(),
value: b"fixture.test".to_vec().into(),
}],
body: body.to_vec().into(),
}
}
async fn route(request: Request, _: Arc<Scope<'static>>) -> Result<Response, String> {
if request.target == "/fail" {
return Err("owned fixture failure".into());
}
if request.target == "/wait" {
tokio::time::sleep(Duration::from_secs(60)).await;
}
Ok(Response {
status: 200,
headers: vec![
Header {
name: "x-data".into(),
value: vec![0x80].into(),
},
Header {
name: "x-data".into(),
value: b"second".to_vec().into(),
},
],
body: request.body,
})
}
fn connect(port: i64) -> BufReader<TcpStream> {
let stream = TcpStream::connect(("127.0.0.1", port as u16)).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
stream
.set_write_timeout(Some(Duration::from_secs(3)))
.unwrap();
BufReader::new(stream)
}
fn read_response(stream: &mut BufReader<TcpStream>, head: bool) -> (u16, Vec<u8>) {
let mut line = Vec::new();
stream.read_until(b'\n', &mut line).unwrap();
let status = std::str::from_utf8(&line)
.unwrap()
.split_whitespace()
.nth(1)
.unwrap()
.parse()
.unwrap();
let mut size = 0;
loop {
line.clear();
assert!(stream.read_until(b'\n', &mut line).unwrap() > 0);
if line == b"\r\n" {
break;
}
if line.to_ascii_lowercase().starts_with(b"content-length:") {
size = std::str::from_utf8(&line[15..])
.unwrap()
.trim()
.parse()
.unwrap();
}
}
let mut body = vec![0; if head { 0 } else { size }];
stream.read_exact(&mut body).unwrap();
(status, body)
}
#[test]
fn client_and_independent_peer_cover_keepalive_chunking_and_request_isolation() {
runtime().run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let server = scope.spawn(move |child| async move { http::serve::<_, _, false>(&incoming, &Limits::default(), &child, route).await }).unwrap();
let response = http::send("127.0.0.1", port, request("/echo", b"owned body"), &Limits::default()).await.unwrap();
assert_eq!(response.body.as_ref(), b"owned body");
let headers = response.headers.iter().filter(|value| value.name == "x-data").map(|value| value.value.clone()).collect::<Vec<_>>();
assert_eq!(headers, vec![vec![0x80], b"second".to_vec()]);
scope.blocking(move || {
let mut stream = connect(port);
for message in [
&b"GET /echo HTTP/1.1\r\nHost: fixture.test\r\n\r\n"[..],
&b"POST /echo HTTP/1.1\r\nHost: fixture.test\r\nContent-Length: 0\r\n\r\n"[..],
&b"POST /echo HTTP/1.1\r\nHost: fixture.test\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n"[..],
] {
stream.get_mut().write_all(message).unwrap();
assert_eq!(read_response(&mut stream, false), (200, vec![]));
}
for (path, status) in [("/echo", 200), ("/fail", 500), ("/echo", 200)] {
write!(stream.get_mut(), "POST {path} HTTP/1.1\r\nHost: fixture.test\r\nContent-Length: 3\r\n\r\nabc").unwrap();
let (actual, body) = read_response(&mut stream, false);
assert_eq!(actual, status);
if status == 200 { assert_eq!(body, b"abc"); }
}
stream.get_mut().write_all(b"HEAD /echo HTTP/1.1\r\nHost: fixture.test\r\nContent-Length: 3\r\n\r\nabc").unwrap();
assert_eq!(read_response(&mut stream, true), (200, vec![]));
stream.get_mut().write_all(b"POST /echo HTTP/1.1\r\nHost: fixture.test\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n3\r\none\r\n3\r\ntwo\r\n0\r\n\r\n").unwrap();
assert_eq!(read_response(&mut stream, false), (200, b"onetwo".to_vec()));
let mut tail = Vec::new();
stream.read_to_end(&mut tail).unwrap();
assert!(tail.is_empty());
}).await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
}).unwrap();
}
+264
View File
@@ -0,0 +1,264 @@
use super::*;
use dever_runtime::{http::PoolLimits, tls};
fn message() -> Request {
Request {
method: "POST".into(),
target: "/echo".into(),
headers: vec![],
body: b"pooled".to_vec().into(),
}
}
#[test]
fn origins_reject_invalid_ports_before_connecting() {
runtime()
.run(async |scope| {
for host in ["127.0.0.1", "[::1]", "localhost"] {
for port in ["99999", "65536", "abc", "-1", ""] {
for scheme in ["http", "https"] {
assert!(
http::client(
&format!("{scheme}://{host}:{port}"),
&tls::system(),
&Limits::default(),
&PoolLimits::default(),
scope
)
.is_err()
);
}
for scheme in ["ws", "wss"] {
let error = http::websocket::open(
&format!("{scheme}://{host}:{port}/"),
&tls::system(),
&http::websocket::Limits::default(),
scope,
)
.await
.unwrap_err();
assert!(
error.contains("port") || error.contains("invalid WebSocket URL"),
"{error}"
);
}
}
for suffix in ["", ":80", ":443", ":65535"] {
let client = http::client(
&format!("http://{host}{suffix}"),
&tls::system(),
&Limits::default(),
&PoolLimits::default(),
scope,
)
.unwrap();
client.close().await.unwrap();
}
}
})
.unwrap();
}
#[test]
fn pool_bounds_http2_concurrency_and_cancels_active_requests() {
use std::sync::atomic::{AtomicUsize, Ordering};
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let limits = Limits {
connections: 1,
http2: Some(http::Http2Limits {
streams: 2,
..Default::default()
}),
..Default::default()
};
let active = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let peak_read = peak.clone();
let incoming = listener.clone();
let server_limits = limits.clone();
let server = scope
.spawn(move |child| async move {
http::serve::<_, _, false>(
&incoming,
&server_limits,
&child,
move |request, _| {
let active = active.clone();
let peak = peak.clone();
async move {
let count = active.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(count, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(20)).await;
active.fetch_sub(1, Ordering::SeqCst);
Ok(Response {
status: 200,
headers: vec![],
body: request.body,
})
}
},
)
.await
})
.unwrap();
let client = http::client(
&format!("http://127.0.0.1:{port}"),
&tls::system(),
&limits,
&PoolLimits::default(),
scope,
)
.unwrap();
let replies =
futures_util::future::join_all((0..8).map(|_| client.request(message()))).await;
for response in replies {
assert_eq!(response.unwrap().body.as_ref(), b"pooled");
}
assert_eq!(peak_read.load(Ordering::SeqCst), 2);
let alias = client.clone();
let pending = scope
.spawn_leaf(move || async move { alias.request(message()).await })
.unwrap();
tokio::time::sleep(Duration::from_millis(5)).await;
client.close().await.unwrap();
assert!(pending.wait().await.unwrap().is_err());
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn pool_http1_http2_tls_reuse_close_and_scope_ownership() {
for protocol in [false, true] {
for secure in [false, true] {
let escaped = runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let limits = Limits {
connections: 1,
http2: protocol.then(Default::default),
..Limits::default()
};
let incoming = listener.clone();
let server_limits = limits.clone();
let server = scope
.spawn(move |child| async move {
if secure {
let config =
tls::server(super::tls::CERTIFICATE, super::tls::KEY).unwrap();
http::serve_tls::<_, _, false>(
&incoming,
&server_limits,
&child,
&config,
route,
)
.await
} else {
http::serve::<_, _, false>(&incoming, &server_limits, &child, route)
.await
}
})
.unwrap();
let origin = format!(
"{}://localhost:{port}",
if secure { "https" } else { "http" }
);
let config = tls::client(super::tls::ROOT).unwrap();
let client =
http::client(&origin, &config, &limits, &PoolLimits::default(), scope)
.unwrap();
for _ in 0..3 {
assert_eq!(
client.request(message()).await.unwrap().body.as_ref(),
b"pooled"
);
}
let mut changed = message();
changed.headers.push(Header {
name: "Host".into(),
value: b"other.test".to_vec().into(),
});
assert!(client.request(changed).await.is_err());
client.close().await.unwrap();
client.close().await.unwrap();
assert!(client.request(message()).await.is_err());
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
http::client(&origin, &config, &limits, &PoolLimits::default(), scope).unwrap()
})
.unwrap();
runtime()
.run(async |_| {
assert!(
escaped
.request(message())
.await
.unwrap_err()
.contains("closed")
);
escaped.close().await.unwrap();
})
.unwrap();
}
}
}
#[test]
fn pool_reuses_one_tcp_connection_and_closes_it_before_returning() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let peer = std::thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
let mut stream = BufReader::new(stream);
for _ in 0..3 {
let mut size = 0;
loop {
let mut line = String::new();
assert!(stream.read_line(&mut line).unwrap() > 0);
if line == "\r\n" {
break;
}
if let Some(length) = line.to_ascii_lowercase().strip_prefix("content-length:") {
size = length.trim().parse().unwrap();
}
}
let mut body = vec![0; size];
stream.read_exact(&mut body).unwrap();
assert_eq!(body, b"pooled");
stream
.get_mut()
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
.unwrap();
}
assert_eq!(stream.read(&mut [0]).unwrap(), 0);
});
runtime()
.run(async |scope| {
let client = http::client(
&format!("http://127.0.0.1:{port}"),
&tls::system(),
&Limits::default(),
&PoolLimits::default(),
scope,
)
.unwrap();
for _ in 0..3 {
assert_eq!(
client.request(message()).await.unwrap().body.as_ref(),
b"ok"
);
}
client.close().await.unwrap();
})
.unwrap();
peer.join().unwrap();
}
@@ -0,0 +1,239 @@
use super::*;
#[test]
fn headers_return_before_body_and_download_ignores_buffered_total_limit() {
let (opened, permit) = std::sync::mpsc::channel();
let (port, server) = peer(move |listener| {
let mut socket = accept(&listener);
headers(&mut socket);
socket
.get_mut()
.write_all(
b"HTTP/1.1 200 OK\r\nContent-Length: 100003\r\nX-Test: one\r\nX-Test: two\r\n\r\n",
)
.unwrap();
permit.recv_timeout(Duration::from_secs(2)).unwrap();
socket.get_mut().write_all(&vec![b'x'; 100003]).unwrap();
assert_eq!(socket.read(&mut [0]).unwrap(), 0);
});
runtime()
.run(async |scope| {
let client = client(
scope,
port,
&Limits {
body_bytes: 1,
..Default::default()
},
&PoolLimits {
chunk_bytes: 7,
..Default::default()
},
);
let response = client.open(message(), event).await.unwrap();
assert_eq!(
response
.headers
.iter()
.filter(|header| header.name == "x-test")
.count(),
2
);
opened.send(()).unwrap();
let body = collect(&response.body, 7).await;
assert_eq!(body, vec![b'x'; 100003]);
assert!(response.body.next().await.unwrap().is_none());
client.close().await.unwrap();
})
.unwrap();
server.join().unwrap();
}
#[test]
fn unread_body_holds_slot_and_idle_clock_starts_after_eof() {
let (port, server) = peer(|listener| {
let mut socket = accept(&listener);
for _ in 0..2 {
headers(&mut socket);
socket
.get_mut()
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nabcdef")
.unwrap();
}
let start = std::time::Instant::now();
assert_eq!(socket.read(&mut [0]).unwrap(), 0);
assert!(start.elapsed() < Duration::from_secs(1));
});
runtime()
.run(async |scope| {
let client = client(
scope,
port,
&Limits {
connections: 1,
timeout_ms: 80,
..Default::default()
},
&PoolLimits {
idle_ms: 20,
chunk_bytes: 2,
..Default::default()
},
);
let response = client.open(message(), event).await.unwrap();
assert!(
client
.open(message(), event)
.await
.unwrap_err()
.contains("timed out")
);
assert_eq!(collect(&response.body, 2).await, b"abcdef");
assert_eq!(
client.request(message()).await.unwrap().body.as_ref(),
b"abcdef"
);
tokio::time::sleep(Duration::from_millis(80)).await;
client.close().await.unwrap();
})
.unwrap();
server.join().unwrap();
}
#[test]
fn early_close_of_buffered_frame_closes_http1_and_next_request_reconnects() {
let (port, server) = peer(|listener| {
let mut first = accept(&listener);
headers(&mut first);
first
.get_mut()
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nabcdef")
.unwrap();
assert_eq!(first.read(&mut [0]).unwrap(), 0);
let mut second = accept(&listener);
headers(&mut second);
second
.get_mut()
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
.unwrap();
});
runtime()
.run(async |scope| {
let client = client(
scope,
port,
&Limits {
connections: 1,
..Default::default()
},
&PoolLimits::default(),
);
let response = client.open(message(), event).await.unwrap();
let alias = response.body.clone();
tokio::time::sleep(Duration::from_millis(30)).await;
response.body.close();
assert!(alias.next().await.unwrap().is_none());
assert_eq!(
client.request(message()).await.unwrap().body.as_ref(),
b"ok"
);
client.close().await.unwrap();
})
.unwrap();
server.join().unwrap();
}
#[test]
fn read_timeout_and_protocol_failure_emit_one_error_then_eof() {
for body in [
b"".as_slice(),
b"3\r\nabc\r\n0\r\nX-End: bad\r\n\r\n",
b"invalid\r\n",
] {
let (port, server) = peer(move |listener| {
let mut socket = accept(&listener);
headers(&mut socket);
socket
.get_mut()
.write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n")
.unwrap();
socket.get_mut().write_all(body).unwrap();
let _ = socket.read(&mut [0]);
});
runtime()
.run(async |scope| {
let client = client(
scope,
port,
&Limits::default(),
&PoolLimits {
read_ms: 40,
..Default::default()
},
);
let response = client.open(message(), event).await.unwrap();
let mut failures = 0;
while let Some(item) = response.body.next().await.unwrap() {
failures += usize::from(item.is_err());
}
assert_eq!(failures, 1);
assert!(response.body.next().await.unwrap().is_none());
client.close().await.unwrap();
})
.unwrap();
server.join().unwrap();
}
}
#[test]
fn cancelling_consumer_closes_body_and_pool_close_wakes_body_and_slot_waiters() {
for cancel in [true, false] {
let (port, server) = peer(|listener| {
let mut socket = accept(&listener);
headers(&mut socket);
socket
.get_mut()
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 20\r\n\r\n")
.unwrap();
assert_eq!(socket.read(&mut [0]).unwrap(), 0);
});
runtime()
.run(async |scope| {
let client = client(
scope,
port,
&Limits {
connections: 1,
..Default::default()
},
&PoolLimits::default(),
);
let response = client.open(message(), event).await.unwrap();
let alias = response.body.clone();
if cancel {
let consume = async {
let _owner = alias.close_on_drop();
alias.next().await
};
assert!(
tokio::time::timeout(Duration::from_millis(20), consume)
.await
.is_err()
);
assert!(response.body.next().await.unwrap().is_none());
} else {
let (item, waiting, ()) =
tokio::join!(response.body.next(), client.open(message(), event), async {
tokio::time::sleep(Duration::from_millis(20)).await;
client.close().await.unwrap();
});
assert!(item.unwrap().unwrap().unwrap_err().contains("closed"));
assert!(waiting.unwrap_err().contains("closed"));
assert!(response.body.next().await.unwrap().is_none());
}
client.close().await.unwrap();
})
.unwrap();
server.join().unwrap();
}
}
+95
View File
@@ -0,0 +1,95 @@
//! 独立回环协议对端验证客户端按需拉取与连接所有权。
use super::*;
use bytes::Bytes;
use dever_runtime::{
async_stream::AsyncStream,
http::{HttpClient, PoolLimits, StreamRequest},
io::Error,
tls,
};
use std::net::TcpListener;
mod download;
mod protocol;
mod upload;
type Event = Result<Bytes, Error>;
fn event(value: Event) -> Event {
value
}
fn message() -> Request {
Request {
method: "POST".into(),
target: "/stream".into(),
headers: vec![],
body: bytes::Bytes::new(),
}
}
fn client(scope: &Scope<'_>, port: u16, limits: &Limits, pool: &PoolLimits) -> HttpClient {
http::client(
&format!("http://127.0.0.1:{port}"),
&tls::system(),
limits,
pool,
scope,
)
.unwrap()
}
fn peer(work: impl FnOnce(TcpListener) + Send + 'static) -> (u16, std::thread::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
(port, std::thread::spawn(move || work(listener)))
}
fn accept(listener: &TcpListener) -> BufReader<TcpStream> {
listener.set_nonblocking(true).unwrap();
let start = std::time::Instant::now();
let socket = loop {
match listener.accept() {
Ok((socket, _)) => break socket,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
assert!(
start.elapsed() < Duration::from_secs(5),
"peer accept timed out"
);
std::thread::sleep(Duration::from_millis(2));
}
Err(error) => panic!("{error}"),
}
};
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
socket
.set_write_timeout(Some(Duration::from_secs(5)))
.unwrap();
BufReader::new(socket)
}
fn headers(socket: &mut BufReader<TcpStream>) -> String {
let mut headers = String::new();
loop {
let mut line = String::new();
assert!(socket.read_line(&mut line).unwrap() > 0);
if line == "\r\n" {
return headers;
}
headers.push_str(&line);
}
}
async fn collect(body: &AsyncStream<Event>, chunk: usize) -> Vec<u8> {
let _guard = body.close_on_drop();
let mut bytes = vec![];
while let Some(event) = body.next().await.unwrap() {
let part = event.unwrap();
assert!(!part.is_empty() && part.len() <= chunk);
bytes.extend_from_slice(&part);
}
bytes
}
@@ -0,0 +1,173 @@
use super::*;
#[test]
fn http1_http2_tls_streams_roundtrip_and_buffered_limits_remain_enforced() {
for http2 in [false, true] {
for secure in [false, true] {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let limits = Limits {
connections: 1,
http2: http2.then(Default::default),
..Default::default()
};
let incoming = listener.clone();
let protocol = limits.clone();
let server = scope
.spawn(move |child| async move {
if secure {
let tls = tls::server(
super::super::tls::CERTIFICATE,
super::super::tls::KEY,
)
.unwrap();
http::serve_tls::<_, _, false>(
&incoming, &protocol, &child, &tls, route,
)
.await
} else {
http::serve::<_, _, false>(&incoming, &protocol, &child, route)
.await
}
})
.unwrap();
let origin = format!(
"{}://localhost:{port}",
if secure { "https" } else { "http" }
);
let trust = tls::client(super::super::tls::ROOT).unwrap();
let client = http::client(
&origin,
&trust,
&Limits {
body_bytes: 1,
..limits
},
&PoolLimits {
chunk_bytes: 3,
..Default::default()
},
scope,
)
.unwrap();
let body = AsyncStream::values([
Ok(Bytes::from_static(b"abc")),
Ok(Bytes::from_static(b"def")),
]);
let request = StreamRequest {
method: "POST".into(),
target: "/echo".into(),
headers: vec![],
body,
};
let response = client.open_stream(request, event, event).await.unwrap();
assert_eq!(collect(&response.body, 3).await, b"abcdef");
let mut request = message();
request.body = b"ab".to_vec().into();
assert!(
client
.open(request.clone(), event)
.await
.unwrap_err()
.contains("byte limit")
);
assert!(
client
.request(request)
.await
.unwrap_err()
.contains("byte limit")
);
client.close().await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
}
}
#[test]
fn http2_closing_one_response_preserves_neighbor_and_releases_only_its_slot() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let limits = Limits {
connections: 1,
http2: Some(http::Http2Limits {
streams: 2,
..Default::default()
}),
..Default::default()
};
let incoming = listener.clone();
let protocol = limits.clone();
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&protocol,
&http::LiveLimits::default(),
&child,
|request: Request, reply: http::HttpReply, _| async move {
if request.target == "/ordinary" {
return reply
.respond(Response {
status: 200,
headers: vec![],
body: b"ok".to_vec().into(),
})
.await
.map_err(|error| error.to_string());
}
reply
.start(200, vec![])
.await
.map_err(|error| error.to_string())?;
reply
.write(b"first")
.await
.map_err(|error| error.to_string())?;
tokio::time::sleep(Duration::from_millis(150)).await;
reply
.write(b"last")
.await
.map_err(|error| error.to_string())
},
)
.await
})
.unwrap();
let client = client(
scope,
port as u16,
&limits,
&PoolLimits {
idle_ms: 20,
..Default::default()
},
);
let first = client.open(message(), event).await.unwrap();
let second = client.open(message(), event).await.unwrap();
assert_eq!(
first.body.next().await.unwrap().unwrap().unwrap(),
b"first"[..]
);
assert_eq!(
second.body.next().await.unwrap().unwrap().unwrap(),
b"first"[..]
);
first.body.close();
let mut ordinary = message();
ordinary.target = "/ordinary".into();
assert_eq!(client.request(ordinary).await.unwrap().body.as_ref(), b"ok");
assert_eq!(collect(&second.body, 65536).await, b"last");
client.close().await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
+163
View File
@@ -0,0 +1,163 @@
use super::*;
fn chunk(socket: &mut BufReader<TcpStream>) -> Vec<u8> {
let mut line = String::new();
assert!(socket.read_line(&mut line).unwrap() > 0);
let count = usize::from_str_radix(line.trim(), 16).unwrap();
let mut body = vec![0; count];
socket.read_exact(&mut body).unwrap();
let mut ending = [0; 2];
socket.read_exact(&mut ending).unwrap();
assert_eq!(&ending, b"\r\n");
body
}
fn request(body: AsyncStream<Event>) -> StreamRequest<Event> {
StreamRequest {
method: "POST".into(),
target: "/stream".into(),
headers: vec![],
body,
}
}
#[test]
fn duplex_upload_sends_first_chunk_before_source_eof_and_returns_response_early() {
let (port, server) = peer(|listener| {
let mut socket = accept(&listener);
let head = headers(&mut socket).to_ascii_lowercase();
assert!(head.contains("transfer-encoding: chunked"));
assert!(!head.contains("content-length:"));
assert_eq!(chunk(&mut socket), b"abc");
socket
.get_mut()
.write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n")
.unwrap();
assert_eq!(chunk(&mut socket), b"def");
assert!(chunk(&mut socket).is_empty());
socket
.get_mut()
.write_all(b"3\r\ndef\r\n0\r\n\r\n")
.unwrap();
assert_eq!(socket.read(&mut [0]).unwrap(), 0);
});
runtime()
.run(async |scope| {
let client = client(
scope,
port,
&Limits {
body_bytes: 1,
..Default::default()
},
&PoolLimits {
chunk_bytes: 3,
..Default::default()
},
);
let proceed = Arc::new(tokio::sync::Notify::new());
let source = proceed.clone();
let body = AsyncStream::new(futures_util::stream::unfold(0, move |index| {
let source = source.clone();
async move {
match index {
0 => Some((Ok(Ok(Bytes::from_static(b"abc"))), 1)),
1 => {
source.notified().await;
Some((Ok(Ok(Bytes::from_static(b"def"))), 2))
}
_ => None,
}
}
}));
let alias = body.clone();
let response = client
.open_stream(request(body), event, event)
.await
.unwrap();
assert_eq!(
response.body.next().await.unwrap().unwrap().unwrap(),
b"abc"[..]
);
proceed.notify_one();
assert_eq!(collect(&response.body, 3).await, b"def");
assert!(alias.next().await.unwrap().is_none());
client.close().await.unwrap();
})
.unwrap();
server.join().unwrap();
}
#[test]
fn upload_rejects_oversized_chunks_source_errors_and_stalled_reads() {
for mode in 0..3 {
let (port, server) = peer(|listener| {
let mut socket = accept(&listener);
let mut request = Vec::new();
socket.read_to_end(&mut request).unwrap();
assert!(!request.windows(8).any(|window| window == b"oversize"));
});
runtime()
.run(async |scope| {
let client = client(
scope,
port,
&Limits {
timeout_ms: 1000,
..Default::default()
},
&PoolLimits {
chunk_bytes: 3,
read_ms: 40,
..Default::default()
},
);
let body = match mode {
0 => AsyncStream::values([Ok(Bytes::from_static(b"oversize"))]),
1 => AsyncStream::values([Err("source failed".into())]),
_ => AsyncStream::new(futures_util::stream::pending()),
};
let alias = body.clone();
assert!(
client
.open_stream(request(body), event, event)
.await
.is_err()
);
assert!(alias.next().await.unwrap().is_none());
client.close().await.unwrap();
})
.unwrap();
server.join().unwrap();
}
}
#[test]
fn cancelling_request_before_headers_closes_upload_source_and_socket() {
let (port, server) = peer(|listener| {
let mut socket = accept(&listener);
let mut bytes = vec![];
socket.read_to_end(&mut bytes).unwrap();
});
runtime()
.run(async |scope| {
let client = client(scope, port, &Limits::default(), &PoolLimits::default());
let body = AsyncStream::<Event>::new(futures_util::stream::pending());
assert!(
tokio::time::timeout(
Duration::from_millis(40),
client.open_stream(request(body.clone()), event, event)
)
.await
.is_err()
);
tokio::time::timeout(Duration::from_secs(1), body.next())
.await
.unwrap()
.unwrap();
assert!(body.next().await.unwrap().is_none());
client.close().await.unwrap();
})
.unwrap();
server.join().unwrap();
}
+104
View File
@@ -0,0 +1,104 @@
use super::*;
use http_body_util::{BodyExt, Full};
use hyper_util::rt::{TokioExecutor, TokioIo};
pub(super) const ROOT: &[u8] = include_bytes!("../../../dever-tests/fixtures/tls/root.pem");
pub(super) const CERTIFICATE: &[u8] =
include_bytes!("../../../dever-tests/fixtures/tls/server.pem");
pub(super) const KEY: &[u8] = include_bytes!("../../../dever-tests/fixtures/tls/server-key.pem");
#[test]
fn tls_verifies_ca_hostname_alpn_and_keeps_bad_handshakes_isolated() {
runtime()
.run(async |scope| {
for http2 in [false, true] {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let server = scope
.spawn(move |child| async move {
let tls = dever_runtime::tls::server(CERTIFICATE, KEY).unwrap();
let limits = Limits {
http2: http2.then(http::Http2Limits::default),
timeout_ms: 1000,
..Limits::default()
};
http::serve_tls::<_, _, false>(&incoming, &limits, &child, &tls, route)
.await
})
.unwrap();
let custom = dever_runtime::tls::client(ROOT).unwrap();
for (tls, host, protocol) in [
(dever_runtime::tls::system(), "localhost", http2),
(custom.clone(), "wrong.test", http2),
(custom.clone(), "localhost", !http2),
] {
let stream = tokio::net::TcpStream::connect(("127.0.0.1", port as u16))
.await
.unwrap();
assert!(tls.connect(host, stream, protocol).await.is_err());
}
let stream = tokio::net::TcpStream::connect(("127.0.0.1", port as u16))
.await
.unwrap();
let stream = custom.connect("localhost", stream, http2).await.unwrap();
let message = hyper::Request::builder()
.method("POST")
.uri(if http2 {
"https://localhost/echo"
} else {
"/echo"
})
.header("Host", "localhost")
.body(Full::new(bytes::Bytes::from_static(b"verified TLS")))
.unwrap();
let response = if http2 {
let (mut sender, connection) =
hyper::client::conn::http2::Builder::new(TokioExecutor::new())
.handshake(TokioIo::new(stream))
.await
.unwrap();
let driver = scope.spawn_leaf(move || connection).unwrap();
let response = sender.send_request(message).await.unwrap();
let body = response.into_body().collect().await.unwrap().to_bytes();
drop(sender);
driver.stop().await.unwrap();
body
} else {
let (mut sender, connection) =
hyper::client::conn::http1::handshake(TokioIo::new(stream))
.await
.unwrap();
let driver = scope.spawn_leaf(move || connection).unwrap();
let response = sender.send_request(message).await.unwrap();
let body = response.into_body().collect().await.unwrap().to_bytes();
drop(sender);
driver.stop().await.unwrap();
body
};
assert_eq!(response, b"verified TLS"[..]);
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
}
})
.unwrap();
}
#[test]
fn tls_configuration_rejects_invalid_inputs_without_exposing_them() {
assert!(
dever_runtime::tls::client(b"private-input")
.unwrap_err()
.contains("empty")
);
let error = dever_runtime::tls::server(CERTIFICATE, b"private-key-input").unwrap_err();
assert!(!error.contains("private-key-input"));
assert!(dever_runtime::tls::server(ROOT, KEY).is_err());
let mut keys = KEY.to_vec();
keys.extend_from_slice(KEY);
assert!(
dever_runtime::tls::server(CERTIFICATE, &keys)
.unwrap_err()
.contains("one private key")
);
}
+60
View File
@@ -0,0 +1,60 @@
use super::Transport;
use std::{io, time::Duration};
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
#[test]
fn vectored_write_preserves_parts_and_stall_timeout() {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(async {
let (writer, mut reader) = tokio::io::duplex(8);
let mut transport = Transport::new(writer, Duration::from_millis(10));
assert!(transport.is_write_vectored());
let parts = [io::IoSlice::new(b"head"), io::IoSlice::new(b"body")];
assert_eq!(transport.write_vectored(&parts).await.unwrap(), 8);
let error =
tokio::time::timeout(Duration::from_secs(1), transport.write_vectored(&parts))
.await
.unwrap()
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::TimedOut);
let mut received = [0; 8];
reader.read_exact(&mut received).await.unwrap();
assert_eq!(&received, b"headbody");
});
}
#[test]
fn vectored_write_preserves_partial_progress_and_large_bodies() {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(async {
for length in [0, 1, 31, 4095, 4096, 4097, 65536] {
let (writer, mut reader) = tokio::io::duplex(17);
let mut transport = Transport::new(writer, Duration::from_secs(2));
let header = b"header";
let body: Vec<u8> = (0..length).map(|index| (index % 251) as u8).collect();
let expected = [header.as_slice(), body.as_slice()].concat();
let send = async {
let mut parts = [io::IoSlice::new(header), io::IoSlice::new(&body)];
let mut remaining = &mut parts[..];
while !remaining.is_empty() {
let count = transport.write_vectored(remaining).await.unwrap();
assert!(count > 0);
io::IoSlice::advance_slices(&mut remaining, count);
}
transport.shutdown().await.unwrap();
};
let receive = async {
let mut received = Vec::new();
reader.read_to_end(&mut received).await.unwrap();
assert_eq!(received, expected);
};
tokio::join!(send, receive);
}
});
}
+61
View File
@@ -0,0 +1,61 @@
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[test]
fn pooled_socket_preserves_vectored_writes_and_idle_expiry() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let origin = Arc::new(
Origin::parse(&format!("http://{}", listener.local_addr().unwrap())).unwrap(),
);
let limits = crate::http::Limits {
timeout_ms: 100,
..Default::default()
}
.check()
.unwrap();
let mut connector = Connector::new(
origin.clone(),
&crate::tls::system(),
&limits,
Duration::from_millis(100),
);
let mut socket = connector
.call(origin.uri.clone())
.await
.unwrap()
.into_inner();
let (mut peer, _) = listener.accept().await.unwrap();
assert!(socket.is_write_vectored());
let body = vec![7; 65536];
let send = async {
let mut parts = [io::IoSlice::new(b"head"), io::IoSlice::new(&body)];
let mut remaining = &mut parts[..];
while !remaining.is_empty() {
let count = socket.write_vectored(remaining).await.unwrap();
assert_ne!(count, 0);
io::IoSlice::advance_slices(&mut remaining, count);
}
};
let receive = async {
let mut received = vec![0; 4 + body.len()];
peer.read_exact(&mut received).await.unwrap();
assert_eq!(&received[..4], b"head");
assert_eq!(&received[4..], body);
};
tokio::join!(send, receive);
tokio::time::sleep(Duration::from_millis(150)).await;
assert_eq!(
socket
.write_vectored(&[io::IoSlice::new(b"expired")])
.await
.unwrap_err()
.kind(),
io::ErrorKind::TimedOut
);
});
}
@@ -0,0 +1,325 @@
use super::*;
use futures_util::{SinkExt, StreamExt};
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::oneshot;
#[test]
fn websocket_client_scope_ends_all_aliases_and_timeout_stream_fails_once() {
runtime()
.run(async |scope| {
let (listener, server) = server(scope).await;
let port = listener.port().unwrap();
let owner = scope
.spawn(move |child| async move {
ws::connect("127.0.0.1", port, "/", &limits(), &child)
.await
.unwrap()
})
.unwrap();
let escaped = owner.wait().await.unwrap();
assert!(escaped.send(Message::Text("closed".into())).await.is_err());
let socket = ws::connect(
"127.0.0.1",
port,
"/",
&ws::Limits {
idle_ms: 25,
..limits()
},
scope,
)
.await
.unwrap();
let stream = socket.messages(|message| message);
let alias = stream.clone();
let failure = stream.next().await.unwrap().unwrap().unwrap_err();
assert!(failure.contains("timed out"));
assert!(alias.next().await.unwrap().is_none());
assert!(socket.send(Message::Text("closed".into())).await.is_err());
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn websocket_listener_drain_keeps_session_and_parent_stop_cleans_handler() {
runtime()
.run(async |scope| {
let (listener, mut server) = server(scope).await;
let socket = ws::connect("127.0.0.1", listener.port().unwrap(), "/", &limits(), scope)
.await
.unwrap();
listener.close().unwrap();
tokio::select! {
_ = &mut server => panic!("server must retain its upgraded session"),
() = tokio::time::sleep(Duration::from_millis(25)) => (),
}
socket
.send(Message::Text("after listener close".into()))
.await
.unwrap();
assert_eq!(
socket.receive().await.unwrap(),
Some(Message::Text("after listener close".into()))
);
socket.close(1000, "").await.unwrap();
server.wait().await.unwrap().unwrap();
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let incoming = listener.clone();
let active = Arc::new(AtomicUsize::new(0));
let running = active.clone();
let (sender, receiver) = oneshot::channel::<WebSocket>();
let sender = Arc::new(std::sync::Mutex::new(Some(sender)));
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&Limits::default(),
&http::LiveLimits::default(),
&child,
move |_, reply, scope| {
let sender = sender.clone();
let running = running.clone();
async move {
let socket = ws::accept(&reply, &limits(), &scope).await.unwrap();
running.fetch_add(1, Ordering::SeqCst);
let _running = Running(running);
let _ = sender.lock().unwrap().take().unwrap().send(socket.clone());
let _ = socket.receive().await;
Ok(())
}
},
)
.await
})
.unwrap();
let socket = ws::connect("127.0.0.1", listener.port().unwrap(), "/", &limits(), scope)
.await
.unwrap();
let escaped = receiver.await.unwrap();
server.stop().await.unwrap();
assert_eq!(active.load(Ordering::SeqCst), 0);
assert!(escaped.send(Message::Text("gone".into())).await.is_err());
assert!(socket.receive().await.is_err());
listener.close().unwrap();
})
.unwrap();
}
struct Running(Arc<AtomicUsize>);
impl Drop for Running {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::SeqCst);
}
}
#[test]
fn websocket_owner_stop_wakes_receive_in_another_task() {
runtime()
.run(async |scope| {
let (listener, server) = server(scope).await;
let port = listener.port().unwrap();
let (sender, receiver) = oneshot::channel();
let owner = scope
.spawn(move |child| async move {
let socket = ws::connect("127.0.0.1", port, "/", &limits(), &child)
.await
.unwrap();
sender.send(socket).unwrap();
std::future::pending::<()>().await;
})
.unwrap();
let socket = receiver.await.unwrap();
let (started, ready) = oneshot::channel();
let reader = scope
.spawn_leaf(move || async move {
let receiving = socket.receive();
tokio::pin!(receiving);
assert!(futures_util::poll!(&mut receiving).is_pending());
started.send(()).unwrap();
tokio::time::timeout(Duration::from_millis(200), receiving)
.await
.unwrap()
.unwrap_err()
})
.unwrap();
ready.await.unwrap();
owner.stop().await.unwrap();
assert!(reader.wait().await.unwrap().contains("WebSocket is closed"));
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn websocket_closing_message_stream_preserves_independent_socket_alias() {
runtime()
.run(async |scope| {
let (listener, server) = server(scope).await;
let socket = ws::connect("127.0.0.1", listener.port().unwrap(), "/", &limits(), scope)
.await
.unwrap();
let stream = socket.messages(|message| message);
stream.close();
assert!(stream.next().await.unwrap().is_none());
socket
.send(Message::Binary(vec![1, 2].into()))
.await
.unwrap();
assert_eq!(
socket.receive().await.unwrap(),
Some(Message::Binary(vec![1, 2].into()))
);
socket.close(1000, "").await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn websocket_cancelled_writer_closes_only_after_it_owns_the_direction() {
runtime()
.run(async |scope| {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let (started, receiving) = oneshot::channel();
let (release, released) = oneshot::channel();
let peer = scope
.spawn_leaf(move || async move {
let (stream, _) = listener.accept().await.unwrap();
let wire = tokio_tungstenite::accept_async(stream).await.unwrap();
wire.get_ref().readable().await.unwrap();
let _ = started.send(());
let _ = released.await;
})
.unwrap();
let socket = ws::connect(
"127.0.0.1",
i64::from(port),
"/",
&ws::Limits {
message_bytes: 16 * 1024 * 1024,
write_ms: 1000,
..limits()
},
scope,
)
.await
.unwrap();
let sender = socket.clone();
let mut writing = scope
.spawn_leaf(move || async move {
sender
.send(Message::Binary(vec![1; 16 * 1024 * 1024].into()))
.await
})
.unwrap();
receiving.await.unwrap();
let waiting = socket.clone();
let waiter = scope
.spawn_leaf(
move || async move { waiting.send(Message::Text("queued".into())).await },
)
.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
waiter.stop().await.unwrap();
assert!(
tokio::time::timeout(Duration::from_millis(10), &mut writing)
.await
.is_err()
);
writing.stop().await.unwrap();
assert!(socket.send(Message::Text("closed".into())).await.is_err());
release.send(()).unwrap();
peer.wait().await.unwrap();
})
.unwrap();
}
#[test]
fn websocket_close_keeps_the_handshake_when_a_reader_is_waiting() {
runtime()
.run(async |scope| {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let (replied, reply) = oneshot::channel();
let peer = scope
.spawn_leaf(move || async move {
let (stream, _) = listener.accept().await.unwrap();
let mut wire = tokio_tungstenite::accept_async(stream).await.unwrap();
assert!(matches!(
wire.next().await,
Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_)))
));
wire.flush().await.unwrap();
replied.send(()).unwrap();
})
.unwrap();
let socket = ws::connect("127.0.0.1", i64::from(port), "/", &limits(), scope)
.await
.unwrap();
let receiving = socket.receive();
tokio::pin!(receiving);
assert!(futures_util::poll!(&mut receiving).is_pending());
let closing = socket.close(1000, "");
tokio::pin!(closing);
assert!(futures_util::poll!(&mut closing).is_pending());
reply.await.unwrap();
tokio::time::sleep(Duration::from_millis(25)).await;
let received = receiving.await;
closing.await.unwrap();
assert!(received.unwrap_err().contains("WebSocket is closing"));
peer.wait().await.unwrap();
})
.unwrap();
}
#[test]
fn websocket_close_deadline_releases_all_aliases() {
runtime()
.run(async |scope| {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let (release, released) = oneshot::channel();
let peer = scope
.spawn_leaf(move || async move {
let (stream, _) = listener.accept().await.unwrap();
let _wire = tokio_tungstenite::accept_async(stream).await.unwrap();
let _ = released.await;
})
.unwrap();
let socket = ws::connect(
"127.0.0.1",
i64::from(port),
"/",
&ws::Limits {
write_ms: 50,
..limits()
},
scope,
)
.await
.unwrap();
let alias = socket.clone();
let receiving = alias.receive();
tokio::pin!(receiving);
assert!(futures_util::poll!(&mut receiving).is_pending());
let (closed, received) = tokio::join!(socket.close(1000, ""), receiving);
assert!(closed.unwrap_err().contains("timed out"));
assert!(received.unwrap_err().contains("WebSocket is closing"));
assert!(
alias
.send(Message::Text("closed".into()))
.await
.unwrap_err()
.contains("WebSocket is closed")
);
release.send(()).unwrap();
peer.wait().await.unwrap();
})
.unwrap();
}
+242
View File
@@ -0,0 +1,242 @@
use super::*;
use dever_runtime::http::websocket::{self as ws, Message, WebSocket};
mod lifecycle;
mod protocol;
mod shared;
#[path = "../../../performance/websocket/shared/mod.rs"]
mod shared_reference;
fn limits() -> ws::Limits {
ws::Limits {
idle_ms: 1000,
write_ms: 1000,
..ws::Limits::default()
}
}
async fn echo(
_: Request,
reply: http::HttpReply,
scope: Arc<Scope<'static>>,
) -> Result<(), String> {
let socket = ws::accept(&reply, &limits(), &scope)
.await
.map_err(|error| error.to_string())?;
while let Ok(Some(message)) = socket.receive().await {
if matches!(message, Message::Text(_) | Message::Binary(_)) {
socket
.send(message)
.await
.map_err(|error| error.to_string())?;
}
}
Ok(())
}
async fn server(
scope: &Scope<'_>,
) -> (
net::Listener,
dever_runtime::task::Task<Result<(), http::ServeError>>,
) {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let incoming = listener.clone();
let task = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&Limits::default(),
&http::LiveLimits::default(),
&child,
echo,
)
.await
})
.unwrap();
(listener, task)
}
#[test]
fn websocket_live_upgrade_echo_control_frames_and_close() {
runtime()
.run(async |scope| {
let (listener, server) = server(scope).await;
let socket = ws::connect(
"127.0.0.1",
listener.port().unwrap(),
"/echo?key=value",
&limits(),
scope,
)
.await
.unwrap();
for message in [
Message::Text("你好".into()),
Message::Binary(vec![0, 255, 17].into()),
] {
socket.send(message.clone()).await.unwrap();
assert_eq!(socket.receive().await.unwrap(), Some(message));
}
socket
.send(Message::Ping(b"ping".to_vec().into()))
.await
.unwrap();
assert_eq!(
socket.receive().await.unwrap(),
Some(Message::Pong(b"ping".to_vec().into()))
);
assert!(
socket
.send(Message::Ping(vec![0; 126].into()))
.await
.is_err()
);
assert!(socket.close(1005, "").await.is_err());
assert!(socket.close(1000, &"界".repeat(42)).await.is_err());
socket
.send(Message::Text("still open".into()))
.await
.unwrap();
assert_eq!(
socket.receive().await.unwrap(),
Some(Message::Text("still open".into()))
);
socket.close(1000, "结束").await.unwrap();
assert!(socket.receive().await.unwrap().is_none());
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn websocket_aliases_send_whole_messages_while_receiving() {
runtime()
.run(async |scope| {
let (listener, server) = server(scope).await;
let socket = ws::connect("127.0.0.1", listener.port().unwrap(), "/", &limits(), scope)
.await
.unwrap();
let writers = futures_util::future::join_all((0u8..4).map(|writer| {
let socket = &socket;
async move {
for sequence in 0u8..16 {
let mut bytes = vec![writer; 65536];
bytes[1] = sequence;
socket.send(Message::Binary(bytes.into())).await.unwrap();
}
}
}));
let receiving = async {
let mut next = [0u8; 4];
for _ in 0..64 {
let Some(Message::Binary(bytes)) = socket.receive().await.unwrap() else {
panic!("expected a complete binary message");
};
assert_eq!(bytes.len(), 65536);
let writer = bytes[0];
assert_eq!(bytes[1], next[usize::from(writer)]);
assert!(bytes[2..].iter().all(|byte| *byte == writer));
next[usize::from(writer)] += 1;
}
assert_eq!(next, [16; 4]);
};
tokio::join!(writers, receiving);
socket.close(1000, "").await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn websocket_wss_uses_supplied_ca_and_dns_with_no_insecure_fallback() {
runtime()
.run(async |scope| {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let server = scope
.spawn(move |child| async move {
let tls = dever_runtime::tls::server(tls::CERTIFICATE, tls::KEY).unwrap();
http::serve_live_tls(
&incoming,
&Limits::default(),
&http::LiveLimits::default(),
&child,
&tls,
echo,
)
.await
})
.unwrap();
let url = format!("wss://localhost:{port}/echo");
assert!(
ws::open(&url, &dever_runtime::tls::system(), &limits(), scope)
.await
.is_err()
);
let trusted = dever_runtime::tls::client(tls::ROOT).unwrap();
let socket = ws::open(&url, &trusted, &limits(), scope).await.unwrap();
socket
.send(Message::Text("verified TLS".into()))
.await
.unwrap();
assert_eq!(
socket.receive().await.unwrap(),
Some(Message::Text("verified TLS".into()))
);
socket.close(1000, "").await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn websocket_invalid_inputs_fail_before_connecting() {
runtime()
.run(async |scope| {
for limit in [
ws::Limits {
message_bytes: 0,
..limits()
},
ws::Limits {
idle_ms: 0,
..limits()
},
ws::Limits {
write_ms: -1,
..limits()
},
] {
assert!(
ws::connect("127.0.0.1", 0, "/", &limit, scope)
.await
.is_err()
);
}
for url in [
"http://localhost/",
"ws://user:password@localhost/",
"ws://localhost/#fragment",
"ws://localhost:99999/",
] {
assert!(
ws::open(url, &dever_runtime::tls::system(), &limits(), scope)
.await
.is_err()
);
}
for target in ["http://localhost/", "*", "/#fragment"] {
assert!(
ws::connect("127.0.0.1", 0, target, &limits(), scope)
.await
.is_err()
);
}
})
.unwrap();
}
@@ -0,0 +1,243 @@
use super::*;
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::{
Message as Frame,
protocol::frame::{
Frame as RawFrame,
coding::{Data, OpCode},
},
};
#[test]
#[ignore = "explicit release WebSocket handshake performance probe"]
fn websocket_upgrade_cost() {
runtime()
.run(async |scope| {
let (listener, serving) = server(scope).await;
let port = listener.port().unwrap();
println!("\ncase,sample,iterations,elapsed_ns");
for sample in 0..16 {
let started = std::time::Instant::now();
for _ in 0..20 {
let socket = ws::connect("127.0.0.1", port, "/echo", &limits(), scope)
.await
.unwrap();
socket.send(Message::Text("check".into())).await.unwrap();
assert_eq!(
socket.receive().await.unwrap(),
Some(Message::Text("check".into()))
);
socket.close(1000, "").await.unwrap();
}
let elapsed = started.elapsed().as_nanos();
if sample != 0 {
println!("websocket_upgrade,{},20,{elapsed}", sample - 1);
}
}
listener.close().unwrap();
serving.wait().await.unwrap().unwrap();
})
.unwrap();
}
#[test]
fn websocket_client_limits_handshake_without_limiting_later_frames() {
runtime()
.run(async |scope| {
for header_bytes in [60000, 66000] {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let peer = scope
.spawn_leaf(move || async move {
let (stream, _) = listener.accept().await.unwrap();
let handshake = tokio_tungstenite::accept_hdr_async(
stream,
#[expect(clippy::result_large_err, reason = "Tungstenite 的握手回调要求直接返回 HTTP 错误响应")]
move |_: &tokio_tungstenite::tungstenite::handshake::server::Request,
mut response: tokio_tungstenite::tungstenite::handshake::server::Response| {
response.headers_mut().insert(
"x-padding",
"x".repeat(header_bytes).parse().unwrap(),
);
Ok(response)
},
)
.await;
if header_bytes == 60000 {
let mut wire = handshake.unwrap();
wire.send(Frame::Binary(vec![17; 70000].into())).await.unwrap();
assert!(matches!(wire.next().await, Some(Ok(Frame::Close(_)))));
wire.flush().await.unwrap();
}
})
.unwrap();
let connection = ws::connect("127.0.0.1", i64::from(port), "/", &limits(), scope).await;
if header_bytes == 60000 {
let socket = connection.unwrap();
assert_eq!(socket.receive().await.unwrap(), Some(Message::Binary(vec![17; 70000].into())));
socket.close(1000, "").await.unwrap();
} else {
assert!(connection.is_err());
}
peer.wait().await.unwrap();
}
})
.unwrap();
}
#[test]
fn websocket_client_reassembles_fragments_and_rejects_cumulative_limit_and_utf8() {
runtime()
.run(async |scope| {
for invalid in [false, true] {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let peer = scope
.spawn_leaf(move || async move {
let (stream, _) = listener.accept().await.unwrap();
let mut wire = tokio_tungstenite::accept_async(stream).await.unwrap();
wire.send(Frame::Frame(RawFrame::message(
vec![0xe4, 0xb8],
OpCode::Data(Data::Text),
false,
)))
.await
.unwrap();
wire.send(Frame::Frame(RawFrame::message(
vec![0xad],
OpCode::Data(Data::Continue),
true,
)))
.await
.unwrap();
if invalid {
wire.send(Frame::Frame(RawFrame::message(
vec![0xff],
OpCode::Data(Data::Text),
true,
)))
.await
.unwrap();
} else {
wire.send(Frame::Frame(RawFrame::message(
b"12345".to_vec(),
OpCode::Data(Data::Text),
false,
)))
.await
.unwrap();
wire.send(Frame::Frame(RawFrame::message(
b"6789".to_vec(),
OpCode::Data(Data::Continue),
true,
)))
.await
.unwrap();
}
let _ = wire.next().await;
})
.unwrap();
let socket = ws::connect(
"127.0.0.1",
i64::from(port),
"/",
&ws::Limits {
message_bytes: 8,
..limits()
},
scope,
)
.await
.unwrap();
assert_eq!(
socket.receive().await.unwrap(),
Some(Message::Text("中".into()))
);
assert!(socket.receive().await.is_err());
assert!(socket.send(Message::Text("closed".into())).await.is_err());
peer.wait().await.unwrap();
}
})
.unwrap();
}
#[test]
fn websocket_accept_failure_can_send_http_error_and_http2_cannot_upgrade() {
runtime()
.run(async |scope| {
for http2 in [false, true] {
let listener = net::listen("127.0.0.1", 0, scope).await.unwrap();
let port = listener.port().unwrap();
let incoming = listener.clone();
let limits = Limits {
http2: http2.then(http::Http2Limits::default),
..Limits::default()
};
let server_limits = limits.clone();
let server = scope
.spawn(move |child| async move {
http::serve_live(
&incoming,
&server_limits,
&http::LiveLimits::default(),
&child,
|_, reply, scope| async move {
assert!(
ws::accept(&reply, &super::limits(), &scope).await.is_err()
);
reply
.respond(Response {
status: 400,
headers: vec![],
body: b"not a websocket".to_vec().into(),
})
.await
.map_err(|error| error.to_string())
},
)
.await
})
.unwrap();
let response = http::send("127.0.0.1", port, request("/", &[]), &limits)
.await
.unwrap();
assert_eq!(response.status, 400);
assert_eq!(response.body.as_ref(), b"not a websocket");
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
}
})
.unwrap();
}
#[test]
fn websocket_upgrade_preserves_prefetched_frames_and_rejects_unmasked_clients() {
runtime().run(async |scope| {
let (listener, server) = server(scope).await;
let port = listener.port().unwrap();
scope.blocking(move || {
let mut peer = TcpStream::connect(("127.0.0.1", port as u16)).unwrap();
peer.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
peer.set_write_timeout(Some(Duration::from_secs(2))).unwrap();
let mut request = b"GET /echo HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n".to_vec();
request.extend([0x81, 0x81, 1, 2, 3, 4, b'x' ^ 1]);
peer.write_all(&request).unwrap();
let mut peer = BufReader::new(peer);
let mut line = String::new();
peer.read_line(&mut line).unwrap();
assert!(line.starts_with("HTTP/1.1 101"), "{line}");
loop {
line.clear();
assert!(peer.read_line(&mut line).unwrap() > 0);
if line == "\r\n" { break; }
}
let mut echo = [0; 3];
peer.read_exact(&mut echo).unwrap();
assert_eq!(echo, [0x81, 1, b'x']);
peer.get_mut().write_all(&[0x81, 1, b'y']).unwrap();
assert_eq!(peer.read(&mut echo).unwrap(), 0);
}).await.unwrap();
listener.close().unwrap();
server.wait().await.unwrap().unwrap();
}).unwrap();
}
+185
View File
@@ -0,0 +1,185 @@
use super::shared_reference::{Limits, Owner, Socket};
use futures_util::{SinkExt, StreamExt};
use std::time::Duration;
use tokio::{
net::{TcpListener, TcpStream},
sync::oneshot,
};
use tokio_tungstenite::{
WebSocketStream,
tungstenite::{Message, protocol::WebSocketConfig},
};
fn limits() -> Limits {
Limits {
message: 16 * 1024 * 1024,
idle: Duration::from_secs(2),
write: Duration::from_secs(2),
}
}
async fn pair(limits: Limits) -> (Socket, Owner, WebSocketStream<TcpStream>) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let config = WebSocketConfig::default()
.max_message_size(Some(limits.message))
.max_frame_size(Some(limits.message))
.write_buffer_size(0);
let client = async {
let stream = TcpStream::connect(address).await.unwrap();
stream.set_nodelay(true).unwrap();
tokio_tungstenite::client_async_with_config(
format!("ws://{address}/"),
stream,
Some(config),
)
.await
.unwrap()
.0
};
let server = async {
let (stream, _) = listener.accept().await.unwrap();
stream.set_nodelay(true).unwrap();
tokio_tungstenite::accept_async_with_config(stream, Some(config))
.await
.unwrap()
};
let (client, server) = tokio::join!(client, server);
let (socket, owner) = Socket::new(client, limits);
(socket, owner, server)
}
#[tokio::test]
async fn websocket_rust_reference_owner_exit_wakes_reader_and_releases_tcp() {
let (socket, owner, mut peer) = pair(limits()).await;
let alias = socket.clone();
let reading = alias.receive();
tokio::pin!(reading);
assert!(futures_util::poll!(&mut reading).is_pending());
drop(owner);
assert!(
tokio::time::timeout(Duration::from_millis(100), reading)
.await
.unwrap()
.is_err()
);
assert!(socket.send(Message::Text("closed".into())).await.is_err());
assert!(
tokio::time::timeout(Duration::from_millis(100), peer.next())
.await
.unwrap()
.unwrap()
.is_err()
);
}
#[tokio::test]
async fn websocket_rust_reference_full_duplex_aliases_preserve_messages() {
let (socket, _owner, mut peer) = pair(limits()).await;
let server = tokio::spawn(async move {
while let Some(message) = peer.next().await {
let message = message.unwrap();
if message.is_close() {
peer.flush().await.unwrap();
break;
}
peer.send(message).await.unwrap();
}
});
let sender = |writer: u8| {
let socket = socket.clone();
async move {
for sequence in 0..16 {
let mut bytes = vec![writer; 65536];
bytes[1] = sequence;
socket.send(Message::Binary(bytes.into())).await.unwrap();
}
}
};
let receiver = async {
let mut sequences = [0_u8; 2];
for _ in 0..32 {
let Message::Binary(bytes) = socket.receive().await.unwrap().unwrap() else {
panic!("binary expected")
};
let writer = usize::from(bytes[0]);
assert_eq!(bytes[1], sequences[writer]);
assert_eq!(bytes.len(), 65536);
assert!(bytes[2..].iter().all(|byte| *byte == writer as u8));
sequences[writer] += 1;
}
assert_eq!(sequences, [16, 16]);
};
tokio::time::timeout(Duration::from_secs(5), async {
tokio::join!(sender(0), sender(1), receiver);
})
.await
.unwrap();
socket.close(1000, "").await.unwrap();
server.await.unwrap();
}
#[tokio::test]
async fn websocket_rust_reference_close_has_priority_over_readable_reply() {
let (socket, _owner, mut peer) = pair(limits()).await;
let (sent, ready) = oneshot::channel();
let server = tokio::spawn(async move {
assert!(peer.next().await.unwrap().unwrap().is_close());
peer.flush().await.unwrap();
sent.send(()).unwrap();
});
let reading = socket.receive();
tokio::pin!(reading);
assert!(futures_util::poll!(&mut reading).is_pending());
let closing = socket.close(1000, "");
tokio::pin!(closing);
assert!(futures_util::poll!(&mut closing).is_pending());
ready.await.unwrap();
tokio::time::sleep(Duration::from_millis(25)).await;
assert!(reading.await.is_err());
closing.await.unwrap();
server.await.unwrap();
}
#[tokio::test]
async fn websocket_rust_reference_cancelled_writer_only_closes_after_lock() {
let (socket, _owner, _peer) = pair(limits()).await;
let mut writing = Box::pin(socket.send(Message::Binary(vec![1; limits().message].into())));
assert!(futures_util::poll!(&mut writing).is_pending());
let mut queued = Box::pin(socket.send(Message::Text("queued".into())));
assert!(futures_util::poll!(&mut queued).is_pending());
drop(queued);
assert!(futures_util::poll!(&mut writing).is_pending());
drop(writing);
assert!(socket.send(Message::Text("closed".into())).await.is_err());
}
#[tokio::test]
async fn websocket_rust_reference_limits_ping_and_idle_close() {
let (socket, _owner, mut peer) = pair(Limits {
message: 128,
idle: Duration::from_millis(25),
..limits()
})
.await;
assert!(
socket
.send(Message::Binary(vec![0; 129].into()))
.await
.is_err()
);
assert!(
socket
.send(Message::Ping(vec![0; 126].into()))
.await
.is_err()
);
peer.send(Message::Ping(vec![7].into())).await.unwrap();
assert!(socket.receive().await.unwrap().unwrap().is_ping());
assert_eq!(
peer.next().await.unwrap().unwrap(),
Message::Pong(vec![7].into())
);
assert!(socket.receive().await.unwrap_err().contains("timed out"));
assert!(socket.send(Message::Text("closed".into())).await.is_err());
}