diff --git a/.gitignore b/.gitignore index 2879dc6..c870aac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ /storage /result flamegraph.svg -perf.data* -.env \ No newline at end of file +perf.data* \ No newline at end of file diff --git a/API.md b/API.md new file mode 100644 index 0000000..4f01332 --- /dev/null +++ b/API.md @@ -0,0 +1,53 @@ +# transbeam websocket api + +- After opening the connection, the client sends an upload manifest to + the server. This is a JSON object containing the following keys: + - `files`: a list of metadata objects for all the files to be + uploaded, in the exact order they will be sent. This list must + contain at least 1 file and at most 256 files. Each file metadata + object has the following keys, all required: + - `name`: The name of the file. This will be sanitised on the + server side, but the sanitisation library isn't especially + restrictive; most Unicode code points will be allowed through + as-is. + - `size`: The exact size of the file, in bytes. + - `modtime`: The modification time of the file, as milliseconds + since the Unix epoch. + - `lifetime`: an integer number of days the files should be kept + for. + +- Once the server receives the metadata, it will respond with a + JSON-encoded object containing at least the field `type`, and + possibly other fields as well. The types of message, and their + associated extra fields if any, are as follows: + - `ready`: The server will accept the upload and is ready to receive + data. + - `code`: A code string that can be used to download the files, + starting now. + - `too_big`: The upload is rejected because the total size of the + files is bigger than the server is willing to accept. + - `max_size`: The maximum total upload size the server will + accept. This is subject to change if the admin changes the + config, or if the server's storage space is filling up. + - `too_long`: The upload is rejected because the requested lifetime + is longer than the server will allow. + - `max_days`: The maximum number of days the client can request + files be kept for. + - `error`: A miscellaneous error has occurred. + - `details`: A string with more information about the error. + + If the message type is anything other than `ready`, the connection + will be closed by the server. + +- If the server is ready to receive files, the client begins sending + chunks of data from the files, as raw binary blobs. The client must + transmit each file's data in order from start to finish, and must + transmit the files in the same order they were listed in the + metadata. The size of the chunks isn't currently specified, and + it's fine for a chunk to span the end of one file and the start of + the next. After sending each chunk (that is, each complete + websocket message), the client must wait for the server to + acknowledge the chunk by sending back the string "ack", and then + send the next chunk if there is one. Once all chunks have been sent + and acknowledged, or once the server has sent a message other than + "ack" to indicate an error, the connection will be closed. diff --git a/Cargo.lock b/Cargo.lock index 9e19214..58a8d94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,12 +392,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" -[[package]] -name = "bytesize" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" - [[package]] name = "bytestring" version = "1.0.0" @@ -547,12 +541,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "encoding_rs" version = "0.8.31" @@ -1424,9 +1412,7 @@ dependencies = [ "actix-web", "actix-web-actors", "bytes", - "bytesize", "crc32fast", - "dotenv", "env_logger", "futures-core", "inotify", diff --git a/Cargo.toml b/Cargo.toml index 51e2fa3..a2379a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,7 @@ actix-http = "3.0.4" actix-web = "4.0.1" actix-web-actors = "4.1.0" bytes = "1.1.0" -bytesize = "1.1.0" crc32fast = "1.3.2" -dotenv = "0.15" env_logger = "0.9" futures-core = "0.3" inotify = "0.10" diff --git a/README.md b/README.md index 64b38a0..560edbc 100644 --- a/README.md +++ b/README.md @@ -17,33 +17,22 @@ file for receivers, with zero compression so extraction is quick - Sanitises filenames, using sensible non-obnoxious defaults that should be safe across platforms -- Rudimentary password authentication for uploading files - Fires a laser beam that turns you trans ## configuration transbeam is configured with the following environment variables: -- `TRANSBEAM_STORAGE_DIR`: path where uploaded files should be stored - (default: `./storage`) -- `TRANSBEAM_STATIC_DIR`: path where the web app's static files live - (default: `./static`) -- `TRANSBEAM_PORT`: port to listen on localhost for http requests - (default: 8080) -- `TRANSBEAM_MAX_LIFETIME`: maximum number of days files can be kept - for (default: 30) -- `TRANSBEAM_MAX_UPLOAD_SIZE`: maximum size of a fileset being - uploaded (default: 16G) +- `TRANSBEAM_STORAGE_DIR`: path where uploaded files should be stored (default: `./storage`) +- `TRANSBEAM_STATIC_DIR`: path where the web app's static files live (default: `./static`) +- `TRANSBEAM_PORT`: port to listen on localhost for http requests (default: 8080) +- `TRANSBEAM_MAX_LIFETIME`: maximum number of days files can be kept for (default: 30) +- `TRANSBEAM_MAX_UPLOAD_SIZE`: maximum size, in bytes, of a fileset + being uploaded (default: 16GiB) - `TRANSBEAM_MAX_STORAGE_SIZE`: maximum total size, in bytes, of all - files being stored by transbeam (default: 64G) + files being stored by transbeam (default: 64GiB) - `TRANSBEAM_MNEMONIC_CODES`: generate memorable download codes using English words, rather than random alphanumeric strings (default: true) -- `TRANSBEAM_UPLOAD_PASSWORD`: password for uploading files. This - isn't meant to be a hardcore security measure, just a defense - against casual internet randos filling up your storage. I strongly - recommend setting up fail2ban to throttle password attempts; - transbeam logs failed attempts along with IP addresses, in the form - `Incorrect authentication attempt from 203.0.113.12`. - `RUST_LOG`: log levels, for the app as a whole and/or for specific submodules/libraries. See [`env_logger`](https://docs.rs/env_logger/latest/env_logger/)'s @@ -68,3 +57,8 @@ git clone https://git.xeno.science/xenofem/transbeam cd transbeam cargo run --release ``` + +## todo + +- uploader auth +- downloader auth diff --git a/src/main.rs b/src/main.rs index e38e0b5..da58653 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,51 +3,20 @@ mod store; mod upload; mod zip; -use std::{fmt::Debug, fs::File, path::PathBuf, str::FromStr}; +use std::{fs::File, path::PathBuf}; -use actix_files::NamedFile; use actix_web::{ - get, http::StatusCode, middleware::Logger, post, web, App, HttpRequest, HttpResponse, - HttpServer, Responder, + get, middleware::Logger, web, App, HttpRequest, HttpResponse, HttpServer, Responder, }; use actix_web_actors::ws; -use bytesize::ByteSize; -use log::{error, warn}; -use serde::{Deserialize, Serialize}; +use log::error; +use serde::Deserialize; use store::FileStore; use tokio::sync::RwLock; const APP_NAME: &str = "transbeam"; -struct AppState { - file_store: RwLock, - config: Config, -} - -struct Config { - max_upload_size: u64, - max_lifetime: u16, - upload_password: String, - storage_dir: PathBuf, - static_dir: PathBuf, - reverse_proxy: bool, - mnemonic_codes: bool, -} - -pub fn get_ip_addr(req: &HttpRequest, reverse_proxy: bool) -> String { - let conn_info = req.connection_info(); - if reverse_proxy { - conn_info.realip_remote_addr() - } else { - conn_info.peer_addr() - } - .unwrap() - .to_owned() -} - -pub fn log_auth_failure(ip_addr: &str) { - warn!("Incorrect authentication attempt from {}", ip_addr); -} +type AppData = web::Data>; #[derive(Deserialize)] struct DownloadRequest { @@ -58,15 +27,16 @@ struct DownloadRequest { async fn handle_download( req: HttpRequest, download: web::Query, - data: web::Data, + data: AppData, ) -> actix_web::Result { let code = &download.code; if !store::is_valid_storage_code(code) { - return download_not_found(req, data); + return Ok(HttpResponse::NotFound().finish()); } - let info = data.file_store.read().await.lookup_file(code); + let data = data.read().await; + let info = data.lookup_file(code); if let Some(info) = info { - let storage_path = data.config.storage_dir.join(code); + let storage_path = store::storage_dir().join(code); let file = File::open(&storage_path)?; Ok(download::DownloadingFile { file, @@ -75,164 +45,50 @@ async fn handle_download( } .into_response(&req)) } else { - download_not_found(req, data) + Ok(HttpResponse::NotFound().finish()) } } -fn download_not_found( - req: HttpRequest, - data: web::Data, -) -> actix_web::Result { - let ip_addr = get_ip_addr(&req, data.config.reverse_proxy); - log_auth_failure(&ip_addr); - Ok(NamedFile::open(data.config.static_dir.join("404.html"))? - .set_status_code(StatusCode::NOT_FOUND) - .into_response(&req)) -} - #[get("/upload")] -async fn handle_upload( - req: HttpRequest, - stream: web::Payload, - data: web::Data, -) -> impl Responder { - if data.file_store.read().await.full() { - return Ok(HttpResponse::BadRequest().finish()); - } - let ip_addr = get_ip_addr(&req, data.config.reverse_proxy); - ws::start(upload::Uploader::new(data, ip_addr), &req, stream) -} - -#[derive(Deserialize)] -struct UploadPasswordCheck { - password: String, -} - -#[post("/upload/check_password")] -async fn check_upload_password( - req: HttpRequest, - body: web::Json, - data: web::Data, -) -> impl Responder { - let ip_addr = get_ip_addr(&req, data.config.reverse_proxy); - if body.password != data.config.upload_password { - log_auth_failure(&ip_addr); - HttpResponse::Forbidden().finish() - } else { - HttpResponse::NoContent().finish() - } -} - -#[derive(Serialize)] -struct UploadLimits { - open: bool, - max_size: u64, - max_lifetime: u16, -} - -#[get("/upload/limits.json")] -async fn upload_limits(data: web::Data) -> impl Responder { - let file_store = data.file_store.read().await; - let open = !file_store.full(); - let available_size = file_store.available_size(); - let max_size = std::cmp::min(available_size, data.config.max_upload_size); - web::Json(UploadLimits { - open, - max_size, - max_lifetime: data.config.max_lifetime, - }) -} - -fn env_or(var: &str, default: T) -> T -where - ::Err: Debug, -{ - std::env::var(var) - .map(|val| { - val.parse::() - .unwrap_or_else(|_| panic!("Invalid value {} for variable {}", val, var)) - }) - .unwrap_or(default) -} - -fn env_or_else T>(var: &str, default: F) -> T -where - ::Err: Debug, -{ - std::env::var(var) - .map(|val| { - val.parse::() - .unwrap_or_else(|_| panic!("Invalid value {} for variable {}", val, var)) - }) - .ok() - .unwrap_or_else(default) +async fn handle_upload(req: HttpRequest, stream: web::Payload, data: AppData) -> impl Responder { + ws::start(upload::Uploader::new(data), &req, stream) } #[actix_web::main] async fn main() -> std::io::Result<()> { - dotenv::dotenv().ok(); env_logger::init(); - let static_dir: PathBuf = env_or_else("TRANSBEAM_STATIC_DIR", || PathBuf::from("static")); - let storage_dir: PathBuf = env_or_else("TRANSBEAM_STORAGE_DIR", || PathBuf::from("storage")); - let port: u16 = env_or("TRANSBEAM_PORT", 8080); - let mnemonic_codes: bool = env_or("TRANSBEAM_MNEMONIC_CODES", true); - let reverse_proxy: bool = env_or("TRANSBEAM_REVERSE_PROXY", false); - let max_lifetime: u16 = env_or("TRANSBEAM_MAX_LIFETIME", 30); - let max_upload_size: u64 = - env_or::("TRANSBEAM_MAX_UPLOAD_SIZE", ByteSize(16 * bytesize::GB)).as_u64(); - let max_storage_size: u64 = - env_or::("TRANSBEAM_MAX_STORAGE_SIZE", ByteSize(64 * bytesize::GB)).as_u64(); - let upload_password: String = - std::env::var("TRANSBEAM_UPLOAD_PASSWORD").expect("TRANSBEAM_UPLOAD_PASSWORD must be set!"); - - let data = web::Data::new(AppState { - file_store: RwLock::new(FileStore::load(storage_dir.clone(), max_storage_size).await?), - config: Config { - max_upload_size, - max_lifetime, - upload_password, - storage_dir, - static_dir: static_dir.clone(), - reverse_proxy, - mnemonic_codes, - }, - }); + let data: AppData = web::Data::new(RwLock::new(FileStore::load().await?)); start_reaper(data.clone()); + let static_dir = PathBuf::from( + std::env::var("TRANSBEAM_STATIC_DIR").unwrap_or_else(|_| String::from("static")), + ); + let port = std::env::var("TRANSBEAM_PORT") + .ok() + .and_then(|p| p.parse::().ok()) + .unwrap_or(8080); + HttpServer::new(move || { App::new() .app_data(data.clone()) - .wrap(if data.config.reverse_proxy { - Logger::new(r#"%{r}a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#) - } else { - Logger::default() - }) - .service(handle_download) + .wrap(Logger::default()) .service(handle_upload) - .service(check_upload_password) - .service(upload_limits) + .service(handle_download) .service(actix_files::Files::new("/", static_dir.clone()).index_file("index.html")) }) - .bind(( - if reverse_proxy { - "127.0.0.1" - } else { - "0.0.0.0" - }, - port, - ))? + .bind(("127.0.0.1", port))? .run() .await?; Ok(()) } -fn start_reaper(data: web::Data) { +fn start_reaper(data: AppData) { std::thread::spawn(move || { actix_web::rt::System::new().block_on(async { loop { actix_web::rt::time::sleep(core::time::Duration::from_secs(86400)).await; - if let Err(e) = data.file_store.write().await.remove_expired_files().await { + if let Err(e) = data.write().await.remove_expired_files().await { error!("Error reaping expired files: {}", e); } } diff --git a/src/store.rs b/src/store.rs index 98ef749..319f988 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,10 +1,6 @@ -use std::{ - collections::HashMap, - io::ErrorKind, - path::{Path, PathBuf}, -}; +use std::{collections::HashMap, io::ErrorKind, path::PathBuf, str::FromStr}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use rand::{ distributions::{Alphanumeric, DistString}, thread_rng, Rng, @@ -17,13 +13,17 @@ use tokio::{ }; const STATE_FILE_NAME: &str = "files.json"; -const MAX_STORAGE_FILES: usize = 1024; +const DEFAULT_STORAGE_DIR: &str = "storage"; +const DEFAULT_MAX_LIFETIME: u32 = 30; +const GIGA: u64 = 1024 * 1024 * 1024; +const DEFAULT_MAX_UPLOAD_SIZE: u64 = 16 * GIGA; +const DEFAULT_MAX_STORAGE_SIZE: u64 = 64 * GIGA; -pub fn gen_storage_code(use_mnemonic: bool) -> String { - if use_mnemonic { - mnemonic::to_string(thread_rng().gen::<[u8; 4]>()) - } else { +pub fn gen_storage_code() -> String { + if std::env::var("TRANSBEAM_MNEMONIC_CODES").as_deref() == Ok("false") { Alphanumeric.sample_string(&mut thread_rng(), 8) + } else { + mnemonic::to_string(thread_rng().gen::<[u8; 4]>()) } } @@ -33,6 +33,32 @@ pub fn is_valid_storage_code(s: &str) -> bool { .all(|c| c.is_ascii_alphanumeric() || c == &b'-') } +pub(crate) fn storage_dir() -> PathBuf { + PathBuf::from( + std::env::var("TRANSBEAM_STORAGE_DIR") + .unwrap_or_else(|_| String::from(DEFAULT_STORAGE_DIR)), + ) +} + +fn parse_env_var(var: &str, default: T) -> T { + std::env::var(var) + .ok() + .and_then(|val| val.parse::().ok()) + .unwrap_or(default) +} + +pub(crate) fn max_lifetime() -> u32 { + parse_env_var("TRANSBEAM_MAX_LIFETIME", DEFAULT_MAX_LIFETIME) +} + +pub(crate) fn max_single_size() -> u64 { + parse_env_var("TRANSBEAM_MAX_UPLOAD_SIZE", DEFAULT_MAX_UPLOAD_SIZE) +} + +pub(crate) fn max_total_size() -> u64 { + parse_env_var("TRANSBEAM_MAX_STORAGE_SIZE", DEFAULT_MAX_STORAGE_SIZE) +} + #[derive(Clone, Deserialize, Serialize)] pub struct StoredFile { pub name: String, @@ -84,13 +110,13 @@ pub(crate) mod timestamp { } } -async fn is_valid_entry(key: &str, info: &StoredFile, storage_dir: &Path) -> bool { +async fn is_valid_entry(key: &str, info: &StoredFile) -> bool { if info.expiry < OffsetDateTime::now_utc() { info!("File {} has expired", key); return false; } - let file = if let Ok(f) = File::open(storage_dir.join(&key)).await { + let file = if let Ok(f) = File::open(storage_dir().join(&key)).await { f } else { error!( @@ -115,35 +141,10 @@ async fn is_valid_entry(key: &str, info: &StoredFile, storage_dir: &Path) -> boo true } -async fn delete_file_if_exists(file: &PathBuf) -> std::io::Result<()> { - if let Err(e) = tokio::fs::remove_file(file).await { - if e.kind() != ErrorKind::NotFound { - error!("Failed to delete file {}: {}", file.to_string_lossy(), e); - return Err(e); - } - } - Ok(()) -} - -#[derive(thiserror::Error, Debug)] -pub enum FileAddError { - #[error("Failed to write metadata to filesystem")] - FileSystem(#[from] std::io::Error), - #[error("File was too large, available space is {0} bytes")] - TooBig(u64), - #[error("File store is full")] - Full, -} - -pub struct FileStore { - files: HashMap, - storage_dir: PathBuf, - max_storage_size: u64, -} - +pub(crate) struct FileStore(HashMap); impl FileStore { - pub(crate) async fn load(storage_dir: PathBuf, max_storage_size: u64) -> std::io::Result { - let open_result = File::open(storage_dir.join(STATE_FILE_NAME)).await; + pub(crate) async fn load() -> std::io::Result { + let open_result = File::open(storage_dir().join(STATE_FILE_NAME)).await; match open_result { Ok(mut f) => { let mut buf = String::new(); @@ -159,28 +160,22 @@ impl FileStore { error!("Invalid key in persistent storage: {}", key); continue; } - if is_valid_entry(&key, &info, &storage_dir).await { + if is_valid_entry(&key, &info).await { filtered.insert(key, info); } else { info!("Deleting file {}", key); - delete_file_if_exists(&storage_dir.join(&key)).await?; + if let Err(e) = tokio::fs::remove_file(storage_dir().join(&key)).await { + warn!("Failed to delete file {}: {}", key, e); + } } } - let mut loaded = Self { - files: filtered, - storage_dir, - max_storage_size, - }; + let mut loaded = Self(filtered); loaded.save().await?; Ok(loaded) } Err(e) => { if let ErrorKind::NotFound = e.kind() { - Ok(Self { - files: HashMap::new(), - storage_dir, - max_storage_size, - }) + Ok(Self(HashMap::new())) } else { Err(e) } @@ -189,25 +184,17 @@ impl FileStore { } fn total_size(&self) -> u64 { - self.files.iter().fold(0, |acc, (_, f)| acc + f.size) - } - - pub fn available_size(&self) -> u64 { - self.max_storage_size.saturating_sub(self.total_size()) + self.0.iter().fold(0, |acc, (_, f)| acc + f.size) } async fn save(&mut self) -> std::io::Result<()> { - info!("saving updated state: {} entries", self.files.len()); - File::create(self.storage_dir.join(STATE_FILE_NAME)) + info!("saving updated state: {} entries", self.0.len()); + File::create(storage_dir().join(STATE_FILE_NAME)) .await? - .write_all(&serde_json::to_vec_pretty(&self.files)?) + .write_all(&serde_json::to_vec_pretty(&self.0)?) .await } - pub fn full(&self) -> bool { - self.available_size() == 0 || self.files.len() >= MAX_STORAGE_FILES - } - /// Attempts to add a file to the store. Returns an I/O error if /// something's broken, or a u64 of the maximum allowed file size /// if the file was too big, or a unit if everything worked. @@ -215,42 +202,37 @@ impl FileStore { &mut self, key: String, file: StoredFile, - ) -> Result<(), FileAddError> { - if self.full() { - return Err(FileAddError::Full); + ) -> std::io::Result> { + let remaining_size = max_total_size().saturating_sub(self.total_size()); + let allowed_size = std::cmp::min(remaining_size, max_single_size()); + if file.size > allowed_size { + return Ok(Err(allowed_size)); } - let available_size = self.available_size(); - if file.size > available_size { - return Err(FileAddError::TooBig(available_size)); - } - self.files.insert(key, file); - self.save().await?; - Ok(()) + self.0.insert(key, file); + self.save().await.map(Ok) } pub(crate) fn lookup_file(&self, key: &str) -> Option { - self.files.get(key).cloned() + self.0.get(key).cloned() } pub(crate) async fn remove_file(&mut self, key: &str) -> std::io::Result<()> { debug!("removing entry {} from state", key); - self.files.remove(key); - self.save().await?; - if is_valid_storage_code(key) { - delete_file_if_exists(&self.storage_dir.join(key)).await?; - } - Ok(()) + self.0.remove(key); + self.save().await } pub(crate) async fn remove_expired_files(&mut self) -> std::io::Result<()> { info!("Checking for expired files"); let now = OffsetDateTime::now_utc(); - for (key, file) in std::mem::take(&mut self.files).into_iter() { + for (key, file) in std::mem::take(&mut self.0).into_iter() { if file.expiry > now { - self.files.insert(key, file); + self.0.insert(key, file); } else { info!("Deleting expired file {}", key); - delete_file_if_exists(&self.storage_dir.join(&key)).await?; + if let Err(e) = tokio::fs::remove_file(storage_dir().join(&key)).await { + warn!("Failed to delete expired file {}: {}", key, e); + } } } self.save().await diff --git a/src/upload.rs b/src/upload.rs index b06e012..2083807 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -2,7 +2,6 @@ use std::{collections::HashSet, fs::File, io::Write}; use actix::{fut::future::ActorFutureExt, Actor, ActorContext, AsyncContext, StreamHandler}; use actix_http::ws::{CloseReason, Item}; -use actix_web::web; use actix_web_actors::ws::{self, CloseCode}; use bytes::Bytes; use log::{debug, error, info, trace}; @@ -10,11 +9,7 @@ use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use unicode_normalization::UnicodeNormalization; -use crate::{ - log_auth_failure, - store::{self, FileAddError, StoredFile}, - AppState, -}; +use crate::store::{self, storage_dir, StoredFile}; const MAX_FILES: usize = 256; const FILENAME_DATE_FORMAT: &[time::format_description::FormatItem] = @@ -34,14 +29,10 @@ enum Error { NoFiles, #[error("Number of files submitted by client exceeded the maximum limit")] TooManyFiles, - #[error("Requested lifetime was too long, can be at most {0} days")] - TooLong(u16), + #[error("Requested lifetime was too long")] + TooLong, #[error("Upload size was too large, can be at most {0} bytes")] TooBig(u64), - #[error("File storage is full")] - Full, - #[error("Incorrect password")] - IncorrectPassword, #[error("Websocket was closed by client before completing transfer")] ClosedEarly(Option), #[error("Client sent more data than they were supposed to")] @@ -59,10 +50,8 @@ impl Error { Self::DuplicateFilename | Self::NoFiles | Self::TooManyFiles - | Self::TooLong(_) - | Self::TooBig(_) - | Self::Full - | Self::IncorrectPassword => CloseCode::Policy, + | Self::TooLong + | Self::TooBig(_) => CloseCode::Policy, } } } @@ -70,19 +59,17 @@ impl Error { pub struct Uploader { writer: Option>, storage_filename: String, - app_state: web::Data, + app_data: super::AppData, bytes_remaining: u64, - ip_addr: String, } impl Uploader { - pub(crate) fn new(app_state: web::Data, ip_addr: String) -> Self { + pub(crate) fn new(app_data: super::AppData) -> Self { Self { writer: None, - storage_filename: store::gen_storage_code(app_state.config.mnemonic_codes), - app_state, + storage_filename: store::gen_storage_code(), + app_data, bytes_remaining: 0, - ip_addr, } } } @@ -130,8 +117,7 @@ impl RawUploadedFile { #[derive(Deserialize)] struct UploadManifest { files: Vec, - lifetime: u16, - password: String, + lifetime: u32, } #[derive(Serialize)] @@ -139,8 +125,7 @@ struct UploadManifest { enum ServerMessage { Ready { code: String }, TooBig { max_size: u64 }, - TooLong { max_days: u16 }, - IncorrectPassword, + TooLong { max_days: u32 }, Error { details: String }, } @@ -150,10 +135,9 @@ impl From<&Error> for ServerMessage { Error::TooBig(max_size) => ServerMessage::TooBig { max_size: *max_size, }, - Error::TooLong(max_days) => ServerMessage::TooLong { - max_days: *max_days, + Error::TooLong => ServerMessage::TooLong { + max_days: store::max_lifetime(), }, - Error::IncorrectPassword => ServerMessage::IncorrectPassword, _ => ServerMessage::Error { details: e.to_string(), }, @@ -205,9 +189,6 @@ fn ack(ctx: &mut Context) { impl Uploader { fn notify_error_and_cleanup(&mut self, e: Error, ctx: &mut Context) { error!("{}", e); - if let Error::IncorrectPassword = e { - log_auth_failure(&self.ip_addr); - } ctx.text(serde_json::to_string(&ServerMessage::from(&e)).unwrap()); ctx.close(Some(ws::CloseReason { code: e.close_code(), @@ -226,13 +207,9 @@ impl Uploader { let UploadManifest { files: raw_files, lifetime, - password, } = serde_json::from_slice(text.as_bytes())?; - if std::env::var("TRANSBEAM_UPLOAD_PASSWORD") != Ok(password) { - return Err(Error::IncorrectPassword); - } - if lifetime > self.app_state.config.max_lifetime { - return Err(Error::TooLong(self.app_state.config.max_lifetime)); + if lifetime > store::max_lifetime() { + return Err(Error::TooLong); } info!("Received file list: {} files", raw_files.len()); debug!("{:?}", raw_files); @@ -257,14 +234,7 @@ impl Uploader { self.bytes_remaining += file.size; files.push(file); } - if self.bytes_remaining > self.app_state.config.max_upload_size { - return Err(Error::TooBig(self.app_state.config.max_upload_size)); - } - let storage_path = self - .app_state - .config - .storage_dir - .join(self.storage_filename.clone()); + let storage_path = storage_dir().join(self.storage_filename.clone()); info!("storing to: {:?}", storage_path); let writer = File::options() .write(true) @@ -294,32 +264,25 @@ impl Uploader { modtime, expiry: OffsetDateTime::now_utc() + lifetime * time::Duration::DAY, }; - let state = self.app_state.clone(); + let data = self.app_data.clone(); let storage_filename = self.storage_filename.clone(); ctx.spawn( actix::fut::wrap_future(async move { debug!("Spawned future to add entry {} to state", storage_filename); - state - .file_store - .write() + data.write() .await .add_file(storage_filename, stored_file) .await }) .map(|res, u: &mut Self, ctx: &mut Context| match res { - Ok(()) => ctx.text( + Ok(Ok(())) => ctx.text( serde_json::to_string(&ServerMessage::Ready { code: u.storage_filename.clone(), }) .unwrap(), ), - Err(FileAddError::TooBig(size)) => { - u.notify_error_and_cleanup(Error::TooBig(size), ctx) - } - Err(FileAddError::Full) => u.notify_error_and_cleanup(Error::Full, ctx), - Err(FileAddError::FileSystem(e)) => { - u.notify_error_and_cleanup(Error::from(e), ctx) - } + Ok(Err(size)) => u.notify_error_and_cleanup(Error::TooBig(size), ctx), + Err(e) => u.notify_error_and_cleanup(Error::from(e), ctx), }), ); } @@ -369,20 +332,17 @@ impl Uploader { "Cleaning up after failed upload of {}", self.storage_filename ); - let state = self.app_state.clone(); + let data = self.app_data.clone(); let filename = self.storage_filename.clone(); ctx.wait( actix::fut::wrap_future(async move { debug!("Spawned future to remove entry {} from state", filename); - state - .file_store - .write() - .await - .remove_file(&filename) - .await - .unwrap(); + data.write().await.remove_file(&filename).await.unwrap(); }) .map(stop_and_flush), ); + if let Err(e) = std::fs::remove_file(storage_dir().join(&self.storage_filename)) { + error!("Failed to remove file {}: {}", self.storage_filename, e); + } } } diff --git a/static/404.html b/static/404.html deleted file mode 100644 index 05bf6a2..0000000 --- a/static/404.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - transbeam - - - -
-

The download code you entered wasn't found. The download may have expired.

-
-
- -
- -
-
-
-

< Back

-
- - - diff --git a/static/index.html b/static/index.html index ff438e6..f800d1c 100644 --- a/static/index.html +++ b/static/index.html @@ -3,82 +3,63 @@ - - + + - - - + + transbeam - + -
-

Download

+
+
+
+ +
+
+ +
+
+
+
+
Download code:
+
+
Link copied!
+
+
+
+
+
+ +
+ +
- -
-

Upload

-
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
- -
-
-
-
-
Download code:
-
-
Link copied!
-
-
-
-
-
- -
- -
-