diff --git a/Cargo.lock b/Cargo.lock index a1e0b01..97852a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,9 +340,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.51" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "jobserver", @@ -503,15 +503,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", @@ -837,9 +837,9 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -879,9 +879,9 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "litemap" @@ -1059,18 +1059,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -1103,9 +1103,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom", ] @@ -1219,9 +1219,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -1315,9 +1315,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.112" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1337,30 +1337,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", @@ -1378,9 +1378,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -1406,9 +1406,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -1457,9 +1457,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" @@ -1481,9 +1481,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -1832,18 +1832,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", @@ -1906,9 +1906,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.7" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" [[package]] name = "zstd" diff --git a/data/html/index.html b/data/html/index.html index fc05ed4..6db5796 100644 --- a/data/html/index.html +++ b/data/html/index.html @@ -15,7 +15,7 @@ -

Black Hole Share

+

Black Hole Share

@@ -368,6 +368,26 @@ } }); + function canTriggerUpload() { + return ( + currentContentData && + window.getComputedStyle(uploadBtn).display !== "none" && + zoomOverlay.style.display !== "flex" + ); + } + + // ENTER TO UPLOAD (when content is ready) + document.addEventListener( + "keydown", + function (e) { + if ((e.key === "Enter" || e.code === "NumpadEnter") && canTriggerUpload()) { + e.preventDefault(); + uploadBtn.click(); + } + }, + true + ); + window.addEventListener("resize", function () { if (currentContentData) { displayContent(currentContentData); @@ -376,4 +396,4 @@ - \ No newline at end of file + diff --git a/data/html/stats.html b/data/html/stats.html index 1d63b0d..aa00a9b 100644 --- a/data/html/stats.html +++ b/data/html/stats.html @@ -82,7 +82,7 @@ } .activity-list { - max-height: 300px; + max-height: 260px; overflow-y: auto; font-family: "JetBrains Mono", monospace; font-size: 0.85em; diff --git a/data/html/style.css b/data/html/style.css index 5c5bde9..c5de58b 100644 --- a/data/html/style.css +++ b/data/html/style.css @@ -29,7 +29,7 @@ body { height: 100vh; margin: 0 auto; padding: 20px; - padding-bottom: 140px; + padding-bottom: 80px; background-color: var(--bg-tertiary); background-image: radial-gradient(1200px 800px at 10% -20%, var(--bg-glow), transparent 60%), @@ -453,7 +453,7 @@ h1 .home-link:hover { /* View page styles */ body.view-page { width: 860px; - padding-bottom: 140px; + padding-bottom: 80px; } .view-container { @@ -476,6 +476,8 @@ body.view-page { border-top: 1px solid var(--border-color); font-size: 0.9em; color: var(--text-secondary); + width: 100%; + z-index: 10; } .powered-by .home-link { diff --git a/data/html/test/test.txt b/data/html/test/test.txt new file mode 100644 index 0000000..e69de29 diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index ff100ed..0000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "1.90.0" diff --git a/src/api.rs b/src/api.rs index 27103e3..ff45fc9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -6,7 +6,7 @@ use serde_json::json; use crate::{ LOG_FILE_NAME, - data_mgt::{Asset, AssetTracker}, + data_mgt::{AppState, Asset}, logs::{LogEvent, LogEventType, log_event}, }; @@ -21,8 +21,24 @@ pub struct UploadRequest { async fn api_upload( req: HttpRequest, body: web::Json, - assets: web::Data, + app_state: web::Data, ) -> Result { + // Check for rate limiting + let connection_info = req.connection_info(); + + let uploader_ip = connection_info + .realip_remote_addr() + .map(|s| s.to_string()) + .or_else(|| connection_info.peer_addr().map(|value| value.to_string())) + .ok_or_else(|| actix_web::error::ErrorBadRequest("Cannot determine client ip"))?; + + match app_state.connection_tracker.is_allowed(&uploader_ip).await { + true => {} + false => { + return Ok(HttpResponse::TooManyRequests().body("Upload limit exceeded")); + } + } + // Convert to bytes let content_bytes = if body.content_type == "text/plain" { body.content.as_bytes().to_vec() @@ -32,12 +48,6 @@ async fn api_upload( Err(_) => return Ok(HttpResponse::BadRequest().body("Invalid base64 payload")), } }; - let connection_info = req.connection_info(); - let uploader_ip = connection_info - .realip_remote_addr() - .or_else(|| connection_info.peer_addr()) - .unwrap_or("-") - .to_string(); let asset = crate::data_mgt::Asset::new( body.duration, @@ -48,7 +58,7 @@ async fn api_upload( let id = asset.id(); log_event(LogEventType::AssetUploaded(asset.to_value())); - assets.add_asset(asset).await; + app_state.assets.add_asset(asset).await; let response_body = json!({ "link": format!("/bhs/{}", id) }); Ok(HttpResponse::Ok().json(response_body)) } @@ -57,11 +67,11 @@ async fn api_upload( async fn api_get_asset( req: HttpRequest, path: web::Path, - assets: web::Data, + app_state: web::Data, ) -> Result { log_event(LogEventType::HttpRequest(req.into())); - match assets.get_asset(&path.into_inner()).await { + match app_state.assets.get_asset(&path.into_inner()).await { None => Ok(HttpResponse::NotFound().body("Asset not found")), Some(asset) => Ok(HttpResponse::Ok() .content_type(asset.mime()) @@ -90,11 +100,11 @@ struct ActivityItem { } #[get("/api/stats")] -async fn api_stats(assets: web::Data) -> Result { +async fn api_stats(app_state: web::Data) -> Result { use crate::LOG_DIR; use std::fs; - let (active_assets, storage_bytes, image_count, text_count) = assets.stats_summary().await; + let (active_assets, storage_bytes, image_count, text_count) = app_state.assets.stats_summary().await; let mut total_uploads = 0; let mut total_deleted = 0; diff --git a/src/data_mgt.rs b/src/data_mgt.rs index 0abef20..c6fa128 100644 --- a/src/data_mgt.rs +++ b/src/data_mgt.rs @@ -1,5 +1,5 @@ -use std::fmt::Debug; use std::sync::Arc; +use std::{collections::HashMap, fmt::Debug}; use anyhow::Result; use chrono::{Duration, Utc}; @@ -7,7 +7,11 @@ use futures::lock::Mutex; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::logs::{LogEventType, log_event}; +use crate::MAX_ASSETS; +use crate::{ + MAX_UPLOADS_PER_HOUR_PER_USER, + logs::{LogEventType, log_event}, +}; #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct Asset { @@ -85,31 +89,30 @@ impl Asset { pub fn to_value(&self) -> Value { serde_json::to_value(self).unwrap_or(Value::Null) } - - // pub fn save(&self) -> Result { - // let id = self.id.clone(); - // let path = format!("{}{}", DATA_STORAGE, self.id); - // std::fs::create_dir_all(DATA_STORAGE)?; - // std::fs::write(&path, self.to_bytes()?)?; - // Ok(id) - // } } -#[derive(Clone)] -pub struct AssetTracker { +#[derive(Clone, Debug, Default)] +pub struct AppState { + pub assets: AssetStorage, + pub connection_tracker: RateLimiter, +} + +#[derive(Clone, Debug, Default)] +pub struct AssetStorage { assets: Arc>>, } #[allow(dead_code)] -impl AssetTracker { +impl AssetStorage { pub fn new() -> Self { - AssetTracker { - assets: Arc::new(Mutex::new(Vec::new())), + Self { + assets: Arc::new(Mutex::new(Vec::with_capacity(MAX_ASSETS))), } } pub async fn add_asset(&self, asset: Asset) { print!("[{}] Adding asset: {}", chrono::Local::now().to_rfc3339(), asset.id()); + self.assets.lock().await.push(asset); self.show_assets().await; } @@ -173,7 +176,43 @@ impl AssetTracker { } } -pub async fn clear_assets(assets: AssetTracker) -> Result<()> { - assets.remove_expired().await; +#[derive(Clone, Debug, Default)] +pub struct RateLimiter { + pub clients: Arc>>>, +} + +impl RateLimiter { + pub async fn is_allowed(&self, client_ip: &str) -> bool { + let mut clients = self.clients.lock().await; + let now = Utc::now().timestamp_millis(); + let one_hour_ago = now - Duration::hours(1).num_milliseconds(); + + let entry = clients.entry(client_ip.to_string()).or_insert_with(Vec::new); + entry.retain(|×tamp| timestamp > one_hour_ago); + + let ret_val = if entry.len() < MAX_UPLOADS_PER_HOUR_PER_USER { + entry.push(now); + true + } else { + false + }; + println!("{:?}", clients); + ret_val + } + + pub async fn clear_expired(&self) { + let mut clients = self.clients.lock().await; + let now = Utc::now().timestamp_millis(); + let one_hour_ago = now - Duration::hours(1).num_milliseconds(); + + for timestamps in clients.values_mut() { + timestamps.retain(|×tamp| timestamp > one_hour_ago); + } + } +} + +pub async fn clear_app_data(app_state: &AppState) -> Result<()> { + app_state.assets.remove_expired().await; + app_state.connection_tracker.clear_expired().await; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 8d39ac0..9802a33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,9 @@ use std::{env, fs, path::PathBuf, sync::LazyLock}; pub static HTML_DIR: &str = "data/html/"; pub static LOG_DIR: &str = "data/logs/"; pub static LOG_FILE_NAME: &str = "log.txt"; +pub static MAX_ASSETS: usize = 1000; +pub static MAX_ASSET_SIZE_BYTES: usize = 3 * 1024 * 1024; // 3 MB +pub static MAX_UPLOADS_PER_HOUR_PER_USER: usize = 10; pub static BIND_ADDR: LazyLock = LazyLock::new(|| match env::var("BIND_ADDR") { Ok(addr) => { @@ -71,6 +74,7 @@ async fn view_asset(req: HttpRequest) -> actix_web::Result { #[route("/{tail:.*}", method = "GET", method = "POST")] async fn catch_all(req: HttpRequest, _payload: Option>) -> actix_web::Result { + println!("Catch-all route triggered for path: {}", req.uri().path()); let response = match req.uri().path() { path if STATIC_PAGES.contains(&path[1..].into()) => { let file_path = HTML_DIR.to_string() + path; @@ -108,24 +112,24 @@ async fn main() -> std::io::Result<()> { }); println!("Rotated log file to: {}_{}", time_tag, &LOG_FILE_NAME); } - let assets = data_mgt::AssetTracker::new(); + let app_state = data_mgt::AppState::default(); println!("Starting server at http://{}:{}/", *BIND_ADDR, *BIND_PORT); - let assets_clone = assets.clone(); + let inner_appt_state = app_state.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1)); loop { interval.tick().await; - if let Err(e) = data_mgt::clear_assets(assets_clone.clone()).await { + if let Err(e) = data_mgt::clear_app_data(&inner_appt_state).await { eprintln!("Error clearing assets: {}", e); } } }); HttpServer::new(move || { App::new() - .app_data(web::JsonConfig::default().limit(1024 * 1024 * 3)) - .app_data(web::Data::new(assets.clone())) + .app_data(web::JsonConfig::default().limit(1024 * 1024 * 3)) // 3MB limit + .app_data(web::Data::new(app_state.clone())) .service(index) .service(stats) .service(view_asset)