barebones web

webpack
Edward Shen 2021-10-19 02:18:33 -07:00
parent 006850e35d
commit 26ac52e74b
Signed by: edward
GPG Key ID: 19182661E818369F
14 changed files with 666 additions and 340 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
/database
**/database
/web/dist/

195
Cargo.lock generated
View File

@ -41,6 +41,12 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "async-trait"
version = "0.1.51"
@ -194,7 +200,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db507a7679252d2276ed0dd8113c6875ec56d3089f9225b2b42c30cc1f8e5c89"
dependencies = [
"nom",
"nom 6.1.2",
]
[[package]]
@ -243,6 +249,8 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"serde",
"time",
"winapi",
]
@ -326,13 +334,10 @@ dependencies = [
]
[[package]]
name = "ed25519"
name = "downcast-rs"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc"
dependencies = [
"signature",
]
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "encoding_rs"
@ -447,8 +452,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@ -469,6 +476,18 @@ dependencies = [
"gloo-timers",
]
[[package]]
name = "gloo-console"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "770942b86a2ab86330201eeafc5fe526fb203e54dbc6ef82a36453cebcb90e4c"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-console-timer"
version = "0.1.0"
@ -715,6 +734,19 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec",
"bitflags",
"cfg-if",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.103"
@ -743,18 +775,6 @@ dependencies = [
"libc",
]
[[package]]
name = "libsodium-sys"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd"
dependencies = [
"cc",
"libc",
"pkg-config",
"walkdir",
]
[[package]]
name = "log"
version = "0.4.14"
@ -813,6 +833,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "nom"
version = "6.1.2"
@ -873,13 +904,20 @@ dependencies = [
"omegaupload-common",
"reqwest",
"secrecy",
"sodiumoxide",
"url",
]
[[package]]
name = "omegaupload-common"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"chacha20poly1305",
"rand",
"sha2",
"thiserror",
"url",
]
[[package]]
name = "omegaupload-server"
@ -889,6 +927,7 @@ dependencies = [
"axum",
"bincode",
"bytes",
"chrono",
"headers",
"lazy_static",
"rand",
@ -903,8 +942,15 @@ dependencies = [
name = "omegaupload-web"
version = "0.1.0"
dependencies = [
"chacha20poly1305",
"anyhow",
"downcast-rs",
"getrandom",
"gloo-console",
"http",
"omegaupload-common",
"web-sys",
"yew",
"yew-router",
]
[[package]]
@ -969,12 +1015,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
[[package]]
name = "poly1305"
version = "0.7.2"
@ -1204,15 +1244,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "sct"
version = "0.6.1"
@ -1289,6 +1320,19 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
dependencies = [
"block-buffer",
"cfg-if",
"cpufeatures",
"digest",
"opaque-debug",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
@ -1304,12 +1348,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "signature"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335"
[[package]]
name = "slab"
version = "0.4.5"
@ -1332,24 +1370,18 @@ dependencies = [
"winapi",
]
[[package]]
name = "sodiumoxide"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028"
dependencies = [
"ed25519",
"libc",
"libsodium-sys",
"serde",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.10.0"
@ -1716,17 +1748,6 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.0"
@ -1750,6 +1771,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if",
"serde",
"serde_json",
"wasm-bindgen-macro",
]
@ -1925,6 +1948,48 @@ dependencies = [
"syn",
]
[[package]]
name = "yew-router"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27666236d9597eac9be560e841e415e20ba67020bc8cd081076be178e159c8bc"
dependencies = [
"cfg-if",
"cfg-match",
"gloo",
"js-sys",
"log",
"nom 5.1.2",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
"yew",
"yew-router-macro",
"yew-router-route-parser",
]
[[package]]
name = "yew-router-macro"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c0ace2924b7a175e2d1c0e62ee7022a5ad840040dcd52414ce5f410ab322dba"
dependencies = [
"proc-macro2",
"quote",
"syn",
"yew-router-route-parser",
]
[[package]]
name = "yew-router-route-parser"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de4a67208fb46b900af18a7397938b01f379dfc18da34799cfa8347eec715697"
dependencies = [
"nom 5.1.2",
]
[[package]]
name = "zeroize"
version = "1.4.2"

View File

@ -12,6 +12,4 @@ anyhow = "1"
atty = "0.2"
clap = "3.0.0-beta.4"
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] }
secrecy = { version = "0.8", features = ["serde"] }
sodiumoxide = "0.2"
url = "2"
secrecy = { version = "0.8", features = ["serde"] }

View File

@ -1,17 +1,16 @@
#![warn(clippy::nursery, clippy::pedantic)]
#![deny(unsafe_code)]
use std::io::{Read, Write};
use std::str::FromStr;
use anyhow::{anyhow, bail, Context, Result};
use atty::Stream;
use clap::Clap;
use omegaupload_common::crypto::{gen_key_nonce, open, seal, Key};
use omegaupload_common::{base64, hash, ParsedUrl, Url};
use reqwest::blocking::Client;
use reqwest::StatusCode;
use secrecy::{ExposeSecret, SecretString};
use sodiumoxide::base64;
use sodiumoxide::base64::Variant::UrlSafe;
use sodiumoxide::crypto::hash::sha256;
use sodiumoxide::crypto::secretbox::{gen_key, gen_nonce, open, seal, Key, Nonce, KEYBYTES};
use url::Url;
#[derive(Clap)]
struct Opts {
@ -32,7 +31,6 @@ enum Action {
}
fn main() -> Result<()> {
sodiumoxide::init().map_err(|_| anyhow!("Failed to init sodiumoxide"))?;
let opts = Opts::parse();
match opts.action {
@ -51,24 +49,24 @@ fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> {
}
let (data, nonce, key, pw_used) = {
let enc_key = gen_key();
let nonce = gen_nonce();
let (enc_key, nonce) = gen_key_nonce();
let mut container = Vec::new();
std::io::stdin().read_to_end(&mut container)?;
let mut enc = seal(&container, &nonce, &enc_key);
let mut enc =
seal(&container, &nonce, &enc_key).map_err(|_| anyhow!("Failed to encrypt data"))?;
let pw_used = if let Some(password) = password {
assert_eq!(sha256::DIGESTBYTES, KEYBYTES);
let pw_hash = sha256::hash(password.expose_secret().as_bytes());
let pw_key = Key::from_slice(pw_hash.as_ref()).expect("to succeed");
enc = seal(&enc, &nonce.increment_le(), &pw_key);
let pw_hash = hash(password.expose_secret().as_bytes());
let pw_key = Key::from_slice(pw_hash.as_ref());
enc = seal(&enc, &nonce.increment(), pw_key)
.map_err(|_| anyhow!("Failed to encrypt data"))?;
true
} else {
false
};
let key = base64::encode(&enc_key, UrlSafe);
let nonce = base64::encode(&nonce, UrlSafe);
let key = base64::encode(&enc_key);
let nonce = base64::encode(&nonce);
(enc, nonce, key, pw_used)
};
@ -122,11 +120,10 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
std::io::stdin().read_line(&mut input)?;
input.pop(); // last character is \n, we need to drop it.
assert_eq!(sha256::DIGESTBYTES, KEYBYTES);
let pw_hash = sha256::hash(input.as_bytes());
let pw_key = Key::from_slice(pw_hash.as_ref()).expect("to succeed");
let pw_hash = hash(input.as_bytes());
let pw_key = Key::from_slice(pw_hash.as_ref());
data = open(&data, &url.nonce.increment_le(), &pw_key)
data = open(&data, &url.nonce.increment(), pw_key)
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect password?"))?;
}
@ -145,69 +142,3 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
Ok(())
}
struct ParsedUrl {
sanitized_url: Url,
decryption_key: Key,
nonce: Nonce,
needs_password: bool,
}
impl FromStr for ParsedUrl {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut url = Url::from_str(s)?;
let fragment = url
.fragment()
.context("Missing fragment. The decryption key is part of the fragment.")?;
if fragment.is_empty() {
bail!("Empty fragment. The decryption key is part of the fragment.");
}
let args = fragment.split('!').filter_map(|kv| {
let (k, v) = {
let mut iter = kv.split(':');
(iter.next(), iter.next())
};
Some((k?, v))
});
let mut decryption_key = None;
let mut needs_password = false;
let mut nonce = None;
for (key, value) in args {
match (key, value) {
("key", Some(value)) => {
let key = base64::decode(value, UrlSafe)
.map_err(|_| anyhow!("Failed to decode key"))?;
let key = Key::from_slice(&key).context("Failed to parse key")?;
decryption_key = Some(key);
}
("pw", _) => {
needs_password = true;
}
("nonce", Some(value)) => {
nonce = Some(
Nonce::from_slice(
&base64::decode(value, UrlSafe)
.map_err(|_| anyhow!("Failed to decode nonce"))?,
)
.context("Invalid nonce provided")?,
);
}
_ => (),
}
}
url.set_fragment(None);
Ok(Self {
sanitized_url: url,
decryption_key: decryption_key.context("Missing decryption key")?,
needs_password,
nonce: nonce.context("Missing nonce")?,
})
}
}

View File

@ -6,3 +6,10 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
base64 = "0.13"
chacha20poly1305 = "0.9"
rand = "0.8"
sha2 = "0.9"
thiserror = "1"
url = "2"

View File

@ -1,7 +1,205 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
#![warn(clippy::nursery, clippy::pedantic)]
#![deny(unsafe_code)]
//! Contains common functions and structures used by multiple projects
use std::str::FromStr;
use sha2::{Digest, Sha256};
use thiserror::Error;
pub use url::Url;
use crate::crypto::{Key, Nonce};
pub mod base64 {
/// URL-safe Base64 encoding.
pub fn encode(input: impl AsRef<[u8]>) -> String {
base64::encode_config(input, base64::URL_SAFE)
}
/// URL-safe Base64 decoding.
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, base64::DecodeError> {
base64::decode_config(input, base64::URL_SAFE)
}
}
/// Hashes an input to output a usable key.
pub fn hash(data: impl AsRef<[u8]>) -> crypto::Key {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize()
}
pub mod crypto {
use std::ops::{Deref, DerefMut};
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::aead::{Aead, Error, NewAead};
use chacha20poly1305::XChaCha20Poly1305;
use chacha20poly1305::XNonce;
use rand::{thread_rng, Rng};
pub use chacha20poly1305::Key;
/// Securely generates a random key and nonce.
#[must_use]
pub fn gen_key_nonce() -> (Key, Nonce) {
let mut rng = thread_rng();
let mut key: Key = GenericArray::default();
rng.fill(key.as_mut_slice());
let mut nonce = Nonce::default();
rng.fill(nonce.as_mut_slice());
(key, nonce)
}
pub fn seal(plaintext: &[u8], nonce: &Nonce, key: &Key) -> Result<Vec<u8>, Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.encrypt(nonce, plaintext)
}
pub fn open(encrypted: &[u8], nonce: &Nonce, key: &Key) -> Result<Vec<u8>, Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.decrypt(nonce, encrypted)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Nonce(XNonce);
impl Default for Nonce {
fn default() -> Self {
Self(GenericArray::default())
}
}
impl Deref for Nonce {
type Target = XNonce;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Nonce {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<[u8]> for Nonce {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl Nonce {
#[must_use]
pub fn increment(&self) -> Self {
todo!()
}
#[must_use]
pub fn from_slice(slice: &[u8]) -> Self {
Self(*XNonce::from_slice(slice))
}
}
}
pub struct ParsedUrl {
pub sanitized_url: Url,
pub decryption_key: Key,
pub nonce: Nonce,
pub needs_password: bool,
}
#[derive(Default)]
pub struct PartialParsedUrl {
pub decryption_key: Option<Key>,
pub nonce: Option<Nonce>,
pub needs_password: bool,
}
impl From<&str> for PartialParsedUrl {
fn from(fragment: &str) -> Self {
let args = fragment.split('!').filter_map(|kv| {
let (k, v) = {
let mut iter = kv.split(':');
(iter.next(), iter.next())
};
Some((k?, v))
});
let mut decryption_key = None;
let mut needs_password = false;
let mut nonce = None;
for (key, value) in args {
match (key, value) {
("key", Some(value)) => {
decryption_key = base64::decode(value)
.map(|k| Key::from_slice(&k).clone())
.ok();
}
("pw", _) => {
needs_password = true;
}
("nonce", Some(value)) => {
nonce = base64::decode(value).as_deref().map(Nonce::from_slice).ok();
}
_ => (),
}
}
Self {
decryption_key,
nonce,
needs_password,
}
}
}
#[derive(Debug, Error)]
pub enum ParseUrlError {
#[error("The provided url was bad")]
BadUrl,
#[error("Missing decryption key")]
NeedKey,
#[error("Missing nonce")]
NeedNonce,
#[error("Missing decryption key and nonce")]
NeedKeyAndNonce,
}
impl FromStr for ParsedUrl {
type Err = ParseUrlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut url = Url::from_str(s).map_err(|_| ParseUrlError::BadUrl)?;
let fragment = url.fragment().ok_or(ParseUrlError::NeedKeyAndNonce)?;
if fragment.is_empty() {
return Err(ParseUrlError::NeedKeyAndNonce);
}
let PartialParsedUrl {
decryption_key,
needs_password,
nonce,
} = PartialParsedUrl::from(fragment);
url.set_fragment(None);
let (decryption_key, nonce) = match (&decryption_key, nonce) {
(None, None) => Err(ParseUrlError::NeedKeyAndNonce),
(None, Some(_)) => Err(ParseUrlError::NeedKey),
(Some(_), None) => Err(ParseUrlError::NeedNonce),
(Some(k), Some(v)) => Ok((*k, v)),
}?;
Ok(Self {
sanitized_url: url,
decryption_key,
needs_password,
nonce,
})
}
}

View File

@ -11,7 +11,8 @@ axum = { version = "0.2", features = ["http2", "headers"] }
bincode = "1"
# We don't care about which version (We want to match with axum), we just need
# to enable the feature
bytes = { version = "*", features= ["serde"] }
bytes = { version = "*", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
# We just need to pull in whatever axum is pulling in
headers = "*"
lazy_static = "1"

View File

@ -1,33 +1,31 @@
#![warn(clippy::nursery, clippy::pedantic)]
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use axum::http::StatusCode;
use paste::Expiration;
use rand::prelude::StdRng;
use rand::{Rng, SeedableRng};
use rocksdb::IteratorMode;
use rocksdb::WriteBatch;
use rocksdb::{Options, DB};
use short_code::ShortCode;
use anyhow::Result;
use axum::body::Bytes;
use axum::extract::{Extension, Path, TypedHeader};
use axum::handler::{get, post};
use axum::http::header::EXPIRES;
use axum::http::StatusCode;
use axum::{AddExtensionLayer, Router};
use chrono::Duration;
use headers::HeaderMap;
use rand::thread_rng;
use rand::Rng;
use rocksdb::IteratorMode;
use rocksdb::WriteBatch;
use rocksdb::{Options, DB};
use tokio::task;
use tracing::warn;
use tracing::{error, instrument};
use crate::paste::Paste;
use crate::time::FIVE_MINUTES;
use crate::paste::{Expiration, Paste};
use crate::short_code::ShortCode;
mod paste;
mod short_code;
mod time;
#[tokio::main]
async fn main() -> Result<()> {
@ -41,7 +39,7 @@ async fn main() -> Result<()> {
let stop_signal = Arc::new(AtomicBool::new(false));
task::spawn(cleanup(Arc::clone(&stop_signal), Arc::clone(&db)));
axum::Server::bind(&"0.0.0.0:8080".parse()?)
axum::Server::bind(&"0.0.0.0:8081".parse()?)
.serve(
Router::new()
.route("/", post(upload::<SHORT_CODE_SIZE>))
@ -50,7 +48,6 @@ async fn main() -> Result<()> {
get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
)
.layer(AddExtensionLayer::new(db))
.layer(AddExtensionLayer::new(StdRng::from_entropy()))
.into_make_service(),
)
.await?;
@ -64,7 +61,6 @@ async fn main() -> Result<()> {
#[instrument(skip(db), err)]
async fn upload<const N: usize>(
Extension(db): Extension<Arc<DB>>,
Extension(mut rng): Extension<StdRng>,
maybe_expires: Option<TypedHeader<Expiration>>,
body: Bytes,
) -> Result<Vec<u8>, StatusCode> {
@ -83,7 +79,7 @@ async fn upload<const N: usize>(
// Try finding a code; give up after 1000 attempts
// Statistics show that this is very unlikely to happen
for _ in 0..1000 {
let code: ShortCode<N> = rng.sample(short_code::Generator);
let code: ShortCode<N> = thread_rng().sample(short_code::Generator);
let db = Arc::clone(&db);
let key = code.as_bytes();
let query = task::spawn_blocking(move || db.key_may_exist(key)).await;
@ -121,7 +117,7 @@ async fn upload<const N: usize>(
async fn paste<const N: usize>(
Extension(db): Extension<Arc<DB>>,
Path(url): Path<ShortCode<N>>,
) -> Result<Bytes, StatusCode> {
) -> Result<(HeaderMap, Bytes), StatusCode> {
let key = url.as_bytes();
let parsed: Paste = {
@ -172,7 +168,11 @@ async fn paste<const N: usize>(
})?;
}
Ok(parsed.bytes)
let mut map = HeaderMap::new();
if let Some(expiration) = parsed.expiration {
map.insert(EXPIRES, expiration.into());
}
Ok((map, parsed.bytes))
}
#[instrument(skip(db))]
@ -189,7 +189,7 @@ async fn delete<const N: usize>(
/// Periodic clean-up task that deletes expired entries.
async fn cleanup(stop_signal: Arc<AtomicBool>, db: Arc<DB>) {
while !stop_signal.load(Ordering::Acquire) {
tokio::time::sleep(*FIVE_MINUTES).await;
tokio::time::sleep(Duration::minutes(5).to_std().expect("infallible")).await;
let mut batch = WriteBatch::default();
for (key, value) in db.snapshot().iterator(IteratorMode::Start) {
// TODO: only partially decode struct for max perf

View File

@ -1,15 +1,12 @@
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use axum::body::Bytes;
use chrono::{DateTime, Duration, Utc};
use headers::{Header, HeaderName, HeaderValue};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use crate::time::{FIVE_MINUTES, ONE_DAY, ONE_HOUR, TEN_MINUTES};
#[derive(Serialize, Deserialize)]
pub struct Paste {
expiration: Option<Expiration>,
pub expiration: Option<Expiration>,
pub bytes: Bytes,
}
@ -25,10 +22,7 @@ impl Paste {
self.expiration
.map(|expires| match expires {
Expiration::BurnAfterReading => false,
Expiration::UnixTime(expiration) => {
let now = time_since_unix();
expiration < now
}
Expiration::UnixTime(expiration) => expiration < Utc::now(),
})
.unwrap_or_default()
}
@ -41,7 +35,7 @@ impl Paste {
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub enum Expiration {
BurnAfterReading,
UnixTime(Duration),
UnixTime(DateTime<Utc>),
}
lazy_static! {
@ -58,34 +52,44 @@ impl Header for Expiration {
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
let now = time_since_unix();
match values
.next()
.ok_or_else(headers::Error::invalid)?
.as_bytes()
{
b"read" => Ok(Self::BurnAfterReading),
b"5m" => Ok(Self::UnixTime(now + *FIVE_MINUTES)),
b"10m" => Ok(Self::UnixTime(now + *TEN_MINUTES)),
b"1h" => Ok(Self::UnixTime(now + *ONE_HOUR)),
b"1d" => Ok(Self::UnixTime(now + *ONE_DAY)),
b"5m" => Ok(Self::UnixTime(Utc::now() + Duration::minutes(5))),
b"10m" => Ok(Self::UnixTime(Utc::now() + Duration::minutes(10))),
b"1h" => Ok(Self::UnixTime(Utc::now() + Duration::hours(1))),
b"1d" => Ok(Self::UnixTime(Utc::now() + Duration::days(1))),
_ => Err(headers::Error::invalid()),
}
}
fn encode<E: Extend<HeaderValue>>(&self, _: &mut E) {
unimplemented!("This shouldn't need implementation")
fn encode<E: Extend<HeaderValue>>(&self, container: &mut E) {
container.extend(std::iter::once(self.into()));
}
}
impl From<&Expiration> for HeaderValue {
fn from(expiration: &Expiration) -> Self {
unsafe {
HeaderValue::from_maybe_shared_unchecked(match expiration {
Expiration::BurnAfterReading => Bytes::from_static(b"0"),
Expiration::UnixTime(duration) => Bytes::from(duration.to_rfc3339()),
})
}
}
}
impl From<Expiration> for HeaderValue {
fn from(expiration: Expiration) -> Self {
(&expiration).into()
}
}
impl Default for Expiration {
fn default() -> Self {
Self::UnixTime(time_since_unix() + *ONE_DAY)
Self::UnixTime(Utc::now() + Duration::days(1))
}
}
fn time_since_unix() -> Duration {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time since epoch to always work")
}

View File

@ -1,10 +0,0 @@
use std::time::Duration;
use lazy_static::lazy_static;
lazy_static! {
pub static ref FIVE_MINUTES: Duration = Duration::from_secs(5 * 60);
pub static ref TEN_MINUTES: Duration = Duration::from_secs(5 * 60);
pub static ref ONE_HOUR: Duration = Duration::from_secs(5 * 60);
pub static ref ONE_DAY: Duration = Duration::from_secs(5 * 60);
}

View File

@ -11,6 +11,8 @@ omegaupload-common = { path = "../common" }
getrandom = { version = "*", features = ["js"] }
anyhow = "1"
downcast-rs = "1"
gloo-console = "0.1"
http = "0.2"
web-sys = { version = "0.3", features = ["Request", "Window"] }
yew = { version = "0.18", features = ["wasm-bindgen-futures"] }

8
web/dist/index.html vendored
View File

@ -1,11 +1,13 @@
<!DOCTYPE html><html><head>
<meta charset="utf-8">
<title>Omegaupload</title>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js"></script>
<link rel="preload" href="/index-8214e6336313b7fe_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="modulepreload" href="/index-8214e6336313b7fe.js"></head>
<link rel="preload" href="/index-2ccb59df4818eaff_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="modulepreload" href="/index-2ccb59df4818eaff.js"></head>
<body><script type="module">import init from '/index-8214e6336313b7fe.js';init('/index-8214e6336313b7fe_bg.wasm');</script><script>(function () {
<body><script type="module">import init from '/index-2ccb59df4818eaff.js';init('/index-2ccb59df4818eaff_bg.wasm');</script><script>(function () {
var url = 'ws://' + window.location.host + '/_trunk/ws';
var poll_interval = 5000;
var reload_upon_connect = () => {

View File

@ -4,6 +4,8 @@
<head>
<meta charset="utf-8" />
<title>Omegaupload</title>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js"></script>
</head>
</html>

View File

@ -1,13 +1,19 @@
use std::convert::TryFrom;
use std::fmt::Debug;
use std::rc::Rc;
use std::str::FromStr;
use anyhow::anyhow;
use anyhow::{anyhow, bail};
use downcast_rs::{impl_downcast, Downcast};
use gloo_console::log;
use http::uri::{Authority, PathAndQuery};
use omegaupload_common::crypto::{Key, Nonce};
use omegaupload_common::ParsedUrl;
use omegaupload_common::crypto::{open, Key, Nonce};
use omegaupload_common::{ParsedUrl, PartialParsedUrl};
use yew::format::{Binary, Nothing};
use yew::services::fetch::{FetchTask, Request, Response, StatusCode, Uri};
use yew::services::{ConsoleService, FetchService};
use yew::services::FetchService;
use yew::utils::window;
use yew::Properties;
use yew::{html, Component, ComponentLink, Html, ShouldRender};
use yew_router::router::Router;
use yew_router::Switch;
@ -64,87 +70,13 @@ fn render_route(route: Route) -> Html {
}
struct Paste {
state: PasteState,
state: Box<dyn PasteState>,
// Need to keep this alive so that the fetch request doesn't get dropped
_fetch_handle: FetchTask,
}
#[derive(Clone, PartialEq, Eq)]
enum PasteState {
NotFound,
Error,
NeedInformation {
data: Option<Vec<u8>>,
key: Option<Key>,
nonce: Option<Nonce>,
needs_pw: bool,
},
Done {
data: Vec<u8>,
key: Key,
nonce: Nonce,
password: Option<Key>,
},
}
impl PasteState {
fn set_data(&mut self, new_data: Vec<u8>) {
match self {
PasteState::NeedInformation { data, .. } => {
assert!(data.is_none());
*data = Some(new_data);
}
_ => panic!("Tried to set data in invalid state"),
}
}
fn set_key(&mut self, new_key: Key) {
match self {
PasteState::NeedInformation { key, .. } => {
assert!(key.is_none());
*key = Some(new_key);
}
_ => panic!("Tried to set key in invalid state"),
}
}
fn set_nonce(&mut self, new_nonce: Nonce) {
match self {
PasteState::NeedInformation { nonce, .. } => {
assert!(nonce.is_none());
*nonce = Some(new_nonce);
}
_ => panic!("Tried to set key in invalid state"),
}
}
fn is_completed(&self) -> bool {
match self {
PasteState::NeedInformation {
data,
key,
nonce,
needs_pw,
} => todo!(),
_ => panic!(),
}
assert!(matches!(self, PasteState::NeedInformation { .. }));
true
}
}
enum PasteMessage {
Data(Vec<u8>),
Error(anyhow::Error),
DecryptionKey(Key),
Nonce(Nonce),
Password(Key),
NotFound,
_fetch_handle: Option<FetchTask>,
}
impl Component for Paste {
type Message = PasteMessage;
type Message = Box<dyn PasteState>;
type Properties = ();
@ -152,46 +84,53 @@ impl Component for Paste {
let url = String::from(window().location().to_string());
let request_uri = {
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
uri_parts
.authority
.as_mut()
.map(|auth| *auth = Authority::from_str(auth.host()).unwrap());
uri_parts.path_and_query.as_mut().map(|parts| {
*parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap()
});
Uri::from_parts(uri_parts).unwrap()
};
ConsoleService::log(&request_uri.to_string());
let link_clone = link.clone();
let fetch = FetchService::fetch_binary(
Request::get(request_uri).body(Nothing).unwrap(),
link.callback(move |resp: Response<Binary>| match resp.status() {
StatusCode::OK => PasteMessage::Data(resp.into_body().unwrap()),
StatusCode::NOT_FOUND => PasteMessage::NotFound,
code => PasteMessage::Error(anyhow!("Got resp error: {}", code)),
Request::get(&request_uri).body(Nothing).unwrap(),
link.callback_once(move |resp: Response<Binary>| match resp.status() {
StatusCode::OK => {
let partial = PastePartial::new(
resp,
url.split_once('#')
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
.unwrap_or_default(),
link_clone,
);
if let Ok(completed) = PasteComplete::try_from(partial.clone()) {
Box::new(completed) as Box<dyn PasteState>
} else {
Box::new(partial) as Box<dyn PasteState>
}
}
StatusCode::NOT_FOUND => Box::new(PasteNotFound) as Box<dyn PasteState>,
code => {
Box::new(PasteError(anyhow!("Got resp error: {}", code))) as Box<dyn PasteState>
}
}),
);
Self {
state: PasteState::NeedInformation {
data: None,
key: None,
nonce: None,
needs_pw: false,
match fetch {
Ok(task) => Self {
state: Box::new(PasteLoading),
_fetch_handle: Some(task),
},
Err(e) => Self {
state: Box::new(PasteError(e)) as Box<dyn PasteState>,
_fetch_handle: None,
},
_fetch_handle: fetch.unwrap(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
PasteMessage::Data(data) => self.state.set_data(data),
PasteMessage::Error(e) => self.state = PasteState::Error,
PasteMessage::NotFound => self.state = PasteState::NotFound,
PasteMessage::DecryptionKey(key) => self.state.set_key(key),
PasteMessage::Nonce(nonce) => self.state.set_nonce(nonce),
PasteMessage::Password(_) => todo!(),
}
self.state = msg;
true
}
@ -200,21 +139,207 @@ impl Component for Paste {
}
fn view(&self) -> Html {
match self.state {
PasteState::NeedInformation { .. } => todo!(),
PasteState::Done { .. } => {
todo!()
}
PasteState::Error => html! {
<main>
{"An error occurred. Please try again later."}
</main>
},
PasteState::NotFound => html! {
<main>
{"The paste you are looking for is not here."}
</main>
},
if self.state.is::<PasteLoading>() {
return html! {
<p>{ "loading" }</p>
};
}
if self.state.is::<PasteNotFound>() {
return html! {
<p>{ "Either the paste has been burned or one never existed." }</p>
};
}
if let Some(error) = self.state.downcast_ref::<PasteError>() {
return html! {
<p>{ error.0.to_string() }</p>
};
}
if let Some(partial_paste) = self.state.downcast_ref::<PastePartial>() {
return partial_paste.view();
}
if let Some(paste) = self.state.downcast_ref::<PasteComplete>() {
return paste.view();
}
html! {
"An internal error occurred: client is in unknown state!"
}
}
}
struct PasteLoading;
struct PasteNotFound;
struct PasteError(anyhow::Error);
#[derive(Properties, Clone, Debug)]
struct PastePartial {
parent: ComponentLink<Paste>,
data: Option<Rc<Vec<u8>>>,
key: Option<Key>,
nonce: Option<Nonce>,
password: Option<Key>,
needs_pw: bool,
}
#[derive(Properties, Clone)]
struct PasteComplete {
data: Rc<Vec<u8>>,
key: Key,
nonce: Nonce,
password: Option<Key>,
}
trait PasteState: Downcast {}
impl_downcast!(PasteState);
impl PasteState for PasteLoading {}
impl PasteState for PasteNotFound {}
impl PasteState for PasteError {}
impl PasteState for PastePartial {}
impl PasteState for PasteComplete {}
impl PastePartial {
fn new(
resp: Response<Binary>,
partial_parsed_url: PartialParsedUrl,
parent: ComponentLink<Paste>,
) -> Self {
Self {
parent,
data: Some(Rc::new(resp.into_body().unwrap())),
key: partial_parsed_url.decryption_key,
nonce: partial_parsed_url.nonce,
password: None,
needs_pw: partial_parsed_url.needs_password,
}
}
}
enum PartialPasteMessage {
DecryptionKey(Key),
Nonce(Nonce),
Password(Key),
}
impl Component for PastePartial {
type Message = PartialPasteMessage;
type Properties = Self;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
props
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
PartialPasteMessage::DecryptionKey(key) => self.key = Some(key),
PartialPasteMessage::Nonce(nonce) => self.nonce = Some(nonce),
PartialPasteMessage::Password(password) => self.password = Some(password),
}
match (self.data.clone(), self.key, self.nonce, self.password) {
(Some(data), Some(key), Some(nonce), Some(password)) if self.needs_pw => {
self.parent.callback(move |Nothing| {
Box::new(PasteComplete::new(
Rc::clone(&data),
key,
nonce,
Some(password),
)) as Box<dyn PasteState>
});
}
(Some(data), Some(key), Some(nonce), None) if !self.needs_pw => {
self.parent.callback(move |Nothing| {
Box::new(PasteComplete::new(Rc::clone(&data), key, nonce, None))
as Box<dyn PasteState>
});
}
_ => (),
}
// parent should re-render so this element should be dropped.
false
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
"got partial data"
}
}
}
impl TryFrom<PastePartial> for PasteComplete {
type Error = anyhow::Error;
fn try_from(partial: PastePartial) -> Result<Self, Self::Error> {
match partial {
PastePartial {
data: Some(data),
key: Some(key),
nonce: Some(nonce),
password: Some(password),
needs_pw: true,
..
} => Ok(PasteComplete {
data,
key,
nonce,
password: Some(password),
}),
PastePartial {
data: Some(data),
key: Some(key),
nonce: Some(nonce),
needs_pw: false,
..
} => Ok(PasteComplete {
data,
key,
nonce,
password: None,
}),
_ => bail!("missing field"),
}
}
}
impl PasteComplete {
fn new(data: Rc<Vec<u8>>, key: Key, nonce: Nonce, password: Option<Key>) -> Self {
Self {
data,
key,
nonce,
password,
}
}
fn view(&self) -> Html {
let stage_one = if let Some(password) = self.password {
open(&self.data, &self.nonce.increment(), &password).unwrap()
} else {
self.data.to_vec()
};
let decrypted = open(&stage_one, &self.nonce, &self.key).unwrap();
if let Ok(str) = String::from_utf8(decrypted) {
html! {
<>
<pre><code>{str}</code></pre>
<script>{ "hljs.highlightAll();" }</script>
</>
}
} else {
html! { "binary" }
}
}
}