global: snapshot

This commit is contained in:
nym21
2026-01-14 16:38:53 +01:00
parent ddb1db7a8e
commit d75c2a881b
226 changed files with 7776 additions and 20942 deletions

View File

@@ -10,121 +10,63 @@ use axum::{
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use brk_error::{Error, Result};
use brk_error::Result;
use quick_cache::sync::GuardResult;
use tracing::{error, info};
use crate::{AppState, HeaderMapExtended, ModifiedState, ResponseExtended};
use crate::{
AppState, EMBEDDED_WEBSITE, HeaderMapExtended, ModifiedState, ResponseExtended, WebsiteSource,
};
pub async fn file_handler(
headers: HeaderMap,
State(state): State<AppState>,
path: extract::Path<String>,
) -> Response {
any_handler(headers, state, Some(path))
any_handler(headers, state, Some(path.0))
}
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Response {
any_handler(headers, state, None)
}
fn any_handler(
headers: HeaderMap,
state: AppState,
path: Option<extract::Path<String>>,
) -> Response {
let files_path = state.files_path.as_ref().unwrap();
if let Some(path) = path.as_ref() {
// Sanitize path components to prevent traversal attacks
let sanitized: String = path
.0
.split('/')
.filter(|component| !component.is_empty() && *component != "." && *component != "..")
.collect::<Vec<_>>()
.join("/");
let mut path = files_path.join(&sanitized);
// Canonicalize and verify the path stays within the project root
// (allows symlinks to modules/ which is outside the website directory)
if let Ok(canonical) = path.canonicalize()
&& let Ok(canonical_base) = files_path.canonicalize()
{
// Allow paths within files_path OR within project root (2 levels up)
let project_root = canonical_base.parent().and_then(|p| p.parent());
let allowed = canonical.starts_with(&canonical_base)
|| project_root.is_some_and(|root| canonical.starts_with(root));
if !allowed {
let mut response: Response<Body> =
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
}
}
// Strip hash from import-mapped URLs (e.g., foo.abc12345.js -> foo.js)
if !path.exists()
&& let Some(unhashed) = strip_importmap_hash(&path)
&& unhashed.exists()
{
path = unhashed;
}
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let mut response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
"File doesn't exist".to_string(),
)
.into_response();
response.headers_mut().insert_cors();
return response;
} else {
path = files_path.join("index.html");
}
}
path_to_response(&headers, &state, &path)
} else {
path_to_response(&headers, &state, &files_path.join("index.html"))
}
}
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
match path_to_response_(headers, state, path) {
Ok(response) => response,
Err(error) => {
let mut response =
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
response.headers_mut().insert_cors();
response
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Response {
match &state.website {
WebsiteSource::Disabled => unreachable!("routes not added when disabled"),
WebsiteSource::Embedded => embedded_handler(&state, path),
WebsiteSource::Filesystem(files_path) => {
filesystem_handler(headers, &state, files_path, path)
}
}
}
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
let (modified, date) = headers.check_if_modified_since(path)?;
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
return Ok(Response::new_not_modified());
}
/// Sanitize path to prevent traversal attacks
fn sanitize_path(path: &str) -> String {
path.split('/')
.filter(|c| !c.is_empty() && *c != "." && *c != "..")
.collect::<Vec<_>>()
.join("/")
}
let serialized_path = path.to_str().unwrap();
/// Check if path requires revalidation (HTML files, service worker)
fn must_revalidate(path: &Path) -> bool {
path.extension().is_some_and(|ext| ext == "html")
|| path
.to_str()
.is_some_and(|p| p.ends_with("service-worker.js"))
}
let must_revalidate = path
.extension()
.is_some_and(|extension| extension == "html")
|| serialized_path.ends_with("service-worker.js");
/// Build response with proper headers and caching
fn build_response(state: &AppState, path: &Path, content: Vec<u8>, cache_key: &str) -> Response {
let must_revalidate = must_revalidate(path);
// Use cache for non-HTML files in release mode
let guard_res = if !cfg!(debug_assertions) && !must_revalidate {
Some(state.cache.get_value_or_guard(
&path.to_str().unwrap().to_owned(),
Some(Duration::from_millis(50)),
))
Some(
state
.cache
.get_value_or_guard(&cache_key.to_owned(), Some(Duration::from_millis(50))),
)
} else {
None
};
@@ -132,19 +74,10 @@ fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Resu
let mut response = if let Some(GuardResult::Value(v)) = guard_res {
Response::new(Body::from(v))
} else {
let content = fs::read(path).unwrap_or_else(|error| {
error!("{error}");
let path = path.to_str().unwrap();
info!("Can't read file {path}");
panic!("")
});
if let Some(GuardResult::Guard(g)) = guard_res {
g.insert(content.clone().into())
.map_err(|_| Error::QuickCacheError)?;
let _ = g.insert(content.clone().into());
}
Response::new(content.into())
Response::new(Body::from(content))
};
let headers = response.headers_mut();
@@ -157,7 +90,129 @@ fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Resu
headers.insert_cache_control_immutable();
}
headers.insert_last_modified(date);
response
}
fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
let path = path.unwrap_or_else(|| "index.html".to_string());
let sanitized = sanitize_path(&path);
// Try to get file, with importmap hash stripping and SPA fallback
let file = EMBEDDED_WEBSITE
.get_file(&sanitized)
.or_else(|| {
strip_importmap_hash(Path::new(&sanitized))
.and_then(|unhashed| EMBEDDED_WEBSITE.get_file(unhashed.to_str()?))
})
.or_else(|| {
// If no extension, serve index.html (SPA routing)
if Path::new(&sanitized).extension().is_none() {
EMBEDDED_WEBSITE.get_file("index.html")
} else {
None
}
});
let Some(file) = file else {
let mut response: Response<Body> =
(StatusCode::NOT_FOUND, "File not found".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
};
build_response(
state,
Path::new(file.path()),
file.contents().to_vec(),
&file.path().to_string_lossy(),
)
}
fn filesystem_handler(
headers: HeaderMap,
state: &AppState,
files_path: &Path,
path: Option<String>,
) -> Response {
let path = if let Some(path) = path {
let sanitized = sanitize_path(&path);
let mut path = files_path.join(&sanitized);
// Canonicalize and verify the path stays within the project root
// (allows symlinks to modules/ which is outside the website directory)
if let Ok(canonical) = path.canonicalize()
&& let Ok(canonical_base) = files_path.canonicalize()
{
let project_root = canonical_base.parent().and_then(|p| p.parent());
let allowed = canonical.starts_with(&canonical_base)
|| project_root.is_some_and(|root| canonical.starts_with(root));
if !allowed {
let mut response: Response<Body> =
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
}
}
// Strip hash from import-mapped URLs
if !path.exists()
&& let Some(unhashed) = strip_importmap_hash(&path)
&& unhashed.exists()
{
path = unhashed;
}
// SPA fallback
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let mut response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
"File doesn't exist".to_string(),
)
.into_response();
response.headers_mut().insert_cors();
return response;
} else {
path = files_path.join("index.html");
}
}
path
} else {
files_path.join("index.html")
};
path_to_response(&headers, state, &path)
}
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
match path_to_response_(headers, state, path) {
Ok(response) => response,
Err(error) => {
let mut response =
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
response.headers_mut().insert_cors();
response
}
}
}
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
let (modified, date) = headers.check_if_modified_since(path)?;
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
return Ok(Response::new_not_modified());
}
let content = fs::read(path).unwrap_or_else(|error| {
error!("{error}");
let path = path.to_str().unwrap();
info!("Can't read file {path}");
panic!("")
});
let cache_key = path.to_str().unwrap();
let mut response = build_response(state, path, content, cache_key);
response.headers_mut().insert_last_modified(date);
Ok(response)
}

View File

@@ -1,21 +1,19 @@
use std::path::PathBuf;
use aide::axum::ApiRouter;
use axum::{response::Redirect, routing::get};
use super::AppState;
use super::{AppState, WebsiteSource};
mod file;
use file::{file_handler, index_handler};
pub trait FilesRoutes {
fn add_files_routes(self, path: Option<&PathBuf>) -> Self;
fn add_files_routes(self, website: &WebsiteSource) -> Self;
}
impl FilesRoutes for ApiRouter<AppState> {
fn add_files_routes(self, path: Option<&PathBuf>) -> Self {
if path.is_some() {
fn add_files_routes(self, website: &WebsiteSource) -> Self {
if website.is_enabled() {
self.route("/{*path}", get(file_handler))
.route("/", get(index_handler))
} else {

View File

@@ -15,11 +15,29 @@ use axum::{
use brk_error::Result;
use brk_mcp::route::mcp_router;
use brk_query::AsyncQuery;
use include_dir::{include_dir, Dir};
use quick_cache::sync::Cache;
use tokio::net::TcpListener;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tracing::{error, info};
/// Embedded website assets
pub static EMBEDDED_WEBSITE: Dir = include_dir!("$CARGO_MANIFEST_DIR/../../website");
/// Source for serving the website
#[derive(Debug, Clone)]
pub enum WebsiteSource {
Disabled,
Embedded,
Filesystem(PathBuf),
}
impl WebsiteSource {
pub fn is_enabled(&self) -> bool {
!matches!(self, Self::Disabled)
}
}
mod api;
pub mod cache;
mod extended;
@@ -37,12 +55,12 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub struct Server(AppState);
impl Server {
pub fn new(query: &AsyncQuery, data_path: PathBuf, files_path: Option<PathBuf>) -> Self {
pub fn new(query: &AsyncQuery, data_path: PathBuf, website: WebsiteSource) -> Self {
Self(AppState {
client: query.client().clone(),
query: query.clone(),
data_path,
files_path,
website,
cache: Arc::new(Cache::new(5_000)),
started_at: jiff::Timestamp::now(),
started_instant: Instant::now(),
@@ -87,7 +105,7 @@ impl Server {
let vecs = state.query.inner().vecs();
let router = ApiRouter::new()
.add_api_routes()
.add_files_routes(state.files_path.as_ref())
.add_files_routes(&state.website)
.route(
"/discord",
get(Redirect::temporary("https://discord.gg/WACpShCB7M")),

View File

@@ -13,7 +13,7 @@ use quick_cache::sync::Cache;
use serde::Serialize;
use crate::{
CacheParams, CacheStrategy,
CacheParams, CacheStrategy, WebsiteSource,
extended::{ResponseExtended, ResultExtended},
};
@@ -22,7 +22,7 @@ pub struct AppState {
#[deref]
pub query: AsyncQuery,
pub data_path: PathBuf,
pub files_path: Option<PathBuf>,
pub website: WebsiteSource,
pub cache: Arc<Cache<String, Bytes>>,
pub client: Client,
pub started_at: Timestamp,