johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! 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<Viewer>`.
#[derive(Clone)]
pub struct Viewer(pub Option<String>);
/// 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<Arc<App>>, 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<String>,
delegated: Option<Delegated>,
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<Delegated, String> {
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(),
})
}
}