1//! Whiskers' backend as a Cloudflare Worker with one Durable Object per household. See README.md. 2//! 3//! Everything that decides anything is in `whiskers-service` and `whiskers-core`. This crate is the 4//! adapter: it turns a `fetch` into a `whiskers_service::Request`, hands the service a backend whose 5//! hubs live in the household's object, and turns the `Response` back. The same build runs under 6//! Cloudflare's `wrangler dev` and under `celld dev`. 7 8mod backend; 9pub mod documents; 10pub mod household; 11mod hub; 12mod logging; 13mod object; 14pub mod wire; 15 16use log::{error, info, warn}; 17use whiskers_ports::Diagnostic; 18use whiskers_service::routes::{ChatSync, HouseholdSync, MemorySync}; 19use whiskers_service::{Body, Method, Service}; 20use worker::{Context, Env, Headers, Request, Response, ResponseBody, Result, event}; 21 22use crate::backend::ObjectBackend; 23use crate::household::{HOUSEHOLD_HEADER, Households, Unnamed}; 24 25pub use object::HouseholdObject; 26 27fn refuse(status: u16) -> Result<Response> { 28 Ok(Response::from_body(ResponseBody::Empty)?.with_status(status)) 29} 30 31fn settings(env: &Env) -> Option<Households> { 32 let var = |name: &str| env.var(name).ok().map(|v| v.to_string()); 33 match Households::from_settings(var("HOUSEHOLD").as_deref(), var("TRUST_HOUSEHOLD_HEADER").as_deref()) { 34 Ok(h) => Some(h), 35 Err(e) => { 36 error!("the Worker is misconfigured: HOUSEHOLD: {e}"); 37 None 38 } 39 } 40} 41 42#[event(fetch)] 43async fn fetch(mut req: Request, env: Env, _ctx: Context) -> Result<Response> { 44 logging::install(&env); 45 let started = worker::Date::now().as_millis(); 46 let path = req.path(); 47 let class = wire::route_class(&path); 48 49 let Some(households) = settings(&env) else { 50 return refuse(500); 51 }; 52 let named = req.headers().get(HOUSEHOLD_HEADER)?; 53 let household = match households.of(named.as_deref()) { 54 Ok(h) => h, 55 Err(Unnamed::BadHeader(why)) => { 56 warn!("{class}: refused: the household header: {why}"); 57 return refuse(400); 58 } 59 }; 60 61 if let Some(object_path) = path.strip_prefix("/_hub") 62 && households.is_testing() 63 && req.method() == worker::Method::Post 64 { 65 info!("{class}: test-mode hub request"); 66 let body = req.text().await?; 67 return hub::through(&env.durable_object(hub::OBJECTS)?, &household, object_path, body).await; 68 } 69 70 let method = match req.method() { 71 worker::Method::Post => Method::Post, 72 other => Method::Other(other.to_string()), 73 }; 74 // Only a POST has its body read, as the native server does; anything else is answered 405 unread. 75 let body = if method == Method::Post { 76 match req.bytes().await.map(String::from_utf8) { 77 Ok(Ok(text)) => Body::Text(text), 78 Ok(Err(_)) => Body::Unreadable(Diagnostic::new("the request body is not text")), 79 Err(e) => Body::Unreadable(Diagnostic::new(e.to_string())), 80 } 81 } else { 82 Body::Text(String::new()) 83 }; 84 85 let backend = match ObjectBackend::new(&env, &household) { 86 Ok(b) => b, 87 Err(e) => { 88 error!("the Worker is misconfigured: the household object binding: {e}"); 89 return refuse(500); 90 } 91 }; 92 // The hubs are all this Worker has, so the hubs are all it serves; any other route is a 404. 93 let service = Service::new(backend).serve(MemorySync).serve(HouseholdSync).serve(ChatSync); 94 let response = service.handle(whiskers_service::Request { method, path, body }).await; 95 96 info!("{class} -> {} in {} ms", response.status, worker::Date::now().as_millis().saturating_sub(started)); 97 let headers = Headers::new(); 98 if let Some(kind) = wire::content_type(response.content) { 99 headers.set("content-type", kind)?; 100 } 101 Ok(Response::from_body(ResponseBody::Body(response.body))?.with_status(response.status).with_headers(headers)) 102}