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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
402 lines · 16.6 KB Raw History
//! Private repositories, on a server people log in to: each read as the
//! viewer asking, with the session they logged in to the pages with, and
//! never as this server.
//!
//! A query is answered within [`reading`], which names its viewer: the
//! account the session cookie or service token proves, or no one. A
//! repository no one may read publicly is then looked for in its space as
//! that viewer ([`App::as_viewer`]), whose own PDS mints the delegation
//! token and whose membership the owner's PDS checks as it issues the
//! credential. What is found is kept for that viewer alone: its mirror
//! under their cache, its entry under their DID. Anything else reads as no
//! one, and so finds public repositories only: a card, a clone, a task
//! spawned to refresh a mirror.
//!
//! Membership is asked again, with a fresh credential, once the last
//! answer is older than [`REPROVE`], so someone removed from a repository
//! stops reading it here within that; in between it is read as found
//! then, with no round trip to find it again. Logging out forgets what the
//! viewer was shown at once ([`forget`]).
//!
//! An answer that read anything private is the viewer's alone, kept by no
//! cache, the browser's included ([`unshared`]).
//!
//! A private repository read here is indexed here: its space is synced
//! into [`App::spaces`] with the session of the member who last read it
//! ([`crate::spaces`]), first as they read it, and its pulls, issues and
//! checks are read from that, as a public repository's are from the
//! appview's index, by viewers proven members as above. Its owner's own
//! are found as they log in or look at their profile
//! ([`crate::spaces::discover`]), so their list of repositories is read
//! from the index too, never from their PDS as they look.

use crate::{App, AppError, Located};
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::Response;
use knotless::env::Ctx;
use knotless::remote::Remote;
use knotless::space::Space;
use knotless::url::RepoUrl;
use std::future::Future;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};

/// How long a viewer's reading of a private repository is taken as
/// allowed before the owner's PDS is asked again: how long someone removed
/// from it may still read it here.
pub const REPROVE: Duration = Duration::from_secs(60);

/// Who a request is answered for, and whether the answer read anything
/// private.
struct Reading {
    viewer: Mutex<Option<String>>,
    delegated: Option<Delegated>,
    private: AtomicBool,
}

/// A delegation token a query came with, checked and spent here
/// ([`App::spend`]): it reads the one private repository whose space it
/// names, as the member it names, with no session of theirs here.
#[derive(Clone)]
pub struct Delegated {
    pub did: String,
    pub space: String,
    pub token: String,
}

tokio::task_local! {
    static READING: Reading;
}

/// `answer`, as `viewer` reads, or as `delegated` names once it proves
/// them a member, and whether it read anything private.
pub async fn reading<F: Future>(
    viewer: Option<String>,
    delegated: Option<Delegated>,
    answer: F,
) -> (F::Output, bool) {
    let reading = Reading {
        viewer: Mutex::new(viewer),
        delegated,
        private: AtomicBool::new(false),
    };
    let read = async {
        let out = answer.await;
        (out, READING.with(|r| r.private.load(Ordering::Relaxed)))
    };
    READING.scope(reading, read).await
}

/// The viewer of the request being answered: none outside [`reading`].
pub fn viewer() -> Option<String> {
    READING
        .try_with(|r| r.viewer.lock().unwrap().clone())
        .ok()
        .flatten()
}

/// The delegation token the request being answered came with, if it is
/// for the space `uri`, and from its viewer if it has one.
pub fn delegated(uri: &str) -> Option<Delegated> {
    let found = READING.try_with(|r| r.delegated.clone()).ok().flatten()?;
    let theirs = viewer().is_none_or(|v| v == found.did);
    (found.space == uri && theirs).then_some(found)
}

/// Takes `did`, whom a delegation token proved a member of what it reads,
/// as the request's viewer from now on.
fn proven(did: &str) {
    let _ = READING.try_with(|r| *r.viewer.lock().unwrap() = Some(did.to_string()));
}

/// Marks the answer being made as holding something private.
pub fn touched() {
    let _ = READING.try_with(|r| r.private.store(true, Ordering::Relaxed));
}

/// `res`, for its viewer alone, and kept by no cache at all.
pub fn unshared(mut res: Response) -> Response {
    res.headers_mut().insert(
        header::CACHE_CONTROL,
        HeaderValue::from_static("private, no-store"),
    );
    res
}

/// Forgets what `did` was shown of private repositories, and the
/// credentials and mirrors it was read with: as they log out. Which
/// spaces they were proven to read is kept, to be asked again at their
/// next login.
pub fn forget(app: &App, did: &str) {
    app.viewed.lock().unwrap().retain(|(v, _, _), _| v != did);
    if let Some(index) = &app.spaces {
        let _ = index.drop_syncer(did);
    }
    let ctx = crate::login::ctx_for(app, did);
    knotless::space::forget(&ctx);
    let _ = std::fs::remove_dir_all(&ctx.env.cache_dir);
}

type Viewed = (String, String, String);

impl App {
    /// Whether `viewer` was shown `did`'s repository `name` as private.
    pub(crate) fn shown_private(&self, viewer: &str, did: &str, name: &str) -> bool {
        let key = (viewer.to_string(), did.to_string(), name.to_string());
        self.viewed.lock().unwrap().contains_key(&key)
    }

    /// `did`'s private repository `url`, read as `viewer`: `missing` (or
    /// that there is no such repository) if they may not read it, or it is
    /// not private, so that a non-member learns nothing of it. With
    /// `delegated`, a delegation token of theirs for its space, they are
    /// proven a member with it rather than with a session of theirs here.
    pub(crate) async fn as_viewer(
        &self,
        viewer: &str,
        did: &str,
        url: &RepoUrl,
        missing: Option<AppError>,
        delegated: Option<&Delegated>,
    ) -> Result<Located, AppError> {
        let none = format!("{url}: no such repository");
        let missing = missing.unwrap_or(AppError(StatusCode::NOT_FOUND, none));
        let ctx = crate::login::ctx_for(self, viewer);
        let key = (viewer.to_string(), did.to_string(), url.name.clone());
        let entry = self
            .viewed
            .lock()
            .unwrap()
            .entry(key.clone())
            .or_default()
            .clone();
        let mut entry = entry.lock().await;
        let reprove = Duration::from_millis(self.reprove.load(Ordering::Relaxed));
        let stale = entry.proved.is_none_or(|at| at.elapsed() >= reprove);
        // Between provings the space is read as it was found: its owner's
        // session and a member's credential each last a minute more.
        let remote = match entry.remote.clone().filter(|_| !stale) {
            Some(remote) => remote,
            None => {
                // A credential is asked for afresh, which the owner's PDS
                // gives only to a member.
                let opened = match delegated {
                    Some(d) => Remote::delegated(&ctx, url, viewer, &d.token).await,
                    None => {
                        knotless::space::forget(&ctx);
                        Remote::open(&ctx, url).await
                    }
                };
                match opened {
                    Ok(remote) if remote.space.is_some() => remote,
                    // Not private, or not theirs to read: as if there were
                    // none.
                    other => {
                        let failed = other.err().filter(|e| !knotless::space::refused(e));
                        if failed.is_none() {
                            self.reads(viewer, &knotless::space::uri(did, &url.name), false);
                        }
                        let e = failed.map_or(missing, AppError::upstream);
                        return self.unview(&key, Err(e));
                    }
                }
            }
        };
        let space = entry.space.clone().unwrap_or_default();
        let moves = self.moves_of(&space);
        let entry_key = entry.key(did);
        let any = self.events_for(did);
        let events = self.stamp(did, entry_key.as_deref());
        // A space no one here has a session for is synced only as it is
        // read, with what proved its reader a member.
        let followed = self.space_followed(&space);
        let fresh = match (&entry.git, entry.synced) {
            // Nothing is written to a space but its sync finds, within
            // seconds, or the pages touch.
            (Some(_), Some(_)) if followed => entry.moves == moves && entry.events == events,
            (Some(_), Some(at)) => at.elapsed() < self.ttl && entry.events == events,
            _ => false,
        };
        // No `?` ends a block, which coverage would count as a line never
        // run.
        let mut read = Ok(());
        if !fresh {
            read = self.sync(&ctx, remote.clone(), url, &mut entry).await;
        }
        if !fresh && read.is_ok() {
            entry.moves = moves;
            let found = entry.key(did);
            entry.events = self.settled(did, entry_key.as_deref(), events, any, found.as_deref());
        }
        // Fresh or not, it is synced as whoever read it last: once its
        // syncer logs out, the next to read it takes over. A reader with a
        // delegation token has no session to sync it with after: it is
        // synced with their credential as they read it, when nobody's
        // session syncs it.
        if read.is_ok() {
            let space = entry.space.clone().unwrap_or_default();
            read = match &remote.space {
                Some(joined) if delegated.is_some() => self.keep_read(&ctx, joined).await,
                _ => self.keep(&space, viewer).await,
            };
        }
        self.unview(&key, read)?;
        // Theirs to read, it is in their lists from now on.
        if viewer != did {
            self.reads(viewer, entry.space.as_deref().unwrap_or_default(), true);
        }
        if stale {
            entry.proved = Some(Instant::now());
            entry.remote = Some(remote);
        }
        if delegated.is_some() {
            proven(viewer);
        }
        touched();
        Ok(self.found(&entry, did, ctx))
    }

    /// Keeps an index of the private repository in the space `joined`, as
    /// a member reads it with a delegation token: synced now with their
    /// credential if it never was, or if no session here syncs it. A client
    /// shows the appview each private repository it writes to so
    /// ([`knotless::space::show_appview`]), so what it wrote is heard of.
    async fn keep_read(&self, ctx: &Ctx, joined: &Space) -> Result<(), AppError> {
        let none = "this server keeps no index of private repositories";
        let none = AppError(StatusCode::INTERNAL_SERVER_ERROR, none.into());
        let index = self.spaces.as_ref().ok_or(none)?;
        let uri = joined.uri.as_str();
        let kept = index.keep_space(uri).map_err(anyhow::Error::from);
        kept.map_err(AppError::internal)?;
        let synced = index.space_synced(uri).map_err(anyhow::Error::from);
        let synced = synced.map_err(AppError::internal)?;
        // Synced as the reader, too, when its syncer has not lately.
        let sync = !synced || self.syncer_of(uri).is_none() || self.sync_stale(uri);
        if !sync {
            return Ok(());
        }
        let read = crate::spaces::sync_with(ctx, index, joined).await;
        self.read_now(uri, read, synced)
    }

    /// What a reader's sync of the space `uri` makes of their read, with
    /// `synced` whether it was ever synced before. A sync that failed
    /// counts as one made, so it is tried again only after
    /// [`STALE`](crate::spaces::STALE), not at each read; and a space
    /// already synced is then read from its index as it was, the failure
    /// logged (`space not synced`), rather than failing the read. One never
    /// synced has nothing to be read from, and its failure is the read's.
    fn read_now(
        &self,
        uri: &str,
        read: anyhow::Result<bool>,
        synced: bool,
    ) -> Result<(), AppError> {
        if read.is_ok() || synced {
            self.synced_now(uri);
        }
        if matches!(read, Ok(true)) {
            self.moved(uri);
        }
        let read = read.map(drop);
        let (logged, read) = if synced {
            (read, Ok(()))
        } else {
            (Ok(()), read)
        };
        crate::telemetry::space_synced(uri, logged);
        read.map_err(AppError::upstream)
    }

    /// Notes whether `viewer` may read the private repository in the
    /// space `uri`, for their lists of repositories ([`Index::read_of`]).
    ///
    /// [`Index::read_of`]: crate::index::Index::read_of
    pub(crate) fn reads(&self, viewer: &str, uri: &str, may: bool) {
        let Some(index) = &self.spaces else { return };
        let _ = match may {
            true => index.read_by(uri, viewer),
            false => index.unread_by(uri, viewer),
        };
    }

    /// Whether the space `uri` is synced as its authority tells of writes
    /// there, so that it is current until a sync of it finds one
    /// ([`App::moves_of`]).
    pub(crate) fn space_followed(&self, uri: &str) -> bool {
        self.following.load(Ordering::Relaxed) && self.syncer_of(uri).is_some()
    }

    /// The member whose session here syncs the space `uri`, if any.
    fn syncer_of(&self, uri: &str) -> Option<String> {
        let index = self.spaces.as_ref()?;
        index.syncer_of(uri).ok().flatten()
    }

    /// Keeps an index of the private repository in `space`, synced as
    /// `syncer`, a member, who reads it: synced now if it never was, so its
    /// pages read from the index from their first look.
    pub async fn keep(&self, space: &str, syncer: &str) -> Result<(), AppError> {
        let none = "this server keeps no index of private repositories";
        let none = AppError(StatusCode::INTERNAL_SERVER_ERROR, none.into());
        let index = self.spaces.as_ref().ok_or(none)?;
        let followed = index.follow_space(space, syncer);
        followed
            .map_err(anyhow::Error::from)
            .map_err(AppError::internal)?;
        self.follow_org(space, syncer);
        let synced = index.space_synced(space);
        let synced = synced.map_err(anyhow::Error::from);
        let synced = synced.map_err(AppError::internal)?;
        // Synced, and lately: what was written since its sync will reach it.
        if synced && !self.sync_stale(space) {
            return Ok(());
        }
        let ctx = crate::login::syncer_ctx(self, syncer);
        // Synced before, it is read as it was while it syncs aside, as a
        // sync reads the whole space and can take seconds: only one never
        // synced has nothing to read until its sync is done. Taken as
        // synced now, so reads meanwhile start no other.
        if synced {
            self.synced_now(space);
            let (moves, last) = (self.moves.clone(), self.last_synced.clone());
            let (index, space) = (index.clone(), space.to_string());
            tokio::spawn(async move {
                let read = crate::spaces::sync(&ctx, &index, &space).await;
                last.lock().unwrap().insert(space.clone(), Instant::now());
                if matches!(read, Ok(true)) {
                    *moves.lock().unwrap().entry(space.clone()).or_default() += 1;
                }
                crate::telemetry::space_synced(&space, read.map(drop));
            });
            return Ok(());
        }
        let read = crate::spaces::sync(&ctx, index, space).await;
        self.read_now(space, read, synced)
    }

    /// Stops syncing the space `uri`, and forgets what it gave: its sync
    /// failed, as it does once its syncer is no longer a member.
    pub fn unfollow(&self, index: &crate::index::Index, uri: &str) {
        let _ = index.forget_space(uri);
        self.moved(uri);
    }

    /// The spaces to sync, each with the member whose session it is synced
    /// with.
    pub fn syncers(&self) -> Vec<(String, String)> {
        let index = self.spaces.as_ref();
        let syncers = index.map(|i| i.syncers().unwrap_or_default());
        syncers.unwrap_or_default()
    }

    /// `read`, having forgotten that the viewer `key` names was shown its
    /// repository if reading it again failed.
    fn unview<T>(&self, key: &Viewed, read: Result<T, AppError>) -> Result<T, AppError> {
        if read.is_err() {
            self.viewed.lock().unwrap().remove(key);
        }
        read
    }
}