Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
Reference in new issue
Block a user