sark 0.13.0

Simple Asynchronous Rust webKit - Server
Documentation
#![cfg(target_os = "linux")]

mod support;

use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::{Duration, Instant};

use dope_test::Harness;
use http::StatusCode;
use sark::{Executor, Throughput, driver};

#[sark_gen::request]
struct HelloRequest {}

#[sark_gen::response(raw)]
struct HelloReply {
    status: StatusCode,
    body: Vec<u8>,
}

#[sark_gen::handler]
fn hello(_req: HelloRequest, _state: &()) -> HelloReply {
    let mut body = Vec::new();
    body.extend_from_slice(b"hello");
    HelloReply {
        status: StatusCode::OK,
        body,
    }
}

sark_gen::define_route! {
    SlowlorisDispatch: () => {
        GET "/hello" => hello,
    }
}

const HEAD_TIMEOUT: Duration = Duration::from_millis(500);

fn server(bind: std::net::SocketAddr) -> support::TestHttpServer {
    support::http_server(bind, HEAD_TIMEOUT)
}

fn read_to_close(stream: &mut TcpStream) -> (Vec<u8>, Duration) {
    let start = Instant::now();
    let mut buf = Vec::new();
    let mut chunk = [0u8; 4096];
    loop {
        match stream.read(&mut chunk) {
            Ok(0) => break,
            Ok(n) => buf.extend_from_slice(&chunk[..n]),
            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
            Err(e) => panic!("read: {e}"),
        }
    }
    (buf, start.elapsed())
}

#[test]
fn partial_head_then_stall_is_closed_after_deadline() {
    let harness = Harness::bind().expect("reserve test address");
    let bind = harness.addr();
    harness
        .run_with_trigger(
            |_ctx, trigger| {
                let driver_config =
                    driver::Config::for_tcp_profile::<Throughput>(support::MAX_CONNECTIONS);
                let executor = Executor::new(driver_config)?
                    .with_storage(dope_net::link::egress::storage::Storage::default());
                executor.enter(|mut session| {
                    let timer = sark::Timer::new();
                    server(bind).serve(
                        &mut session,
                        SlowlorisDispatch::new(
                            &(),
                            &timer,
                            sark::app::Config {
                                task_capacity: support::MAX_CONNECTIONS,
                            },
                        ),
                        Some(trigger),
                    )
                })
            },
            |bind| {
                let mut sock = TcpStream::connect(bind).expect("connect");
                sock.set_read_timeout(Some(Duration::from_secs(5)))
                    .expect("set timeout");
                sock.write_all(b"GET /hello HTTP/1.1\r\nHost: x\r\n")
                    .expect("write partial head");

                let (buf, elapsed) = read_to_close(&mut sock);
                assert!(
                    elapsed >= Duration::from_millis(300),
                    "closed too early ({elapsed:?}); deadline must bound from first byte"
                );
                assert!(
                    elapsed < Duration::from_secs(4),
                    "not closed by head deadline ({elapsed:?})"
                );
                let text = String::from_utf8_lossy(&buf);
                assert!(
                    buf.is_empty() || text.contains("408"),
                    "expected close or 408, got: {text:?}"
                );
            },
        )
        .expect("harness");
}

#[test]
fn normal_fast_request_is_unaffected() {
    let harness = Harness::bind().expect("reserve test address");
    let bind = harness.addr();
    harness
        .run_with_trigger(
            |_ctx, trigger| {
                let driver_config =
                    driver::Config::for_tcp_profile::<Throughput>(support::MAX_CONNECTIONS);
                let executor = Executor::new(driver_config)?
                    .with_storage(dope_net::link::egress::storage::Storage::default());
                executor.enter(|mut session| {
                    let timer = sark::Timer::new();
                    server(bind).serve(
                        &mut session,
                        SlowlorisDispatch::new(
                            &(),
                            &timer,
                            sark::app::Config {
                                task_capacity: support::MAX_CONNECTIONS,
                            },
                        ),
                        Some(trigger),
                    )
                })
            },
            |bind| {
                let mut sock = TcpStream::connect(bind).expect("connect");
                sock.set_read_timeout(Some(Duration::from_secs(2)))
                    .expect("set timeout");
                sock.write_all(b"GET /hello HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n")
                    .expect("write");
                let (buf, _) = read_to_close(&mut sock);
                let text = String::from_utf8_lossy(&buf);
                assert!(text.starts_with("HTTP/1.1 200 "), "status: {text:?}");
                assert!(!text.contains("408"), "false-positive deadline: {text:?}");
                assert!(text.ends_with("hello"), "body: {text:?}");
            },
        )
        .expect("harness");
}

#[test]
fn slow_but_progressing_within_deadline_completes() {
    let harness = Harness::bind().expect("reserve test address");
    let bind = harness.addr();
    harness
        .run_with_trigger(
            |_ctx, trigger| {
                let driver_config =
                    driver::Config::for_tcp_profile::<Throughput>(support::MAX_CONNECTIONS);
                let executor = Executor::new(driver_config)?
                    .with_storage(dope_net::link::egress::storage::Storage::default());
                executor.enter(|mut session| {
                    let timer = sark::Timer::new();
                    server(bind).serve(
                        &mut session,
                        SlowlorisDispatch::new(
                            &(),
                            &timer,
                            sark::app::Config {
                                task_capacity: support::MAX_CONNECTIONS,
                            },
                        ),
                        Some(trigger),
                    )
                })
            },
            |bind| {
                let mut sock = TcpStream::connect(bind).expect("connect");
                sock.set_read_timeout(Some(Duration::from_secs(2)))
                    .expect("set timeout");
                sock.write_all(b"GET /hello HTTP/1.1\r\n").expect("write 1");
                std::thread::sleep(Duration::from_millis(150));
                sock.write_all(b"Host: x\r\n").expect("write 2");
                std::thread::sleep(Duration::from_millis(150));
                sock.write_all(b"Connection: close\r\n\r\n")
                    .expect("write 3");

                let (buf, _) = read_to_close(&mut sock);
                let text = String::from_utf8_lossy(&buf);
                assert!(text.starts_with("HTTP/1.1 200 "), "status: {text:?}");
                assert!(
                    !text.contains("408"),
                    "false close on slow progress: {text:?}"
                );
                assert!(text.ends_with("hello"), "body: {text:?}");
            },
        )
        .expect("harness");
}

#[test]
fn zero_fast_timer_capacity_tracks_every_connection() {
    let harness = Harness::bind().expect("reserve test address");
    let bind = harness.addr();
    harness
        .run_with_trigger(
            |_ctx, trigger| {
                let mut driver_config =
                    driver::Config::for_tcp_profile::<Throughput>(support::MAX_CONNECTIONS);
                driver_config.timer_slots = 0;
                let executor = Executor::new(driver_config)?
                    .with_storage(dope_net::link::egress::storage::Storage::default());
                executor.enter(|mut session| {
                    let timer = sark::Timer::new();
                    server(bind).serve(
                        &mut session,
                        SlowlorisDispatch::new(
                            &(),
                            &timer,
                            sark::app::Config {
                                task_capacity: support::MAX_CONNECTIONS,
                            },
                        ),
                        Some(trigger),
                    )
                })
            },
            |bind| {
                let mut tracked = TcpStream::connect(bind).expect("connect tracked");
                tracked
                    .set_read_timeout(Some(Duration::from_secs(2)))
                    .expect("set timeout");
                tracked
                    .write_all(b"GET /hello HTTP/1.1\r\nHost: tracked\r\n")
                    .expect("write tracked head");
                std::thread::sleep(Duration::from_millis(50));

                let mut second = TcpStream::connect(bind).expect("connect second");
                second
                    .set_read_timeout(Some(Duration::from_secs(2)))
                    .expect("set timeout");
                second
                    .write_all(b"GET /hello HTTP/1.1\r\nHost: second\r\n")
                    .expect("write second head");

                let (second_reply, elapsed) = read_to_close(&mut second);
                assert!(
                    elapsed >= Duration::from_millis(300),
                    "second connection closed before its deadline: {elapsed:?}"
                );
                assert!(
                    elapsed < Duration::from_secs(2),
                    "second connection was not deadline-tracked: {elapsed:?}"
                );
                let (first_reply, _) = read_to_close(&mut tracked);
                for reply in [first_reply, second_reply] {
                    let text = String::from_utf8_lossy(&reply);
                    assert!(
                        reply.is_empty() || text.contains("408"),
                        "expected close or 408, got: {text:?}"
                    );
                }
            },
        )
        .expect("harness");
}