Compare commits

...

6 Commits

Author SHA1 Message Date
60f81f6d4e dev: reduce build sizes (especially debug) 2026-03-24 23:48:33 +01:00
7a4876b0e8 feat: add image etag support 2026-03-24 23:47:38 +01:00
70f77f3501 perf: use const-str's include_asset! 2026-03-24 22:31:22 +01:00
8a65bb2c92 dev: do something about HEAD requests 2026-03-24 21:47:20 +01:00
af3d1a567b perf(neglible): completely veto Arc from AppState 2026-03-24 15:37:31 +01:00
4449607449 fix: bit mixing
so the fn was basically $ka + kb + kc + ... = k(a + b + c + d)$, so completely useless, this mixes better, and also position independent

and now my lovely dancing lain falls under the root domain path hash
2026-03-24 14:37:40 +01:00
10 changed files with 190 additions and 50 deletions

60
Cargo.lock generated
View File

@@ -457,6 +457,23 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-macros"
version = "0.1.2"
source = "git+https://git.javalsai.tuxcord.net/rust/const-macros.git#6f91e1e1a6ce2e5cdb5866a1e1d7841ed5e3cd6f"
dependencies = [
"const-macros-proc",
]
[[package]]
name = "const-macros-proc"
version = "0.1.0"
source = "git+https://git.javalsai.tuxcord.net/rust/const-macros.git#6f91e1e1a6ce2e5cdb5866a1e1d7841ed5e3cd6f"
dependencies = [
"magic",
"sha2",
]
[[package]]
name = "const-str"
version = "1.1.0"
@@ -1075,7 +1092,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "magic"
version = "0.16.7"
source = "git+https://github.com/javalsai/rust-magic.git?branch=dbpaths-clone#c5a357bd482b7625e407c39f5f5af5f2d425a2fb"
source = "git+https://github.com/javalsai/rust-magic.git?branch=dbpaths-clone#211e458a706c728aebffb1a48175b5c6edef3493"
dependencies = [
"bitflags",
"magic-sys",
@@ -1083,9 +1100,9 @@ dependencies = [
[[package]]
name = "magic-sys"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b275c342ee5dd5b19e51905dcc092b8fd1bb1187abbc600ac8910bbb15840e"
checksum = "9a7813c89073ddea0d1979d29786cfb5bd5c8bfe19fdede15a9e6e27fe919b23"
dependencies = [
"pkg-config",
"vcpkg",
@@ -1175,6 +1192,7 @@ dependencies = [
"anstyle",
"anyhow",
"clap",
"const-macros",
"const-str",
"futures-util",
"libc",
@@ -1182,6 +1200,7 @@ dependencies = [
"moka",
"pamsock",
"serde",
"sha2",
"thiserror",
"tokio",
"toml",
@@ -1519,9 +1538,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.4"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
dependencies = [
"serde_core",
]
@@ -1549,6 +1568,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -1758,9 +1788,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.0.7+spec-1.1.0"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96"
checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
dependencies = [
"indexmap",
"serde_core",
@@ -1773,27 +1803,27 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.0.1+spec-1.1.0"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.10+spec-1.1.0"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.7+spec-1.1.0"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
[[package]]
name = "tracing"
@@ -1841,9 +1871,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b"
[[package]]
name = "unicode-xid"

View File

@@ -10,8 +10,19 @@ keywords = ["oauth", "oauth2", "oidc", "OpenID"]
categories = ["web-programming::http-server", "authentication"]
[profile.release]
lto = true
strip = "symbols" # almost half of binary size smh
# so I don't need "full" just yet and the debug builds are HUGE
[profile.dev]
debug = 1
[profile.dev.package.actix-web]
opt-level = 3
debug = false
[profile.dev.package.tokio]
opt-level = 3
debug = false
[features]
default = ["pamsock"]
pamsock = ["dep:pamsock"]
@@ -25,24 +36,31 @@ actix-web = "4.13"
anstyle = "1.0"
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
const-macros = { git = "https://git.javalsai.tuxcord.net/rust/const-macros.git", version = "0.1.2" }
const-str = { version = "1.1", features = ["proc"] }
futures-util = "0.3"
libc = "0.2"
# https://github.com/robo9k/rust-magic/issues/434
magic = { version = "0.16", git = "https://github.com/javalsai/rust-magic.git", branch = "dbpaths-clone" }
magic = "0.16"
moka = { version = "0.12", features = ["async-lock", "future"] }
serde = { version = "1.0", features = ["derive"] }
sha2 = "0.10"
thiserror = "2.0"
tokio = { version = "1.49", features = ["full"] }
toml = "1.0"
users = "0.11"
[patch.crates-io]
# https://github.com/robo9k/rust-magic/issues/434
magic = { git = "https://github.com/javalsai/rust-magic.git", branch = "dbpaths-clone" }
[lints.clippy]
cargo = { level = "warn", priority = -1 }
multiple_crate_versions = { level = "allow" } # otherwise there's too much
correctness = { level = "deny", priority = -1 }
nursery = { level = "deny", priority = -1 }
option_if_let_else = { level = "allow" } # I personally sometimes prefer what this prevents
pedantic = { level = "deny", priority = -1 }
perf = { level = "deny", priority = -1 }
style = { level = "deny", priority = -1 }

View File

@@ -11,3 +11,7 @@ This is a reimplementation of the oauth2 server I was making and forgot where I
# License
All code licensed under the GPL-2.0-only.
# Known Issues
HTTP `HEAD` requests are not responded to properly, I'm exploring solutions but each one has its drawbacks to consider.

View File

@@ -2,6 +2,8 @@
use std::time::Duration;
use const_macros::{file::FileAsset, include_asset};
/// Max [`moka`] pfp cache capacity
pub const MAX_PFP_CACHE_CAPACITY: u64 = 1024;
@@ -9,8 +11,7 @@ pub const MAX_PFP_CACHE_CAPACITY: u64 = 1024;
pub const MAX_PFP_SIZE: u64 = 8 * 1024 * 1024; // 8 MiB; TODO: might lower, high for prototyping
/// Default user image to use if users have none
pub const DEFAULT_USER_PFP: &[u8] = include_bytes!("../assets/default-pfp.png");
pub const DEFAULT_USER_PFP_MIME: &str = "image/png; charset=binary";
pub const DEFAULT_USER_PFP: FileAsset = include_asset!("assets/default-pfp.png");
/// Basically [`USER_CACHES_HEADER`] but much longer for the [`DEFAULT_USER_PFP`]
pub const DEFAULT_USER_PFP_CACHES_HEADER: &str =
crate::utils::web::make_static_cache_header!(Duration::from_hours(2), Duration::from_days(1));

View File

@@ -1,7 +1,8 @@
use std::{ffi::OsStr, io, os::unix::fs::MetadataExt, path::Path, sync::Arc};
use ::users::os::unix::UserExt;
use ::users::os::unix::UserExt as _;
use moka::future::{Cache, CacheBuilder};
use sha2::Digest as _;
use tokio::{fs::File, io::AsyncReadExt};
use crate::{
@@ -14,12 +15,13 @@ pub mod users;
const MIME_IMAGE_PREFIX: &str = "image/";
pub struct MimeWithBytes {
pub struct ImageInfo {
pub mime: Box<str>,
pub bytes: Box<[u8]>,
pub shasum: Box<str>,
}
pub type Image = Arc<MimeWithBytes>;
pub type Image = Arc<ImageInfo>;
// TODO: most of the cache methods here block the executor, if we wanna commit to async we'd have
// to consider that
@@ -115,15 +117,23 @@ impl<'a> AppCache<'a> {
for subpath in consts::USER_PFP_PATHS {
let path = home.join(subpath);
// I'm relying too much on this condition
if let Ok(img_buf) = read_limited_path::<{ consts::MAX_PFP_SIZE }>(&path).await
&& let Ok(Ok(mime)) = self.magic_mime_cookie.buffer(&img_buf) // TODO: first layer
// error is actually
// relevant
&& mime.starts_with(MIME_IMAGE_PREFIX)
{
return Some(Arc::new(MimeWithBytes {
let shasum = sha2::Sha256::digest(&img_buf);
let shasum = format!(
"\"{}\"",
crate::utils::shasum::sha256sum_to_hex_string(&shasum)
);
return Some(Arc::new(ImageInfo {
mime: mime.into_boxed_str(),
bytes: img_buf.into_boxed_slice(),
shasum: shasum.into_boxed_str(),
}));
}
}

View File

@@ -4,6 +4,7 @@ use actix_web::{App, HttpServer, web};
pub mod caches;
pub mod services;
pub mod static_app_data;
pub struct AppState {
pub args: crate::args::Args,
@@ -11,6 +12,9 @@ pub struct AppState {
pub cache: caches::AppCache<'static>,
}
/// Type alias to be used just as `data: AppData,` extractor in [`actix_web`] request handlers.
pub type AppData = static_app_data::StaticAppDataExtractor<&'static AppState>;
/// Leaks memory for the sake of not atomic'ing all over.
#[expect(clippy::missing_errors_doc)]
pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) -> io::Result<()> {
@@ -18,10 +22,7 @@ pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) ->
let config = Box::leak(Box::new(config));
let cache = caches::AppCache::new(
&config.unix.magic_paths,
&config.unix.groups,
);
let cache = caches::AppCache::new(&config.unix.magic_paths, &config.unix.groups);
let app: &AppState = Box::leak(Box::new(AppState {
args,
@@ -36,7 +37,7 @@ pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) ->
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(app))
.app_data(app)
.service(services::images::make_scope(ws::IMAGES))
.default_service(web::to(services::not_found::not_found))
})

View File

@@ -1,12 +1,10 @@
//! Scope for the image get backend.
//! - `/`: Gives the default image.
//! - `/{username}`: Gives username's pfp or redirects to the default image's path (for better
//! - `/default`: Gives the default image.
//! - `/user/{username}`: Gives username's pfp or redirects to the default image's path (for better
//! cache control) if there's no image.
//!
//! Must be scoped at [`ws::IMAGES`]
// TODO: etags
use actix_web::{
HttpResponse, get,
http::header,
@@ -15,7 +13,7 @@ use actix_web::{
use crate::{
consts::{self, web_scopes as ws},
server,
server::AppData,
};
#[must_use]
@@ -25,33 +23,39 @@ pub fn make_scope(path: &str) -> actix_web::Scope {
.service(get_image)
}
#[get("/")]
async fn get_default_image(
_data: web::Data<&server::AppState>,
_username: web::Path<String>,
) -> HttpResponse {
#[get("/default")]
async fn get_default_image() -> HttpResponse {
HttpResponse::Ok()
.insert_header((
header::CACHE_CONTROL,
consts::DEFAULT_USER_PFP_CACHES_HEADER,
))
.content_type(consts::DEFAULT_USER_PFP_MIME)
.body(web::Bytes::from_static(consts::DEFAULT_USER_PFP))
.insert_header((
header::ETAG,
const_str::concat!('"', consts::DEFAULT_USER_PFP.shasum_str, '"'),
))
.content_type(consts::DEFAULT_USER_PFP.mime)
.body(web::Bytes::from_static(consts::DEFAULT_USER_PFP.bytes))
}
#[get("/{username}")]
#[get("/user/{username}")]
async fn get_image(
data: web::Data<&server::AppState>,
data: AppData,
username: web::Path<String>,
) -> web::Either<Redirect, HttpResponse> {
let cached_pfp = data.cache.get_pfp(username.to_string()).await;
cached_pfp.as_ref().map_or_else(
|| web::Either::Left(web::Redirect::to(ws::IMAGES).temporary()),
|| {
web::Either::Left(
web::Redirect::to(const_str::concat!(ws::IMAGES, "/default")).temporary(),
)
},
|img| {
web::Either::Right(
HttpResponse::Ok()
.insert_header((header::CACHE_CONTROL, consts::USER_CACHES_HEADER))
.insert_header((header::ETAG, img.shasum.as_ref()))
.content_type(img.mime.as_ref())
.body(web::Bytes::copy_from_slice(img.bytes.as_ref())),
)

View File

@@ -1,7 +1,4 @@
use actix_web::{
HttpRequest, HttpResponse,
web::{self, Bytes},
};
use actix_web::{HttpRequest, HttpResponse, web};
use futures_util::stream;
use crate::consts;
@@ -11,9 +8,10 @@ fn mix_u32s(seed: impl Iterator<Item = u32>) -> u32 {
const U32_BIT_MIXING_CONSTANT: u32 = 0x7feb_352d;
const U32_SELF_BIT_MIXING: u32 = 0x846c_a68b;
let mut random = seed.fold(0_u32, |acc, e| {
let mut random = seed.fold(0_u32, |mut acc, e| {
// mix bits
acc.wrapping_add(e.wrapping_mul(U32_BIT_MIXING_CONSTANT))
acc ^= e.wrapping_mul(e);
acc.wrapping_mul(U32_BIT_MIXING_CONSTANT).wrapping_add(e)
});
// GPT's suggestion for a nicer distribution, like 8 cpu instructions so whatever
@@ -25,6 +23,16 @@ fn mix_u32s(seed: impl Iterator<Item = u32>) -> u32 {
}
pub async fn not_found(req: HttpRequest) -> HttpResponse {
if req.method().as_str().eq_ignore_ascii_case("HEAD") {
return HttpResponse::NotImplemented()
.insert_header((
"Unimplemented",
"The HEAD method is not yet implemented \
and this response is not valid for this endpoint",
))
.body(());
}
let seed = req.path().as_bytes();
let random = mix_u32s(seed.iter().copied().map(u32::from));
@@ -39,7 +47,7 @@ pub async fn not_found(req: HttpRequest) -> HttpResponse {
Ok::<_, !>(web::Bytes::from(format!(
"> {method} '{url}'\n404 NOT FOUND\nLet's all love lain\n\n"
))),
Ok(Bytes::from_static(res.as_bytes())),
Ok(web::Bytes::from_static(res.as_bytes())),
]))
// .body(format!(
// "> {method} '{url}'\n404 NOT FOUND\nLet's all love lain\n\n{res}"

View File

@@ -0,0 +1,38 @@
use std::{future::Ready, ops::Deref};
use actix_web::{error::InternalError, http::StatusCode};
/// App data extractor without any [`Arc`] inbetween, perefct if the app data is a simple
/// `&'static` reference that can be copied, no atomic anywhere.
pub struct StaticAppDataExtractor<T> {
pub data: T,
}
impl<T> Deref for StaticAppDataExtractor<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T: Copy + 'static> actix_web::FromRequest for StaticAppDataExtractor<T> {
type Error = InternalError<&'static str>;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(
req: &actix_web::HttpRequest,
_payload: &mut actix_web::dev::Payload,
) -> Self::Future {
let val = match req.app_data::<T>() {
Some(&data) => Ok(Self { data }),
None => Err(InternalError::new(
"Requested application data is not configured correctly. \
View/enable debug logs for more details.",
StatusCode::INTERNAL_SERVER_ERROR,
)),
};
std::future::ready(val)
}
}

View File

@@ -37,3 +37,29 @@ pub mod web {
}
}
}
pub mod shasum {
#[must_use]
pub fn sha256sum_to_hex_string(sha256sum: &[u8]) -> String {
let mut out = String::with_capacity(sha256sum.len() * 2);
for &b in sha256sum {
/// Only correct as long as `n` is ranged within a nibble
#[inline]
const fn nibble_to_hex(n: u8) -> char {
(match n {
0..=9 => b'0' + n,
_ => b'a' + (n - 10),
}) as char
}
let hi = (b >> 4) & 0x0f;
let lo = b & 0x0f;
out.push(nibble_to_hex(hi));
out.push(nibble_to_hex(lo));
}
out
}
}