whiskers.lmjtfy.fun, the Worker: one page, a cat that moods are asked of, and the few small files a page needs. It keeps nothing, asks nobody anything and knows nothing about who is looking. See ../README.md.
Datastar drives the demo: a button posts the page's signals, and the answer is the cat,
its caption and the stages it is busy with, as HTML to morph in. The link preview's PNG and
MP4 are built assets. The PNG is served by wrangler before this code runs; a request for it
that reached here means it was never built, which is logged. The MP4 is routed here first
(run_worker_first) so it can answer byte ranges, which link-preview players rely on and
plain assets do not give (media.rs).
12use std::sync::OnceLock;
14use axum::Router; 15use axum::body::Body; 16use axum::extract::{Json, Query, State}; 17use axum::http::{HeaderValue, Method, Response, StatusCode, header}; 18use axum::routing::{get, post}; 19use site::config::SITE_ORIGIN; 20use site::demo::{self, Signals}; 21use site::host; 22use jev_ui::preview::Origin; 23use tower_service::Service; 24use worker::{Context, Env, HttpRequest, event}; 25 26mod explain; 27mod headers; 28mod log; 29mod media; 30mod view; 31 32const ROBOTS: &str = "User-agent: *\nAllow: /\n"; 33 34#[derive(Clone)] 35struct App { 36 origin: Origin, 37} 38 39#[event(fetch)] 40async fn fetch(request: HttpRequest, env: Env, _context: Context) -> worker::Result<Response<Body>> { 41 let started = worker::Date::now().as_millis(); 42 let route = log::Route::of(request.uri().path()); 43 let method = log::method(request.method()); 44 let origin = origin(&env); 45 let mut response = match gate(&request, &origin) { 46 Some(answer) => answer, 47 None if route == log::Route::Video => video(request, &env).await, 48 None if route == log::Route::Image => image(request, &env).await?, 49 None => router(App { origin }).call(request).await?, 50 }; 51 52 headers::secure(response.headers_mut()); 53 log::write(&log::Served { 54 route, 55 method, 56 status: response.status().as_u16(), 57 millis: worker::Date::now().as_millis().saturating_sub(started), 58 }); 59 Ok(response) 60}
The canonical address is the one SITE_ORIGIN names; a request to any other non-local host is
sent there (site::host). None means this request is answered here.
64fn gate(request: &HttpRequest, origin: &Origin) -> Option<Response<Body>> { 65 let host = request.headers().get(header::HOST).and_then(|h| h.to_str().ok()).or_else(|| request.uri().host()).unwrap_or_default(); 66 let target = request.uri().path_and_query().map_or("/", |t| t.as_str()); 67 let own = request.headers().get("sec-fetch-mode").is_some_and(|mode| mode != "navigate"); 68 let asked = host::Asked { host, method: request.method(), target, own }; 69 match host::gate(origin.as_str(), &asked) { 70 host::Gate::Pass => None, 71 host::Gate::Moved { to, status } => Some(moved(&to, status)), 72 host::Gate::Refused(why) => { 73 let mut response = respond("text/plain; charset=utf-8", "no-store", why); 74 *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED; 75 response.headers_mut().insert(header::ALLOW, HeaderValue::from_static("GET, HEAD")); 76 Some(response) 77 } 78 } 79}
A permanent redirect. Kept a day, as lmjtfy's: long enough to spare the old address its traffic, short enough that a mistake is not stuck in caches.
83fn moved(to: &str, status: StatusCode) -> Response<Body> { 84 let mut response = respond("text/plain; charset=utf-8", "public, max-age=86400", format!("Moved to {to}\n")); 85 *response.status_mut() = status; 86 response.headers_mut().insert(header::LOCATION, HeaderValue::from_str(to).expect("a target of a valid request is a valid header")); 87 response 88}
The link preview's picture, from the assets. It runs through the Worker (run_worker_first
in wrangler.toml) only so the old address can send it on; here it is passed to the assets
as it came, headers included, so its ETag and conditional requests work as before.
/preview/loop.mp4: the built file, from the assets binding, answered with byte ranges.
100async fn video(request: HttpRequest, env: &Env) -> Response<Body> { 101 if !matches!(*request.method(), Method::GET | Method::HEAD) { 102 let mut response = respond("text/plain; charset=utf-8", "no-store", "Only GET and HEAD."); 103 *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED; 104 response.headers_mut().insert(header::ALLOW, HeaderValue::from_static("GET, HEAD")); 105 return response; 106 } 107 match read_video(env).await { 108 Some(bytes) => { 109 let text = |name| request.headers().get(name).and_then(|v| v.to_str().ok()); 110 let asked = media::Asked { range: text(header::RANGE), if_range: text(header::IF_RANGE), if_none_match: text(header::IF_NONE_MATCH) }; 111 media::respond(request.method(), &asked, bytes).map(Body::from) 112 } 113 None => { 114 log::failure("the link preview's video is not in the assets (run web/tools/preview)"); 115 not_found().await 116 } 117 } 118}
The file's bytes, or None if the binding or the file is not there.
121async fn read_video(env: &Env) -> Option<Vec<u8>> { 122 let assets = env.assets(media::BINDING).ok()?; 123 let found = assets.fetch(format!("https://assets.invalid{}", media::PATH), None).await.ok()?; 124 if found.status() != StatusCode::OK { 125 return None; 126 } 127 let bytes = axum::body::to_bytes(Body::new(found.into_body()), usize::MAX).await.ok()?; 128 Some(bytes.to_vec()) 129}
131fn router(app: App) -> Router { 132 Router::new() 133 .route("/", get(page)) 134 .route("/cat.css", get(cat_css)) 135 .route("/favicon.svg", get(favicon)) 136 .route("/robots.txt", get(robots)) 137 .route("/mood", post(mood)) 138 .route("/mascot", post(mascot)) 139 .route("/tick", post(tick)) 140 .route("/tour", post(tour)) 141 .fallback(shared_or_not_found) 142 .with_state(app) 143}
SITE_ORIGIN from the Worker's variables, else the configured address. A variable that is
not an origin is a mistake in wrangler.toml: it is reported and the default is used, so a
bad value cannot put an invalid address in the link preview.
148fn origin(env: &Env) -> Origin { 149 let wanted = env.var("SITE_ORIGIN").map(|v| v.to_string()).unwrap_or_else(|_| SITE_ORIGIN.to_owned()); 150 Origin::parse(&wanted).unwrap_or_else(|why| { 151 log::failure(&format!("SITE_ORIGIN is not usable, so the default is used: {why}")); 152 Origin::parse(SITE_ORIGIN).expect("the configured origin is valid (checked by a test)") 153 }) 154}
The stylesheet's version: it changes when the cat's motion does.
What a link can ask of the page. The value is checked against the closed set of mascots and is never echoed: anything else is the default.
181async fn page(State(app): State<App>, Query(query): Query<PageQuery>) -> Response<Body> { 182 let mascot = cat::Mascot::parse_or_default(query.mascot.as_deref()); 183 let page = view::page(&view::Page { origin: &app.origin, css_version: &css().1, mascot }); 184 respond("text/html; charset=utf-8", headers::HTML_CACHE, page.into_string()) 185}
Every file the shared look brings (jev_ui::asset: its stylesheet and script, Datastar, the
fonts) is answered from the crate's own bytes; any other path is not found.
196async fn cat_css() -> Response<Body> { 197 respond("text/css; charset=utf-8", "public, max-age=31536000, immutable", css().0.clone()) 198} 199 200async fn favicon() -> Response<Body> { 201 respond("image/svg+xml", "public, max-age=86400", view::favicon()) 202} 203 204async fn robots() -> Response<Body> { 205 respond("text/plain; charset=utf-8", "public, max-age=3600", ROBOTS) 206}
The visitor chose a mood. A word that is not one is refused, not guessed at.
The visitor chose who to meet. The mood and the tour carry on as they were; a word that is not a mascot is refused, not guessed at.
A second's worth of the page's counting reached the beat's end.
230async fn tour(Json(signals): Json<Signals>) -> Response<Body> { 231 events(demo::toggle(&signals), signals.mascot()) 232} 233 234fn events(shown: demo::Shown, mascot: cat::Mascot) -> Response<Body> { 235 respond("text/event-stream", "no-cache", view::patches(shown, mascot)) 236} 237 238fn refused(why: &'static str) -> Response<Body> { 239 Response::builder().status(StatusCode::UNPROCESSABLE_ENTITY).header(header::CONTENT_TYPE, "text/plain; charset=utf-8").body(Body::from(why)).expect("static headers are valid") 240} 241 242async fn not_found() -> Response<Body> { 243 let mut response = respond("text/plain; charset=utf-8", "no-store", "Nothing here. The cat is at /"); 244 *response.status_mut() = StatusCode::NOT_FOUND; 245 response.headers_mut().insert("x-content-type-options", HeaderValue::from_static("nosniff")); 246 response 247} 248 249#[cfg(test)] 250mod tests { 251 use super::*; 252 253 fn request(host: &str, method: Method, target: &str, own: bool) -> HttpRequest { 254 let mut builder = axum::http::Request::builder().method(method).uri(format!("https://{host}{target}")).header(header::HOST, host); 255 if own { 256 builder = builder.header("sec-fetch-mode", "cors"); 257 } 258 builder.body(worker::Body::empty()).unwrap() 259 } 260 261 fn canonical() -> Origin { 262 Origin::parse(SITE_ORIGIN).unwrap() 263 } 264 265 #[test] 266 fn the_old_address_gets_a_308_with_the_headers_every_response_has() { 267 let mut response = gate(&request("old.example", Method::GET, "/?mascot=bunny", false), &canonical()).expect("sent on"); 268 headers::secure(response.headers_mut()); 269 assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT); 270 assert_eq!(response.headers()[header::LOCATION], format!("{SITE_ORIGIN}/?mascot=bunny")); 271 assert_eq!(response.headers()[header::CACHE_CONTROL], "public, max-age=86400"); 272 assert_eq!(response.headers()[header::CONTENT_SECURITY_POLICY], headers::CSP); 273 assert_eq!(response.headers()[header::X_CONTENT_TYPE_OPTIONS], "nosniff"); 274 assert!(response.headers().get(header::SET_COOKIE).is_none()); 275 } 276 277 #[test] 278 fn the_old_address_refuses_a_post_that_is_not_a_page_and_lets_a_page_finish() { 279 let refused = gate(&request("old.example", Method::POST, "/mood", false), &canonical()).expect("told"); 280 assert_eq!(refused.status(), StatusCode::METHOD_NOT_ALLOWED); 281 assert_eq!(refused.headers()[header::ALLOW], "GET, HEAD"); 282 assert!(gate(&request("old.example", Method::POST, "/mood", true), &canonical()).is_none()); 283 } 284 285 #[test] 286 fn the_canonical_and_local_hosts_are_answered_here() { 287 for host in ["whiskers.lmjtfy.fun", "localhost:8789", "127.0.0.1:8789"] { 288 for target in ["/", "/preview/card.png", "/robots.txt"] { 289 assert!(gate(&request(host, Method::GET, target, false), &canonical()).is_none(), "{host}{target}"); 290 } 291 } 292 } 293 294 #[test] 295 fn the_default_origin_is_valid() { 296 assert!(Origin::parse(SITE_ORIGIN).is_ok()); 297 } 298 299 #[test] 300 fn the_stylesheet_is_named_by_its_content() { 301 let (sheet, version) = css(); 302 assert!(sheet.contains("@keyframes")); 303 assert_eq!(version.len(), 8); 304 } 305}