Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328mod claim;mod kvmap;mod serviceauth;mod sites;mod xrpc;
use claim::SitesStore;use kvmap::DomainMapping;use worker::*;
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) } }}
// decodes per segment so an encoded slash can't start a new pathfn site_path(path: &str) -> Option<String> { let mut segments = Vec::new(); for segment in path.split('/') { let decoded = percent_encoding::percent_decode_str(segment) .decode_utf8() .ok()?; if decoded == "." || decoded == ".." || decoded.contains(['/', '\\', '\0']) { return None; } segments.push(decoded); } Some(segments.join("/"))}
fn is_service_host(audience: &str, host: &str) -> bool { audience.strip_prefix("did:web:") == Some(host)}
fn 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('.')}
fn with_trailing_slash(url: &Url) -> String { let mut url = url.clone(); url.set_path(&format!("{}/", url.path())); url.to_string()}
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)}
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)}
#[event(fetch)]async fn fetch(req: Request, env: Env, ctx: Context) -> Result<Response> { let origin = req.headers().get("Origin")?.filter(|o| !o.is_empty());
if req.method() == Method::Options { return preflight_response(&origin); }
let resp = fetch_inner(req, env, ctx).await?; let Some(o) = &origin else { return Ok(resp); }; // a response passed through from a fetch has immutable headers let headers = resp.headers().clone(); headers.set("Access-Control-Allow-Origin", o)?; headers.set("Vary", "Origin")?; Ok(resp.with_headers(headers))}
fn preflight_response(origin: &Option<String>) -> Result<Response> { let mut resp = Response::empty()?.with_status(204); if let Some(o) = origin { resp.headers_mut().set("Access-Control-Allow-Origin", o)?; resp.headers_mut() .set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")?; resp.headers_mut().set( "Access-Control-Allow-Headers", "authorization, content-type, accept", )?; resp.headers_mut().set("Access-Control-Max-Age", "86400")?; } Ok(resp)}
const HANDLE_TITLE_TTL: u64 = 300;
// shorter negative cache so fresh signups aren't delayedconst UNKNOWN_HANDLE_TTL: u64 = 60;
async fn handle_title(kv: &KvStore, host: &str) -> Result<Option<String>, String> { let key = format!("handle-title:{}", host); if let Some(memo) = kv.get(&key).text().await.map_err(|e| e.to_string())? { let memo = memo.trim(); return Ok((!memo.is_empty()).then(|| memo.to_string())); }
let resolved = claim::resolve_handle(host.to_string()).await?; kv.put(&key, resolved.as_deref().unwrap_or("")) .map_err(|e| e.to_string())? .expiration_ttl(if resolved.is_none() { UNKNOWN_HANDLE_TTL } else { HANDLE_TITLE_TTL }) .execute() .await .map_err(|e| e.to_string())?; Ok(resolved)}
async fn fetch_inner(req: Request, env: Env, ctx: Context) -> Result<Response> { let url = req.url()?; let path = url.path();
if path.starts_with("/xrpc/") { return xrpc::route(req, env, ctx).await; }
let kv = env.kv("SITES")?; let bucket = env.bucket("SITES_BUCKET")?;
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); } // the service's own host is its did:web, so no site is served there let audience = env .var("SITES_AUDIENCE_DID") .map(|v| v.to_string()) .unwrap_or_default(); if is_service_host(&audience, &host) { return Response::error("Not Found", 404); }
if path == "/.well-known/atproto-did" { // from the domain claim, never the site if req.method() != Method::Get { let mut resp = Response::error("Method Not Allowed", 405)?; resp.headers_mut().set("Allow", "GET")?; return Ok(resp); } // handles are the pdses' to answer, and a fetch on our own zone skips // the worker and lands on the origin in front of them if claim::is_handle_domain(&host) { return Fetch::Request(req).send().await; } let store = SitesStore::d1(env.d1("SITES_DB")?); return match store.select_claim_by_domain(&host).await { Ok(Some(claim)) if claim.deleted.is_none() => { let mut resp = Response::ok(claim.did)?; resp.headers_mut() .set("Cache-Control", "public, max-age=300")?; Ok(resp) } Ok(_) => Response::error("Not Found", 404), Err(e) => { console_warn!("atproto-did claim lookup for {} failed: {}", host, e); Response::error("Internal Error", 500) } }; }
if needs_trailing_slash(path) { let redirect_url = with_trailing_slash(&url); return Response::redirect_with_status(redirect_url.parse()?, 308); }
let mapping = match kv.get(&host).text().await? { Some(raw) => { let trimmed = raw.trim(); if trimmed.is_empty() || trimmed == "{}" { return Response::error("site not found!", 404); } 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), };
if claim::is_handle_domain(&host) { match handle_title(&kv, &host).await { Ok(resolved) if resolved.as_deref() == Some(mapping.did.as_str()) => {} Ok(_) => { console_warn!( "refusing to serve {}: the zone does not resolve it to its mapping's owner", host ); return Response::error("Not Found", 404); } Err(e) => { console_warn!("handle authority lookup for {} failed: {}", host, e); return Response::error("Service Unavailable", 503); } } }
let Some(path) = site_path(url.path()) else { return Response::error("Not Found", 404); }; let path = path.as_str();
let first_segment = path .trim_start_matches('/') .split('/') .next() .unwrap_or("") .to_string();
if !first_segment.is_empty() { if let Some(entry) = mapping.repos.get(&first_segment) { if !entry.is_index() { 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), }; } } }
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)}
#[cfg(test)]mod tests { use super::*;
#[test] fn a_served_path_is_the_uploaded_file_name() { for (requested, key) in [ ("/sp%20ace.txt", "did/site/sp ace.txt"), ("/%C3%BC.txt", "did/site/\u{fc}.txt"), ("/a%2520b.txt", "did/site/a%20b.txt"), ( "/docs/%E6%97%A5%E6%9C%AC/", "did/site/docs/\u{65e5}\u{672c}/index.html", ), ("/plain.txt", "did/site/plain.txt"), ] { let path = site_path(requested).unwrap(); assert_eq!(r2_key("did", "site", &path), key, "{}", requested); } }
#[test] fn the_service_host_is_not_a_site() { let audience = "did:web:sites.next-sites.tngl.sh"; assert!(is_service_host(audience, "sites.next-sites.tngl.sh")); assert!(!is_service_host(audience, "alpha.next-sites.tngl.sh")); assert!(!is_service_host(audience, "next-sites.tngl.sh")); assert!(!is_service_host( "did:plc:sites", "sites.next-sites.tngl.sh" )); }
#[test] fn a_decoded_path_cannot_leave_its_segment() { for requested in [ "/%2e%2e/index.html", "/a/%2E/b", "/x%2f..%2findex.html", "/x%2Fy", "/a%5c..%5cb", "/nul%00.txt", "/bad%ff.txt", ] { assert_eq!(site_path(requested), None, "{}", requested); } }}