#![allow(clippy::too_many_arguments)]
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use dope_extra::testing::run_with_trigger;
use http::StatusCode;
use sark::middleware::{self, Capture, Middleware};
use sark::{Build, ServerCfg};
static ACCESS_LOG_HITS: AtomicU32 = AtomicU32::new(0);
static HANDLER_HITS: AtomicU32 = AtomicU32::new(0);
const UNAUTH_RESP: &[u8] =
b"HTTP/1.1 401 Unauthorized\r\nServer: sark\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
struct AccessLog;
impl Middleware for AccessLog {
type State = ();
fn before(_ctx: &mut middleware::Ctx, _: &(), _capture: &mut Capture) -> bool {
ACCESS_LOG_HITS.fetch_add(1, Ordering::Relaxed);
false
}
}
struct AuthGuard;
impl Middleware for AuthGuard {
type State = ();
fn before(_ctx: &mut middleware::Ctx, _: &(), capture: &mut Capture) -> bool {
capture.close(UNAUTH_RESP);
true
}
}
#[sark_gen::request]
struct Req {}
#[sark_gen::response(raw)]
struct Resp {
status: StatusCode,
body: o3::buffer::Owned,
}
fn ok_body(body: &'static [u8]) -> Resp {
HANDLER_HITS.fetch_add(1, Ordering::Relaxed);
let mut buf = o3::buffer::Owned::new();
buf.extend_from_slice(body);
Resp {
status: StatusCode::OK,
body: buf,
}
}
#[sark_gen::handler]
fn log_pass(_req: Req, _: &()) -> Resp {
ok_body(b"log-pass")
}
#[sark_gen::handler]
fn secret(_req: Req, _: &()) -> Resp {
ok_body(b"secret")
}
#[sark_gen::handler]
fn nested_x(_req: Req, _: &()) -> Resp {
ok_body(b"nested-x")
}
#[sark_gen::handler]
fn open(_req: Req, _: &()) -> Resp {
ok_body(b"open")
}
sark_gen::define_route! {
MwDispatch: () => {
scope "" with (AccessLog,) => [
GET "/log-pass" => log_pass,
],
scope "/protected" with (AuthGuard,) => [
GET "/secret" => secret,
],
scope "/nested" with (AccessLog,) => {
scope "/inner" with (AuthGuard,) => [
GET "/x" => nested_x,
],
},
GET "/open" => open,
}
}
fn http_get_close(addr: std::net::SocketAddr, path: &str) -> Vec<u8> {
let mut stream = TcpStream::connect(addr).expect("connect");
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.expect("set timeout");
let req = format!("GET {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n");
stream.write_all(req.as_bytes()).expect("send request");
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 response: {e}"),
}
}
buf
}
fn status_line(buf: &[u8]) -> &[u8] {
let end = buf
.windows(2)
.position(|w| w == b"\r\n")
.unwrap_or(buf.len());
&buf[..end]
}
#[test]
fn middleware_integration() {
let bind: std::net::SocketAddr = "127.0.0.1:38766".parse().unwrap();
let cfg = ServerCfg {
bind,
max_conn: 16,
backlog: 16,
head_timeout: std::time::Duration::from_secs(10),
};
run_with_trigger(
bind,
|ctx, trigger| Build::http(mw_dispatch::new(&()), cfg.clone(), ctx, Some(trigger)),
|bind| {
ACCESS_LOG_HITS.store(0, Ordering::Relaxed);
HANDLER_HITS.store(0, Ordering::Relaxed);
let resp = http_get_close(bind, "/log-pass");
assert_eq!(status_line(&resp), b"HTTP/1.1 200 OK", "log-pass status");
assert_eq!(ACCESS_LOG_HITS.load(Ordering::Relaxed), 1);
assert_eq!(HANDLER_HITS.load(Ordering::Relaxed), 1);
let resp = http_get_close(bind, "/protected/secret");
assert_eq!(
status_line(&resp),
b"HTTP/1.1 401 Unauthorized",
"secret status"
);
assert_eq!(
ACCESS_LOG_HITS.load(Ordering::Relaxed),
1,
"access log not invoked outside its scope"
);
assert_eq!(
HANDLER_HITS.load(Ordering::Relaxed),
1,
"blocked handler must not run"
);
let resp = http_get_close(bind, "/nested/inner/x");
assert_eq!(
status_line(&resp),
b"HTTP/1.1 401 Unauthorized",
"nested status"
);
assert_eq!(
ACCESS_LOG_HITS.load(Ordering::Relaxed),
2,
"outer access log ran before inner auth blocked"
);
assert_eq!(
HANDLER_HITS.load(Ordering::Relaxed),
1,
"blocked handler must not run"
);
let resp = http_get_close(bind, "/open");
assert_eq!(status_line(&resp), b"HTTP/1.1 200 OK", "open status");
assert_eq!(HANDLER_HITS.load(Ordering::Relaxed), 2);
assert_eq!(
ACCESS_LOG_HITS.load(Ordering::Relaxed),
2,
"access log untouched for non-wrapped route"
);
let resp = http_get_close(bind, "/nonexistent");
assert_eq!(status_line(&resp), b"HTTP/1.1 404 Not Found", "404 status");
assert_eq!(
ACCESS_LOG_HITS.load(Ordering::Relaxed),
2,
"404 must not run any middleware"
);
},
);
}