Compare commits
No commits in common. "856ee63b6defbfa67549d9a5a7634ea0b76435e5" and "17dd44c8ccdc93ad0682ab09e01026364e069f93" have entirely different histories.
856ee63b6d
...
17dd44c8cc
17 changed files with 431 additions and 837 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
||||||
/target
|
/target
|
||||||
**/database
|
**/database
|
||||||
/web/dist/
|
/web/dist/
|
||||||
**/node_modules
|
|
8
.swcrc
8
.swcrc
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"jsc": {
|
|
||||||
"parser": {
|
|
||||||
"syntax": "typescript"
|
|
||||||
},
|
|
||||||
"target": "es2021"
|
|
||||||
}
|
|
||||||
}
|
|
73
Cargo.lock
generated
73
Cargo.lock
generated
|
@ -188,15 +188,6 @@ version = "3.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
|
checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byte-unit"
|
|
||||||
version = "4.0.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "956ffc5b0ec7d7a6949e3f21fd63ba5af4cffdc2ba1e0b7bf62b481458c4ae7f"
|
|
||||||
dependencies = [
|
|
||||||
"utf8-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.7.2"
|
version = "1.7.2"
|
||||||
|
@ -435,6 +426,12 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downcast-rs"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
@ -472,21 +469,6 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures"
|
|
||||||
version = "0.3.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-executor",
|
|
||||||
"futures-io",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
@ -494,7 +476,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
|
checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -503,17 +484,6 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
|
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-executor"
|
|
||||||
version = "0.3.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
@ -552,11 +522,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
|
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
@ -1129,6 +1097,7 @@ dependencies = [
|
||||||
name = "omegaupload-common"
|
name = "omegaupload-common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
|
@ -1165,15 +1134,16 @@ dependencies = [
|
||||||
name = "omegaupload-web"
|
name = "omegaupload-web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byte-unit",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"downcast-rs",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"gloo-console",
|
"gloo-console",
|
||||||
"http",
|
"http",
|
||||||
"image",
|
"image",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"omegaupload-common",
|
"omegaupload-common",
|
||||||
"reqwasm",
|
"reqwest",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
@ -1424,23 +1394,6 @@ version = "0.6.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "reqwasm"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e34bf31941fb867ae9386a4b443b388e6713574944e6517136ee21a6a93cf996"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"futures",
|
|
||||||
"js-sys",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.11.5"
|
version = "0.11.5"
|
||||||
|
@ -2042,12 +1995,6 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8-width"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vec_map"
|
name = "vec_map"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
13
Trunk.toml
13
Trunk.toml
|
@ -1,13 +0,0 @@
|
||||||
|
|
||||||
[build]
|
|
||||||
target = "web/index.html"
|
|
||||||
release = true
|
|
||||||
|
|
||||||
[[proxy]]
|
|
||||||
backend = "http://localhost:8081"
|
|
||||||
rewrite = "/api/"
|
|
||||||
|
|
||||||
[[hooks]]
|
|
||||||
stage="post_build"
|
|
||||||
command="npx"
|
|
||||||
command_arguments=["swc", "$TRUNK_SOURCE_DIR/src/main.ts", "-o", "$TRUNK_STAGING_DIR/main.js"]
|
|
|
@ -6,7 +6,7 @@ use std::io::{Read, Write};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use atty::Stream;
|
use atty::Stream;
|
||||||
use clap::Clap;
|
use clap::Clap;
|
||||||
use omegaupload_common::crypto::{gen_key_nonce, open_in_place, seal_in_place, Key};
|
use omegaupload_common::crypto::{gen_key_nonce, open, seal, Key};
|
||||||
use omegaupload_common::{base64, hash, Expiration, ParsedUrl, Url};
|
use omegaupload_common::{base64, hash, Expiration, ParsedUrl, Url};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use reqwest::header::EXPIRES;
|
use reqwest::header::EXPIRES;
|
||||||
|
@ -53,13 +53,13 @@ fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> {
|
||||||
let (enc_key, nonce) = gen_key_nonce();
|
let (enc_key, nonce) = gen_key_nonce();
|
||||||
let mut container = Vec::new();
|
let mut container = Vec::new();
|
||||||
std::io::stdin().read_to_end(&mut container)?;
|
std::io::stdin().read_to_end(&mut container)?;
|
||||||
seal_in_place(&mut container, &nonce, &enc_key)
|
let mut enc =
|
||||||
.map_err(|_| anyhow!("Failed to encrypt data"))?;
|
seal(&container, &nonce, &enc_key).map_err(|_| anyhow!("Failed to encrypt data"))?;
|
||||||
|
|
||||||
let pw_used = if let Some(password) = password {
|
let pw_used = if let Some(password) = password {
|
||||||
let pw_hash = hash(password.expose_secret().as_bytes());
|
let pw_hash = hash(password.expose_secret().as_bytes());
|
||||||
let pw_key = Key::from_slice(pw_hash.as_ref());
|
let pw_key = Key::from_slice(pw_hash.as_ref());
|
||||||
seal_in_place(&mut container, &nonce.increment(), pw_key)
|
enc = seal(&enc, &nonce.increment(), pw_key)
|
||||||
.map_err(|_| anyhow!("Failed to encrypt data"))?;
|
.map_err(|_| anyhow!("Failed to encrypt data"))?;
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
@ -69,7 +69,7 @@ fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> {
|
||||||
let key = base64::encode(&enc_key);
|
let key = base64::encode(&enc_key);
|
||||||
let nonce = base64::encode(&nonce);
|
let nonce = base64::encode(&nonce);
|
||||||
|
|
||||||
(container, nonce, key, pw_used)
|
(enc, nonce, key, pw_used)
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = Client::new()
|
let res = Client::new()
|
||||||
|
@ -131,11 +131,11 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
|
||||||
let pw_hash = hash(input.as_bytes());
|
let pw_hash = hash(input.as_bytes());
|
||||||
let pw_key = Key::from_slice(pw_hash.as_ref());
|
let pw_key = Key::from_slice(pw_hash.as_ref());
|
||||||
|
|
||||||
open_in_place(&mut data, &url.nonce.increment(), pw_key)
|
data = open(&data, &url.nonce.increment(), pw_key)
|
||||||
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect password?"))?;
|
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect password?"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
open_in_place(&mut data, &url.nonce, &url.decryption_key)
|
data = open(&data, &url.nonce, &url.decryption_key)
|
||||||
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect decryption key?"))?;
|
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect decryption key?"))?;
|
||||||
|
|
||||||
if atty::is(Stream::Stdout) {
|
if atty::is(Stream::Stdout) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bytes = { version = "*", features = ["serde"] }
|
bytes = { version = "*", features = ["serde"] }
|
||||||
chacha20poly1305 = "0.9"
|
chacha20poly1305 = "0.9"
|
||||||
|
|
|
@ -39,7 +39,7 @@ pub mod crypto {
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use chacha20poly1305::aead::generic_array::GenericArray;
|
use chacha20poly1305::aead::generic_array::GenericArray;
|
||||||
use chacha20poly1305::aead::{Aead, AeadInPlace, Buffer, Error, NewAead};
|
use chacha20poly1305::aead::{Aead, Error, NewAead};
|
||||||
use chacha20poly1305::XChaCha20Poly1305;
|
use chacha20poly1305::XChaCha20Poly1305;
|
||||||
use chacha20poly1305::XNonce;
|
use chacha20poly1305::XNonce;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
|
@ -62,21 +62,11 @@ pub mod crypto {
|
||||||
cipher.encrypt(nonce, plaintext)
|
cipher.encrypt(nonce, plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seal_in_place(buffer: &mut impl Buffer, nonce: &Nonce, key: &Key) -> Result<(), Error> {
|
|
||||||
let cipher = XChaCha20Poly1305::new(key);
|
|
||||||
cipher.encrypt_in_place(nonce, &[], buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open(encrypted: &[u8], nonce: &Nonce, key: &Key) -> Result<Vec<u8>, Error> {
|
pub fn open(encrypted: &[u8], nonce: &Nonce, key: &Key) -> Result<Vec<u8>, Error> {
|
||||||
let cipher = XChaCha20Poly1305::new(key);
|
let cipher = XChaCha20Poly1305::new(key);
|
||||||
cipher.decrypt(nonce, encrypted)
|
cipher.decrypt(nonce, encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_in_place(buffer: &mut impl Buffer, nonce: &Nonce, key: &Key) -> Result<(), Error> {
|
|
||||||
let cipher = XChaCha20Poly1305::new(key);
|
|
||||||
cipher.decrypt_in_place(nonce, &[], buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub struct Nonce(XNonce);
|
pub struct Nonce(XNonce);
|
||||||
|
|
||||||
|
@ -153,13 +143,13 @@ impl From<&str> for PartialParsedUrl {
|
||||||
for (key, value) in args {
|
for (key, value) in args {
|
||||||
match (key, value) {
|
match (key, value) {
|
||||||
("key", Some(value)) => {
|
("key", Some(value)) => {
|
||||||
decryption_key = dbg!(base64::decode(value).map(|k| *Key::from_slice(&k)).ok());
|
decryption_key = base64::decode(value).map(|k| *Key::from_slice(&k)).ok();
|
||||||
}
|
}
|
||||||
("pw", _) => {
|
("pw", _) => {
|
||||||
needs_password = true;
|
needs_password = true;
|
||||||
}
|
}
|
||||||
("nonce", Some(value)) => {
|
("nonce", Some(value)) => {
|
||||||
nonce = dbg!(base64::decode(value).as_deref().map(Nonce::from_slice).ok());
|
nonce = base64::decode(value).as_deref().map(Nonce::from_slice).ok();
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
@ -229,12 +219,12 @@ impl Display for Expiration {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Expiration::BurnAfterReading => {
|
Expiration::BurnAfterReading => {
|
||||||
write!(f, "This item has been burned. You now have the only copy.")
|
write!(f, "This paste has been burned. You now have the only copy.")
|
||||||
}
|
}
|
||||||
Expiration::UnixTime(time) => write!(
|
Expiration::UnixTime(time) => write!(
|
||||||
f,
|
f,
|
||||||
"{}",
|
"{}",
|
||||||
time.format("This item will expire on %A, %B %-d, %Y at %T %Z.")
|
time.format("This paste will expire on %A, %B %-d, %Y at %T %Z.")
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,16 +291,7 @@ impl TryFrom<&HeaderValue> for Expiration {
|
||||||
fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
|
fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
|
||||||
value
|
value
|
||||||
.to_str()
|
.to_str()
|
||||||
.map_err(|_| ParseHeaderValueError)
|
.map_err(|_| ParseHeaderValueError)?
|
||||||
.and_then(Self::try_from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for Expiration {
|
|
||||||
type Error = ParseHeaderValueError;
|
|
||||||
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
||||||
value
|
|
||||||
.parse::<DateTime<Utc>>()
|
.parse::<DateTime<Utc>>()
|
||||||
.map_err(|_| ParseHeaderValueError)
|
.map_err(|_| ParseHeaderValueError)
|
||||||
.map(Self::UnixTime)
|
.map(Self::UnixTime)
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"devDependencies": {
|
|
||||||
"@swc/cli": "^0.1.51",
|
|
||||||
"@swc/core": "^1.2.102"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,7 +17,7 @@ use rand::Rng;
|
||||||
use rocksdb::IteratorMode;
|
use rocksdb::IteratorMode;
|
||||||
use rocksdb::{Options, DB};
|
use rocksdb::{Options, DB};
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tracing::{error, instrument, trace};
|
use tracing::{error, instrument};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::paste::Paste;
|
use crate::paste::Paste;
|
||||||
|
@ -109,7 +109,7 @@ fn set_up_expirations(db: Arc<DB>) {
|
||||||
info!("Cleanup timers have been initialized.");
|
info!("Cleanup timers have been initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(db, body), err)]
|
#[instrument(skip(db), err)]
|
||||||
async fn upload<const N: usize>(
|
async fn upload<const N: usize>(
|
||||||
Extension(db): Extension<Arc<DB>>,
|
Extension(db): Extension<Arc<DB>>,
|
||||||
maybe_expires: Option<TypedHeader<Expiration>>,
|
maybe_expires: Option<TypedHeader<Expiration>>,
|
||||||
|
@ -127,30 +127,25 @@ async fn upload<const N: usize>(
|
||||||
let paste = Paste::new(maybe_expires.map(|v| v.0).unwrap_or_default(), body);
|
let paste = Paste::new(maybe_expires.map(|v| v.0).unwrap_or_default(), body);
|
||||||
let mut new_key = None;
|
let mut new_key = None;
|
||||||
|
|
||||||
trace!("Generating short code...");
|
|
||||||
|
|
||||||
// Try finding a code; give up after 1000 attempts
|
// Try finding a code; give up after 1000 attempts
|
||||||
// Statistics show that this is very unlikely to happen
|
// Statistics show that this is very unlikely to happen
|
||||||
for i in 0..1000 {
|
for _ in 0..1000 {
|
||||||
let code: ShortCode<N> = thread_rng().sample(short_code::Generator);
|
let code: ShortCode<N> = thread_rng().sample(short_code::Generator);
|
||||||
let db = Arc::clone(&db);
|
let db = Arc::clone(&db);
|
||||||
let key = code.as_bytes();
|
let key = code.as_bytes();
|
||||||
let query = task::spawn_blocking(move || db.key_may_exist(key)).await;
|
let query = task::spawn_blocking(move || db.key_may_exist(key)).await;
|
||||||
if matches!(query, Ok(false)) {
|
if matches!(query, Ok(false)) {
|
||||||
new_key = Some(key);
|
new_key = Some(key);
|
||||||
trace!("Found new key after {} attempts.", i);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = if let Some(key) = new_key {
|
let key = if let Some(key) = new_key {
|
||||||
key
|
key
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to generate a valid short code!");
|
error!("Failed to generate a valid shortcode");
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Serializing paste...");
|
|
||||||
let value = if let Ok(v) = bincode::serialize(&paste) {
|
let value = if let Ok(v) = bincode::serialize(&paste) {
|
||||||
v
|
v
|
||||||
} else {
|
} else {
|
||||||
|
@ -158,8 +153,6 @@ async fn upload<const N: usize>(
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Finished serializing paste.");
|
|
||||||
|
|
||||||
let db_ref = Arc::clone(&db);
|
let db_ref = Arc::clone(&db);
|
||||||
match task::spawn_blocking(move || db_ref.put(key, value)).await {
|
match task::spawn_blocking(move || db_ref.put(key, value)).await {
|
||||||
Ok(Ok(_)) => {
|
Ok(Ok(_)) => {
|
||||||
|
|
|
@ -10,32 +10,17 @@ omegaupload-common = { path = "../common" }
|
||||||
# Enables wasm support
|
# Enables wasm support
|
||||||
getrandom = { version = "*", features = ["js"] }
|
getrandom = { version = "*", features = ["js"] }
|
||||||
|
|
||||||
|
anyhow = "1"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
byte-unit = "4"
|
downcast-rs = "1"
|
||||||
gloo-console = "0.1"
|
gloo-console = "0.1"
|
||||||
http = "0.2"
|
http = "0.2"
|
||||||
image = "0.23"
|
image = "0.23"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
reqwasm = "0.2"
|
reqwest = { version = "0.11", default_features = false, features = ["tokio-rustls"] }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
|
web-sys = { version = "0.3", features = ["TextDecoder"] }
|
||||||
yew = { version = "0.18", features = ["wasm-bindgen-futures"] }
|
yew = { version = "0.18", features = ["wasm-bindgen-futures"] }
|
||||||
yew-router = "0.15"
|
yew-router = "0.15"
|
||||||
yewtil = "0.4"
|
yewtil = "0.4"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
|
||||||
version = "0.3"
|
|
||||||
features = [
|
|
||||||
"TextDecoder",
|
|
||||||
"IdbFactory",
|
|
||||||
"IdbOpenDbRequest",
|
|
||||||
"IdbRequest",
|
|
||||||
"IdbDatabase",
|
|
||||||
"IdbObjectStore",
|
|
||||||
"IdbTransaction",
|
|
||||||
"IdbTransactionMode",
|
|
||||||
"IdbIndex",
|
|
||||||
"IdbIndexParameters",
|
|
||||||
"Event",
|
|
||||||
"EventTarget"
|
|
||||||
]
|
|
|
@ -9,9 +9,11 @@
|
||||||
<link data-trunk rel="copy-file" href="vendor/highlight.min.js" dest="/" />
|
<link data-trunk rel="copy-file" href="vendor/highlight.min.js" dest="/" />
|
||||||
<link data-trunk rel="copy-file" href="vendor/highlightjs-line-numbers.js/dist/highlightjs-line-numbers.min.js"
|
<link data-trunk rel="copy-file" href="vendor/highlightjs-line-numbers.js/dist/highlightjs-line-numbers.min.js"
|
||||||
dest="/" />
|
dest="/" />
|
||||||
|
<link data-trunk rel="copy-file" href="src/reload_on_hash_change.js" dest="/" />
|
||||||
|
<link data-trunk rel="css" href="vendor/highlight.js/src/styles/github-dark.css" />
|
||||||
<link data-trunk rel="scss" href="src/main.scss" />
|
<link data-trunk rel="scss" href="src/main.scss" />
|
||||||
|
|
||||||
<script src="main.js" async></script>
|
<script src="reload_on_hash_change.js" async></script>
|
||||||
<script src="highlight.min.js" defer></script>
|
<script src="highlight.min.js" defer></script>
|
||||||
<script src="highlightjs-line-numbers.min.js" defer></script>
|
<script src="highlightjs-line-numbers.min.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use gloo_console::log;
|
|
||||||
use image::GenericImageView;
|
|
||||||
use js_sys::{Array, Uint8Array};
|
|
||||||
use omegaupload_common::crypto::{open_in_place, Key, Nonce};
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
use web_sys::Blob;
|
|
||||||
|
|
||||||
use crate::DecryptedData;
|
|
||||||
|
|
||||||
pub fn decrypt(
|
|
||||||
mut container: Vec<u8>,
|
|
||||||
key: Key,
|
|
||||||
nonce: Nonce,
|
|
||||||
maybe_password: Option<Key>,
|
|
||||||
) -> Result<DecryptedData, PasteCompleteConstructionError> {
|
|
||||||
let container = &mut container;
|
|
||||||
log!("stage 1 decryption start");
|
|
||||||
if let Some(password) = maybe_password {
|
|
||||||
open_in_place(container, &nonce.increment(), &password)
|
|
||||||
.map_err(|_| PasteCompleteConstructionError::StageOneFailure)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
log!("stage 2 decryption start");
|
|
||||||
open_in_place(container, &nonce, &key)
|
|
||||||
.map_err(|_| PasteCompleteConstructionError::StageTwoFailure)?;
|
|
||||||
|
|
||||||
log!("stage 2 decryption end");
|
|
||||||
if let Ok(decrypted) = std::str::from_utf8(&container) {
|
|
||||||
Ok(DecryptedData::String(Arc::new(decrypted.to_owned())))
|
|
||||||
} else {
|
|
||||||
log!("blob conversion start");
|
|
||||||
let blob_chunks = Array::new_with_length(container.chunks(65536).len().try_into().unwrap());
|
|
||||||
for (i, chunk) in container.chunks(65536).enumerate() {
|
|
||||||
let array = Uint8Array::new_with_length(chunk.len().try_into().unwrap());
|
|
||||||
array.copy_from(&chunk);
|
|
||||||
blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap());
|
|
||||||
}
|
|
||||||
let blob =
|
|
||||||
Arc::new(Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap());
|
|
||||||
log!("blob conversion end");
|
|
||||||
|
|
||||||
if let Ok(image) = image::load_from_memory(&container) {
|
|
||||||
Ok(DecryptedData::Image(
|
|
||||||
blob,
|
|
||||||
image.dimensions(),
|
|
||||||
container.len(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(DecryptedData::Blob(blob))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum PasteCompleteConstructionError {
|
|
||||||
StageOneFailure,
|
|
||||||
StageTwoFailure,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for PasteCompleteConstructionError {}
|
|
||||||
|
|
||||||
impl Display for PasteCompleteConstructionError {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
PasteCompleteConstructionError::StageOneFailure => {
|
|
||||||
write!(f, "Failed to decrypt stage one.")
|
|
||||||
}
|
|
||||||
PasteCompleteConstructionError::StageTwoFailure => {
|
|
||||||
write!(f, "Failed to decrypt stage two.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
560
web/src/main.rs
560
web/src/main.rs
|
@ -1,41 +1,33 @@
|
||||||
#![warn(clippy::nursery, clippy::pedantic)]
|
#![warn(clippy::nursery, clippy::pedantic)]
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use byte_unit::Byte;
|
use anyhow::{anyhow, bail, Context};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use downcast_rs::{impl_downcast, Downcast};
|
||||||
use gloo_console::log;
|
use gloo_console::log;
|
||||||
use http::header::EXPIRES;
|
use http::header::EXPIRES;
|
||||||
use http::uri::PathAndQuery;
|
use http::uri::PathAndQuery;
|
||||||
use http::{StatusCode, Uri};
|
use http::{StatusCode, Uri};
|
||||||
use js_sys::{Array, JsString, Object, Uint8Array};
|
use js_sys::{Array, ArrayBuffer, Uint8Array};
|
||||||
|
use omegaupload_common::crypto::{open, Key, Nonce};
|
||||||
use omegaupload_common::{Expiration, PartialParsedUrl};
|
use omegaupload_common::{Expiration, PartialParsedUrl};
|
||||||
use reqwasm::http::Request;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
|
||||||
use wasm_bindgen::{JsCast, JsValue};
|
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{Blob, Event, IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode};
|
use web_sys::TextDecoder;
|
||||||
|
use web_sys::{Blob, Url};
|
||||||
use yew::utils::window;
|
use yew::utils::window;
|
||||||
|
use yew::Properties;
|
||||||
use yew::{html, Component, ComponentLink, Html, ShouldRender};
|
use yew::{html, Component, ComponentLink, Html, ShouldRender};
|
||||||
use yew_router::router::Router;
|
use yew_router::router::Router;
|
||||||
use yew_router::Switch;
|
use yew_router::Switch;
|
||||||
use yewtil::future::LinkFuture;
|
use yewtil::future::LinkFuture;
|
||||||
|
|
||||||
use crate::decrypt::decrypt;
|
|
||||||
|
|
||||||
mod decrypt;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
yew::start_app::<App>();
|
yew::start_app::<App>();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
extern "C" {
|
|
||||||
fn loadFromDb();
|
|
||||||
fn createNotFoundUi();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct App;
|
struct App;
|
||||||
impl Component for App {
|
impl Component for App {
|
||||||
type Message = ();
|
type Message = ();
|
||||||
|
@ -77,20 +69,20 @@ fn render_route(route: Route) -> Html {
|
||||||
</main>
|
</main>
|
||||||
},
|
},
|
||||||
Route::Path(_) => html! {
|
Route::Path(_) => html! {
|
||||||
<>
|
<main>
|
||||||
<Paste/>
|
<Paste/>
|
||||||
<section class="hljs fullscreen centered">
|
</main>
|
||||||
<p>{"Loading"}</p>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Paste;
|
struct Paste {
|
||||||
|
state: Box<dyn PasteState>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Component for Paste {
|
impl Component for Paste {
|
||||||
type Message = ();
|
type Message = Box<dyn PasteState>;
|
||||||
|
|
||||||
type Properties = ();
|
type Properties = ();
|
||||||
|
|
||||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||||
|
@ -103,27 +95,20 @@ impl Component for Paste {
|
||||||
Uri::from_parts(uri_parts).unwrap()
|
Uri::from_parts(uri_parts).unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let link_clone = link.clone();
|
||||||
link.send_future(async move {
|
link.send_future(async move {
|
||||||
match Request::get(&request_uri.to_string()).send().await {
|
match reqwest::get(&request_uri.to_string()).await {
|
||||||
Ok(resp) if resp.status() == StatusCode::OK => {
|
Ok(resp) if resp.status() == StatusCode::OK => {
|
||||||
let expires = resp
|
let expires = resp
|
||||||
.headers()
|
.headers()
|
||||||
.get(EXPIRES.as_str())
|
.get(EXPIRES)
|
||||||
.ok()
|
.and_then(|v| Expiration::try_from(v).ok());
|
||||||
.flatten()
|
let bytes = match resp.bytes().await {
|
||||||
.as_deref()
|
Ok(bytes) => bytes,
|
||||||
.and_then(|v| Expiration::try_from(v).ok())
|
Err(e) => {
|
||||||
.as_ref()
|
return Box::new(PasteError(anyhow!("Got {}.", e)))
|
||||||
.map(Expiration::to_string)
|
as Box<dyn PasteState>
|
||||||
.unwrap_or_else(|| "This item does not expire.".to_string());
|
}
|
||||||
|
|
||||||
let data = {
|
|
||||||
Uint8Array::new(
|
|
||||||
&JsFuture::from(resp.as_raw().array_buffer().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.to_vec()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let info = url
|
let info = url
|
||||||
|
@ -133,169 +118,362 @@ impl Component for Paste {
|
||||||
let key = info.decryption_key.unwrap();
|
let key = info.decryption_key.unwrap();
|
||||||
let nonce = info.nonce.unwrap();
|
let nonce = info.nonce.unwrap();
|
||||||
|
|
||||||
let result = decrypt(data, key, nonce, None);
|
if let Ok(completed) = decrypt(bytes, key, nonce, None) {
|
||||||
|
Box::new(PasteComplete::new(link_clone, completed, expires))
|
||||||
let decrypted = match result {
|
as Box<dyn PasteState>
|
||||||
Ok(decrypted) => decrypted,
|
} else {
|
||||||
Err(err) => {
|
todo!()
|
||||||
// log!("decryption error: {}", err);
|
// Box::new(partial) as Box<dyn PasteState>
|
||||||
// return Box::new(PasteError(err));
|
|
||||||
unimplemented!()
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let db_open_req = window()
|
|
||||||
.indexed_db()
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
.open("omegaupload")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// On success callback
|
|
||||||
let on_success = Closure::once(Box::new(move |event: Event| {
|
|
||||||
let target: IdbOpenDbRequest = event.target().unwrap().dyn_into().unwrap();
|
|
||||||
let db: IdbDatabase = target.result().unwrap().dyn_into().unwrap();
|
|
||||||
let transaction: IdbObjectStore = db
|
|
||||||
.transaction_with_str_and_mode(
|
|
||||||
"decrypted data",
|
|
||||||
IdbTransactionMode::Readwrite,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.object_store("decrypted data")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let decrypted_object = Array::new();
|
|
||||||
match &decrypted {
|
|
||||||
DecryptedData::String(s) => {
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("data"));
|
|
||||||
entry.push(&JsValue::from_str(&s));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("type"));
|
|
||||||
entry.push(&JsString::from("string"));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("expiration"));
|
|
||||||
entry.push(&JsString::from(expires.to_string()));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
}
|
|
||||||
DecryptedData::Blob(blob) => {
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("data"));
|
|
||||||
entry.push(blob);
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("type"));
|
|
||||||
entry.push(&JsString::from("blob"));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("expiration"));
|
|
||||||
entry.push(&JsString::from(expires.to_string()));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
}
|
|
||||||
DecryptedData::Image(blob, (width, height), size) => {
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("data"));
|
|
||||||
entry.push(blob);
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("type"));
|
|
||||||
entry.push(&JsString::from("image"));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("width"));
|
|
||||||
entry.push(&JsValue::from(*width));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("height"));
|
|
||||||
entry.push(&JsValue::from(*height));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("button"));
|
|
||||||
entry.push(&JsString::from(format!(
|
|
||||||
"Download {} \u{2014} {} by {}",
|
|
||||||
Byte::from_bytes(*size as u128).get_appropriate_unit(true),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
)));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
|
|
||||||
let entry = Array::new();
|
|
||||||
entry.push(&JsString::from("expiration"));
|
|
||||||
entry.push(&JsString::from(expires.to_string()));
|
|
||||||
decrypted_object.push(&entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let db_entry = Object::from_entries(&decrypted_object).unwrap();
|
|
||||||
transaction
|
|
||||||
.put_with_key(
|
|
||||||
&db_entry,
|
|
||||||
&JsString::from(window().location().pathname().unwrap()),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.set_onsuccess(Some(
|
|
||||||
Closure::once(Box::new(|| {
|
|
||||||
log!("success");
|
|
||||||
loadFromDb();
|
|
||||||
})
|
|
||||||
as Box<dyn FnOnce()>)
|
|
||||||
.into_js_value()
|
|
||||||
.dyn_ref()
|
|
||||||
.unwrap(),
|
|
||||||
));
|
|
||||||
})
|
|
||||||
as Box<dyn FnOnce(Event)>);
|
|
||||||
|
|
||||||
db_open_req.set_onsuccess(Some(on_success.into_js_value().dyn_ref().unwrap()));
|
|
||||||
|
|
||||||
// On upgrade callback
|
|
||||||
let on_upgrade = Closure::wrap(Box::new(move |event: Event| {
|
|
||||||
let target: IdbOpenDbRequest = event.target().unwrap().dyn_into().unwrap();
|
|
||||||
let db: IdbDatabase = target.result().unwrap().dyn_into().unwrap();
|
|
||||||
let _obj_store = db.create_object_store("decrypted data").unwrap();
|
|
||||||
}) as Box<dyn FnMut(Event)>);
|
|
||||||
|
|
||||||
db_open_req
|
|
||||||
.set_onupgradeneeded(Some(on_upgrade.into_js_value().dyn_ref().unwrap()));
|
|
||||||
}
|
}
|
||||||
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
|
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
|
||||||
createNotFoundUi();
|
Box::new(PasteNotFound) as Box<dyn PasteState>
|
||||||
|
}
|
||||||
|
Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {
|
||||||
|
Box::new(PasteBadRequest) as Box<dyn PasteState>
|
||||||
|
}
|
||||||
|
Ok(err) => {
|
||||||
|
Box::new(PasteError(anyhow!("Got {}.", err.status()))) as Box<dyn PasteState>
|
||||||
|
}
|
||||||
|
Err(err) => Box::new(PasteError(anyhow!("Got {}.", err))) as Box<dyn PasteState>,
|
||||||
}
|
}
|
||||||
Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {}
|
|
||||||
Ok(err) => {}
|
|
||||||
Err(err) => {}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
Self {
|
||||||
Self
|
state: Box::new(PasteLoading),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _: Self::Message) -> ShouldRender {
|
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||||
false
|
self.state = msg;
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change(&mut self, _: Self::Properties) -> ShouldRender {
|
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) -> Html {
|
fn view(&self) -> Html {
|
||||||
html! {}
|
if self.state.is::<PasteLoading>() {
|
||||||
|
return html! {
|
||||||
|
<p>{ "loading" }</p>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.state.is::<PasteNotFound>() {
|
||||||
|
return html! {
|
||||||
|
<section class={"hljs centered"}>
|
||||||
|
<p>{ "Either the paste has been burned or one never existed." }</p>
|
||||||
|
</section>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.state.is::<PasteBadRequest>() {
|
||||||
|
return html! {
|
||||||
|
<section class={"hljs centered"}>
|
||||||
|
<p>{ "Bad Request. Is this a valid paste URL?" }</p>
|
||||||
|
</section>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(error) = self.state.downcast_ref::<PasteError>() {
|
||||||
|
return html! {
|
||||||
|
<section class={"hljs centered"}><p>{ error.0.to_string() }</p></section>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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!"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
struct PasteError(anyhow::Error);
|
||||||
pub enum DecryptedData {
|
|
||||||
String(Arc<String>),
|
#[derive(Properties, Clone, Debug)]
|
||||||
Blob(Arc<Blob>),
|
struct PastePartial {
|
||||||
Image(Arc<Blob>, (u32, u32), usize),
|
parent: ComponentLink<Paste>,
|
||||||
|
data: Bytes,
|
||||||
|
expires: Option<Expiration>,
|
||||||
|
key: Option<Key>,
|
||||||
|
nonce: Option<Nonce>,
|
||||||
|
password: Option<Key>,
|
||||||
|
needs_pw: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, Clone)]
|
||||||
|
struct PasteComplete {
|
||||||
|
parent: ComponentLink<Paste>,
|
||||||
|
decrypted: DecryptedData,
|
||||||
|
expires: Option<Expiration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum DecryptedData {
|
||||||
|
String(String),
|
||||||
|
Blob(Blob),
|
||||||
|
Image(Blob),
|
||||||
|
}
|
||||||
|
|
||||||
|
trait PasteState: Downcast {}
|
||||||
|
impl_downcast!(PasteState);
|
||||||
|
|
||||||
|
impl PasteState for PasteError {}
|
||||||
|
impl PasteState for PastePartial {}
|
||||||
|
impl PasteState for PasteComplete {}
|
||||||
|
|
||||||
|
macro_rules! impl_paste_type_state {
|
||||||
|
(
|
||||||
|
$($state:ident),* $(,)?
|
||||||
|
) => {
|
||||||
|
$(
|
||||||
|
struct $state;
|
||||||
|
impl PasteState for $state {}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_paste_type_state!(PasteLoading, PasteNotFound, PasteBadRequest);
|
||||||
|
|
||||||
|
impl PastePartial {
|
||||||
|
fn new(
|
||||||
|
data: Bytes,
|
||||||
|
expires: Option<Expiration>,
|
||||||
|
partial_parsed_url: &PartialParsedUrl,
|
||||||
|
parent: ComponentLink<Paste>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
parent,
|
||||||
|
data,
|
||||||
|
expires,
|
||||||
|
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.key, self.nonce, self.password) {
|
||||||
|
(Some(key), Some(nonce), maybe_password)
|
||||||
|
if (self.needs_pw && maybe_password.is_some())
|
||||||
|
|| (!self.needs_pw && maybe_password.is_none()) =>
|
||||||
|
{
|
||||||
|
let parent = self.parent.clone();
|
||||||
|
let data = self.data.clone();
|
||||||
|
let expires = self.expires;
|
||||||
|
|
||||||
|
self.parent.send_future(async move {
|
||||||
|
match decrypt(data, key, nonce, maybe_password) {
|
||||||
|
Ok(decrypted) => Box::new(PasteComplete::new(parent, decrypted, expires))
|
||||||
|
as Box<dyn PasteState>,
|
||||||
|
Err(e) => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent should re-render so this element should be dropped; no point
|
||||||
|
// in saying this needs to be re-rendered.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
"got partial data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt(
|
||||||
|
encrypted: Bytes,
|
||||||
|
key: Key,
|
||||||
|
nonce: Nonce,
|
||||||
|
maybe_password: Option<Key>,
|
||||||
|
) -> Result<DecryptedData, PasteCompleteConstructionError> {
|
||||||
|
let stage_one = maybe_password.map_or_else(
|
||||||
|
|| Ok(encrypted.to_vec()),
|
||||||
|
|password| open(&encrypted, &nonce.increment(), &password),
|
||||||
|
);
|
||||||
|
|
||||||
|
let stage_one = stage_one.map_err(|_| PasteCompleteConstructionError::StageOneFailure)?;
|
||||||
|
|
||||||
|
let stage_two = open(&stage_one, &nonce, &key)
|
||||||
|
.map_err(|_| PasteCompleteConstructionError::StageTwoFailure)?;
|
||||||
|
|
||||||
|
if let Ok(decrypted) = std::str::from_utf8(&stage_two) {
|
||||||
|
Ok(DecryptedData::String(decrypted.to_owned()))
|
||||||
|
} else {
|
||||||
|
let blob_chunks = Array::new_with_length(stage_two.chunks(65536).len().try_into().unwrap());
|
||||||
|
for (i, chunk) in stage_two.chunks(65536).enumerate() {
|
||||||
|
let array = Uint8Array::new_with_length(chunk.len().try_into().unwrap());
|
||||||
|
array.copy_from(&chunk);
|
||||||
|
blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap());
|
||||||
|
}
|
||||||
|
let blob = Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap();
|
||||||
|
|
||||||
|
if image::guess_format(&stage_two).is_ok() {
|
||||||
|
Ok(DecryptedData::Image(blob))
|
||||||
|
} else {
|
||||||
|
Ok(DecryptedData::Blob(blob))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum PasteCompleteConstructionError {
|
||||||
|
StageOneFailure,
|
||||||
|
StageTwoFailure,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for PasteCompleteConstructionError {}
|
||||||
|
|
||||||
|
impl Display for PasteCompleteConstructionError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
PasteCompleteConstructionError::StageOneFailure => {
|
||||||
|
write!(f, "Failed to decrypt stage one.")
|
||||||
|
}
|
||||||
|
PasteCompleteConstructionError::StageTwoFailure => {
|
||||||
|
write!(f, "Failed to decrypt stage two.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PasteComplete {
|
||||||
|
fn new(
|
||||||
|
parent: ComponentLink<Paste>,
|
||||||
|
decrypted: DecryptedData,
|
||||||
|
expires: Option<Expiration>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
parent,
|
||||||
|
decrypted,
|
||||||
|
expires,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
match &self.decrypted {
|
||||||
|
DecryptedData::String(decrypted) => html! {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<pre class={"paste"}>
|
||||||
|
<header class={"hljs"}>
|
||||||
|
{
|
||||||
|
self.expires.as_ref().map(ToString::to_string).unwrap_or_else(||
|
||||||
|
"This paste will not expire.".to_string()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</header>
|
||||||
|
<hr class={"hljs"} />
|
||||||
|
<code>{decrypted}</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<script>{"
|
||||||
|
hljs.highlightAll();
|
||||||
|
hljs.initLineNumbersOnLoad();
|
||||||
|
"}</script>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DecryptedData::Blob(decrypted) => {
|
||||||
|
let object_url = Url::create_object_url_with_blob(decrypted);
|
||||||
|
if let Ok(object_url) = object_url {
|
||||||
|
let file_name = window().location().pathname().unwrap_or("file".to_string());
|
||||||
|
let mut cloned = self.clone();
|
||||||
|
let decrypted_cloned = decrypted.clone();
|
||||||
|
let display_anyways_callback =
|
||||||
|
self.parent.callback_future_once(|_| async move {
|
||||||
|
let array_buffer: ArrayBuffer =
|
||||||
|
JsFuture::from(decrypted_cloned.array_buffer())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into()
|
||||||
|
.unwrap();
|
||||||
|
let decoder = TextDecoder::new().unwrap();
|
||||||
|
cloned.decrypted = decoder
|
||||||
|
.decode_with_buffer_source(&array_buffer)
|
||||||
|
.map(DecryptedData::String)
|
||||||
|
.unwrap();
|
||||||
|
Box::new(cloned) as Box<dyn PasteState>
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<section class="hljs centered">
|
||||||
|
<div class="centered">
|
||||||
|
<p>{ "Found a binary file." }</p>
|
||||||
|
<a href={object_url} download=file_name class="hljs-meta">{"Download"}</a>
|
||||||
|
</div>
|
||||||
|
<p onclick=display_anyways_callback class="display-anyways hljs-meta">{ "Display anyways?" }</p>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This branch really shouldn't happen, but might as well
|
||||||
|
// try and give a user-friendly error message.
|
||||||
|
html! {
|
||||||
|
<section class="hljs centered">
|
||||||
|
<p>{ "Failed to create an object URL for the decrypted file. Try reloading the page?" }</p>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DecryptedData::Image(decrypted) => {
|
||||||
|
let object_url = Url::create_object_url_with_blob(decrypted);
|
||||||
|
if let Ok(object_url) = object_url {
|
||||||
|
let file_name = window().location().pathname().unwrap_or("file".to_string());
|
||||||
|
html! {
|
||||||
|
<section class="centered">
|
||||||
|
<img src={object_url.clone()} />
|
||||||
|
<a href={object_url} download=file_name class="hljs-meta">{"Download"}</a>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This branch really shouldn't happen, but might as well
|
||||||
|
// try and give a user-friendly error message.
|
||||||
|
html! {
|
||||||
|
<section class="hljs centered">
|
||||||
|
<p>{ "Failed to create an object URL for the decrypted file. Try reloading the page?" }</p>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
@use '../vendor/highlight.js/src/styles/github-dark.css';
|
|
||||||
|
|
||||||
$padding: 1em;
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Mplus Code";
|
font-family: "Mplus Code";
|
||||||
src: url("./MplusCodeLatin[wdth,wght].ttf") format("truetype");
|
src: url("./MplusCodeLatin[wdth,wght].ttf") format("truetype");
|
||||||
|
@ -9,16 +5,17 @@ $padding: 1em;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #404040;
|
background-color: #404040;
|
||||||
|
font-family: 'Mplus Code', sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unselectable {
|
pre header {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
margin: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@extend .hljs;
|
margin: 1em;
|
||||||
margin: $padding 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
@ -28,11 +25,11 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
.paste {
|
.paste {
|
||||||
@extend .hljs;
|
border-radius: 1em;
|
||||||
border-radius: $padding;
|
margin: 1em;
|
||||||
margin: $padding;
|
padding: 1em;
|
||||||
padding: 2 * $padding;
|
background-color: #0d1117;
|
||||||
box-shadow: 0 0 $padding black;
|
box-shadow: 0 0 1em black;
|
||||||
min-width: 120ch;
|
min-width: 120ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,35 +39,35 @@ main {
|
||||||
|
|
||||||
.hljs-ln td.hljs-ln-numbers {
|
.hljs-ln td.hljs-ln-numbers {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-right: $padding;
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered {
|
.centered {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen {
|
|
||||||
min-height: 100vh;
|
|
||||||
min-width: 100vw;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-anyways {
|
.display-anyways {
|
||||||
margin-top: 4em;
|
margin-bottom: 4em;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin-bottom: 4em;
|
margin-bottom: 4em;
|
||||||
max-height: 75vh;
|
|
||||||
max-width: 75vw;
|
|
||||||
border-radius: $padding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
@extend .hljs;
|
background-color: #0d1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
a {
|
||||||
|
}
|
||||||
}
|
}
|
154
web/src/main.ts
154
web/src/main.ts
|
@ -1,154 +0,0 @@
|
||||||
// Exported to main.rs
|
|
||||||
function loadFromDb() {
|
|
||||||
const dbReq = window.indexedDB.open("omegaupload", 1);
|
|
||||||
dbReq.onsuccess = (evt) => {
|
|
||||||
const db = (evt.target as IDBRequest).result;
|
|
||||||
const obj_store = db
|
|
||||||
.transaction("decrypted data", "readonly")
|
|
||||||
.objectStore("decrypted data")
|
|
||||||
.get(window.location.pathname);
|
|
||||||
obj_store.onsuccess = (evt) => {
|
|
||||||
const data = (evt.target as IDBRequest).result;
|
|
||||||
switch (data.type) {
|
|
||||||
case "string":
|
|
||||||
createStringPasteUi(data);
|
|
||||||
break;
|
|
||||||
case "blob":
|
|
||||||
createBlobPasteUi(data);
|
|
||||||
break;
|
|
||||||
case "image":
|
|
||||||
createImagePasteUi(data);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
createBrokenStateUi();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
obj_store.onerror = (evt) => {
|
|
||||||
console.log("err");
|
|
||||||
console.log(evt);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStringPasteUi(data) {
|
|
||||||
let bodyEle = document.getElementsByTagName("body")[0];
|
|
||||||
bodyEle.textContent = '';
|
|
||||||
|
|
||||||
let mainEle = document.createElement("main");
|
|
||||||
let preEle = document.createElement("pre");
|
|
||||||
preEle.classList.add("paste");
|
|
||||||
|
|
||||||
let headerEle = document.createElement("header");
|
|
||||||
headerEle.classList.add("unselectable");
|
|
||||||
headerEle.textContent = data.expiration;
|
|
||||||
preEle.appendChild(headerEle);
|
|
||||||
|
|
||||||
preEle.appendChild(document.createElement("hr"));
|
|
||||||
|
|
||||||
let codeEle = document.createElement("code");
|
|
||||||
codeEle.textContent = data.data;
|
|
||||||
preEle.appendChild(codeEle);
|
|
||||||
|
|
||||||
mainEle.appendChild(preEle);
|
|
||||||
bodyEle.appendChild(mainEle);
|
|
||||||
|
|
||||||
hljs.highlightAll();
|
|
||||||
hljs.initLineNumbersOnLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
function createImagePasteUi(data) {
|
|
||||||
let bodyEle = document.getElementsByTagName("body")[0];
|
|
||||||
bodyEle.textContent = '';
|
|
||||||
|
|
||||||
let mainEle = document.createElement("main");
|
|
||||||
mainEle.classList.add("hljs");
|
|
||||||
mainEle.classList.add("centered");
|
|
||||||
mainEle.classList.add("fullscreen");
|
|
||||||
|
|
||||||
const downloadLink = URL.createObjectURL(data.data);
|
|
||||||
|
|
||||||
let expirationEle = document.createElement("p");
|
|
||||||
expirationEle.textContent = data.expiration;
|
|
||||||
mainEle.appendChild(expirationEle);
|
|
||||||
|
|
||||||
let imgEle = document.createElement("img");
|
|
||||||
imgEle.src = downloadLink;
|
|
||||||
mainEle.appendChild(imgEle);
|
|
||||||
|
|
||||||
|
|
||||||
let downloadEle = document.createElement("a");
|
|
||||||
downloadEle.href = downloadLink;
|
|
||||||
downloadEle.download = window.location.pathname;
|
|
||||||
downloadEle.classList.add("hljs-meta");
|
|
||||||
downloadEle.textContent = data.button;
|
|
||||||
mainEle.appendChild(downloadEle);
|
|
||||||
|
|
||||||
|
|
||||||
bodyEle.appendChild(mainEle);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBlobPasteUi(data) {
|
|
||||||
let bodyEle = document.getElementsByTagName("body")[0];
|
|
||||||
bodyEle.textContent = '';
|
|
||||||
|
|
||||||
let mainEle = document.createElement("main");
|
|
||||||
mainEle.classList.add("hljs");
|
|
||||||
mainEle.classList.add("centered");
|
|
||||||
mainEle.classList.add("fullscreen");
|
|
||||||
|
|
||||||
let divEle = document.createElement("div");
|
|
||||||
divEle.classList.add("centered");
|
|
||||||
|
|
||||||
let expirationEle = document.createElement("p");
|
|
||||||
expirationEle.textContent = data.expiration;
|
|
||||||
divEle.appendChild(expirationEle);
|
|
||||||
|
|
||||||
let downloadEle = document.createElement("a");
|
|
||||||
downloadEle.href = URL.createObjectURL(data.data);
|
|
||||||
downloadEle.download = window.location.pathname;
|
|
||||||
downloadEle.classList.add("hljs-meta");
|
|
||||||
downloadEle.textContent = "Download binary file.";
|
|
||||||
divEle.appendChild(downloadEle);
|
|
||||||
|
|
||||||
|
|
||||||
mainEle.appendChild(divEle);
|
|
||||||
|
|
||||||
let displayAnywayEle = document.createElement("p");
|
|
||||||
displayAnywayEle.classList.add("display-anyways");
|
|
||||||
displayAnywayEle.classList.add("hljs-comment");
|
|
||||||
displayAnywayEle.textContent = "Display anyways?";
|
|
||||||
displayAnywayEle.onclick = () => {
|
|
||||||
data.data.text().then(text => {
|
|
||||||
data.data = text;
|
|
||||||
createStringPasteUi(data);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
mainEle.appendChild(displayAnywayEle);
|
|
||||||
bodyEle.appendChild(mainEle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exported to main.rs
|
|
||||||
function createNotFoundUi() {
|
|
||||||
let body = document.getElementsByTagName("body")[0];
|
|
||||||
body.textContent = '';
|
|
||||||
body.appendChild(createGenericError("Either the paste has been burned or one never existed."));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBrokenStateUi() {
|
|
||||||
let body = document.getElementsByTagName("body")[0];
|
|
||||||
body.textContent = '';
|
|
||||||
body.appendChild(createGenericError("Something went wrong. Try clearing local data."));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createGenericError(message) {
|
|
||||||
let mainEle = document.createElement("main");
|
|
||||||
mainEle.classList.add("hljs");
|
|
||||||
mainEle.classList.add("centered");
|
|
||||||
mainEle.classList.add("fullscreen");
|
|
||||||
mainEle.textContent = message;
|
|
||||||
return mainEle;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("hashchange", () => location.reload());
|
|
1
web/src/reload_on_hash_change.js
Normal file
1
web/src/reload_on_hash_change.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
window.addEventListener("hashchange", () => location.reload());
|
233
yarn.lock
233
yarn.lock
|
@ -1,233 +0,0 @@
|
||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
"@napi-rs/triples@^1.0.3":
|
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c"
|
|
||||||
integrity sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA==
|
|
||||||
|
|
||||||
"@node-rs/helper@^1.0.0":
|
|
||||||
version "1.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@node-rs/helper/-/helper-1.2.1.tgz#e079b05f21ff4329d82c4e1f71c0290e4ecdc70c"
|
|
||||||
integrity sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg==
|
|
||||||
dependencies:
|
|
||||||
"@napi-rs/triples" "^1.0.3"
|
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
|
||||||
version "2.1.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
|
||||||
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
|
|
||||||
dependencies:
|
|
||||||
"@nodelib/fs.stat" "2.0.5"
|
|
||||||
run-parallel "^1.1.9"
|
|
||||||
|
|
||||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
|
||||||
version "2.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
|
|
||||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
|
||||||
|
|
||||||
"@nodelib/fs.walk@^1.2.3":
|
|
||||||
version "1.2.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
|
|
||||||
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
|
|
||||||
dependencies:
|
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
|
||||||
fastq "^1.6.0"
|
|
||||||
|
|
||||||
"@swc/cli@^0.1.51":
|
|
||||||
version "0.1.51"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/cli/-/cli-0.1.51.tgz#720f2d966446558011085c2a40a65fd9079d06a1"
|
|
||||||
integrity sha512-7eqZGpkI4QOYfF+9FV4xpT/V/LSRDs5OMJcm4Z46JnPMvv+sxumAFdCe1hHRzHgnzwis9OtjI8Tt3Srf9JudQw==
|
|
||||||
dependencies:
|
|
||||||
commander "^7.1.0"
|
|
||||||
fast-glob "^3.2.5"
|
|
||||||
slash "3.0.0"
|
|
||||||
source-map "^0.7.3"
|
|
||||||
|
|
||||||
"@swc/core-android-arm64@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-android-arm64/-/core-android-arm64-1.2.102.tgz#972a47217053cc759f0bc0308a508279de1247ba"
|
|
||||||
integrity sha512-03wXXSyzm3I/7E3HihYRwvR/v5Xq8Z6j+oXYAouNoQo0/ODTMH9ATFv30csrK3mRtVEcJUk8VpVvfyh1N4hqkw==
|
|
||||||
|
|
||||||
"@swc/core-darwin-arm64@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.102.tgz#1795a4080d1060ca8ac52f218cc4c64b73a7ef06"
|
|
||||||
integrity sha512-DlC9+qt6gq6gGbmr9MCuMZmdHD/RyfZlf7YfkbQOlRlxaanUWz0lq0TZDWGI6MIofVOgaTle0FImPXby6dI/RA==
|
|
||||||
|
|
||||||
"@swc/core-darwin-x64@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.2.102.tgz#f48ab5b8b56798d36acd38838c62eb0be4af3c4b"
|
|
||||||
integrity sha512-JQhxbDnb8RYZ4m7B1f5J05HlUfmjiniQDnpSrvo5rDhlAZWXxuAKjEJQw8Qas/vqLdmgqrZ9POZmFaMBwIgKwg==
|
|
||||||
|
|
||||||
"@swc/core-linux-arm-gnueabihf@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.102.tgz#a617cea4f5a2ff1aac9f668da12dd7a6a0a6700d"
|
|
||||||
integrity sha512-XWr6Cm3lBOcSGjTjPDLWHBh+lOSkKFMS2gCpLmIC3StAvtcN0oQY59T4cqDfe7VcBgJcdeo/H4dEnrXvnJyCaw==
|
|
||||||
|
|
||||||
"@swc/core-linux-arm64-gnu@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.102.tgz#0abccb26af51783be141dea8b7af0baa0dcc55b4"
|
|
||||||
integrity sha512-HVtLVBpyqpSIkXmonW75nDzpdrRtZXwEYLYG+y6Sw/8AQFQ9WntwnR+xoJ8q9o3Bby2DGpWetjR0V8rr1m+lmg==
|
|
||||||
|
|
||||||
"@swc/core-linux-arm64-musl@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.102.tgz#ab079233626a355995b8e25e3666a759edffe1c5"
|
|
||||||
integrity sha512-XDgnkd90alnkBB+JcXaYIG5lXrv/ppLb9Z5fZ4BIsi8uNsVZgo+H/eAj/BTcYff4mpyGdCdqd7P1lC/WRR8uEg==
|
|
||||||
|
|
||||||
"@swc/core-linux-x64-gnu@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.102.tgz#6336698ffae22a491e782cb9ab7478130ceb2240"
|
|
||||||
integrity sha512-O3XZpJ0GMghNcO5uxfhAvDTJ4FgDOcq8DBPpa4f4Mz7hU6fcGY4Koy4rUeff4BuOKlIzI/O+REszxk7Fiivh2w==
|
|
||||||
|
|
||||||
"@swc/core-linux-x64-musl@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.102.tgz#b9f557dd53647318ac6dd832f174efe8e7386e1a"
|
|
||||||
integrity sha512-bGqatsVX3yc56YoOLGcHMUG23I2PKMe638vCBfuKVWN6UKcGJGMzqZV/efyPiHJDFcWzN/1jYW3GccGQq97G3Q==
|
|
||||||
|
|
||||||
"@swc/core-win32-arm64-msvc@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.102.tgz#e7c7958f0c2adb1e83a375e0d8d6af673e53150d"
|
|
||||||
integrity sha512-A15tUAEDS72a2ixNQl3mKCgMD6RVzntMdWl9pDG71/xRd/U/NVIKx0x12FT5fUQH8PTJ7cgcp2Y0VqKeeEuF5w==
|
|
||||||
|
|
||||||
"@swc/core-win32-ia32-msvc@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.102.tgz#f14c93ac0c8b2ec03a8d5438b86551d962da4b0c"
|
|
||||||
integrity sha512-uePvr9+C1Z0KVElU/Y6ZyXw7vqzRIxl+KSYfn9mCFpgYy/1BRSmxpxqTzR0rkfClXMBi2W9a0JosmWbUvRC8ZQ==
|
|
||||||
|
|
||||||
"@swc/core-win32-x64-msvc@1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.102.tgz#e3cbbb58d99174b7b7d9c1c58467491d31dca269"
|
|
||||||
integrity sha512-VsUducGCqKm0ucFrZiKQZ95Y4EcCSivg/zYBdTXM20eu/7mG9ynBXHCoKW0B+69D6J3IZsrc9Hvcu7gKkT9QfQ==
|
|
||||||
|
|
||||||
"@swc/core@^1.2.102":
|
|
||||||
version "1.2.102"
|
|
||||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.2.102.tgz#c71fd22941fa9b7b3c52c778d494ab57beef2c17"
|
|
||||||
integrity sha512-KQEsDWb8HTSQ/I8N6qVHNSl6al/qcXxsRAjpEP8ZPnnpgThbnP9MeKpT2KeU+Qd4VD0qXM72dMm2QOPzdSlxHQ==
|
|
||||||
dependencies:
|
|
||||||
"@node-rs/helper" "^1.0.0"
|
|
||||||
optionalDependencies:
|
|
||||||
"@swc/core-android-arm64" "1.2.102"
|
|
||||||
"@swc/core-darwin-arm64" "1.2.102"
|
|
||||||
"@swc/core-darwin-x64" "1.2.102"
|
|
||||||
"@swc/core-linux-arm-gnueabihf" "1.2.102"
|
|
||||||
"@swc/core-linux-arm64-gnu" "1.2.102"
|
|
||||||
"@swc/core-linux-arm64-musl" "1.2.102"
|
|
||||||
"@swc/core-linux-x64-gnu" "1.2.102"
|
|
||||||
"@swc/core-linux-x64-musl" "1.2.102"
|
|
||||||
"@swc/core-win32-arm64-msvc" "1.2.102"
|
|
||||||
"@swc/core-win32-ia32-msvc" "1.2.102"
|
|
||||||
"@swc/core-win32-x64-msvc" "1.2.102"
|
|
||||||
|
|
||||||
braces@^3.0.1:
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
|
||||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
|
||||||
dependencies:
|
|
||||||
fill-range "^7.0.1"
|
|
||||||
|
|
||||||
commander@^7.1.0:
|
|
||||||
version "7.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
|
||||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
|
||||||
|
|
||||||
fast-glob@^3.2.5:
|
|
||||||
version "3.2.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
|
|
||||||
integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
|
|
||||||
dependencies:
|
|
||||||
"@nodelib/fs.stat" "^2.0.2"
|
|
||||||
"@nodelib/fs.walk" "^1.2.3"
|
|
||||||
glob-parent "^5.1.2"
|
|
||||||
merge2 "^1.3.0"
|
|
||||||
micromatch "^4.0.4"
|
|
||||||
|
|
||||||
fastq@^1.6.0:
|
|
||||||
version "1.13.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
|
|
||||||
integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
|
|
||||||
dependencies:
|
|
||||||
reusify "^1.0.4"
|
|
||||||
|
|
||||||
fill-range@^7.0.1:
|
|
||||||
version "7.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
|
||||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
|
||||||
dependencies:
|
|
||||||
to-regex-range "^5.0.1"
|
|
||||||
|
|
||||||
glob-parent@^5.1.2:
|
|
||||||
version "5.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
|
||||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
|
||||||
dependencies:
|
|
||||||
is-glob "^4.0.1"
|
|
||||||
|
|
||||||
is-extglob@^2.1.1:
|
|
||||||
version "2.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
|
||||||
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
|
|
||||||
|
|
||||||
is-glob@^4.0.1:
|
|
||||||
version "4.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
|
||||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
|
||||||
dependencies:
|
|
||||||
is-extglob "^2.1.1"
|
|
||||||
|
|
||||||
is-number@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
|
||||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
|
||||||
|
|
||||||
merge2@^1.3.0:
|
|
||||||
version "1.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
|
||||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
|
||||||
|
|
||||||
micromatch@^4.0.4:
|
|
||||||
version "4.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
|
|
||||||
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
|
|
||||||
dependencies:
|
|
||||||
braces "^3.0.1"
|
|
||||||
picomatch "^2.2.3"
|
|
||||||
|
|
||||||
picomatch@^2.2.3:
|
|
||||||
version "2.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
|
||||||
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
|
|
||||||
|
|
||||||
queue-microtask@^1.2.2:
|
|
||||||
version "1.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
|
||||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
|
||||||
|
|
||||||
reusify@^1.0.4:
|
|
||||||
version "1.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
|
||||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
|
||||||
|
|
||||||
run-parallel@^1.1.9:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
|
||||||
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
|
|
||||||
dependencies:
|
|
||||||
queue-microtask "^1.2.2"
|
|
||||||
|
|
||||||
slash@3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
|
||||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
|
||||||
|
|
||||||
source-map@^0.7.3:
|
|
||||||
version "0.7.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
|
|
||||||
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
|
|
||||||
|
|
||||||
to-regex-range@^5.0.1:
|
|
||||||
version "5.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
|
||||||
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
|
|
||||||
dependencies:
|
|
||||||
is-number "^7.0.0"
|
|
Loading…
Reference in a new issue