#![warn(missing_docs)]
use crate::host::{Request, Response};
use crate::sync_cell::SyncCell;
pub mod host;
mod sync_cell;
struct Handler {
guest: Box<dyn Guest>,
}
pub trait Guest {
fn handle_request(&self, _request: &Request, _response: &Response) -> (bool, i32) {
(true, 0)
}
fn handle_response(&self, _req_ctx: i32, _request: &Request, _response: &Response, _is_error: bool) {}
}
static GUEST: SyncCell<Option<Handler>> = SyncCell::new(None);
pub fn register<T: Guest + 'static>(guest: T) {
let slot = unsafe { &mut *GUEST.get() };
if slot.is_none() {
*slot = Some(Handler { guest: Box::new(guest) });
}
}
#[unsafe(export_name = "handle_request")]
fn http_request() -> i64 {
let (next, ctx_next) = match unsafe { &*GUEST.get() } {
Some(handler) => handler.guest.handle_request(&Request::new(), &Response::new()),
None => (true, 0),
};
if next { (ctx_next as i64) << 32 | 1 } else { 0 }
}
#[unsafe(export_name = "handle_response")]
fn http_response(req_ctx: i32, is_error: i32) {
if let Some(handler) = unsafe { &*GUEST.get() } {
handler.guest.handle_response(req_ctx, &Request::new(), &Response::new(), is_error == 1)
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::admin;
use crate::host::feature;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicU32, Ordering};
struct TestPlugin {
request_handled: Arc<AtomicBool>,
response_handled: Arc<AtomicBool>,
continue_request: bool,
ctx_value: i32,
}
impl Guest for TestPlugin {
fn handle_request(&self, _request: &Request, _response: &Response) -> (bool, i32) {
self.request_handled.store(true, Ordering::SeqCst);
(self.continue_request, self.ctx_value)
}
fn handle_response(&self, _req_ctx: i32, _request: &Request, _response: &Response, _is_error: bool) {
self.response_handled.store(true, Ordering::SeqCst);
}
}
#[test]
fn guest_default_implementation() {
struct DefaultGuest;
impl Guest for DefaultGuest {}
let guest = DefaultGuest;
let request = Request::new();
let response = Response::new();
let (cont, ctx) = guest.handle_request(&request, &response);
assert!(cont);
assert_eq!(ctx, 0);
}
#[test]
fn guest_custom_implementation() {
let request_handled = Arc::new(AtomicBool::new(false));
let response_handled = Arc::new(AtomicBool::new(false));
let plugin = TestPlugin {
request_handled: request_handled.clone(),
response_handled: response_handled.clone(),
continue_request: true,
ctx_value: 42,
};
let request = Request::new();
let response = Response::new();
let (cont, ctx) = plugin.handle_request(&request, &response);
assert!(cont);
assert_eq!(ctx, 42);
assert!(request_handled.load(Ordering::SeqCst));
plugin.handle_response(ctx, &request, &response, false);
assert!(response_handled.load(Ordering::SeqCst));
}
#[test]
fn guest_stop_request() {
struct StopPlugin;
impl Guest for StopPlugin {
fn handle_request(&self, _request: &Request, _response: &Response) -> (bool, i32) {
(false, 0)
}
}
let plugin = StopPlugin;
let request = Request::new();
let response = Response::new();
let (cont, _) = plugin.handle_request(&request, &response);
assert!(!cont);
}
#[test]
fn guest_default_handle_response() {
struct DefaultResponsePlugin;
impl Guest for DefaultResponsePlugin {}
let plugin = DefaultResponsePlugin;
let request = Request::new();
let response = Response::new();
plugin.handle_response(42, &request, &response, false);
}
#[test]
fn guest_context_passing() {
let ctx_received = Arc::new(AtomicI32::new(0));
let ctx_clone = ctx_received.clone();
struct ContextPlugin {
ctx_received: Arc<AtomicI32>,
}
impl Guest for ContextPlugin {
fn handle_request(&self, _request: &Request, _response: &Response) -> (bool, i32) {
(true, 12345)
}
fn handle_response(&self, req_ctx: i32, _request: &Request, _response: &Response, _is_error: bool) {
self.ctx_received.store(req_ctx, Ordering::SeqCst);
}
}
let plugin = ContextPlugin { ctx_received: ctx_clone };
let request = Request::new();
let response = Response::new();
let (_, ctx) = plugin.handle_request(&request, &response);
plugin.handle_response(ctx, &Request::new(), &Response::new(), false);
assert_eq!(ctx_received.load(Ordering::SeqCst), 12345);
}
struct BlockingPlugin {
blocked_paths: Vec<&'static str>,
}
impl Guest for BlockingPlugin {
fn handle_request(&self, request: &Request, response: &Response) -> (bool, i32) {
let uri = request.uri();
let uri_str = uri.to_str().unwrap_or("");
for blocked in &self.blocked_paths {
if uri_str.contains(blocked) {
response.set_status(403);
response.body().write(b"Forbidden");
return (false, 0);
}
}
(true, 0)
}
}
#[test]
fn e2e_blocking_plugin_allows() {
let plugin = BlockingPlugin { blocked_paths: vec!["/admin", "/secret"] };
let request = Request::new();
let response = Response::new();
let (cont, _) = plugin.handle_request(&request, &response);
assert!(cont);
}
#[test]
fn e2e_blocking_plugin_blocks() {
let plugin = BlockingPlugin { blocked_paths: vec!["test"] };
let request = Request::new();
let response = Response::new();
let (cont, _) = plugin.handle_request(&request, &response);
assert!(!cont);
}
struct ConfigurablePlugin;
impl Guest for ConfigurablePlugin {
fn handle_request(&self, request: &Request, _response: &Response) -> (bool, i32) {
let config = admin::config();
if config.to_str().unwrap_or("").contains("config") {
request.header().add(b"X-Config-Loaded", b"true");
}
(true, 0)
}
}
#[test]
fn e2e_configurable_plugin() {
let plugin = ConfigurablePlugin;
let request = Request::new();
let response = Response::new();
let (cont, _) = plugin.handle_request(&request, &response);
assert!(cont);
}
struct FeatureEnablingPlugin;
impl Guest for FeatureEnablingPlugin {
fn handle_request(&self, _request: &Request, _response: &Response) -> (bool, i32) {
admin::enable(feature::BufferRequest | feature::BufferResponse);
(true, 0)
}
}
#[test]
fn e2e_feature_enabling_plugin() {
let plugin = FeatureEnablingPlugin;
let request = Request::new();
let response = Response::new();
let (cont, _) = plugin.handle_request(&request, &response);
assert!(cont);
}
struct FullCyclePlugin {
pub request_count: AtomicU32,
}
impl Guest for FullCyclePlugin {
fn handle_request(&self, request: &Request, _response: &Response) -> (bool, i32) {
let count = self.request_count.fetch_add(1, Ordering::SeqCst);
let _method = request.method();
let _uri = request.uri();
let _source = request.source_addr();
request.header().add(b"X-Request-Id", format!("{}", count).as_bytes());
(true, count as i32)
}
fn handle_response(&self, req_ctx: i32, _request: &Request, response: &Response, is_error: bool) {
if !is_error {
response.header().set(b"X-Processed-By", b"FullCyclePlugin");
response.header().add(b"X-Request-Context", format!("{}", req_ctx).as_bytes());
}
}
}
#[test]
fn e2e_full_cycle_plugin() {
let plugin = FullCyclePlugin { request_count: AtomicU32::new(0) };
let request = Request::new();
let response = Response::new();
let (cont1, ctx1) = plugin.handle_request(&request, &response);
assert!(cont1);
assert_eq!(ctx1, 0);
plugin.handle_response(ctx1, &Request::new(), &Response::new(), false);
let req2 = Request::new();
let res2 = Response::new();
let (cont2, ctx2) = plugin.handle_request(&req2, &res2);
assert!(cont2);
assert_eq!(ctx2, 1);
plugin.handle_response(ctx2, &Request::new(), &Response::new(), false);
}
struct SimplePlugin;
impl Guest for SimplePlugin {}
#[test]
fn register_plugin() {
let plugin = SimplePlugin;
register(plugin);
}
#[test]
fn http_request_returns_continue_with_context() {
let result = http_request();
assert_eq!(result & 1, 1);
}
#[test]
fn http_response_does_not_panic() {
http_response(0, 0);
http_response(42, 0);
http_response(0, 1);
http_response(123, 1);
}
#[test]
fn http_response_passes_correct_parameters() {
let ctx_received = Arc::new(AtomicI32::new(-1));
let is_error_received = Arc::new(AtomicU8::new(255));
struct ResponseTrackingPlugin {
ctx_received: Arc<AtomicI32>,
is_error_received: Arc<AtomicU8>,
}
impl Guest for ResponseTrackingPlugin {
fn handle_response(&self, req_ctx: i32, _request: &Request, _response: &Response, is_error: bool) {
self.ctx_received.store(req_ctx, Ordering::SeqCst);
self.is_error_received.store(if is_error { 1 } else { 0 }, Ordering::SeqCst);
}
}
let plugin = ResponseTrackingPlugin { ctx_received: ctx_received.clone(), is_error_received: is_error_received.clone() };
let request = Request::new();
let response = Response::new();
plugin.handle_response(42, &request, &response, false);
assert_eq!(ctx_received.load(Ordering::SeqCst), 42);
assert_eq!(is_error_received.load(Ordering::SeqCst), 0);
plugin.handle_response(123, &Request::new(), &Response::new(), true);
assert_eq!(ctx_received.load(Ordering::SeqCst), 123);
assert_eq!(is_error_received.load(Ordering::SeqCst), 1);
}
#[test]
fn http_request_without_guest_returns_default() {
let result = http_request();
assert_eq!(result & 1, 1);
}
}