lib.rsannotatedlib.rssource102 lines · 3.8 KB · raw
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}