~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
148 lines · 6.0 KB Raw History
//! 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(),
        })
    }
}