Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
7.2 kB · 225 lines
Rust
at ci
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226use std::collections::HashMap;
use serde::Deserialize;use worker::*;
/// The JSON value stored in Workers KV, keyed by domain.////// Example KV entry:/// key: "foo.example.com"/// value: {"did": "did:plc:...",/// "repos": {"my_repo": {"rkey": "3lk...", "is_index": true},/// "other_repo": {"rkey": "3ll...", "is_index": false}}}////// The is_index flag on each entry indicates whether it is the index site/// for the domain (true) or a sub-path site (false). At most one repo may/// be true. The rkey identifies the {did}/{rkey}/ prefix in R2 where the/// site's objects live.#[derive(Deserialize)]struct DomainMapping { #[serde(default)] did: String, /// repo name → entry #[serde(default)] repos: HashMap<String, RepoEntry>,}
/// Deserialises from either {"rkey": "...", "is_index": bool} (new shape)/// or a bare bool (old shape, where the map key itself was the rkey).#[derive(Deserialize)]#[serde(untagged)]enum RepoEntry { New { rkey: String, #[serde(default)] is_index: bool, }, Legacy(bool),}
impl RepoEntry { fn is_index(&self) -> bool { match self { RepoEntry::New { is_index, .. } => *is_index, RepoEntry::Legacy(b) => *b, } }
/// Returns the rkey, falling back to the map key (name) for the legacy /// shape where the key itself was the rkey. fn rkey<'a>(&'a self, name: &'a str) -> &'a str { match self { RepoEntry::New { rkey, .. } => rkey.as_str(), RepoEntry::Legacy(_) => name, } }}
impl DomainMapping { /// Returns the (name, entry) pair for the index site, if any. fn index_repo(&self) -> Option<(&str, &RepoEntry)> { self.repos.iter().find_map(|(name, entry)| { if entry.is_index() { Some((name.as_str(), entry)) } else { None } }) }}
/// Build the R2 object key for a given did/rkey and intra-site path./// `site_path` should start with a `/` or be empty.fn r2_key(did: &str, rkey: &str, site_path: &str) -> String { let base = format!("{}/{}/", did, rkey); if site_path.is_empty() || site_path == "/" { format!("{}index.html", base) } else { let trimmed = site_path.trim_start_matches('/'); if trimmed.is_empty() || trimmed.ends_with('/') { format!("{}{}index.html", base, trimmed) } else { format!("{}{}", base, trimmed) } }}
/// Returns true when a directory-like path is missing a trailing slash.////// Examples:/// - "/docs" => true/// - "/docs/" => false/// - "/file.txt" => false/// - "/" => falsefn needs_trailing_slash(path: &str) -> bool { if path == "/" || path.ends_with('/') { return false; } let last_segment = path.rsplit('/').next().unwrap_or(path); !last_segment.contains('.')}
/// Return the canonical URL with a trailing slash appended to the path.fn with_trailing_slash(url: &Url) -> String { let mut url = url.clone(); url.set_path(&format!("{}/", url.path())); url.to_string()}
/// Fetch an object from R2, falling back to appending /index.html if the/// key looks like a directory (no file extension in the last segment).async fn fetch_from_r2(bucket: &Bucket, key: &str) -> Result<Option<Object>> { if let Some(obj) = bucket.get(key).execute().await? { return Ok(Some(obj)); }
let last_segment = key.rsplit('/').next().unwrap_or(key); if !last_segment.contains('.') { let index_key = format!("{}/index.html", key.trim_end_matches('/')); if let Some(obj) = bucket.get(&index_key).execute().await? { return Ok(Some(obj)); } }
Ok(None)}
/// Build a Response from an R2 Object, forwarding the content-type header.fn response_from_object(obj: Object) -> Result<Response> { let content_type = obj .http_metadata() .content_type .unwrap_or_else(|| "application/octet-stream".to_string());
let body = obj .body() .ok_or_else(|| Error::RustError("empty R2 body".into()))?; let mut resp = Response::from_body(body.response_body()?)?; resp.headers_mut().set("Content-Type", &content_type)?; resp.headers_mut() .set("Cache-Control", "public, max-age=60")?; Ok(resp)}
fn is_excluded(path: &str) -> bool { let excluded = ["/.well-known/atproto-did"]; excluded.iter().any(|&prefix| path.starts_with(prefix))}
#[event(fetch)]async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> { let kv = env.kv("SITES")?; let bucket = env.bucket("SITES_BUCKET")?;
// Extract host, stripping any port. let host = req.headers().get("host")?.unwrap_or_default(); let host = host.split(':').next().unwrap_or("").to_string();
if host.is_empty() { return Response::error("Bad Request: missing host", 400); }
let url = req.url()?; let path = url.path();
if is_excluded(path) { return Fetch::Request(req).send().await; }
// Canonical redirect for directory-like paths. if needs_trailing_slash(path) { let redirect_url = with_trailing_slash(&url); return Response::redirect_with_status(redirect_url.parse()?, 308); }
// Single KV lookup for the whole domain. let mapping = match kv.get(&host).text().await? { Some(raw) => match serde_json::from_str::<DomainMapping>(&raw) { Ok(m) => m, Err(_) => return Response::error("Internal Error: bad mapping", 500), }, None => return Response::error("site not found!", 404), };
let path = url.path(); // always starts with "/"
// First path segment, e.g. "my_repo" from "/my_repo/page.html" let first_segment = path .trim_start_matches('/') .split('/') .next() .unwrap_or("") .to_string();
// 1. sub-path site // If the first path segment matches a non-index repo, serve from it. if !first_segment.is_empty() { if let Some(entry) = mapping.repos.get(&first_segment) { if !entry.is_index() { // Strip the leading "/{first_segment}" to get the intra-site path. let site_path = path .trim_start_matches('/') .trim_start_matches(&first_segment) .to_string();
let key = r2_key(&mapping.did, entry.rkey(&first_segment), &site_path); return match fetch_from_r2(&bucket, &key).await? { Some(obj) => response_from_object(obj), None => Response::error("Not Found", 404), }; } } }
// 2. index site // Fall back to the repo marked as the index site, serving the full path. if let Some((name, entry)) = mapping.index_repo() { let key = r2_key(&mapping.did, entry.rkey(name), path); return match fetch_from_r2(&bucket, &key).await? { Some(obj) => response_from_object(obj), None => Response::error("Not Found", 404), }; }
Response::error("Not Found", 404)}