//! Who is asking a query, as atproto's appviews know it: a service token //! the asker's PDS signs for this server's DID and the one method asked, //! sent by an app signed in to that PDS, or by the PDS itself proxying to //! this server's [`SERVICE`]. A query asked with no token answers as it //! does for anyone; one with a token that does not hold is refused, rather //! than answered as if no one asked. //! //! With no token, the session cookie of someone logged in to the pages //! ([`crate::login`]) says who asks. A cookie that no longer holds, as one //! from before its account logged out, is no one's: the pages answer as //! for anyone rather than fail. //! //! A query may also carry a delegation token for a private repository's //! space ([`DELEGATION_HEADER`]), from the asker's own PDS: spent here //! once, it reads that one repository as its member, with no session of //! theirs here ([`crate::private::Delegated`]). With a service token or a //! cookie as well, it must name the same account. use crate::private::Delegated; use crate::{App, AppError}; use axum::extract::{Request, State}; use axum::http::{HeaderValue, StatusCode, header}; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; use knotless::space::DELEGATION_HEADER; use std::sync::Arc; /// The service in this server's DID document that PDSes proxy queries to. pub const SERVICE: &str = knotless::space::APPVIEW; /// The account a query's service token was signed by, if it came with /// one: a query takes it as `Extension`. #[derive(Clone)] pub struct Viewer(pub Option); /// Checks a query's service token, if it has one, else its session /// cookie, and hands on who sent it. Every answer varies by both, and one /// for a viewer is theirs alone to cache. pub async fn viewer(State(app): State>, mut req: Request, next: Next) -> Response { let delegated = match req.headers().get(DELEGATION_HEADER) { None => None, Some(token) => { let token = token.to_str().unwrap_or_default().to_string(); match app.spend(&token).await { Ok(found) => Some(found), Err(why) => return AppError(StatusCode::UNAUTHORIZED, why).into_response(), } } }; let token = req .headers() .get(header::AUTHORIZATION) .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")) .map(str::to_string); let Some(token) = token else { let did = crate::login::viewer(&app, req.headers()); req.extensions_mut().insert(Viewer(did.clone())); return answered(did, delegated, req, next).await; }; let lxm = req.uri().path().trim_start_matches("/xrpc/").to_string(); let did = match &app.gateway { Some(gw) => knotless::auth::verify(&app.ctx, &token, &gw.did, &lxm) .await .map_err(|e| format!("{e:#}")), None => Err("this server has no DID to take service tokens for".to_string()), }; let did = match did { Ok(did) => did, Err(why) => return AppError(StatusCode::UNAUTHORIZED, why).into_response(), }; if let Some(d) = delegated.as_ref().filter(|d| d.did != did) { let why = format!("a delegation token for {}, from {did}", d.did); return AppError(StatusCode::UNAUTHORIZED, why).into_response(); } req.extensions_mut().insert(Viewer(Some(did.clone()))); answered(Some(did), delegated, req, next).await } /// What lets a page on another site ask the queries, as atproto's appviews /// let one, and read the DID document that says where they are: from any /// origin, with a service token or a delegation token, and never with the /// cookie, which a browser sends only to the pages. pub fn cors() -> tower_http::cors::CorsLayer { let delegation = header::HeaderName::from_static(DELEGATION_HEADER); tower_http::cors::CorsLayer::new() .allow_origin(tower_http::cors::Any) .allow_methods([axum::http::Method::GET]) .allow_headers([header::AUTHORIZATION, delegation]) } /// The answer to `req`, as `did` reads, or `delegated` once it proves its /// member: for anyone's cache if no one asks; theirs alone if someone /// does; and no cache's at all, not even theirs, if it holds anything /// private ([`crate::private`]). async fn answered( did: Option, delegated: Option, req: Request, next: Next, ) -> Response { let anyone = did.is_none() && delegated.is_none(); let (res, touched) = crate::private::reading(did, delegated, next.run(req)).await; let res = vary(res); match (anyone, touched) { (true, _) => res, (false, false) => private(res), (false, true) => crate::private::unshared(res), } } /// An answer for one asker alone. fn private(mut res: Response) -> Response { res.headers_mut().insert( header::CACHE_CONTROL, HeaderValue::from_static("private, no-cache"), ); res } fn vary(mut res: Response) -> Response { let headers = res.headers_mut(); headers.append(header::VARY, HeaderValue::from_static("authorization")); headers.append(header::VARY, HeaderValue::from_static("cookie")); res } impl App { /// The delegation token `token`, checked as a member's PDS signed it /// for a space's authority, and spent: one sent before, whose `jti` /// this server took, is refused, so that one seen on its way here reads /// nothing more. pub async fn spend(&self, token: &str) -> Result { let found = knotless::auth::verify_delegation(&self.ctx, token).await; let found = found.map_err(|e| format!("{e:#}"))?; let now = knotless::keys::now_micros() / 1_000_000; let mut spent = self.spent.lock().unwrap(); spent.retain(|_, exp| *exp > now); if spent.insert(found.jti.clone(), found.exp).is_some() { return Err("a delegation token already spent".to_string()); } Ok(Delegated { did: found.iss, space: found.sub, token: token.to_string(), }) } }