From aef58d133bfc48b86393c56993b7f7b034440204 Mon Sep 17 00:00:00 2001 From: xenofem Date: Tue, 16 Aug 2022 06:16:00 -0400 Subject: [PATCH] make cosmetic changes to state schema, add fileset passwords for forthcoming functionality --- Cargo.lock | 134 +++++++++++++++++++++++++++++++++++++++++------ Cargo.toml | 2 +- src/main.rs | 9 ++-- src/state.rs | 107 +++++++++++++++++++++++++++++++++++-- src/state/v0.rs | 92 ++++++++++++++++++++++++++++++++ src/store.rs | 34 ++---------- src/timestamp.rs | 33 ------------ src/upload.rs | 18 ++----- src/zip.rs | 17 +----- 9 files changed, 327 insertions(+), 119 deletions(-) create mode 100644 src/state/v0.rs diff --git a/Cargo.lock b/Cargo.lock index d506ae1..a2e2954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,6 +433,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + [[package]] name = "byteorder" version = "1.4.3" @@ -496,6 +502,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6127248204b9aba09a362f6c930ef6a78f2c1b2215f8a7b398c06e1083f17af0" +dependencies = [ + "js-sys", + "num-integer", + "num-traits", + "serde", + "wasm-bindgen", + "winapi", +] + [[package]] name = "cipher" version = "0.3.0" @@ -578,9 +598,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.13.4" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" dependencies = [ "darling_core", "darling_macro", @@ -588,9 +608,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.13.4" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" dependencies = [ "fnv", "ident_case", @@ -602,9 +622,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.13.4" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" dependencies = [ "darling_core", "quote", @@ -782,6 +802,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -857,6 +883,7 @@ checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ "autocfg", "hashbrown", + "serde", ] [[package]] @@ -896,6 +923,15 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "jsondb" version = "0.4.0" @@ -1057,6 +1093,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1262,12 +1308,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustversion" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" - [[package]] name = "ryu" version = "1.0.9" @@ -1337,20 +1377,25 @@ dependencies = [ [[package]] name = "serde_with" -version = "1.13.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b827f2113224f3f19a665136f006709194bdfdcb1fdc1e4b2b5cbac8e0cced54" +checksum = "89df7a26519371a3cce44fbb914c2819c84d9b897890987fa3ab096491cc0ea8" dependencies = [ - "rustversion", + "base64", + "chrono", + "hex", + "indexmap", "serde", + "serde_json", "serde_with_macros", + "time", ] [[package]] name = "serde_with_macros" -version = "1.5.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +checksum = "de337f322382fcdfbb21a014f7c224ee041a23785651db67b9827403178f698f" dependencies = [ "darling", "proc-macro2", @@ -1483,6 +1528,7 @@ dependencies = [ "itoa", "libc", "num_threads", + "serde", "time-macros", ] @@ -1695,6 +1741,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ae4d097..a6ace5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ rand = "0.8.5" sanitise-file-name = "1.0.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde_with = "1.13.0" +serde_with = { version = "2", features = ["time_0_3"] } thiserror = "1" time = "0.3.9" tokio = { version = "1.17.0", features = ["full"] } diff --git a/src/main.rs b/src/main.rs index 3805cec..8fe1716 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ mod download; mod state; mod store; -mod timestamp; mod upload; mod zip; @@ -310,7 +309,9 @@ async fn main() -> std::io::Result<()> { let cachebuster: String = env_or_else("TRANSBEAM_CACHEBUSTER", String::new); let state_file: PathBuf = match std::env::var("TRANSBEAM_STATE_FILE") { - Ok(v) => v.parse().unwrap_or_else(|_| panic!("Invalid value {} for variable TRANSBEAM_STATE_FILE", v)), + Ok(v) => v + .parse() + .unwrap_or_else(|_| panic!("Invalid value {} for variable TRANSBEAM_STATE_FILE", v)), Err(_) => { let legacy_state_file = storage_dir.join("files.json"); if legacy_state_file.is_file() { @@ -322,7 +323,9 @@ async fn main() -> std::io::Result<()> { }; let data = web::Data::new(AppData { - state: StateDb::load(state_file).await.expect("Failed to load state file"), + state: StateDb::load(state_file) + .await + .expect("Failed to load state file"), config: Config { base_url, max_upload_size, diff --git a/src/state.rs b/src/state.rs index 1a700f9..3ea75b1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,12 +1,109 @@ use jsondb::JsonDb; -mod v0 { - pub type State = crate::store::StoredFiles; +mod prelude { + pub use std::collections::HashMap; - impl jsondb::SchemaV0 for State { - const VERSION_OPTIONAL: bool = true; + pub use jsondb::Schema; + pub use serde::{Deserialize, Serialize}; + pub use serde_with::serde_as; + pub use serde_with::skip_serializing_none; + pub use time::OffsetDateTime; +} + +mod v0; + +pub mod v1 { + //! Schema version 1 + //! + //! Changes: + //! + //! * Represent times in RFC3339 format instead of epoch seconds + //! * Ditch some pre-JsonDb ad-hoc migrations + //! * Stored files now have optional passwords for + //! deleting/extending them, stored in plaintext because the + //! threat model doesn't warrant anything stronger + + use super::prelude::*; + + #[serde_as] + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct UploadedFile { + pub name: String, + pub size: u64, + #[serde_as(as = "time::format_description::well_known::Rfc3339")] + pub modtime: OffsetDateTime, + } + impl From for UploadedFile { + fn from(old: super::v0::UploadedFile) -> Self { + UploadedFile { + name: old.name, + size: old.size, + modtime: old.modtime, + } + } + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct FileSet { + pub files: Vec, + // Optional for backwards compatibility only + pub directory_name: Option, + } + impl From for FileSet { + fn from(old: super::v0::FileSet) -> Self { + FileSet { + files: old.files.into_iter().map(UploadedFile::from).collect(), + directory_name: old.directory_name, + } + } + } + + #[serde_as] + #[skip_serializing_none] + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct StoredFile { + pub name: String, + pub size: u64, + #[serde_as(as = "time::format_description::well_known::Rfc3339")] + pub modtime: OffsetDateTime, + #[serde_as(as = "time::format_description::well_known::Rfc3339")] + pub expiry: OffsetDateTime, + pub contents: Option, + /// None password means the admin page can't be accessed + pub password: Option, + } + impl From for StoredFile { + fn from(old: super::v0::StoredFile) -> Self { + StoredFile { + name: old.name, + size: old.size, + modtime: old.modtime, + expiry: old.expiry, + contents: old.contents.map(FileSet::from), + password: None, + } + } + } + + #[derive(Debug, Default, Deserialize, Serialize)] + pub struct StoredFiles(pub HashMap); + + pub type State = StoredFiles; + + impl Schema for State { + type Prev = super::v0::State; + } + impl From for State { + fn from(old: super::v0::State) -> Self { + StoredFiles( + old.0 + .into_iter() + .map(|(k, v)| (k, StoredFile::from(v))) + .collect(), + ) + } } } -pub use v0::State; +pub use v1::State; pub type StateDb = JsonDb; diff --git a/src/state/v0.rs b/src/state/v0.rs new file mode 100644 index 0000000..4d5a96f --- /dev/null +++ b/src/state/v0.rs @@ -0,0 +1,92 @@ +use super::prelude::*; + +use serde_with::{FromInto, PickFirst}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct UploadedFile { + pub name: String, + pub size: u64, + #[serde(with = "timestamp")] + pub modtime: OffsetDateTime, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct FileSet { + pub files: Vec, + // Optional for backwards compatibility only + pub directory_name: Option, +} + +impl From> for FileSet { + fn from(files: Vec) -> Self { + Self { + files, + directory_name: None, + } + } +} + +#[serde_as] +#[skip_serializing_none] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct StoredFile { + pub name: String, + pub size: u64, + #[serde(with = "timestamp")] + pub modtime: OffsetDateTime, + #[serde(with = "timestamp")] + pub expiry: OffsetDateTime, + #[serde_as(as = "Option>)>>")] + #[serde(default)] + pub contents: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct StoredFiles(pub HashMap); + +pub type State = StoredFiles; + +impl jsondb::SchemaV0 for State { + const VERSION_OPTIONAL: bool = true; +} + +mod timestamp { + use core::fmt; + + use serde::{de::Visitor, Deserializer, Serializer}; + use time::OffsetDateTime; + + pub(crate) fn serialize( + time: &OffsetDateTime, + ser: S, + ) -> Result { + ser.serialize_i64(time.unix_timestamp()) + } + + struct I64Visitor; + + impl<'de> Visitor<'de> for I64Visitor { + type Value = i64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "an integer") + } + + fn visit_i64(self, v: i64) -> Result { + Ok(v) + } + + fn visit_u64(self, v: u64) -> Result { + Ok(v as i64) + } + } + + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( + de: D, + ) -> Result { + Ok( + OffsetDateTime::from_unix_timestamp(de.deserialize_i64(I64Visitor)?) + .unwrap_or_else(|_| OffsetDateTime::now_utc()), + ) + } +} diff --git a/src/store.rs b/src/store.rs index 45d807d..f7ab3f6 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,7 +1,7 @@ use std::{ - collections::HashMap, io::ErrorKind, - path::{Path, PathBuf}, ops::DerefMut, + ops::DerefMut, + path::{Path, PathBuf}, }; use log::{debug, error, info}; @@ -9,15 +9,9 @@ use rand::{ distributions::{Alphanumeric, DistString}, thread_rng, Rng, }; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -use serde_with::{serde_as, FromInto, PickFirst}; use time::OffsetDateTime; use tokio::fs::File; -use crate::upload::UploadedFile; -use crate::zip::FileSet; - const MAX_STORAGE_FILES: usize = 1024; pub fn gen_storage_code(use_mnemonic: bool) -> String { @@ -34,23 +28,7 @@ pub fn is_valid_storage_code(s: &str) -> bool { .all(|c| c.is_ascii_alphanumeric() || c == &b'-') } -#[serde_as] -#[skip_serializing_none] -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct StoredFile { - pub name: String, - pub size: u64, - #[serde(with = "crate::timestamp")] - pub modtime: OffsetDateTime, - #[serde(with = "crate::timestamp")] - pub expiry: OffsetDateTime, - #[serde_as(as = "Option>)>>")] - #[serde(default)] - pub contents: Option, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct StoredFiles(pub HashMap); +pub use crate::state::v1::{StoredFile, StoredFiles}; async fn is_valid_entry(key: &str, info: &StoredFile, storage_dir: &Path) -> bool { if info.expiry < OffsetDateTime::now_utc() { @@ -130,11 +108,7 @@ impl crate::AppData { /// 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. - pub async fn add_file( - &self, - key: String, - file: StoredFile, - ) -> Result<(), FileAddError> { + pub async fn add_file(&self, key: String, file: StoredFile) -> Result<(), FileAddError> { let mut store = self.state.write().await; if store.full(self.config.max_storage_size) { return Err(FileAddError::Full); diff --git a/src/timestamp.rs b/src/timestamp.rs index 8ee8a82..e69de29 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -1,33 +0,0 @@ -use core::fmt; - -use serde::{de::Visitor, Deserializer, Serializer}; -use time::OffsetDateTime; - -pub(crate) fn serialize(time: &OffsetDateTime, ser: S) -> Result { - ser.serialize_i64(time.unix_timestamp()) -} - -struct I64Visitor; - -impl<'de> Visitor<'de> for I64Visitor { - type Value = i64; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "an integer") - } - - fn visit_i64(self, v: i64) -> Result { - Ok(v) - } - - fn visit_u64(self, v: u64) -> Result { - Ok(v as i64) - } -} - -pub(crate) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { - Ok( - OffsetDateTime::from_unix_timestamp(de.deserialize_i64(I64Visitor)?) - .unwrap_or_else(|_| OffsetDateTime::now_utc()), - ) -} diff --git a/src/upload.rs b/src/upload.rs index dd84448..4d8185f 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -109,13 +109,7 @@ impl Actor for Uploader { type Context = ::Context; -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct UploadedFile { - pub name: String, - pub size: u64, - #[serde(with = "crate::timestamp")] - pub modtime: OffsetDateTime, -} +pub use crate::state::v1::UploadedFile; impl UploadedFile { fn new(name: &str, size: u64, modtime: OffsetDateTime) -> Self { @@ -328,15 +322,14 @@ impl Uploader { modtime, expiry: OffsetDateTime::now_utc() + lifetime * time::Duration::DAY, contents, + password: None, }; let app_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); - app_data - .add_file(storage_filename, stored_file) - .await + app_data.add_file(storage_filename, stored_file).await }) .map(|res, u: &mut Self, ctx: &mut Context| match res { Ok(()) => ctx.text( @@ -406,10 +399,7 @@ impl Uploader { ctx.wait( actix::fut::wrap_future(async move { debug!("Spawned future to remove entry {} from state", filename); - app_data - .remove_file(&filename) - .await - .unwrap(); + app_data.remove_file(&filename).await.unwrap(); }) .map(stop_and_flush), ); diff --git a/src/zip.rs b/src/zip.rs index b9c775e..e2772aa 100644 --- a/src/zip.rs +++ b/src/zip.rs @@ -2,7 +2,6 @@ use std::io::Write; use crc32fast::Hasher; use log::debug; -use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::upload::UploadedFile; @@ -28,21 +27,7 @@ const EOCD_TOTAL_SIZE: u64 = EOCD64_RECORD_SIZE + EOCD64_LOCATOR_SIZE + EOCD_REC const EMPTY_STRING_CRC32: u32 = 0; -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct FileSet { - pub files: Vec, - // Optional for backwards compatibility only - pub directory_name: Option, -} - -impl From> for FileSet { - fn from(files: Vec) -> Self { - Self { - files, - directory_name: None, - } - } -} +pub use crate::state::v1::FileSet; fn full_file_name_len(file: &UploadedFile, directory_name: &Option) -> u64 { file.name.len() as u64