diff --git a/src/api_client.rs b/src/api_client.rs index ee0fe17..7211a23 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -1,12 +1,11 @@ //! Client for downloading repositories from a forge's API. -use std::{collections::HashSet, io::Write, path::PathBuf}; - use crate::{ config::Config, repo::{RepoMetadata, WriteRepoMetadataError}, }; +use std::{collections::HashSet, io::Write, path::PathBuf}; use rand::RngExt; use serde::Deserialize; use sha2::Digest; diff --git a/src/server.rs b/src/server.rs index e482026..5ae35b4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -11,6 +11,7 @@ use std::{ }; use trillium::{Conn, Handler}; use trillium_router::{Router, RouterConnExt}; +use sha2::Digest; #[derive(Clone)] struct PageCacheEntry { @@ -317,6 +318,145 @@ pub fn make_router(config: &'static Config) -> impl Handler { .ok(file_content) } } + }) + .get("/e/:secret/*", move |conn: Conn| { + async move { + let Some(secret) = conn.param("secret") else { + return conn.with_status(401); + }; + + let mut hasher = sha2::Sha256::default(); + let mut repo_key = [0; 32]; + if base64_turbo::URL_SAFE_NO_PAD.decode_into(secret, &mut repo_key) != Ok(32) { + return conn.with_status(401); + } + hasher.update(repo_key); + let repo_id: [u8; 32] = hasher.finalize_reset().into(); + let mut repo_id_str = [0; 22]; + base64_turbo::URL_SAFE_NO_PAD + .encode_into(&repo_id[0..16], &mut repo_id_str) + .expect("unreachable"); + let repo_id_str = str::from_utf8(&repo_id_str).expect("unreachable"); + + let Some((repo_metadata, root)) = metadata_cache + .fetch(repo_id_str.to_string(), |key| { + fetch_metadata(config, &key) + }) + else { + return conn.with_status(404); + }; + + let invalidate_page_cache = |entry: &PageCacheEntry| { + // strict cmp because it's better to spend more on useless invalidations + // than to serve outdated pages + // (e.g. privacy issues if the author thinks sensible data are removed but we still serve them) + entry.time > repo_metadata.time_updated + }; + if let Some(rendered_page) = page_cache.fetch_cond_only( + (repo_id_str.to_string(), conn.path().to_string()), + invalidate_page_cache, + ) { + conn.ok(rendered_page.content) + } else { + let Some(served_entry) = root.find(conn.path().split('/')) else { + page_cache + .update((repo_id_str.to_string(), conn.path().to_string()), None); + return conn.with_status(404); + }; + + let rendered_page = match served_entry { + templates::EntryRef::Directory(served_dir) => { + let mut served_file = None; + for default_filename in config.default_files.split(':') { + if let Some(templates::Entry::File(f)) = + served_dir.entries.get(default_filename) + { + served_file = Some(f); + break; + } + } + let mut html = String::new(); + if let Some(served_file) = served_file { + let Some(file_content) = + fetch_file(config, repo_id_str, &served_file.hash) + else { + return conn.with_status(500); + }; + + let mut file_lang = served_file + .name + .rsplit('.') + .next() + .unwrap_or(giallo::PLAIN_GRAMMAR_NAME) + .to_string(); + file_lang.make_ascii_lowercase(); + + html = renderer.render( + config, + &file_lang, + &file_content, + true, + repo_id_str, + &served_file.path, + ); + } else { + // TODO something? + } + + let template = crate::templates::Repo { + current_entry: served_entry.into(), + content: html.clone(), + root, + path: conn.path().split('/').map(|s| s.to_string()).collect(), + title: repo_metadata.title, + repo_hash: repo_id_str.into(), + }; + template.render().unwrap() + } + templates::EntryRef::File(served_file) => { + let Some(file_content) = + fetch_file(config, repo_id_str, &served_file.hash) + else { + return conn.with_status(500); + }; + + let mut file_lang = served_file + .name + .rsplit('.') + .next() + .unwrap_or(giallo::PLAIN_GRAMMAR_NAME) + .to_string(); + file_lang.make_ascii_lowercase(); + + let html = renderer.render( + config, + &file_lang, + &file_content, + true, + repo_id_str, + &served_file.path, + ); + let template = crate::templates::Repo { + current_entry: served_entry.into(), + content: html.clone(), + root, + path: conn.path().split('/').map(|s| s.to_string()).collect(), + title: repo_metadata.title, + repo_hash: repo_id_str.into(), + }; + template.render().unwrap() + } + }; + page_cache.update( + (repo_id_str.to_string(), conn.path().to_string()), + Some(PageCacheEntry { + time: std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(), + content: rendered_page.clone(), + }), + ); + conn.ok(rendered_page) + } + } }), ) }