sark 0.13.0

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

mod support;

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

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

#[sark_gen::request]
struct PingReq {}

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

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

#[sark_gen::request]
struct EchoReq {}

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

#[sark_gen::handler]
fn echo(_request: EchoReq, _state: &()) -> EchoReply {
    EchoReply {
        status: StatusCode::OK,
        body: body!("echoed"),
    }
}

sark_gen::define_route! {
    ClOverflowDispatch: () => {
        GET "/ping" => ping,
        POST "/echo" => echo,
    }
}

fn read_head(sock: &mut TcpStream) -> Vec<u8> {
    sock.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
    let mut out = Vec::new();
    let mut buf = [0u8; 4096];
    loop {
        match sock.read(&mut buf) {
            Ok(0) => break,
            Ok(n) => {
                out.extend_from_slice(&buf[..n]);
                if out.windows(4).any(|w| w == b"\r\n\r\n") {
                    break;
                }
            }
            Err(e)
                if e.kind() == std::io::ErrorKind::WouldBlock
                    || e.kind() == std::io::ErrorKind::TimedOut =>
            {
                break;
            }
            Err(e) => panic!("read: {e}"),
        }
    }
    out
}

#[test]
fn bad_content_length_does_not_panic_and_server_survives() {
    let bind: std::net::SocketAddr = "127.0.0.1:18920".parse().unwrap();
    let server = support::http_server(bind, Duration::from_secs(10));

    Harness::new(bind).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.clone().serve(
                    &mut session,
                    ClOverflowDispatch::new(
                        &(),
                        &timer,
                        sark::app::Config {
                            task_capacity: support::MAX_CONNECTIONS,
                        },
                    ),
                    Some(trigger),
                )
            })
        },
        |bind| {
            let bad_clens: &[&str] = &[
                "4294967295",
                "99999999999999999999999999999999999999",
                "12abc",
                "18446744073709551615",
                "-5",
                " ",
            ];

            for clen in bad_clens {
                let mut sock = TcpStream::connect(bind).expect("connect");
                let req =
                    format!("POST /echo HTTP/1.1\r\nHost: x\r\nContent-Length: {clen}\r\n\r\n");
                sock.write_all(req.as_bytes()).unwrap();
                let _ = sock.write_all(b"hi");
                let resp = read_head(&mut sock);
                if !resp.is_empty() {
                    let text = String::from_utf8_lossy(&resp);
                    assert!(
                        text.starts_with("HTTP/1.1 4"),
                        "bad Content-Length {clen:?} produced non-4xx response: {text}"
                    );
                }
            }

            let mut sock = TcpStream::connect(bind).expect("connect");
            sock.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
            sock.write_all(b"GET /ping HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n")
                .unwrap();
            let mut last = String::new();
            sock.read_to_string(&mut last).unwrap();
            assert!(last.contains("200 OK"), "server died after bad CL: {last}");
            assert!(last.contains("pong"), "server died after bad CL: {last}");

            let mut sock = TcpStream::connect(bind).expect("connect");
            sock.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
            let body = b"hello-body";
            let req = format!(
                "POST /echo HTTP/1.1\r\nHost: x\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
                body.len()
            );
            sock.write_all(req.as_bytes()).unwrap();
            sock.write_all(body).unwrap();
            let mut resp = String::new();
            sock.read_to_string(&mut resp).unwrap();
            assert!(resp.contains("200 "), "well-formed POST failed: {resp}");
            assert!(resp.contains("echoed"), "echo handler not reached: {resp}");
        },
    ).expect("harness");
}