use std::time::Duration;
use gwk_domain::protocol::{
CONNECTION_BUDGET_WINDOW_SECS, FRAME_BODY_MAX_BYTES, FRAME_BODY_MIN_BYTES,
FRAME_KIND_RESERVED_STREAM, FRAME_LENGTH_PREFIX_BYTES, FrameKind, KernelErrorCode,
};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::time::Instant;
use super::WireError;
#[derive(Debug)]
pub struct Budget {
ingress_per_window: usize,
egress_per_window: usize,
window: Duration,
ingress_spent: usize,
egress_spent: usize,
window_started: Instant,
}
impl Budget {
pub fn new(ingress: usize, egress: usize) -> Self {
Self::with_window(
ingress,
egress,
Duration::from_secs(CONNECTION_BUDGET_WINDOW_SECS),
)
}
pub fn with_window(ingress: usize, egress: usize, window: Duration) -> Self {
Self {
ingress_per_window: ingress,
egress_per_window: egress,
window,
ingress_spent: 0,
egress_spent: 0,
window_started: Instant::now(),
}
}
async fn spend_ingress(&mut self, bytes: usize) -> Result<(), WireError> {
self.spend(bytes, Direction::Ingress).await
}
async fn spend_egress(&mut self, bytes: usize) -> Result<(), WireError> {
self.spend(bytes, Direction::Egress).await
}
async fn spend(&mut self, bytes: usize, direction: Direction) -> Result<(), WireError> {
let per_window = match direction {
Direction::Ingress => self.ingress_per_window,
Direction::Egress => self.egress_per_window,
};
if bytes > per_window {
return Err(WireError::new(
KernelErrorCode::FrameSize,
format!(
"a {direction} frame of {bytes} bytes exceeds the whole \
{per_window}-byte window allowance"
),
));
}
loop {
let elapsed = self.window_started.elapsed();
if elapsed >= self.window {
self.ingress_spent = 0;
self.egress_spent = 0;
self.window_started = Instant::now();
}
let spent = match direction {
Direction::Ingress => &mut self.ingress_spent,
Direction::Egress => &mut self.egress_spent,
};
if *spent + bytes <= per_window {
*spent += bytes;
return Ok(());
}
tokio::time::sleep(self.window - elapsed).await;
}
}
}
#[derive(Debug, Clone, Copy)]
enum Direction {
Ingress,
Egress,
}
impl std::fmt::Display for Direction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Ingress => "received",
Self::Egress => "sent",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
pub kind: FrameKind,
pub body: Vec<u8>,
}
#[derive(Debug)]
pub enum Incoming {
Frame(Frame),
Closed,
}
pub async fn read_frame<R>(
reader: &mut R,
max_body: u32,
budget: &mut Budget,
) -> Result<Incoming, WireError>
where
R: AsyncRead + Unpin,
{
let ceiling = max_body.min(FRAME_BODY_MAX_BYTES);
let mut prefix = [0u8; FRAME_LENGTH_PREFIX_BYTES];
match reader.read_exact(&mut prefix[..1]).await {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(Incoming::Closed),
Err(e) => return Err(WireError::io("read frame length", e)),
}
reader
.read_exact(&mut prefix[1..])
.await
.map_err(|e| WireError::io("read frame length", e))?;
let announced = u32::from_be_bytes(prefix);
if announced < FRAME_BODY_MIN_BYTES {
return Err(WireError::new(
KernelErrorCode::FrameSize,
format!(
"frame body_length {announced} is below the {FRAME_BODY_MIN_BYTES}-byte minimum"
),
));
}
if announced > ceiling {
return Err(WireError::new(
KernelErrorCode::FrameSize,
format!("frame body_length {announced} exceeds the {ceiling}-byte maximum"),
));
}
let total = FRAME_LENGTH_PREFIX_BYTES + announced as usize;
budget.spend_ingress(total).await?;
let mut body = vec![0u8; announced as usize];
reader
.read_exact(&mut body)
.await
.map_err(|e| WireError::io("read frame body", e))?;
let kind_byte = body.remove(0);
let kind = FrameKind::from_u8(kind_byte).ok_or_else(|| {
let detail = if kind_byte == FRAME_KIND_RESERVED_STREAM {
" (reserved for the terminal engine and not accepted in v1)"
} else {
""
};
WireError::new(
KernelErrorCode::Handshake,
format!("unknown frame kind 0x{kind_byte:02x}{detail}"),
)
})?;
Ok(Incoming::Frame(Frame { kind, body }))
}
pub async fn write_frame<W>(
writer: &mut W,
kind: FrameKind,
body: &[u8],
budget: &mut Budget,
) -> Result<(), WireError>
where
W: AsyncWrite + Unpin,
{
let announced = u32::try_from(body.len() + 1).map_err(|_| {
WireError::new(
KernelErrorCode::FrameSize,
format!("frame body {} bytes does not fit a u32 length", body.len()),
)
})?;
if announced > FRAME_BODY_MAX_BYTES {
return Err(WireError::new(
KernelErrorCode::FrameSize,
format!(
"frame body_length {announced} exceeds the {FRAME_BODY_MAX_BYTES}-byte maximum"
),
));
}
budget
.spend_egress(FRAME_LENGTH_PREFIX_BYTES + announced as usize)
.await?;
let mut out = Vec::with_capacity(FRAME_LENGTH_PREFIX_BYTES + announced as usize);
out.extend_from_slice(&announced.to_be_bytes());
out.push(kind.as_u8());
out.extend_from_slice(body);
writer
.write_all(&out)
.await
.map_err(|e| WireError::io("write frame", e))?;
writer
.flush()
.await
.map_err(|e| WireError::io("flush frame", e))
}
#[cfg(test)]
mod tests {
use super::*;
fn budget() -> Budget {
Budget::new(1 << 20, 1 << 20)
}
async fn read_bytes(raw: &[u8], max_body: u32) -> Result<Incoming, WireError> {
read_frame(
&mut std::io::Cursor::new(raw.to_vec()),
max_body,
&mut budget(),
)
.await
}
#[tokio::test]
async fn a_frame_survives_its_own_round_trip() {
let mut wire = Vec::new();
let mut out = budget();
write_frame(
&mut wire,
FrameKind::Json,
b"{\"type\":\"health\"}",
&mut out,
)
.await
.expect("write");
assert_eq!(&wire[..4], &18u32.to_be_bytes());
assert_eq!(wire[4], FrameKind::Json.as_u8());
match read_bytes(&wire, FRAME_BODY_MAX_BYTES).await.expect("read") {
Incoming::Frame(frame) => {
assert_eq!(frame.kind, FrameKind::Json);
assert_eq!(frame.body, b"{\"type\":\"health\"}");
}
Incoming::Closed => panic!("closed on a whole frame"),
}
}
fn block_on<F: Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.build()
.expect("a current-thread runtime")
.block_on(future)
}
proptest::proptest! {
#[test]
fn any_body_survives_the_round_trip(body in proptest::collection::vec(proptest::num::u8::ANY, 0..8192)) {
let read = block_on(async {
let mut wire = Vec::new();
let mut out = budget();
write_frame(&mut wire, FrameKind::Json, &body, &mut out).await.expect("write");
assert_eq!(&wire[..4], &(body.len() as u32 + 1).to_be_bytes());
read_bytes(&wire, FRAME_BODY_MAX_BYTES).await.expect("read")
});
match read {
Incoming::Frame(frame) => {
proptest::prop_assert_eq!(frame.kind, FrameKind::Json);
proptest::prop_assert_eq!(frame.body, body);
}
Incoming::Closed => proptest::prop_assert!(false, "closed on a whole frame"),
}
}
#[test]
fn no_other_kind_byte_is_admitted(kind in proptest::num::u8::ANY) {
proptest::prop_assume!(kind != FrameKind::Json.as_u8());
let mut wire = 2u32.to_be_bytes().to_vec();
wire.push(kind);
wire.push(b'x');
let error = block_on(read_bytes(&wire, FRAME_BODY_MAX_BYTES))
.expect_err("an unknown kind byte was admitted");
proptest::prop_assert_eq!(error.code, KernelErrorCode::Handshake);
}
#[test]
fn arbitrary_bytes_are_answered_and_never_panic(raw in proptest::collection::vec(proptest::num::u8::ANY, 0..512)) {
let _ = block_on(read_bytes(&raw, FRAME_BODY_MAX_BYTES));
}
}
#[tokio::test]
async fn an_illegal_length_is_refused_from_the_prefix_alone() {
for (announced, expected) in [(0u32, "below"), (FRAME_BODY_MAX_BYTES + 1, "exceeds")] {
let error = read_bytes(&announced.to_be_bytes(), FRAME_BODY_MAX_BYTES)
.await
.expect_err("illegal length accepted");
assert_eq!(error.code, KernelErrorCode::FrameSize);
assert!(error.message.contains(expected), "{error}");
}
}
#[tokio::test]
async fn the_hello_cap_is_the_same_codec_with_a_lower_ceiling() {
let announced = gwk_domain::protocol::HELLO_MAX_BYTES + 1;
let error = read_bytes(
&announced.to_be_bytes(),
gwk_domain::protocol::HELLO_MAX_BYTES,
)
.await
.expect_err("oversized hello accepted");
assert_eq!(error.code, KernelErrorCode::FrameSize);
let mut whole = announced.to_be_bytes().to_vec();
whole.push(FrameKind::Json.as_u8());
whole.extend(std::iter::repeat_n(b' ', announced as usize - 1));
assert!(matches!(
read_bytes(&whole, FRAME_BODY_MAX_BYTES)
.await
.expect("read"),
Incoming::Frame(_)
));
}
#[tokio::test]
async fn the_reserved_engine_kind_is_refused_by_name() {
let mut raw = 1u32.to_be_bytes().to_vec();
raw.push(FRAME_KIND_RESERVED_STREAM);
let error = read_bytes(&raw, FRAME_BODY_MAX_BYTES)
.await
.expect_err("reserved kind accepted");
assert_eq!(error.code, KernelErrorCode::Handshake);
assert!(error.message.contains("terminal engine"), "{error}");
}
#[tokio::test]
async fn a_clean_hangup_between_frames_is_not_an_error() {
assert!(matches!(
read_bytes(&[], FRAME_BODY_MAX_BYTES).await.expect("eof"),
Incoming::Closed
));
assert!(read_bytes(&[0, 0], FRAME_BODY_MAX_BYTES).await.is_err());
}
#[tokio::test(start_paused = true)]
async fn a_peer_that_outruns_its_allowance_waits_instead_of_dying() {
let mut small = Budget::new(12, 12);
let mut wire = Vec::new();
let mut writing = Budget::new(1 << 20, 1 << 20);
write_frame(&mut wire, FrameKind::Json, b"ab", &mut writing)
.await
.expect("write");
let mut stream = std::io::Cursor::new([wire.clone(), wire].concat());
let started = Instant::now();
read_frame(&mut stream, FRAME_BODY_MAX_BYTES, &mut small)
.await
.expect("first frame");
assert!(
started.elapsed() < Duration::from_millis(1),
"the first frame should not have waited"
);
read_frame(&mut stream, FRAME_BODY_MAX_BYTES, &mut small)
.await
.expect("the second frame arrives after the refill");
assert!(
started.elapsed() >= Duration::from_secs(CONNECTION_BUDGET_WINDOW_SECS),
"the second frame was not made to wait for its window"
);
}
#[tokio::test]
async fn a_frame_larger_than_a_whole_window_fails_rather_than_hanging() {
let mut sink = Vec::new();
let mut small = Budget::new(0, 6);
let error = write_frame(&mut sink, FrameKind::Json, b"abcd", &mut small)
.await
.expect_err("an unaffordable frame was not refused");
assert_eq!(error.code, KernelErrorCode::FrameSize);
assert!(error.message.contains("sent"), "{error}");
assert!(sink.is_empty());
}
}