Compare commits
116 commits
Author | SHA1 | Date | |
---|---|---|---|
b213253927 | |||
b33dfac117 | |||
909de7f2fa | |||
9004c4ed29 | |||
600d04eba9 | |||
50e76ae3a0 | |||
72d41898cf | |||
170e23cba7 | |||
|
7b7d119831 | ||
|
8d0abe0195 | ||
|
7c383e8cd0 | ||
|
253fccaf78 | ||
|
b57298ffb2 | ||
|
4e7b3dfd3b | ||
|
a9e9a93493 | ||
|
37bdbae640 | ||
|
3ab01300b2 | ||
|
3c9c46da18 | ||
|
37727bfd3d | ||
|
774b13e46c | ||
|
b793139a99 | ||
|
57bcd6371c | ||
|
064fd749a4 | ||
|
4694683b9a | ||
|
c934b36b35 | ||
|
711f79a255 | ||
|
9a7f13f8b9 | ||
c73af50857 | |||
7047be5ff1 | |||
111a9e9b98 | |||
404d78c0ba | |||
5fa69be1d5 | |||
9a8658e744 | |||
6859bffe05 | |||
08f3e940e4 | |||
3393f40482 | |||
e5519f7d3a | |||
783028fc5d | |||
8504572b83 | |||
0eb8adc20e | |||
d6b2d53249 | |||
f1ad421777 | |||
ae5965a7da | |||
18ee415f46 | |||
732e2bd2f3 | |||
4e2cfcfac6 | |||
41d7feb4df | |||
faac1936a9 | |||
0dbc5fb44e | |||
6337642b3c | |||
ca71815d22 | |||
b7d5425a73 | |||
9b2f53bddf | |||
f7431ca6a4 | |||
67aa009746 | |||
8a09da764e | |||
3cb95a9f04 | |||
b45b2debe9 | |||
|
468ce8a178 | ||
9cc00b4e5b | |||
bccf84cfc1 | |||
c2bdd089e9 | |||
4c7ae4feb2 | |||
1fca1c51e4 | |||
83779545b9 | |||
e720007cbe | |||
73b7b50ed4 | |||
|
2b6fc073fb | ||
247a5ad6f6 | |||
f1c40d64c7 | |||
112b75afae | |||
65c258bd0d | |||
a17f4d94e4 | |||
|
2ebde58c86 | ||
3079c33e45 | |||
b3bb1e0106 | |||
67aff383a2 | |||
8630eaa21e | |||
a14e3e2afe | |||
26950c8c62 | |||
21c28a77f9 | |||
642a8973a4 | |||
8454607859 | |||
76e2494011 | |||
3732549873 | |||
22e3ebed43 | |||
5bb3ad2d0d | |||
3e2f608e27 | |||
d6818e8237 | |||
8cbc1cd3f4 | |||
f748fbf265 | |||
1d4d37b6ea | |||
3bc4541390 | |||
de4d4f90b7 | |||
a9e994820c | |||
4e0e08f3f0 | |||
4f5d1c46d3 | |||
05d736e88e | |||
b833f97c55 | |||
24eff63a5e | |||
9a85d13bd9 | |||
8e05c622af | |||
ac52c20e3b | |||
d2084f369e | |||
a5b105f486 | |||
bb35f710b2 | |||
8a08e8e100 | |||
c29340c93b | |||
364a467626 | |||
06a9522514 | |||
372a160558 | |||
d2755f82c9 | |||
16bce1d11b | |||
0d5f9f3288 | |||
9bb265670e | |||
|
6ef8d48db0 |
72 changed files with 7032 additions and 6027 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -2,4 +2,7 @@
|
|||
**/database
|
||||
**/dist/
|
||||
**/node_modules
|
||||
test.*
|
||||
test.*
|
||||
dist.tar.zst
|
||||
.env
|
||||
web/pkg
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -4,9 +4,6 @@
|
|||
[submodule "web/vendor/highlight.js"]
|
||||
path = web/vendor/highlight.js
|
||||
url = git@github.com:highlightjs/highlight.js.git
|
||||
[submodule "web/vendor/text-fragments-polyfill"]
|
||||
path = web/vendor/text-fragments-polyfill
|
||||
url = git@github.com:GoogleChromeLabs/text-fragments-polyfill.git
|
||||
[submodule "web/vendor/highlightjs-line-numbers.js"]
|
||||
path = web/vendor/highlightjs-line-numbers.js
|
||||
url = git@github.com:wcoder/highlightjs-line-numbers.js.git
|
||||
|
|
1
.shellcheckrc
Normal file
1
.shellcheckrc
Normal file
|
@ -0,0 +1 @@
|
|||
external-sources=true
|
9
.swcrc
Normal file
9
.swcrc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true
|
||||
},
|
||||
"target": "es2021"
|
||||
}
|
||||
}
|
2186
Cargo.lock
generated
2186
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
116
README.md
Normal file
116
README.md
Normal file
|
@ -0,0 +1,116 @@
|
|||
# OmegaUpload
|
||||
|
||||
OmegaUpload is a zero-knowledge temporary file hosting service.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Uploading a file:
|
||||
$ omegaupload upload https://paste.example.com path/to/file
|
||||
https://paste.example.com/PgRG8Hfrr9rR#I1FG2oejo2gSjB3Ym1mEmRfcN4X8GXc2pZtZeiSsWFo=
|
||||
|
||||
# Uploading a file with a password:
|
||||
$ omegaupload upload -p https://paste.example.com path/to/file
|
||||
Please set the password for this paste:
|
||||
https://paste.example.com/862vhXVp3v9R#key:tbGxzHBNnXjS2eq89X9uvZKz_i8bvapLPEp8g0waQrc=!pw
|
||||
|
||||
# Downloading a file:
|
||||
$ omegaupload download https://paste.example.com/PgRG8Hfrr9rR#I1FG2oejo2gSjB3Ym1mEmRfcN4X8GXc2pZtZeiSsWFo=
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Server has zero knowledge of uploaded data when uploading through a supported
|
||||
frontend (Direct, plaintext upload is possible but unsupported).
|
||||
- Only metadata stored on server is expiration time. This is a strong guarantee.
|
||||
- All cryptographic functions are performed on the client side and are done via
|
||||
a single common library, to minimize risk of programming error.
|
||||
- Modern crypto functions are used with recommended parameters:
|
||||
XChaCha20Poly1305 for encryption and Argon2id for KDF.
|
||||
- Customizable expiration times, from burn-after-read to 1 day.
|
||||
|
||||
## Building from source
|
||||
|
||||
Prerequisites:
|
||||
- `yarn` 1.22.17 or later (Earlier versions untested but likely to work)
|
||||
- Cargo, with support for the latest Rust version
|
||||
- _(Optional)_ zstd, for zipping up the file for distribution
|
||||
|
||||
First, run `git submodule update --init --recursive`.
|
||||
|
||||
Then, run `./bin/build.sh` for a `dist.tar.zst` to be generated, where you can
|
||||
simply extract that folder and run the binary provided. The server will listen
|
||||
on port `8080`.
|
||||
|
||||
### Running a local server
|
||||
|
||||
After running `./bin/build.sh`, you can cd into the `dist` and run
|
||||
`./omegaupload-server`. It will run on port 8000, and will respond to HTTP
|
||||
requests.
|
||||
|
||||
You can then point an omegaupload CLI instance (or run
|
||||
`cargo run --bin omegaupload`) as an upload server.
|
||||
|
||||
If you're only changing the frontend (and not updating the server code), you can
|
||||
run `yarn build` for faster iteration.
|
||||
|
||||
## Why OmegaUpload?
|
||||
|
||||
OmegaUpload's primary benefit is that the frontends use a unified common library
|
||||
utilizing XChaCha20Poly1305 to encrypt and decrypt files.
|
||||
|
||||
### Security
|
||||
|
||||
The primary goal was to provide a unified library across both a CLI tool and
|
||||
through the web frontend to minimize risk of compromise. As a result, the CLI
|
||||
tool and the web frontend both utilize a Rust library whose crypto module
|
||||
exposes two functions to encrypt and decrypt that only accept a message and
|
||||
necessarily key material or return only necessary key material. This small API
|
||||
effectively makes it impossible to have differences between the frontend, and
|
||||
ensures that the attack surface is limited to these functions.
|
||||
|
||||
#### Password KDF
|
||||
|
||||
If a password is provided at encryption time, argon2 is used as a key derivation
|
||||
function. Specifically, the library meets or exceeds OWASP recommended
|
||||
parameters:
|
||||
- Argon2id is used.
|
||||
- Algorithm version is `0x13`.
|
||||
- Parameters are `m = 15MiB`, `t = 2`, `p = 2`.
|
||||
|
||||
Additionally, a salt size of 16 bytes are used.
|
||||
|
||||
#### Blob Encryption
|
||||
|
||||
XChaCha20Poly1305 was used as the encryption method as it is becoming the
|
||||
mainstream recommended method for encrypting messages. This was chosen over AES
|
||||
primarily due to its strength in related-key attacks, as well as its widespread
|
||||
recognition and usage in WireGuard, Quic, and TLS.
|
||||
|
||||
As this crate uses `XChaCha20`, a 24 byte nonce and a 32 bytes key are used.
|
||||
|
||||
#### Secrecy
|
||||
|
||||
Encryption and decryption functions offered by the common crate only accept or
|
||||
return key material that will be properly zeroed on destruction. This is
|
||||
enforced by the `secrecy` crate, which, on top of offering type wrappers that
|
||||
zero the memory on drop, provide an easy way to audit when secrets are exposed.
|
||||
|
||||
This also means that to use these two functions necessarily requires the caller
|
||||
to enclose key material in the wrapped type first, reducing possibility for key
|
||||
material to remain in memory.
|
||||
|
||||
#### Memory Safety
|
||||
|
||||
Rust eliminates an entire class of memory-related bugs, and any `unsafe` block
|
||||
is documented with a safety comment. This allows for easy auditing of memory
|
||||
suspect code, and permits
|
||||
|
||||
## Why not OmegaUpload?
|
||||
|
||||
There are a few reasons to not use OmegaUpload:
|
||||
- Limited to 3GB uploads—this is a soft limit of RocksDB.
|
||||
- Cannot download files larger than 512 MiB through the web frontend—this
|
||||
is a technical limitation of the current web frontend not using a web worker
|
||||
in addition to the fact that browsers are not optimized for XChaCha20.
|
||||
- The frontend uses WASM, which is a novel attack surface.
|
38
bin/build.sh
Executable file
38
bin/build.sh
Executable file
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# OmegaUpload Build Script
|
||||
# Copyright (C) 2021 Edward Shen
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)" || exit 1
|
||||
|
||||
# Clean resources
|
||||
rm -rf dist
|
||||
|
||||
# Build frontend code
|
||||
yarn
|
||||
yarn build
|
||||
mv dist/static/index.html dist
|
||||
|
||||
# Build server
|
||||
cargo build --release --bin omegaupload-server
|
||||
strip target/release/omegaupload-server
|
||||
cp target/release/omegaupload-server dist/omegaupload-server
|
||||
|
||||
tar -cvf dist.tar dist
|
||||
rm -rf dist.tar.zst
|
||||
zstd -T0 --ultra --rm -22 dist.tar
|
27
bin/update_web_deps.sh
Executable file
27
bin/update_web_deps.sh
Executable file
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# OmegaUpload Update Web Dependencies Script
|
||||
# Copyright (C) 2021 Edward Shen
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
CUR_DIR=$(pwd)
|
||||
PROJECT_TOP_LEVEL=$(git rev-parse --show-toplevel)
|
||||
|
||||
cd "$PROJECT_TOP_LEVEL" || exit 1
|
||||
git submodule update --remote
|
||||
|
||||
cd "$CUR_DIR"
|
38
bin/upload_test_files.sh
Executable file
38
bin/upload_test_files.sh
Executable file
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# OmegaUpload Upload Test Script
|
||||
# Copyright (C) 2021 Edward Shen
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
source .env
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)" || exit 1
|
||||
|
||||
cargo build --release --bin omegaupload
|
||||
|
||||
TEST_PATH="test/*"
|
||||
|
||||
PADDING=0
|
||||
|
||||
for file in $TEST_PATH; do
|
||||
if [ $PADDING -lt ${#file} ]; then
|
||||
PADDING=${#file}
|
||||
fi
|
||||
done
|
||||
|
||||
for file in $TEST_PATH; do
|
||||
printf "%$((PADDING - ${#TEST_PATH} + 1))s: " "${file#$TEST_PATH}"
|
||||
./target/release/omegaupload upload "$PASTE_URL" "$file"
|
||||
done
|
|
@ -1,15 +1,19 @@
|
|||
[package]
|
||||
name = "omegaupload-cli"
|
||||
version = "0.1.0"
|
||||
name = "omegaupload"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
description = "OmegaUpload CLI tool"
|
||||
repository = "https://git.eddie.sh/edward/omegaupload"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
omegaupload-common = { path = "../common" }
|
||||
|
||||
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"] }
|
||||
anyhow = "1.0.58"
|
||||
atty = "0.2.14"
|
||||
bytes = "1"
|
||||
clap = { version = "3.2.15", features = ["derive"] }
|
||||
indicatif = "0.17"
|
||||
reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "blocking"] }
|
||||
rpassword = "7.0.0"
|
||||
|
|
232
cli/src/main.rs
232
cli/src/main.rs
|
@ -1,32 +1,74 @@
|
|||
#![warn(clippy::nursery, clippy::pedantic)]
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
use std::io::{Read, Write};
|
||||
// OmegaUpload CLI Client
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use atty::Stream;
|
||||
use clap::Clap;
|
||||
use omegaupload_common::crypto::{gen_key_nonce, open_in_place, seal_in_place, Key};
|
||||
use omegaupload_common::{base64, hash, Expiration, ParsedUrl, Url};
|
||||
use reqwest::blocking::Client;
|
||||
use bytes::Bytes;
|
||||
use clap::Parser;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use omegaupload_common::crypto::{open_in_place, seal_in_place};
|
||||
use omegaupload_common::fragment::Builder;
|
||||
use omegaupload_common::secrecy::{ExposeSecret, SecretString, SecretVec};
|
||||
use omegaupload_common::{
|
||||
base64, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
|
||||
};
|
||||
use reqwest::blocking::{Body, Client};
|
||||
use reqwest::header::EXPIRES;
|
||||
use reqwest::StatusCode;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use rpassword::prompt_password;
|
||||
|
||||
#[derive(Clap)]
|
||||
#[derive(Parser)]
|
||||
struct Opts {
|
||||
#[clap(subcommand)]
|
||||
action: Action,
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
#[derive(Parser)]
|
||||
enum Action {
|
||||
/// Upload a paste to an omegaupload server.
|
||||
Upload {
|
||||
/// The OmegaUpload instance to upload data to.
|
||||
url: Url,
|
||||
/// Encrypt the uploaded paste with the provided password, preventing
|
||||
/// public access.
|
||||
#[clap(short, long)]
|
||||
password: Option<SecretString>,
|
||||
password: bool,
|
||||
/// How long for the paste to last, or until someone has read it.
|
||||
#[clap(short, long, possible_values = Expiration::variants())]
|
||||
duration: Option<Expiration>,
|
||||
/// The path to the file to upload. If none is provided, then reads
|
||||
/// stdin instead.
|
||||
path: Option<PathBuf>,
|
||||
/// Hint that the uploaded file should be syntax highlighted with a
|
||||
/// specific language.
|
||||
#[clap(short, long)]
|
||||
language: Option<String>,
|
||||
/// Don't provide a file name hint.
|
||||
#[clap(short = 'F', long)]
|
||||
no_file_name_hint: bool,
|
||||
},
|
||||
/// Download a paste from an omegaupload server.
|
||||
Download {
|
||||
/// The paste to download.
|
||||
url: ParsedUrl,
|
||||
},
|
||||
}
|
||||
|
@ -35,47 +77,87 @@ fn main() -> Result<()> {
|
|||
let opts = Opts::parse();
|
||||
|
||||
match opts.action {
|
||||
Action::Upload { url, password } => handle_upload(url, password),
|
||||
Action::Upload {
|
||||
url,
|
||||
password,
|
||||
duration,
|
||||
path,
|
||||
language,
|
||||
no_file_name_hint,
|
||||
} => handle_upload(url, password, duration, path, language, no_file_name_hint),
|
||||
Action::Download { url } => handle_download(url),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> {
|
||||
fn handle_upload(
|
||||
mut url: Url,
|
||||
password: bool,
|
||||
duration: Option<Expiration>,
|
||||
path: Option<PathBuf>,
|
||||
language: Option<String>,
|
||||
no_file_name_hint: bool,
|
||||
) -> Result<()> {
|
||||
url.set_fragment(None);
|
||||
|
||||
if atty::is(Stream::Stdin) {
|
||||
bail!("This tool requires non interactive CLI. Pipe something in!");
|
||||
if password && path.is_none() {
|
||||
bail!("Reading data from stdin is incompatible with a password. Provide a path to a file to upload.");
|
||||
}
|
||||
|
||||
let (data, nonce, key, pw_used) = {
|
||||
let (enc_key, nonce) = gen_key_nonce();
|
||||
let mut container = Vec::new();
|
||||
std::io::stdin().read_to_end(&mut container)?;
|
||||
seal_in_place(&mut container, &nonce, &enc_key)
|
||||
.map_err(|_| anyhow!("Failed to encrypt data"))?;
|
||||
|
||||
let pw_used = if let Some(password) = password {
|
||||
let pw_hash = hash(password.expose_secret().as_bytes());
|
||||
let pw_key = Key::from_slice(pw_hash.as_ref());
|
||||
seal_in_place(&mut container, &nonce.increment(), pw_key)
|
||||
.map_err(|_| anyhow!("Failed to encrypt data"))?;
|
||||
true
|
||||
let (data, key) = {
|
||||
let mut container = if let Some(ref path) = path {
|
||||
std::fs::read(path)?
|
||||
} else {
|
||||
false
|
||||
let mut container = vec![];
|
||||
std::io::stdin().lock().read_to_end(&mut container)?;
|
||||
container
|
||||
};
|
||||
|
||||
let key = base64::encode(&enc_key);
|
||||
let nonce = base64::encode(&nonce);
|
||||
if container.is_empty() {
|
||||
bail!("Nothing to upload.");
|
||||
}
|
||||
|
||||
(container, nonce, key, pw_used)
|
||||
let password = if password {
|
||||
let maybe_password = prompt_password("Please set the password for this paste: ")?;
|
||||
Some(SecretVec::new(maybe_password.into_bytes()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let enc_key = seal_in_place(&mut container, password)?;
|
||||
let key = SecretString::new(base64::encode(&enc_key.expose_secret().as_ref()));
|
||||
(container, key)
|
||||
};
|
||||
|
||||
let res = Client::new()
|
||||
.post(url.as_ref())
|
||||
.body(data)
|
||||
.send()
|
||||
let mut req = Client::new().post(url.as_ref());
|
||||
|
||||
if let Some(duration) = duration {
|
||||
req = req.header(&*EXPIRATION_HEADER_NAME, duration);
|
||||
}
|
||||
|
||||
let data_size = data.len() as u64;
|
||||
let progress_style = ProgressStyle::with_template(
|
||||
"[{elapsed_precise}] {bar:40} {bytes}/{total_bytes} {eta_precise}",
|
||||
)
|
||||
.unwrap();
|
||||
let progress_bar = ProgressBar::new(data_size).with_style(progress_style);
|
||||
let res = req
|
||||
.body(Body::sized(
|
||||
WrappedBody::new(
|
||||
move |amt| {
|
||||
progress_bar.inc(amt as u64);
|
||||
},
|
||||
data,
|
||||
),
|
||||
data_size,
|
||||
))
|
||||
.build()
|
||||
.expect("Failed to build body");
|
||||
let res = reqwest::blocking::ClientBuilder::new()
|
||||
.timeout(None)
|
||||
.build()?
|
||||
.execute(res)
|
||||
.context("Request to server failed")?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
|
@ -86,20 +168,59 @@ fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> {
|
|||
.map_err(|_| anyhow!("Failed to get base URL"))?
|
||||
.extend(std::iter::once(res.text()?));
|
||||
|
||||
let mut fragment = format!("key:{}!nonce:{}", key, nonce);
|
||||
|
||||
if pw_used {
|
||||
fragment.push_str("!pw");
|
||||
let mut fragment = Builder::new(key);
|
||||
if password {
|
||||
fragment = fragment.needs_password();
|
||||
}
|
||||
|
||||
url.set_fragment(Some(&fragment));
|
||||
if !no_file_name_hint {
|
||||
let file_name = path.and_then(|path| {
|
||||
path.file_name()
|
||||
.map(|str| str.to_string_lossy().to_string())
|
||||
});
|
||||
if let Some(file_name) = file_name {
|
||||
fragment = fragment.file_name(file_name);
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", url);
|
||||
if let Some(language) = language {
|
||||
fragment = fragment.language(language);
|
||||
}
|
||||
|
||||
url.set_fragment(Some(fragment.build().expose_secret()));
|
||||
|
||||
println!("{url}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_download(url: ParsedUrl) -> Result<()> {
|
||||
struct WrappedBody<Callback> {
|
||||
callback: Callback,
|
||||
inner: Cursor<Bytes>,
|
||||
}
|
||||
|
||||
impl<Callback> WrappedBody<Callback> {
|
||||
fn new(callback: Callback, data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
callback,
|
||||
inner: Cursor::new(Bytes::from(data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Callback: FnMut(usize)> Read for WrappedBody<Callback> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let res = self.inner.read(buf);
|
||||
if let Ok(size) = res {
|
||||
(self.callback)(size);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_download(mut url: ParsedUrl) -> Result<()> {
|
||||
url.sanitized_url
|
||||
.set_path(&format!("{API_ENDPOINT}{}", url.sanitized_url.path()));
|
||||
let res = Client::new()
|
||||
.get(url.sanitized_url)
|
||||
.send()
|
||||
|
@ -109,7 +230,8 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
|
|||
bail!("Got bad response from server: {}", res.status());
|
||||
}
|
||||
|
||||
let expiration_text = dbg!(res.headers())
|
||||
let expiration_text = res
|
||||
.headers()
|
||||
.get(EXPIRES)
|
||||
.and_then(|v| Expiration::try_from(v).ok())
|
||||
.as_ref()
|
||||
|
@ -120,25 +242,15 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
|
|||
|
||||
let mut data = res.bytes()?.as_ref().to_vec();
|
||||
|
||||
if url.needs_password {
|
||||
let password = if url.needs_password {
|
||||
// Only print prompt on interactive, else it messes with output
|
||||
if atty::is(Stream::Stdout) {
|
||||
print!("Please enter the password to access this document: ");
|
||||
std::io::stdout().flush()?;
|
||||
}
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
input.pop(); // last character is \n, we need to drop it.
|
||||
let maybe_password = prompt_password("Please enter the password to access this paste: ")?;
|
||||
Some(SecretVec::new(maybe_password.into_bytes()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pw_hash = hash(input.as_bytes());
|
||||
let pw_key = Key::from_slice(pw_hash.as_ref());
|
||||
|
||||
open_in_place(&mut data, &url.nonce.increment(), pw_key)
|
||||
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect password?"))?;
|
||||
}
|
||||
|
||||
open_in_place(&mut data, &url.nonce, &url.decryption_key)
|
||||
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect decryption key?"))?;
|
||||
open_in_place(&mut data, &url.decryption_key, password)?;
|
||||
|
||||
if atty::is(Stream::Stdout) {
|
||||
if let Ok(data) = String::from_utf8(data) {
|
||||
|
@ -150,7 +262,7 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
|
|||
std::io::stdout().write_all(&data)?;
|
||||
}
|
||||
|
||||
eprintln!("{}", expiration_text);
|
||||
eprintln!("{expiration_text}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,27 +1,32 @@
|
|||
[package]
|
||||
name = "omegaupload-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
description = "Common library for OmegaUpload"
|
||||
repository = "https://git.eddie.sh/edward/omegaupload"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.13"
|
||||
bytes = { version = "*", features = ["serde"] }
|
||||
chacha20poly1305 = "0.9"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
headers = "*"
|
||||
lazy_static = "1"
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
sha2 = "0.9"
|
||||
thiserror = "1"
|
||||
url = "2"
|
||||
base64 = "0.21.0"
|
||||
bytes = { version = "1.2.0", features = ["serde"] }
|
||||
chacha20poly1305 = { version = "0.10", features = ["stream", "std"] }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
headers = "0.3.7"
|
||||
lazy_static = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
secrecy = "0.8.0"
|
||||
serde = { version = "1.0.140", features = ["derive"] }
|
||||
thiserror = "1.0.31"
|
||||
typenum = "1.15.0"
|
||||
url = "2.2.2"
|
||||
argon2 = "0.5"
|
||||
|
||||
# Wasm deps
|
||||
web-sys = { version = "0.3", features = ["Headers"], optional = true }
|
||||
http = { version = "0.2", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
# Wasm features
|
||||
gloo-console = { version = "0.3", optional = true }
|
||||
reqwasm = { version = "0.5.0", optional = true }
|
||||
http = { version = "0.2.8", optional = true }
|
||||
|
||||
[features]
|
||||
wasm = ["web-sys", "http", "wasm-bindgen"]
|
||||
wasm = ["gloo-console", "reqwasm", "http"]
|
||||
|
|
42
common/src/base64.rs
Normal file
42
common/src/base64.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2021 Edward Shen
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
use base64::alphabet::URL_SAFE;
|
||||
use base64::engine::general_purpose::GeneralPurpose;
|
||||
use base64::engine::general_purpose::GeneralPurposeConfig;
|
||||
use base64::DecodeError;
|
||||
use base64::Engine;
|
||||
|
||||
const URL_BASE64: GeneralPurpose = GeneralPurpose::new(&URL_SAFE, GeneralPurposeConfig::new());
|
||||
|
||||
/// URL-safe Base64 encoding.
|
||||
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
||||
URL_BASE64.encode(input)
|
||||
}
|
||||
|
||||
/// URL-safe Base64 decoding.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if a buffer cannot be decoded, such as if there's an
|
||||
/// incorrect number of bytes.
|
||||
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
|
||||
URL_BASE64.decode(input)
|
||||
}
|
334
common/src/crypto.rs
Normal file
334
common/src/crypto.rs
Normal file
|
@ -0,0 +1,334 @@
|
|||
// Copyright (c) 2021 Edward Shen
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use argon2::{Argon2, ParamsBuilder};
|
||||
use chacha20poly1305::aead::generic_array::sequence::GenericSequence;
|
||||
use chacha20poly1305::aead::generic_array::GenericArray;
|
||||
use chacha20poly1305::aead::{AeadInPlace};
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
use chacha20poly1305::XNonce;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use secrecy::{DebugSecret, ExposeSecret, Secret, SecretVec, Zeroize};
|
||||
use typenum::Unsigned;
|
||||
use chacha20poly1305::KeyInit;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid password.")]
|
||||
Password,
|
||||
#[error("Invalid secret key.")]
|
||||
SecretKey,
|
||||
#[error("An error occurred while trying to decrypt the blob.")]
|
||||
Encryption,
|
||||
#[error("An error occurred while trying to derive a secret key.")]
|
||||
Kdf,
|
||||
}
|
||||
|
||||
// This struct intentionally prevents implement Clone or Copy
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
pub struct Key(chacha20poly1305::Key);
|
||||
|
||||
impl Key {
|
||||
/// Encloses a secret key in a secret `Key` struct.
|
||||
pub fn new_secret(vec: Vec<u8>) -> Option<Secret<Self>> {
|
||||
chacha20poly1305::Key::from_exact_iter(vec.into_iter())
|
||||
.map(Self)
|
||||
.map(Secret::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugSecret for Key {}
|
||||
|
||||
impl AsRef<chacha20poly1305::Key> for Key {
|
||||
fn as_ref(&self) -> &chacha20poly1305::Key {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Key {
|
||||
type Target = chacha20poly1305::Key;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Key {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Zeroize for Key {
|
||||
fn zeroize(&mut self) {
|
||||
self.0.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
/// Seals the provided message with an optional password, returning the secret
|
||||
/// key used to encrypt the message and mutating the buffer to contain necessary
|
||||
/// metadata.
|
||||
///
|
||||
/// The resulting sealed message has the nonce used to encrypt the message
|
||||
/// appended to it as well as a salt string used to derive the key. In other
|
||||
/// words, the modified buffer is one of the following to possibilities,
|
||||
/// depending if there was a password provided:
|
||||
///
|
||||
/// ```text
|
||||
/// modified = C(message, rng_key, nonce) || nonce
|
||||
/// ```
|
||||
/// or
|
||||
/// ```text
|
||||
/// modified = C(C(message, rng_key, nonce), kdf(pw, salt), nonce + 1) || nonce || salt
|
||||
/// ```
|
||||
///
|
||||
/// Where:
|
||||
/// - `C(message, key, nonce)` represents encrypting a provided message with
|
||||
/// `XChaCha20Poly1305`.
|
||||
/// - `rng_key` represents a randomly generated key.
|
||||
/// - `kdf(pw, salt)` represents a key derived from Argon2.
|
||||
/// - `nonce` represents a randomly generated nonce.
|
||||
///
|
||||
/// Note that the lengths for the nonce, key, and salt follow recommended
|
||||
/// values. As of writing this doc (2021-10-31), the nonce size is 24 bytes, the
|
||||
/// salt size is 16 bytes, and the key size is 32 bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This message will return an error if and only if there was a problem
|
||||
/// encrypting the message or deriving a secret key from the password, if one
|
||||
/// was provided.
|
||||
pub fn seal_in_place(
|
||||
message: &mut Vec<u8>,
|
||||
pw: Option<SecretVec<u8>>,
|
||||
) -> Result<Secret<Key>, Error> {
|
||||
let (key, nonce) = gen_key_nonce();
|
||||
let cipher = XChaCha20Poly1305::new(key.expose_secret());
|
||||
cipher
|
||||
.encrypt_in_place(&nonce, &[], message)
|
||||
.map_err(|_| Error::Encryption)?;
|
||||
|
||||
let mut maybe_salt_string = None;
|
||||
if let Some(password) = pw {
|
||||
let (key, salt_string) = kdf(&password).map_err(|_| Error::Kdf)?;
|
||||
maybe_salt_string = Some(salt_string);
|
||||
let cipher = XChaCha20Poly1305::new(key.expose_secret());
|
||||
cipher
|
||||
.encrypt_in_place(&nonce.increment(), &[], message)
|
||||
.map_err(|_| Error::Encryption)?;
|
||||
}
|
||||
|
||||
message.extend_from_slice(nonce.as_slice());
|
||||
if let Some(maybe_salted_string) = maybe_salt_string {
|
||||
message.extend_from_slice(maybe_salted_string.as_ref());
|
||||
}
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Opens a message that has been sealed with `seal_in_place`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if there was a decryption failure or if there was a problem
|
||||
/// deriving a secret key from the password.
|
||||
pub fn open_in_place(
|
||||
data: &mut Vec<u8>,
|
||||
key: &Secret<Key>,
|
||||
password: Option<SecretVec<u8>>,
|
||||
) -> Result<(), Error> {
|
||||
let pw_key = if let Some(password) = password {
|
||||
let salt_buf = data.split_off(data.len() - Salt::SIZE);
|
||||
let argon = get_argon2();
|
||||
let mut pw_key = Key::default();
|
||||
argon
|
||||
.hash_password_into(password.expose_secret(), &salt_buf, &mut pw_key)
|
||||
.map_err(|_| Error::Kdf)?;
|
||||
Some(Secret::new(pw_key))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let nonce = Nonce::from_slice(&data.split_off(data.len() - Nonce::SIZE));
|
||||
|
||||
// At this point we should have a buffer that's only the ciphertext.
|
||||
|
||||
if let Some(key) = pw_key {
|
||||
let cipher = XChaCha20Poly1305::new(key.expose_secret());
|
||||
cipher
|
||||
.decrypt_in_place(&nonce.increment(), &[], data)
|
||||
.map_err(|_| Error::Password)?;
|
||||
}
|
||||
|
||||
let cipher = XChaCha20Poly1305::new(key.expose_secret());
|
||||
cipher
|
||||
.decrypt_in_place(&nonce, &[], data)
|
||||
.map_err(|_| Error::SecretKey)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn gen_key_nonce() -> (Secret<Key>, Nonce) {
|
||||
let mut rng = get_csrng();
|
||||
let mut key = GenericArray::default();
|
||||
rng.fill(key.as_mut_slice());
|
||||
let mut nonce = Nonce::default();
|
||||
rng.fill(nonce.as_mut_slice());
|
||||
(Secret::new(Key(key)), nonce)
|
||||
}
|
||||
|
||||
// Type alias; to ensure that we're consistent on what the inner impl is.
|
||||
type NonceImpl = XNonce;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
struct Nonce(NonceImpl);
|
||||
|
||||
impl Deref for Nonce {
|
||||
type Target = NonceImpl;
|
||||
|
||||
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 {
|
||||
const SIZE: usize = <NonceImpl as GenericSequence<_>>::Length::USIZE;
|
||||
|
||||
#[must_use]
|
||||
pub fn increment(&self) -> Self {
|
||||
let mut inner = self.0;
|
||||
inner.as_mut_slice()[0] += 1;
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_slice(slice: &[u8]) -> Self {
|
||||
Self(*NonceImpl::from_slice(slice))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
struct Salt([u8; Self::SIZE]);
|
||||
|
||||
impl Salt {
|
||||
const SIZE: usize = argon2::password_hash::Salt::RECOMMENDED_LENGTH;
|
||||
|
||||
fn random() -> Self {
|
||||
let mut salt = [0_u8; Self::SIZE];
|
||||
get_csrng().fill(&mut salt);
|
||||
Self(salt)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Salt {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes an input to output a usable key.
|
||||
fn kdf(password: &SecretVec<u8>) -> Result<(Secret<Key>, Salt), argon2::Error> {
|
||||
let salt = Salt::random();
|
||||
let hasher = get_argon2();
|
||||
let mut key = Key::default();
|
||||
hasher.hash_password_into(password.expose_secret().as_ref(), salt.as_ref(), &mut key)?;
|
||||
|
||||
Ok((Secret::new(key), salt))
|
||||
}
|
||||
|
||||
/// Returns Argon2id configured as follows:
|
||||
/// - 15MiB of memory (`m`),
|
||||
/// - an iteration count of 2 (`t`),
|
||||
/// - and 2 degrees of parallelism (`p`).
|
||||
///
|
||||
/// This follows the [minimum recommended parameters suggested by OWASP][rec].
|
||||
///
|
||||
/// [rec]: https://link.eddie.sh/vaQ6a.
|
||||
fn get_argon2() -> Argon2<'static> {
|
||||
let mut params = ParamsBuilder::new();
|
||||
params
|
||||
.m_cost(15 * 1024) // 15 MiB
|
||||
.t_cost(2)
|
||||
.p_cost(2);
|
||||
let params = params.build().expect("Hard coded params to work");
|
||||
Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
|
||||
}
|
||||
|
||||
/// Fetches a cryptographically secure random number generator. This indirection
|
||||
/// is used for better auditing the quality of rng. Notably, this function
|
||||
/// returns a `Rng` with the `CryptoRng` marker trait, preventing
|
||||
/// non-cryptographically secure RNGs from being used.
|
||||
#[must_use]
|
||||
pub fn get_csrng() -> impl CryptoRng + Rng {
|
||||
rand::thread_rng()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::open_in_place;
|
||||
use super::seal_in_place;
|
||||
use crate::crypto::SecretVec;
|
||||
|
||||
macro_rules! test_encryption {
|
||||
($($name:ident, $content:expr, $password:expr),*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut m = $content;
|
||||
let n: Vec<u8> = $content;
|
||||
let key = seal_in_place(&mut m, $password).unwrap();
|
||||
assert_ne!(m, n);
|
||||
assert!(open_in_place(&mut m, &key, $password).is_ok());
|
||||
assert_eq!(m, n);
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_encryption!(empty, vec![], None);
|
||||
test_encryption!(
|
||||
empty_password,
|
||||
vec![],
|
||||
Some(SecretVec::from(b"password".to_vec()))
|
||||
);
|
||||
test_encryption!(
|
||||
normal,
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
None
|
||||
);
|
||||
test_encryption!(
|
||||
normal_password,
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
Some(SecretVec::from(b"password".to_vec()))
|
||||
);
|
||||
}
|
66
common/src/fragment.rs
Normal file
66
common/src/fragment.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use crate::secrecy::{ExposeSecret, SecretString};
|
||||
|
||||
pub struct Builder {
|
||||
decryption_key: SecretString,
|
||||
needs_password: bool,
|
||||
file_name: Option<String>,
|
||||
language: Option<String>,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
#[must_use]
|
||||
pub fn new(decryption_key: SecretString) -> Self {
|
||||
Self {
|
||||
decryption_key,
|
||||
needs_password: false,
|
||||
file_name: None,
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn needs_password(mut self) -> Self {
|
||||
self.needs_password = true;
|
||||
self
|
||||
}
|
||||
|
||||
// False positive
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
#[must_use]
|
||||
pub fn file_name(mut self, name: String) -> Self {
|
||||
self.file_name = Some(name);
|
||||
self
|
||||
}
|
||||
|
||||
// False positive
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
#[must_use]
|
||||
pub fn language(mut self, language: String) -> Self {
|
||||
self.language = Some(language);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn build(self) -> SecretString {
|
||||
if !self.needs_password && self.file_name.is_none() && self.language.is_none() {
|
||||
return self.decryption_key;
|
||||
}
|
||||
let mut args = String::new();
|
||||
if self.needs_password {
|
||||
args.push_str("!pw");
|
||||
}
|
||||
if let Some(file_name) = self.file_name {
|
||||
args.push_str("!name:");
|
||||
args.push_str(&file_name);
|
||||
}
|
||||
if let Some(language) = self.language {
|
||||
args.push_str("!lang:");
|
||||
args.push_str(&language);
|
||||
}
|
||||
SecretString::new(format!(
|
||||
"key:{}{}",
|
||||
self.decryption_key.expose_secret(),
|
||||
args
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,7 +1,29 @@
|
|||
#![warn(clippy::nursery, clippy::pedantic)]
|
||||
// False positive: https://github.com/rust-lang/rust-clippy/issues/6902
|
||||
#![allow(clippy::use_self)]
|
||||
|
||||
//! Contains common functions and structures used by multiple projects
|
||||
|
||||
// Copyright (c) 2021 Edward Shen
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -9,134 +31,75 @@ use bytes::Bytes;
|
|||
use chrono::{DateTime, Duration, Utc};
|
||||
use headers::{Header, HeaderName, HeaderValue};
|
||||
use lazy_static::lazy_static;
|
||||
pub use secrecy;
|
||||
use secrecy::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use thiserror::Error;
|
||||
pub use url::Url;
|
||||
|
||||
use crate::crypto::{Key, Nonce};
|
||||
use crate::crypto::Key;
|
||||
|
||||
pub mod base64 {
|
||||
/// URL-safe Base64 encoding.
|
||||
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
||||
base64::encode_config(input, base64::URL_SAFE)
|
||||
}
|
||||
pub mod base64;
|
||||
pub mod crypto;
|
||||
pub mod fragment;
|
||||
|
||||
/// 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, AeadInPlace, Buffer, 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 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> {
|
||||
let cipher = XChaCha20Poly1305::new(key);
|
||||
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)]
|
||||
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 {
|
||||
let mut inner = self.0;
|
||||
inner.as_mut_slice()[0] += 1;
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_slice(slice: &[u8]) -> Self {
|
||||
Self(*XNonce::from_slice(slice))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub const API_ENDPOINT: &str = "/api";
|
||||
|
||||
pub struct ParsedUrl {
|
||||
pub sanitized_url: Url,
|
||||
pub decryption_key: Key,
|
||||
pub nonce: Nonce,
|
||||
pub decryption_key: Secret<Key>,
|
||||
pub needs_password: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct PartialParsedUrl {
|
||||
pub decryption_key: Option<Key>,
|
||||
pub nonce: Option<Nonce>,
|
||||
pub decryption_key: Option<Secret<Key>>,
|
||||
pub needs_password: bool,
|
||||
pub name: Option<String>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&str> for PartialParsedUrl {
|
||||
fn from(fragment: &str) -> Self {
|
||||
#[cfg(test)]
|
||||
impl PartialEq for PartialParsedUrl {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
use secrecy::ExposeSecret;
|
||||
let decryption_key_matches = {
|
||||
match (self.decryption_key.as_ref(), other.decryption_key.as_ref()) {
|
||||
(Some(key), Some(other)) => key.expose_secret() == other.expose_secret(),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
|
||||
decryption_key_matches && self.needs_password == other.needs_password
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum PartialParsedUrlParseError {
|
||||
#[error("A decryption key that was not valid web base64 was provided.")]
|
||||
InvalidDecryptionKey,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for PartialParsedUrl {
|
||||
type Error = PartialParsedUrlParseError;
|
||||
|
||||
fn try_from(fragment: &str) -> Result<Self, Self::Error> {
|
||||
// Short circuit if the fragment only contains the key.
|
||||
|
||||
// Base64 has an interesting property that the length of an encoded text
|
||||
// is always 4/3rds larger than the original data.
|
||||
if !fragment.contains("key:") {
|
||||
let decryption_key = base64::decode(fragment)
|
||||
.map_err(|_| PartialParsedUrlParseError::InvalidDecryptionKey)?;
|
||||
let decryption_key = Key::new_secret(decryption_key);
|
||||
|
||||
return Ok(Self {
|
||||
decryption_key,
|
||||
..Self::default()
|
||||
});
|
||||
}
|
||||
|
||||
let args = fragment.split('!').filter_map(|kv| {
|
||||
let (k, v) = {
|
||||
let mut iter = kv.split(':');
|
||||
|
@ -148,28 +111,39 @@ impl From<&str> for PartialParsedUrl {
|
|||
|
||||
let mut decryption_key = None;
|
||||
let mut needs_password = false;
|
||||
let mut nonce = None;
|
||||
let mut name = None;
|
||||
let mut language = None;
|
||||
|
||||
for (key, value) in args {
|
||||
match (key, value) {
|
||||
("key", Some(value)) => {
|
||||
decryption_key = dbg!(base64::decode(value).map(|k| *Key::from_slice(&k)).ok());
|
||||
let key = base64::decode(value)
|
||||
.map_err(|_| PartialParsedUrlParseError::InvalidDecryptionKey)?;
|
||||
decryption_key = Key::new_secret(key);
|
||||
}
|
||||
("pw", _) => {
|
||||
needs_password = true;
|
||||
}
|
||||
("nonce", Some(value)) => {
|
||||
nonce = dbg!(base64::decode(value).as_deref().map(Nonce::from_slice).ok());
|
||||
}
|
||||
("name", Some(provided_name)) => name = Some(provided_name.to_owned()),
|
||||
("lang", Some(provided_lang)) => language = Some(provided_lang.to_owned()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
decryption_key,
|
||||
nonce,
|
||||
needs_password,
|
||||
}
|
||||
name,
|
||||
language,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PartialParsedUrl {
|
||||
type Err = PartialParsedUrlParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,10 +153,8 @@ pub enum ParseUrlError {
|
|||
BadUrl,
|
||||
#[error("Missing decryption key")]
|
||||
NeedKey,
|
||||
#[error("Missing nonce")]
|
||||
NeedNonce,
|
||||
#[error("Missing decryption key and nonce")]
|
||||
NeedKeyAndNonce,
|
||||
#[error(transparent)]
|
||||
InvalidKey(#[from] PartialParsedUrlParseError),
|
||||
}
|
||||
|
||||
impl FromStr for ParsedUrl {
|
||||
|
@ -190,31 +162,25 @@ impl FromStr for ParsedUrl {
|
|||
|
||||
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)?;
|
||||
let fragment = url.fragment().ok_or(ParseUrlError::NeedKey)?;
|
||||
if fragment.is_empty() {
|
||||
return Err(ParseUrlError::NeedKeyAndNonce);
|
||||
return Err(ParseUrlError::NeedKey);
|
||||
}
|
||||
|
||||
let PartialParsedUrl {
|
||||
decryption_key,
|
||||
mut decryption_key,
|
||||
needs_password,
|
||||
nonce,
|
||||
} = PartialParsedUrl::from(fragment);
|
||||
..
|
||||
} = PartialParsedUrl::try_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)),
|
||||
}?;
|
||||
let decryption_key = decryption_key.take().ok_or(ParseUrlError::NeedKey)?;
|
||||
|
||||
Ok(Self {
|
||||
sanitized_url: url,
|
||||
decryption_key,
|
||||
needs_password,
|
||||
nonce,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -222,16 +188,55 @@ impl FromStr for ParsedUrl {
|
|||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub enum Expiration {
|
||||
BurnAfterReading,
|
||||
BurnAfterReadingWithDeadline(DateTime<Utc>),
|
||||
UnixTime(DateTime<Utc>),
|
||||
}
|
||||
|
||||
// This impl is used for the CLI. We use a macro here to ensure that possible
|
||||
// expressed by the CLI are the same supported by the server.
|
||||
macro_rules! expiration_from_str {
|
||||
{
|
||||
$($str_repr:literal => $duration:expr),* $(,)?
|
||||
} => {
|
||||
impl FromStr for Expiration {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
$($str_repr => Ok($duration),)*
|
||||
_ => Err(s.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Expiration {
|
||||
#[must_use]
|
||||
pub const fn variants() -> &'static [&'static str] {
|
||||
&[
|
||||
$($str_repr,)*
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
expiration_from_str! {
|
||||
"read" => Self::BurnAfterReading,
|
||||
"5m" => Self::UnixTime(Utc::now() + Duration::minutes(5)),
|
||||
"10m" => Self::UnixTime(Utc::now() + Duration::minutes(10)),
|
||||
"1h" => Self::UnixTime(Utc::now() + Duration::hours(1)),
|
||||
"1d" => Self::UnixTime(Utc::now() + Duration::days(1)),
|
||||
"3d" => Self::UnixTime(Utc::now() + Duration::days(1)),
|
||||
"1w" => Self::UnixTime(Utc::now() + Duration::weeks(1)),
|
||||
}
|
||||
|
||||
impl Display for Expiration {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Expiration::BurnAfterReading => {
|
||||
Self::BurnAfterReading | Self::BurnAfterReadingWithDeadline(_) => {
|
||||
write!(f, "This item has been burned. You now have the only copy.")
|
||||
}
|
||||
Expiration::UnixTime(time) => write!(
|
||||
Self::UnixTime(time) => write!(
|
||||
f,
|
||||
"{}",
|
||||
time.format("This item will expire on %A, %B %-d, %Y at %T %Z.")
|
||||
|
@ -246,7 +251,7 @@ lazy_static! {
|
|||
|
||||
impl Header for Expiration {
|
||||
fn name() -> &'static HeaderName {
|
||||
&*EXPIRATION_HEADER_NAME
|
||||
&EXPIRATION_HEADER_NAME
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
|
||||
|
@ -254,19 +259,9 @@ impl Header for Expiration {
|
|||
Self: Sized,
|
||||
I: Iterator<Item = &'i HeaderValue>,
|
||||
{
|
||||
match values
|
||||
.next()
|
||||
.ok_or_else(headers::Error::invalid)?
|
||||
.as_bytes()
|
||||
{
|
||||
b"read" => Ok(Self::BurnAfterReading),
|
||||
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))),
|
||||
// We disallow permanent pastes
|
||||
_ => Err(headers::Error::invalid()),
|
||||
}
|
||||
let bytes = values.next().ok_or_else(headers::Error::invalid)?;
|
||||
|
||||
Self::try_from(bytes).map_err(|_| headers::Error::invalid())
|
||||
}
|
||||
|
||||
fn encode<E: Extend<HeaderValue>>(&self, container: &mut E) {
|
||||
|
@ -280,7 +275,9 @@ impl From<&Expiration> for HeaderValue {
|
|||
// so we don't need the extra check.
|
||||
unsafe {
|
||||
Self::from_maybe_shared_unchecked(match expiration {
|
||||
Expiration::BurnAfterReading => Bytes::from_static(b"0"),
|
||||
Expiration::BurnAfterReadingWithDeadline(_) | Expiration::BurnAfterReading => {
|
||||
Bytes::from_static(b"0")
|
||||
}
|
||||
Expiration::UnixTime(duration) => Bytes::from(duration.to_rfc3339()),
|
||||
})
|
||||
}
|
||||
|
@ -288,34 +285,41 @@ impl From<&Expiration> for HeaderValue {
|
|||
}
|
||||
|
||||
impl From<Expiration> for HeaderValue {
|
||||
// False positive: https://github.com/rust-lang/rust-clippy/issues/9095
|
||||
#[allow(clippy::needless_borrow)]
|
||||
fn from(expiration: Expiration) -> Self {
|
||||
(&expiration).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
impl TryFrom<web_sys::Headers> for Expiration {
|
||||
pub struct ParseHeaderValueError;
|
||||
|
||||
// #[cfg(feature = "wasm")]
|
||||
// impl TryFrom<reqwest::header::HeaderMap<&str>> for Expiration {
|
||||
// type Error = ParseHeaderValueError;
|
||||
|
||||
// fn try_from(headers: reqwest::header::HeaderMap) -> Result<Self, Self::Error> {
|
||||
// headers
|
||||
// .get(http::header::EXPIRES.as_str())
|
||||
// .as_deref()
|
||||
// .and_then(|v| Self::try_from(v).ok())
|
||||
// .ok_or(ParseHeaderValueError)
|
||||
// }
|
||||
// }
|
||||
|
||||
impl TryFrom<HeaderValue> for Expiration {
|
||||
type Error = ParseHeaderValueError;
|
||||
|
||||
fn try_from(headers: web_sys::Headers) -> Result<Self, Self::Error> {
|
||||
headers
|
||||
.get(http::header::EXPIRES.as_str())
|
||||
.ok()
|
||||
.flatten()
|
||||
.as_deref()
|
||||
.and_then(|v| Self::try_from(v).ok())
|
||||
.ok_or(ParseHeaderValueError)
|
||||
fn try_from(value: HeaderValue) -> Result<Self, Self::Error> {
|
||||
Self::try_from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ParseHeaderValueError;
|
||||
|
||||
impl TryFrom<&HeaderValue> for Expiration {
|
||||
type Error = ParseHeaderValueError;
|
||||
|
||||
fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
|
||||
value
|
||||
.to_str()
|
||||
std::str::from_utf8(value.as_bytes())
|
||||
.map_err(|_| ParseHeaderValueError)
|
||||
.and_then(Self::try_from)
|
||||
}
|
||||
|
@ -325,6 +329,10 @@ impl TryFrom<&str> for Expiration {
|
|||
type Error = ParseHeaderValueError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if value == "0" {
|
||||
return Ok(Self::BurnAfterReading);
|
||||
}
|
||||
|
||||
value
|
||||
.parse::<DateTime<Utc>>()
|
||||
.map_err(|_| ParseHeaderValueError)
|
||||
|
@ -337,3 +345,120 @@ impl Default for Expiration {
|
|||
Self::UnixTime(Utc::now() + Duration::days(1))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod partial_parsed_url_parsing {
|
||||
use secrecy::Secret;
|
||||
|
||||
use crate::base64;
|
||||
use crate::crypto::Key;
|
||||
use crate::PartialParsedUrl;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!("".parse(), Ok(PartialParsedUrl::default()));
|
||||
}
|
||||
|
||||
const DECRYPTION_KEY_STRING: &str = "ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=";
|
||||
|
||||
fn decryption_key() -> Option<Secret<Key>> {
|
||||
Key::new_secret(base64::decode(DECRYPTION_KEY_STRING).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_no_password() {
|
||||
assert_eq!(
|
||||
DECRYPTION_KEY_STRING.parse(),
|
||||
Ok(PartialParsedUrl {
|
||||
decryption_key: decryption_key(),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_password() {
|
||||
let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=";
|
||||
assert_eq!(
|
||||
input.parse(),
|
||||
Ok(PartialParsedUrl {
|
||||
decryption_key: decryption_key(),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_password() {
|
||||
let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!pw";
|
||||
assert_eq!(
|
||||
input.parse(),
|
||||
Ok(PartialParsedUrl {
|
||||
decryption_key: decryption_key(),
|
||||
needs_password: true,
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_name() {
|
||||
let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!name:test_file.rs";
|
||||
assert_eq!(
|
||||
input.parse(),
|
||||
Ok(PartialParsedUrl {
|
||||
decryption_key: decryption_key(),
|
||||
name: Some("test_file.rs".to_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lang() {
|
||||
let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!lang:rust";
|
||||
assert_eq!(
|
||||
input.parse(),
|
||||
Ok(PartialParsedUrl {
|
||||
decryption_key: decryption_key(),
|
||||
language: Some("rust".to_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_does_not_matter() {
|
||||
let input = "pw!key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=";
|
||||
assert_eq!(
|
||||
input.parse(),
|
||||
Ok(PartialParsedUrl {
|
||||
decryption_key: decryption_key(),
|
||||
needs_password: true,
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_key_pair_gracefully_fails() {
|
||||
let input = "!!!key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!!!";
|
||||
assert_eq!(
|
||||
input.parse(),
|
||||
Ok(PartialParsedUrl {
|
||||
decryption_key: decryption_key(),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_decryption_key_fails() {
|
||||
assert!("invalid key".parse::<PartialParsedUrl>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_fields_fail() {
|
||||
assert!("!!a!!b!!c".parse::<PartialParsedUrl>().is_err());
|
||||
}
|
||||
}
|
||||
|
|
27
package.json
Normal file
27
package.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.51",
|
||||
"@swc/core": "^1.2.102",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"sass": "^1.48.0",
|
||||
"sass-loader": "^12.4.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"swc-loader": "^0.1.15",
|
||||
"webpack": "^5.66.0",
|
||||
"webpack-cli": "^4.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.4.0",
|
||||
"highlightjs-line-numbers.js": "^2.8.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"source-map-loader": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"clean": "(git rev-parse --show-toplevel && rm -rf node_modules dist web/pkg)"
|
||||
}
|
||||
}
|
|
@ -7,18 +7,24 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
omegaupload-common = { path = "../common" }
|
||||
anyhow = "1"
|
||||
axum = { version = "0.2", features = ["http2", "headers"] }
|
||||
bincode = "1"
|
||||
anyhow = "1.0.58"
|
||||
axum = { version = "0.6", features = ["http2", "headers"] }
|
||||
bincode = "1.3.3"
|
||||
# We don't care about which version (We want to match with axum), we just need
|
||||
# to enable the feature
|
||||
bytes = { version = "*", features = ["serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
bytes = { version = "1.2.0", features = ["serde"] }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
futures = "0.3.21"
|
||||
# We just need to pull in whatever axum is pulling in
|
||||
headers = "*"
|
||||
rand = "0.8"
|
||||
rocksdb = { version = "0.17", default_features = false, features = ["zstd"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
tracing = { version = "0.1" }
|
||||
tracing-subscriber = "0.2"
|
||||
headers = "0.3.7"
|
||||
lazy_static = "1.4.0"
|
||||
# Disable `random()` and `thread_rng()`
|
||||
rand = { version = "0.8.5", default-features = false }
|
||||
rocksdb = { version = "0.21", default-features = false, features = ["zstd"] }
|
||||
serde = { version = "1.0.140", features = ["derive"] }
|
||||
signal-hook = "0.3.14"
|
||||
signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] }
|
||||
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.4", features = ["fs"] }
|
||||
tracing = "0.1.35"
|
||||
tracing-subscriber = "0.3.15"
|
||||
|
|
3
server/README.md
Normal file
3
server/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
This server responds to four paths:
|
||||
|
||||
`GET /`
|
|
@ -1,23 +1,46 @@
|
|||
#![warn(clippy::nursery, clippy::pedantic)]
|
||||
|
||||
// OmegaUpload Zero Knowledge File Hosting
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::body::Bytes;
|
||||
use axum::error_handling::HandleError;
|
||||
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 axum::routing::{get, get_service, post};
|
||||
use axum::Router;
|
||||
use chrono::Utc;
|
||||
use futures::stream::StreamExt;
|
||||
use headers::HeaderMap;
|
||||
use omegaupload_common::Expiration;
|
||||
use rand::thread_rng;
|
||||
use lazy_static::lazy_static;
|
||||
use omegaupload_common::crypto::get_csrng;
|
||||
use omegaupload_common::{Expiration, API_ENDPOINT};
|
||||
use rand::Rng;
|
||||
use rocksdb::{ColumnFamilyDescriptor, IteratorMode};
|
||||
use rocksdb::{Options, DB};
|
||||
use tokio::task;
|
||||
use signal_hook::consts::SIGUSR1;
|
||||
use signal_hook_tokio::Signals;
|
||||
use tokio::task::{self, JoinHandle};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use tracing::{error, instrument, trace};
|
||||
use tracing::{info, warn};
|
||||
|
||||
|
@ -28,6 +51,10 @@ mod short_code;
|
|||
const BLOB_CF_NAME: &str = "blob";
|
||||
const META_CF_NAME: &str = "meta";
|
||||
|
||||
lazy_static! {
|
||||
static ref MAX_PASTE_AGE: chrono::Duration = chrono::Duration::days(1);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
const PASTE_DB_PATH: &str = "database";
|
||||
|
@ -48,27 +75,50 @@ async fn main() -> Result<()> {
|
|||
],
|
||||
)?);
|
||||
|
||||
set_up_expirations(&db);
|
||||
set_up_expirations::<SHORT_CODE_SIZE>(&db);
|
||||
|
||||
axum::Server::bind(&"0.0.0.0:8081".parse()?)
|
||||
.serve(
|
||||
let signals = Signals::new(&[SIGUSR1])?;
|
||||
let signals_handle = signals.handle();
|
||||
let signals_task = tokio::spawn(handle_signals(signals, Arc::clone(&db)));
|
||||
|
||||
let root_service = HandleError::new(get_service(ServeDir::new("static")), |_| async {
|
||||
Ok::<_, Infallible>(StatusCode::NOT_FOUND)
|
||||
});
|
||||
|
||||
let index_service = HandleError::new(get_service(ServeFile::new("index.html")), |_| async {
|
||||
Ok::<_, Infallible>(StatusCode::NOT_FOUND)
|
||||
});
|
||||
|
||||
axum::Server::bind(&"0.0.0.0:8080".parse()?)
|
||||
.serve({
|
||||
info!("Now serving on 0.0.0.0:8080");
|
||||
Router::new()
|
||||
.route("/", post(upload::<SHORT_CODE_SIZE>))
|
||||
.route(
|
||||
"/:code",
|
||||
"/",
|
||||
post(upload::<SHORT_CODE_SIZE>).get_service(index_service.clone()),
|
||||
)
|
||||
.route_service("/:code", index_service)
|
||||
.nest_service("/static", root_service)
|
||||
.route(
|
||||
&format!("{API_ENDPOINT}/:code"),
|
||||
get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
|
||||
)
|
||||
.layer(AddExtensionLayer::new(db))
|
||||
.into_make_service(),
|
||||
)
|
||||
.layer(axum::Extension(db))
|
||||
.into_make_service()
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Must be called for correct shutdown
|
||||
DB::destroy(&Options::default(), PASTE_DB_PATH)?;
|
||||
|
||||
signals_handle.close();
|
||||
signals_task.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_up_expirations(db: &Arc<DB>) {
|
||||
// See https://link.eddie.sh/5JHlD
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn set_up_expirations<const N: usize>(db: &Arc<DB>) {
|
||||
let mut corrupted = 0;
|
||||
let mut expired = 0;
|
||||
let mut pending = 0;
|
||||
|
@ -79,43 +129,37 @@ fn set_up_expirations(db: &Arc<DB>) {
|
|||
|
||||
let db_ref = Arc::clone(db);
|
||||
|
||||
let delete_entry = move |key: &[u8]| {
|
||||
let blob_cf = db_ref.cf_handle(BLOB_CF_NAME).unwrap();
|
||||
let meta_cf = db_ref.cf_handle(META_CF_NAME).unwrap();
|
||||
if let Err(e) = db_ref.delete_cf(blob_cf, &key) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
if let Err(e) = db_ref.delete_cf(meta_cf, &key) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
};
|
||||
for item in db.iterator_cf(meta_cf, IteratorMode::Start) {
|
||||
let (key, value) = item.unwrap();
|
||||
let key: [u8; N] = (*key).try_into().unwrap();
|
||||
|
||||
for (key, value) in db.iterator_cf(meta_cf, IteratorMode::Start) {
|
||||
let expiration = if let Ok(value) = bincode::deserialize::<Expiration>(&value) {
|
||||
value
|
||||
} else {
|
||||
corrupted += 1;
|
||||
delete_entry(&key);
|
||||
delete_entry(Arc::clone(&db_ref), key);
|
||||
continue;
|
||||
};
|
||||
|
||||
let expiration_time = match expiration {
|
||||
Expiration::BurnAfterReading => {
|
||||
panic!("Got burn after reading expiration time? Invariant violated");
|
||||
warn!("Found unbounded burn after reading. Defaulting to max age");
|
||||
Utc::now() + *MAX_PASTE_AGE
|
||||
}
|
||||
Expiration::BurnAfterReadingWithDeadline(deadline) => deadline,
|
||||
Expiration::UnixTime(time) => time,
|
||||
};
|
||||
|
||||
let sleep_duration = (expiration_time - Utc::now()).to_std().unwrap_or_default();
|
||||
if sleep_duration == Duration::default() {
|
||||
expired += 1;
|
||||
delete_entry(&key);
|
||||
delete_entry(Arc::clone(&db_ref), key);
|
||||
} else {
|
||||
pending += 1;
|
||||
let delete_entry_ref = delete_entry.clone();
|
||||
task::spawn_blocking(move || async move {
|
||||
let db = Arc::clone(&db_ref);
|
||||
task::spawn(async move {
|
||||
tokio::time::sleep(sleep_duration).await;
|
||||
delete_entry_ref(&key);
|
||||
delete_entry(db, key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -123,14 +167,26 @@ fn set_up_expirations(db: &Arc<DB>) {
|
|||
if corrupted == 0 {
|
||||
info!("No corrupted pastes found.");
|
||||
} else {
|
||||
warn!("Found {} corrupted pastes.", corrupted);
|
||||
warn!("Found {corrupted} corrupted pastes.");
|
||||
}
|
||||
|
||||
info!("Found {} expired pastes.", expired);
|
||||
info!("Found {} active pastes.", pending);
|
||||
info!("Found {expired} expired pastes.");
|
||||
info!("Found {pending} active pastes.");
|
||||
info!("Cleanup timers have been initialized.");
|
||||
}
|
||||
|
||||
async fn handle_signals(mut signals: Signals, db: Arc<DB>) {
|
||||
while let Some(signal) = signals.next().await {
|
||||
if signal == SIGUSR1 {
|
||||
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
|
||||
info!(
|
||||
"Active paste count: {}",
|
||||
db.iterator_cf(meta_cf, IteratorMode::Start).count()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(db, body), err)]
|
||||
async fn upload<const N: usize>(
|
||||
Extension(db): Extension<Arc<DB>>,
|
||||
|
@ -141,6 +197,15 @@ async fn upload<const N: usize>(
|
|||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
if let Some(header) = maybe_expires {
|
||||
if let Expiration::UnixTime(time) = header.0 {
|
||||
if (time - Utc::now()) > *MAX_PASTE_AGE {
|
||||
warn!("{time} exceeds allowed paste lifetime");
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3GB max; this is a soft-limit of RocksDb
|
||||
if body.len() >= 3_221_225_472 {
|
||||
return Err(StatusCode::PAYLOAD_TOO_LARGE);
|
||||
|
@ -153,7 +218,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 i in 0..1000 {
|
||||
let code: ShortCode<N> = thread_rng().sample(short_code::Generator);
|
||||
let code: ShortCode<N> = get_csrng().sample(short_code::Generator);
|
||||
let db = Arc::clone(&db);
|
||||
let key = code.as_bytes();
|
||||
let query = task::spawn_blocking(move || {
|
||||
|
@ -162,7 +227,7 @@ async fn upload<const N: usize>(
|
|||
.await;
|
||||
if matches!(query, Ok(false)) {
|
||||
new_key = Some(key);
|
||||
trace!("Found new key after {} attempts.", i);
|
||||
trace!("Found new key after {i} attempts.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -174,10 +239,6 @@ async fn upload<const N: usize>(
|
|||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
};
|
||||
|
||||
trace!("Serializing paste...");
|
||||
|
||||
trace!("Finished serializing paste.");
|
||||
|
||||
let db_ref = Arc::clone(&db);
|
||||
match task::spawn_blocking(move || {
|
||||
let blob_cf = db_ref.cf_handle(BLOB_CF_NAME).unwrap();
|
||||
|
@ -185,6 +246,11 @@ async fn upload<const N: usize>(
|
|||
let data = bincode::serialize(&body).expect("bincode to serialize");
|
||||
db_ref.put_cf(blob_cf, key, data)?;
|
||||
let expires = maybe_expires.map(|v| v.0).unwrap_or_default();
|
||||
let expires = if let Expiration::BurnAfterReading = expires {
|
||||
Expiration::BurnAfterReadingWithDeadline(Utc::now() + *MAX_PASTE_AGE)
|
||||
} else {
|
||||
expires
|
||||
};
|
||||
let meta = bincode::serialize(&expires).expect("bincode to serialize");
|
||||
if db_ref.put_cf(meta_cf, key, meta).is_err() {
|
||||
// try and roll back on metadata write failure
|
||||
|
@ -196,26 +262,20 @@ async fn upload<const N: usize>(
|
|||
{
|
||||
Ok(Ok(_)) => {
|
||||
if let Some(expires) = maybe_expires {
|
||||
if let Expiration::UnixTime(expiration_time) = expires.0 {
|
||||
if let Expiration::UnixTime(expiration_time)
|
||||
| Expiration::BurnAfterReadingWithDeadline(expiration_time) = expires.0
|
||||
{
|
||||
let sleep_duration =
|
||||
(expiration_time - Utc::now()).to_std().unwrap_or_default();
|
||||
|
||||
task::spawn_blocking(move || async move {
|
||||
task::spawn(async move {
|
||||
tokio::time::sleep(sleep_duration).await;
|
||||
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap();
|
||||
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
|
||||
if let Err(e) = db.delete_cf(blob_cf, key) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
if let Err(e) = db.delete_cf(meta_cf, key) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
delete_entry(db, key);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
e => {
|
||||
error!("Failed to insert paste into db: {:?}", e);
|
||||
error!("Failed to insert paste into db: {e:?}");
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +293,7 @@ async fn paste<const N: usize>(
|
|||
let metadata: Expiration = {
|
||||
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
|
||||
let query_result = db.get_cf(meta_cf, key).map_err(|e| {
|
||||
error!("Failed to fetch initial query: {}", e);
|
||||
error!("Failed to fetch initial query: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
|
@ -251,22 +311,10 @@ async fn paste<const N: usize>(
|
|||
// Check if paste has expired.
|
||||
if let Expiration::UnixTime(expires) = metadata {
|
||||
if expires < Utc::now() {
|
||||
task::spawn_blocking(move || {
|
||||
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap();
|
||||
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
|
||||
if let Err(e) = db.delete_cf(blob_cf, &key) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
if let Err(e) = db.delete_cf(meta_cf, &key) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to join handle: {}", e);
|
||||
delete_entry(db, url.as_bytes()).await.map_err(|e| {
|
||||
error!("Failed to join handle: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
})??;
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +323,7 @@ async fn paste<const N: usize>(
|
|||
// not sure if perf of get_pinned is better than spawn_blocking
|
||||
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap();
|
||||
let query_result = db.get_pinned_cf(blob_cf, key).map_err(|e| {
|
||||
error!("Failed to fetch initial query: {}", e);
|
||||
error!("Failed to fetch initial query: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
|
@ -291,18 +339,14 @@ async fn paste<const N: usize>(
|
|||
};
|
||||
|
||||
// Check if we need to burn after read
|
||||
if matches!(metadata, Expiration::BurnAfterReading) {
|
||||
let join_handle = task::spawn_blocking(move || db.delete(key))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to join handle: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
join_handle.map_err(|e| {
|
||||
error!("Failed to burn paste after read: {}", e);
|
||||
if matches!(
|
||||
metadata,
|
||||
Expiration::BurnAfterReading | Expiration::BurnAfterReadingWithDeadline(_)
|
||||
) {
|
||||
delete_entry(db, key).await.map_err(|e| {
|
||||
error!("Failed to join handle: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
})??;
|
||||
}
|
||||
|
||||
let mut map = HeaderMap::new();
|
||||
|
@ -316,24 +360,24 @@ async fn delete<const N: usize>(
|
|||
Extension(db): Extension<Arc<DB>>,
|
||||
Path(url): Path<ShortCode<N>>,
|
||||
) -> StatusCode {
|
||||
match task::spawn_blocking(move || {
|
||||
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap();
|
||||
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
|
||||
if let Err(e) = db.delete_cf(blob_cf, url.as_bytes()) {
|
||||
warn!("{}", e);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if let Err(e) = db.delete_cf(meta_cf, url.as_bytes()) {
|
||||
warn!("{}", e);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
{
|
||||
match delete_entry(db, url.as_bytes()).await {
|
||||
Ok(_) => StatusCode::OK,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_entry<const N: usize>(db: Arc<DB>, key: [u8; N]) -> JoinHandle<Result<(), StatusCode>> {
|
||||
task::spawn_blocking(move || {
|
||||
let blob_cf = db.cf_handle(BLOB_CF_NAME).unwrap();
|
||||
let meta_cf = db.cf_handle(META_CF_NAME).unwrap();
|
||||
if let Err(e) = db.delete_cf(blob_cf, &key) {
|
||||
warn!("{e}");
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
if let Err(e) = db.delete_cf(meta_cf, &key) {
|
||||
warn!("{e}");
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,23 @@
|
|||
// OmegaUpload Zero Knowledge File Hosting
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use rand::prelude::Distribution;
|
||||
use rand::Rng;
|
||||
use serde::de::{Unexpected, Visitor};
|
||||
use serde::Deserialize;
|
||||
|
||||
|
@ -109,7 +126,7 @@ pub struct Generator;
|
|||
const ALPHABET: &[u8; 32] = b"23456789CFGHJMPQRVWXcfghjmpqrvwx";
|
||||
|
||||
impl Distribution<ShortCodeChar> for Generator {
|
||||
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> ShortCodeChar {
|
||||
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> ShortCodeChar {
|
||||
let value = rng.gen_range(0..32);
|
||||
assert!(value < 32);
|
||||
ShortCodeChar(ALPHABET[value] as char)
|
||||
|
@ -117,7 +134,7 @@ impl Distribution<ShortCodeChar> for Generator {
|
|||
}
|
||||
|
||||
impl<const N: usize> Distribution<ShortCode<N>> for Generator {
|
||||
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> ShortCode<N> {
|
||||
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> ShortCode<N> {
|
||||
let mut arr = [ShortCodeChar('\0'); N];
|
||||
|
||||
for c in arr.iter_mut() {
|
||||
|
|
442
test/0000-test-patch.patch
Normal file
442
test/0000-test-patch.patch
Normal file
|
@ -0,0 +1,442 @@
|
|||
From 960344b240161b36cca35c22b6a685162b0f217e Mon Sep 17 00:00:00 2001
|
||||
From: William Tan <code@wtan.me>
|
||||
Date: Tue, 11 Jan 2022 22:31:18 -0500
|
||||
Subject: [PATCH] Update dependencies
|
||||
|
||||
---
|
||||
Cargo.lock | 144 ++++++++++++++++++++++---------------------------
|
||||
cli/Cargo.toml | 4 +-
|
||||
2 files changed, 65 insertions(+), 83 deletions(-)
|
||||
|
||||
diff --git a/Cargo.lock b/Cargo.lock
|
||||
index fc97ae7..a16f56f 100644
|
||||
--- a/Cargo.lock
|
||||
+++ b/Cargo.lock
|
||||
@@ -28,9 +28,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
-version = "1.0.51"
|
||||
+version = "1.0.52"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
|
||||
+checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
@@ -149,9 +149,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
-version = "0.10.0"
|
||||
+version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "a58bdf5134c5beae6fc382002c4d88950bad1feea20f8f7165494b6b43b049de"
|
||||
+checksum = "b94ba84325db59637ffc528bbe8c7f86c02c57cff5c0e2b9b00f9a851f42f309"
|
||||
dependencies = [
|
||||
"digest 0.10.1",
|
||||
]
|
||||
@@ -176,9 +176,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
-version = "3.8.0"
|
||||
+version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
|
||||
+checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
|
||||
|
||||
[[package]]
|
||||
name = "byte-unit"
|
||||
@@ -295,9 +295,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
-version = "3.0.0-rc.7"
|
||||
+version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "9468f8012246b0836c6fd11725102b0844254985f2462b6c637d50040ef49df0"
|
||||
+checksum = "1957aa4a5fb388f0a0a73ce7556c5b42025b874e5cdc2c670775e346e97adec0"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
@@ -312,9 +312,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
-version = "3.0.0-rc.7"
|
||||
+version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "b72e1af32a4de4d21a43d26de33fe69c00e895371bd8b1523d674f011b610467"
|
||||
+checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
@@ -392,9 +392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
-version = "0.4.0"
|
||||
+version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "398ea4fabe40b9b0d885340a2a991a44c8a645624075ad966d21f88688e2b69e"
|
||||
+checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
@@ -515,9 +515,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
-version = "0.14.4"
|
||||
+version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
@@ -556,9 +556,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
-version = "0.3.9"
|
||||
+version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd"
|
||||
+checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -606,12 +606,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
-version = "0.3.3"
|
||||
+version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
||||
-dependencies = [
|
||||
- "unicode-segmentation",
|
||||
-]
|
||||
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
@@ -624,13 +621,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
-version = "0.2.5"
|
||||
+version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
|
||||
+checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
- "itoa 0.4.8",
|
||||
+ "itoa 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -706,9 +703,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
-version = "1.7.0"
|
||||
+version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
||||
+checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
@@ -921,9 +918,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
-version = "1.13.0"
|
||||
+version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
|
||||
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
@@ -1086,18 +1083,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
-version = "1.0.8"
|
||||
+version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08"
|
||||
+checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
-version = "1.0.8"
|
||||
+version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389"
|
||||
+checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1106,9 +1103,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
-version = "0.2.7"
|
||||
+version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
|
||||
+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
@@ -1129,9 +1126,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
-version = "0.2.15"
|
||||
+version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
|
||||
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
@@ -1159,18 +1156,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
-version = "1.0.34"
|
||||
+version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1"
|
||||
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
-version = "1.0.10"
|
||||
+version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||
+checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -1258,15 +1255,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
-version = "0.11.7"
|
||||
+version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5"
|
||||
+checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
+ "h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
@@ -1343,7 +1341,7 @@ dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"sct",
|
||||
- "webpki 0.22.0",
|
||||
+ "webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1382,18 +1380,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
-version = "1.0.132"
|
||||
+version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
|
||||
+checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
-version = "1.0.132"
|
||||
+version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
|
||||
+checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1402,9 +1400,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
-version = "1.0.73"
|
||||
+version = "1.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
|
||||
+checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142"
|
||||
dependencies = [
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
@@ -1453,9 +1451,9 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
-version = "0.3.12"
|
||||
+version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "c35dfd12afb7828318348b8c408383cf5071a086c1d4ab1c0f9840ec92dbb922"
|
||||
+checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
@@ -1472,9 +1470,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-tokio"
|
||||
-version = "0.3.0"
|
||||
+version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8"
|
||||
+checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"libc",
|
||||
@@ -1524,9 +1522,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
-version = "1.0.82"
|
||||
+version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
|
||||
+checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1643,7 +1641,7 @@ checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
- "webpki 0.22.0",
|
||||
+ "webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1794,9 +1792,9 @@ checksum = "e73fc24a5427b3b15e2b0bcad8ef61b5affb1da8ac89c8bf3f196c8692d57f02"
|
||||
|
||||
[[package]]
|
||||
name = "tree_magic_mini"
|
||||
-version = "3.0.2"
|
||||
+version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "7a7581560dc616314f7d73e81419c783d93a92e7fc7331b3041ff57bab240ea6"
|
||||
+checksum = "91adfd0607cacf6e4babdb870e9bec4037c1c4b151cfd279ccefc5e0c7feaa6d"
|
||||
dependencies = [
|
||||
"bytecount",
|
||||
"fnv",
|
||||
@@ -1815,9 +1813,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
-version = "1.14.0"
|
||||
+version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
|
||||
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
@@ -1843,12 +1841,6 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
-[[package]]
|
||||
-name = "unicode-segmentation"
|
||||
-version = "1.8.0"
|
||||
-source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||
-
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
@@ -1891,9 +1883,9 @@ checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
-version = "0.9.3"
|
||||
+version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
@@ -1989,16 +1981,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
-[[package]]
|
||||
-name = "webpki"
|
||||
-version = "0.21.4"
|
||||
-source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
|
||||
-dependencies = [
|
||||
- "ring",
|
||||
- "untrusted",
|
||||
-]
|
||||
-
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.0"
|
||||
@@ -2011,11 +1993,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
-version = "0.21.1"
|
||||
+version = "0.22.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
|
||||
+checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449"
|
||||
dependencies = [
|
||||
- "webpki 0.21.4",
|
||||
+ "webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
|
||||
index 6d3ffe3..b47554d 100644
|
||||
--- a/cli/Cargo.toml
|
||||
+++ b/cli/Cargo.toml
|
||||
@@ -13,6 +13,6 @@ omegaupload-common = "0.1"
|
||||
|
||||
anyhow = "1"
|
||||
atty = "0.2"
|
||||
-clap = { version = "3.0.0-rc.7", features = ["derive"] }
|
||||
+clap = { version = "3", features = ["derive"] }
|
||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] }
|
||||
-rpassword = "5"
|
||||
\ No newline at end of file
|
||||
+rpassword = "5"
|
||||
--
|
||||
2.34.1
|
||||
|
41
test/LICENSE.md
Normal file
41
test/LICENSE.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
This folder contains mixed media that are under different licenses.
|
||||
- `music.mp3`, written by Kevin McLeod, was sourced at https://freepd.com under
|
||||
the CC0 1.0 Universal license. The file was trimmed to the first 30 seconds.
|
||||
- `movie.mp4` is a 10 second snippet of Big Buck Bunny sourced at
|
||||
http://bbb3d.renderfarming.net/download.html under the Attribution 3.0
|
||||
Unported (CC BY 3.0) license.
|
||||
- `movie.mkv` is identical to `movie.mp4` but transcoded as a `.mkv` file. It is
|
||||
under the same license as `movie.mp4`.
|
||||
- `image.png` has all rights reserved, with the sole exception of copying and
|
||||
distribution for testing purposes for this project only.
|
||||
- `image.png.gz` is under the same license as `image.png`.
|
||||
- `image.webp` is under the same license as `image.png`.
|
||||
- All other files are dual-licensed under the CC0 1.0 Universal License or MIT
|
||||
No Attribution License, at your convenience.
|
||||
|
||||
The CC BY 3.0 License may be found at
|
||||
https://creativecommons.org/licenses/by/3.0/.
|
||||
|
||||
The CC0 1.0 Universal License is may be found at
|
||||
https://creativecommons.org/publicdomain/zero/1.0/legalcode.
|
||||
|
||||
The MIT No Attribution is displayed below:
|
||||
|
||||
```
|
||||
MIT No Attribution
|
||||
|
||||
Copyright 2021 Edward Shen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
```
|
BIN
test/archive.zip
Normal file
BIN
test/archive.zip
Normal file
Binary file not shown.
229
test/code.rs
Normal file
229
test/code.rs
Normal file
|
@ -0,0 +1,229 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use argon2::Argon2;
|
||||
use chacha20poly1305::aead::generic_array::sequence::GenericSequence;
|
||||
use chacha20poly1305::aead::generic_array::GenericArray;
|
||||
use chacha20poly1305::aead::{AeadInPlace, NewAead};
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
use chacha20poly1305::XNonce;
|
||||
use rand::{thread_rng, Rng};
|
||||
use secrecy::{ExposeSecret, Secret, SecretVec, Zeroize};
|
||||
use typenum::Unsigned;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid password.")]
|
||||
Password,
|
||||
#[error("Invalid secret key.")]
|
||||
SecretKey,
|
||||
#[error("An error occurred while trying to decrypt the blob.")]
|
||||
Encryption,
|
||||
#[error("An error occurred while trying to derive a secret key.")]
|
||||
Kdf,
|
||||
}
|
||||
|
||||
// This struct intentionally prevents implement Clone or Copy
|
||||
#[derive(Default)]
|
||||
pub struct Key(chacha20poly1305::Key);
|
||||
|
||||
impl Key {
|
||||
pub fn new_secret(vec: Vec<u8>) -> Option<Secret<Self>> {
|
||||
chacha20poly1305::Key::from_exact_iter(vec.into_iter())
|
||||
.map(Self)
|
||||
.map(Secret::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<chacha20poly1305::Key> for Key {
|
||||
fn as_ref(&self) -> &chacha20poly1305::Key {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Key {
|
||||
type Target = chacha20poly1305::Key;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl DerefMut for Key {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Zeroize for Key {
|
||||
fn zeroize(&mut self) {
|
||||
self.0.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
/// Seals the provided message with an optional message. The resulting sealed
|
||||
/// message has the nonce used to encrypt the message appended to it as well as
|
||||
/// a salt string used to derive the key. In other words, the modified buffer is
|
||||
/// one of the following to possibilities, depending if there was a password
|
||||
/// provided:
|
||||
///
|
||||
/// ```
|
||||
/// modified = C(message, rng_key, nonce) || nonce
|
||||
/// ```
|
||||
/// or
|
||||
/// ```
|
||||
/// modified = C(C(message, rng_key, nonce), kdf(pw, salt), nonce + 1) || nonce || salt
|
||||
/// ```
|
||||
///
|
||||
/// Where:
|
||||
/// - `C(message, key, nonce)` represents encrypting a provided message with
|
||||
/// `XChaCha20Poly1305`.
|
||||
/// - `rng_key` represents a randomly generated key.
|
||||
/// - `kdf(pw, salt)` represents a key derived from Argon2.
|
||||
pub fn seal_in_place(
|
||||
message: &mut Vec<u8>,
|
||||
pw: Option<SecretVec<u8>>,
|
||||
) -> Result<Secret<Key>, Error> {
|
||||
let (key, nonce) = gen_key_nonce();
|
||||
let cipher = XChaCha20Poly1305::new(key.expose_secret());
|
||||
cipher
|
||||
.encrypt_in_place(&nonce, &[], message)
|
||||
.map_err(|_| Error::Encryption)?;
|
||||
|
||||
let mut maybe_salt_string = None;
|
||||
if let Some(password) = pw {
|
||||
let (key, salt_string) = kdf(&password).map_err(|_| Error::Kdf)?;
|
||||
maybe_salt_string = Some(salt_string);
|
||||
let cipher = XChaCha20Poly1305::new(key.expose_secret());
|
||||
cipher
|
||||
.encrypt_in_place(&nonce.increment(), &[], message)
|
||||
.map_err(|_| Error::Encryption)?;
|
||||
}
|
||||
|
||||
message.extend_from_slice(nonce.as_slice());
|
||||
if let Some(maybe_salted_string) = maybe_salt_string {
|
||||
message.extend_from_slice(maybe_salted_string.as_ref());
|
||||
}
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn open_in_place(
|
||||
data: &mut Vec<u8>,
|
||||
key: &Secret<Key>,
|
||||
password: Option<SecretVec<u8>>,
|
||||
) -> Result<(), Error> {
|
||||
let pw_key = if let Some(password) = password {
|
||||
let salt_buf = data.split_off(data.len() - Salt::SIZE);
|
||||
let argon = Argon2::default();
|
||||
let mut pw_key = Key::default();
|
||||
argon
|
||||
.hash_password_into(password.expose_secret(), &salt_buf, &mut pw_key)
|
||||
.map_err(|_| Error::Kdf)?;
|
||||
Some(Secret::new(pw_key))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let nonce = Nonce::from_slice(&data.split_off(data.len() - Nonce::SIZE));
|
||||
|
||||
// At this point we should have a buffer that's only the ciphertext.
|
||||
|
||||
if let Some(key) = pw_key {
|
||||
let cipher = XChaCha20Poly1305::new(key.expose_secret());
|
||||
cipher
|
||||
.decrypt_in_place(&nonce.increment(), &[], data)
|
||||
.map_err(|_| Error::Password)?;
|
||||
}
|
||||
|
||||
let cipher = XChaCha20Poly1305::new(key.expose_secret());
|
||||
cipher
|
||||
.decrypt_in_place(&nonce, &[], data)
|
||||
.map_err(|_| Error::SecretKey)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Securely generates a random key and nonce.
|
||||
#[must_use]
|
||||
fn gen_key_nonce() -> (Secret<Key>, Nonce) {
|
||||
let mut rng = thread_rng();
|
||||
let mut key = GenericArray::default();
|
||||
rng.fill(key.as_mut_slice());
|
||||
let mut nonce = Nonce::default();
|
||||
rng.fill(nonce.as_mut_slice());
|
||||
(Secret::new(Key(key)), nonce)
|
||||
}
|
||||
|
||||
// Type alias; to ensure that we're consistent on what the inner impl is.
|
||||
type NonceImpl = XNonce;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
struct Nonce(NonceImpl);
|
||||
|
||||
impl Default for Nonce {
|
||||
fn default() -> Self {
|
||||
Self(GenericArray::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Nonce {
|
||||
type Target = NonceImpl;
|
||||
|
||||
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 {
|
||||
const SIZE: usize = <NonceImpl as GenericSequence<_>>::Length::USIZE;
|
||||
|
||||
#[must_use]
|
||||
pub fn increment(&self) -> Self {
|
||||
let mut inner = self.0;
|
||||
inner.as_mut_slice()[0] += 1;
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_slice(slice: &[u8]) -> Self {
|
||||
Self(*NonceImpl::from_slice(slice))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
struct Salt([u8; Self::SIZE]);
|
||||
|
||||
impl Salt {
|
||||
const SIZE: usize = argon2::password_hash::Salt::RECOMMENDED_LENGTH;
|
||||
|
||||
fn random() -> Self {
|
||||
let mut salt = [0_u8; Self::SIZE];
|
||||
thread_rng().fill(&mut salt);
|
||||
Self(salt)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Salt {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes an input to output a usable key.
|
||||
fn kdf(password: &SecretVec<u8>) -> Result<(Secret<Key>, Salt), argon2::Error> {
|
||||
let salt = Salt::random();
|
||||
let hasher = Argon2::default();
|
||||
let mut key = Key::default();
|
||||
hasher.hash_password_into(password.expose_secret().as_ref(), salt.as_ref(), &mut key)?;
|
||||
|
||||
Ok((Secret::new(key), salt))
|
||||
}
|
BIN
test/image.png
Normal file
BIN
test/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 166 KiB |
BIN
test/image.png.gz
Normal file
BIN
test/image.png.gz
Normal file
Binary file not shown.
21
test/image.svg
Normal file
21
test/image.svg
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 4417 3259" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||
<g transform="matrix(4.16667,0,0,4.16667,0,0)">
|
||||
<path d="M525.403,293.05C393.77,293.05 274.175,308.875 185.633,334.665L185.633,554.963C274.175,580.753 393.77,596.577 525.403,596.577C676.06,596.577 810.938,575.848 901.537,543.175L901.537,346.457C810.938,313.781 676.06,293.05 525.403,293.05Z" style="fill:rgb(143,30,28);fill-rule:nonzero;"/>
|
||||
<path d="M907.423,492.442C903.566,481.779 902.794,468.288 906.062,455.28C911.912,431.991 928.483,419.082 943.075,426.447C946.693,428.274 949.849,431.178 952.462,434.865C952.701,434.864 952.94,434.865 953.177,434.881C953.177,434.881 997.729,487.987 956.49,550.884C955.595,554.453 879.956,642.602 862.447,645.408C850.987,647.244 877.338,555.41 907.423,492.442Z" style="fill:rgb(143,30,28);fill-rule:nonzero;"/>
|
||||
<path d="M176.479,482.021C181.779,472.391 183.637,459.233 180.696,445.596C175.43,421.18 156.786,404.486 139.054,408.311C134.656,409.259 130.729,411.383 127.388,414.409C127.106,414.351 126.824,414.296 126.543,414.256C126.543,414.256 70.251,456.208 114.486,528.18C115.291,531.921 198.337,637.018 218.797,643.943C232.188,648.475 207.55,551.418 176.479,482.021Z" style="fill:rgb(143,30,28);fill-rule:nonzero;"/>
|
||||
<path d="M97.467,488.066L97.474,488.081C97.659,488.226 97.831,488.357 97.467,488.066Z" style="fill:rgb(227,58,37);fill-rule:nonzero;"/>
|
||||
<path d="M993.119,412.903C992.239,409.839 991.363,406.777 990.457,403.741L1021.14,359.29C1024.27,354.768 1024.91,348.892 1022.87,343.735C1020.83,338.605 1016.38,334.925 1011.11,334.025L959.224,325.22C957.216,321.118 955.108,317.078 952.994,313.07L974.791,263.167C977.034,258.08 976.56,252.172 973.588,247.559C970.627,242.923 965.598,240.215 960.239,240.426L907.583,242.339C904.856,238.789 902.087,235.271 899.261,231.818L911.362,178.328C912.587,172.895 911.04,167.21 907.259,163.264C903.497,159.332 898.03,157.705 892.833,158.981L841.544,171.589C838.223,168.654 834.845,165.756 831.43,162.916L833.278,108.002C833.476,102.443 830.885,97.161 826.434,94.077C821.988,90.973 816.341,90.504 811.478,92.811L763.631,115.558C759.777,113.348 755.903,111.158 751.987,109.041L743.532,54.926C742.675,49.444 739.147,44.788 734.206,42.661C729.283,40.523 723.638,41.213 719.315,44.469L676.656,76.476C672.456,75.08 668.237,73.743 663.964,72.465L645.578,21.148C643.708,15.919 639.397,12.077 634.14,10.997C628.901,9.926 623.51,11.74 619.877,15.799L583.97,55.971C579.628,55.471 575.285,55.015 570.927,54.639L543.204,7.926C540.394,3.194 535.434,0.314 530.088,0.314C524.754,0.314 519.784,3.194 516.998,7.926L489.265,54.639C484.907,55.015 480.543,55.471 476.209,55.971L440.299,15.799C436.663,11.74 431.252,9.926 426.031,10.997C420.776,12.089 416.458,15.919 414.598,21.148L396.196,72.465C391.936,73.743 387.715,75.092 383.505,76.476L340.861,44.469C336.525,41.203 330.881,40.514 325.945,42.661C321.026,44.788 317.484,49.444 316.632,54.926L308.171,109.041C304.257,111.158 300.382,113.335 296.518,115.558L248.676,92.811C243.818,90.496 238.147,90.973 233.722,94.077C229.277,97.161 226.68,102.443 226.882,108.002L228.717,162.916C225.312,165.756 221.943,168.654 218.605,171.589L167.326,158.981C162.115,157.716 156.656,159.332 152.885,163.264C149.09,167.21 147.553,172.895 148.772,178.328L160.851,231.818C158.049,235.285 155.276,238.789 152.558,242.339L99.903,240.426C94.588,240.269 89.516,242.923 86.547,247.559C83.572,252.172 83.122,258.08 85.336,263.167L107.15,313.07C105.031,317.078 102.926,321.118 100.901,325.22L49.018,334.025C43.747,334.913 39.304,338.591 37.254,343.735C35.217,348.892 35.878,354.768 38.989,359.29L69.679,403.741C69.442,404.525 69.224,405.317 68.989,406.105L52.126,424.017L97.467,488.066C97.467,488.066 532.619,688.798 936.264,491.462C982.372,483.189 993.119,412.903 993.119,412.903Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
|
||||
<path d="M608.303,376.759C608.303,376.759 656.46,324.03 704.618,376.759C704.618,376.759 742.458,447.071 704.618,482.222C704.618,482.222 642.701,531.439 608.303,482.222C608.303,482.222 567.024,443.55 608.303,376.759Z" style="fill:rgb(3,4,4);fill-rule:nonzero;"/>
|
||||
<path d="M664.057,396.32C664.057,416.853 651.954,433.499 637.027,433.499C622.103,433.499 610,416.853 610,396.32C610,375.788 622.103,359.14 637.027,359.14C651.954,359.14 664.057,375.788 664.057,396.32Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M393.365,362.361C393.365,362.361 475.973,325.785 498.519,407.423C498.519,407.423 522.137,502.577 430.682,507.948C430.682,507.948 314.06,485.486 393.365,362.361Z" style="fill:rgb(3,4,4);fill-rule:nonzero;"/>
|
||||
<path d="M434.855,397.668C434.855,418.841 422.375,436.014 406.978,436.014C391.587,436.014 379.104,418.841 379.104,397.668C379.104,376.49 391.587,359.322 406.978,359.322C422.375,359.322 434.855,376.49 434.855,397.668Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M111.602,499.216C122.569,486.753 149.213,471.659 147.172,452.934C143.519,419.407 115.716,394.935 85.073,398.275C77.473,399.103 70.415,401.567 64.149,405.311C63.687,405.204 63.224,405.1 62.761,405.017C62.761,405.017 -40.87,455.89 18.197,557.674C18.754,562.811 136.045,713.342 168.985,724.805C190.544,732.307 149.074,596.165 111.602,499.216Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
|
||||
<path d="M953.549,494.673C940.856,483.973 907.387,474.255 906.629,455.435C905.273,421.737 929.141,393.414 959.941,392.175C967.579,391.867 974.925,393.258 981.676,396.032C982.118,395.858 982.56,395.686 983.005,395.535C983.005,395.535 1093.03,430.486 1049.7,539.901C1049.91,545.064 956.232,711.317 925.355,727.536C905.146,738.151 930.861,596.105 953.549,494.673Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
|
||||
<path d="M191.142,495.558C191.142,495.558 189.759,632.854 324.308,663.49L352.362,607.127C352.362,607.127 254.867,616.558 247.367,495.558L191.142,495.558Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
|
||||
<path d="M876.362,495.558C876.362,495.558 877.744,632.854 743.195,663.49L715.141,607.127C715.141,607.127 812.636,616.558 820.136,495.558L876.362,495.558Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
|
||||
<path d="M779.167,635.591C758.917,586.649 693.572,567.218 633.216,592.191C580.09,614.172 548.579,663.223 555.592,708.036C597.538,707.384 642.532,704.665 686.328,698.318C686.328,698.318 660.491,740.081 622.471,776.529C648.037,783.128 677.854,781.297 706.547,769.425C766.904,744.452 799.417,684.532 779.167,635.591Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
|
||||
<path d="M404.746,695.984C404.746,695.984 459.949,703.279 535.416,705.14C542.026,657.629 506.036,607.348 448.615,587.897C385.177,566.409 319.626,590.689 302.201,642.129C284.776,693.569 322.077,752.689 385.515,774.178C413.636,783.704 442.168,784.227 466.744,777.385C429.833,740.88 404.746,695.984 404.746,695.984Z" style="fill:rgb(228,58,37);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.1 KiB |
BIN
test/image.webp
Normal file
BIN
test/image.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
BIN
test/movie.mkv
Normal file
BIN
test/movie.mkv
Normal file
Binary file not shown.
BIN
test/movie.mp4
Normal file
BIN
test/movie.mp4
Normal file
Binary file not shown.
BIN
test/music.mp3
Normal file
BIN
test/music.mp3
Normal file
Binary file not shown.
BIN
test/omegaupload
Executable file
BIN
test/omegaupload
Executable file
Binary file not shown.
3
test/text.pgp
Normal file
3
test/text.pgp
Normal file
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PGP MESSAGE-----
|
||||
-----END PGP MESSAGE-----
|
||||
|
6
web/.gitignore
vendored
6
web/.gitignore
vendored
|
@ -1,6 +0,0 @@
|
|||
node_modules
|
||||
/dist
|
||||
/target
|
||||
/pkg
|
||||
/wasm-pack.log
|
||||
yarn-error.log
|
|
@ -1,11 +1,6 @@
|
|||
# You must change these to your own details.
|
||||
[package]
|
||||
name = "rust-webpack-template"
|
||||
description = "My super awesome Rust, WebAssembly, and Webpack project!"
|
||||
name = "omegaupload-web"
|
||||
version = "0.1.0"
|
||||
authors = ["You <you@example.com>"]
|
||||
categories = ["wasm"]
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
@ -14,23 +9,30 @@ crate-type = ["cdylib"]
|
|||
[dependencies]
|
||||
omegaupload-common = { path = "../common", features = ["wasm"] }
|
||||
# Enables wasm support
|
||||
getrandom = { version = "*", features = ["js"] }
|
||||
getrandom = { version = "0.2.7", features = ["js"] }
|
||||
|
||||
anyhow = "1"
|
||||
bytes = "1"
|
||||
byte-unit = "4"
|
||||
console_error_panic_hook = "0.1"
|
||||
gloo-console = "0.1"
|
||||
http = "0.2"
|
||||
js-sys = "0.3"
|
||||
reqwasm = "0.2"
|
||||
tree_magic_mini = { version = "3", features = ["with-gpl-data"] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
anyhow = "1.0.58"
|
||||
bytes = "1.2.0"
|
||||
byte-unit = "4.0.14"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
gloo-console = "0.3"
|
||||
http = "0.2.8"
|
||||
js-sys = "0.3.59"
|
||||
mime_guess = "2.0.4"
|
||||
tree_magic_mini = { version = "3.0.3", features = ["with-gpl-data"] }
|
||||
serde = { version = "1.0.140", features = ["derive"] }
|
||||
serde-wasm-bindgen = { version = "0.6" }
|
||||
wasm-bindgen = { version = "0.2.82", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4.32"
|
||||
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
|
||||
flate2 = "1.0.24"
|
||||
tar = "0.4.38"
|
||||
reqwest = "0.11"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
version = "0.3.59"
|
||||
features = [
|
||||
"BlobPropertyBag",
|
||||
"TextDecoder",
|
||||
"IdbFactory",
|
||||
"IdbOpenDbRequest",
|
||||
|
@ -47,6 +49,3 @@ features = [
|
|||
"Performance",
|
||||
"Location",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.2.45"
|
||||
|
|
674
web/LICENSE
Normal file
674
web/LICENSE
Normal file
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
@ -1,48 +1,10 @@
|
|||
## How to install
|
||||
Contains the codebase used for the frontend
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
Notes on licensing:
|
||||
|
||||
## How to run in debug mode
|
||||
https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility
|
||||
|
||||
```sh
|
||||
# Builds the project and opens it in a new browser tab. Auto-reloads when the project changes.
|
||||
npm start
|
||||
```
|
||||
Because there is a statically linked in dependency on `shared-mime-types`, this
|
||||
crate MUST be under a GPLv2 or later license. This has been confirmed as of
|
||||
2021-10-24.
|
||||
|
||||
## How to build in release mode
|
||||
|
||||
```sh
|
||||
# Builds the project and places it into the `dist` folder.
|
||||
npm run build
|
||||
```
|
||||
|
||||
## How to run unit tests
|
||||
|
||||
```sh
|
||||
# Runs tests in Firefox
|
||||
npm test -- --firefox
|
||||
|
||||
# Runs tests in Chrome
|
||||
npm test -- --chrome
|
||||
|
||||
# Runs tests in Safari
|
||||
npm test -- --safari
|
||||
```
|
||||
|
||||
## What does each file do?
|
||||
|
||||
* `Cargo.toml` contains the standard Rust metadata. You put your Rust dependencies in here. You must change this file with your details (name, description, version, authors, categories)
|
||||
|
||||
* `package.json` contains the standard npm metadata. You put your JavaScript dependencies in here. You must change this file with your details (author, name, version)
|
||||
|
||||
* `webpack.config.js` contains the Webpack configuration. You shouldn't need to change this, unless you have very special needs.
|
||||
|
||||
* The `js` folder contains your JavaScript code (`index.js` is used to hook everything into Webpack, you don't need to change it).
|
||||
|
||||
* The `src` folder contains your Rust code.
|
||||
|
||||
* The `static` folder contains any files that you want copied as-is into the final build. It contains an `index.html` file which loads the `index.js` file.
|
||||
|
||||
* The `tests` folder contains your Rust unit tests.
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
import { renderMessage } from './ui';
|
||||
import * as index from "../pkg/index.js";
|
||||
renderMessage("wtf");
|
||||
console.log(index);
|
196
web/js/ui.js
196
web/js/ui.js
|
@ -1,196 +0,0 @@
|
|||
import hljs from 'hljs';
|
||||
|
||||
window.addEventListener("hashchange", () => location.reload());
|
||||
|
||||
function loadFromDb() {
|
||||
const dbReq = window.indexedDB.open("omegaupload", 1);
|
||||
dbReq.onsuccess = (evt) => {
|
||||
const db = evt.target.result;
|
||||
const obj_store = db
|
||||
.transaction("decrypted data")
|
||||
.objectStore("decrypted data");
|
||||
let fetchReq = obj_store.get(window.location.pathname);
|
||||
fetchReq.onsuccess = (evt) => {
|
||||
const data = evt.target.result;
|
||||
switch (data.type) {
|
||||
case "string":
|
||||
createStringPasteUi(data);
|
||||
break;
|
||||
case "blob":
|
||||
createBlobPasteUi(data);
|
||||
break;
|
||||
case "image":
|
||||
createImagePasteUi(data);
|
||||
break;
|
||||
case "audio":
|
||||
createAudioPasteUi(data);
|
||||
break;
|
||||
case "video":
|
||||
createVideoPasteUi(data);
|
||||
break;
|
||||
default:
|
||||
renderMessage("Something went wrong. Try clearing local data.");
|
||||
break;
|
||||
}
|
||||
|
||||
// IDB was only used as a temporary medium;
|
||||
window.onbeforeunload = (e) => {
|
||||
// See https://link.eddie.sh/NrIIq on why .commit is necessary.
|
||||
const transaction = db.transaction("decrypted data", "readwrite");
|
||||
transaction
|
||||
.objectStore("decrypted data")
|
||||
.delete(window.location.pathname);
|
||||
transaction.commit();
|
||||
transaction.oncomplete = () => {
|
||||
console.log("Item deleted from cache");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
fetchReq.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 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);
|
||||
}
|
||||
|
||||
function createImagePasteUi({ expiration, data, file_size }) {
|
||||
createMultiMediaPasteUi("img", expiration, data, (downloadEle, imgEle) => {
|
||||
imgEle.onload = () => {
|
||||
downloadEle.textContent = "Download " + file_size + " \u2014 " + imgEle.naturalWidth + " by " + imgEle.naturalHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createAudioPasteUi({ expiration, data }) {
|
||||
createMultiMediaPasteUi("audio", expiration, data, "Download");
|
||||
}
|
||||
|
||||
function createVideoPasteUi({ expiration, data }) {
|
||||
createMultiMediaPasteUi("video", expiration, data, "Download");
|
||||
}
|
||||
|
||||
function createMultiMediaPasteUi(tag, expiration, data, on_create) {
|
||||
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);
|
||||
|
||||
let expirationEle = document.createElement("p");
|
||||
expirationEle.textContent = expiration;
|
||||
mainEle.appendChild(expirationEle);
|
||||
|
||||
let mediaEle = document.createElement(tag);
|
||||
mediaEle.src = downloadLink;
|
||||
mediaEle.controls = true;
|
||||
mainEle.appendChild(mediaEle);
|
||||
|
||||
let downloadEle = document.createElement("a");
|
||||
downloadEle.href = downloadLink;
|
||||
downloadEle.download = window.location.pathname;
|
||||
downloadEle.classList.add("hljs-meta");
|
||||
mainEle.appendChild(downloadEle);
|
||||
|
||||
bodyEle.appendChild(mainEle);
|
||||
|
||||
if (on_create instanceof Function) {
|
||||
on_create(downloadEle, mediaEle);
|
||||
} else {
|
||||
downloadEle.textContent = on_create;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMessage(message) {
|
||||
let body = document.getElementsByTagName("body")[0];
|
||||
body.textContent = '';
|
||||
let mainEle = document.createElement("main");
|
||||
mainEle.classList.add("hljs");
|
||||
mainEle.classList.add("centered");
|
||||
mainEle.classList.add("fullscreen");
|
||||
mainEle.textContent = message;
|
||||
body.appendChild(mainEle);
|
||||
}
|
||||
|
||||
function renderIndex() {
|
||||
console.log("index");
|
||||
// TODO: find a way to not hard code this.
|
||||
// https://docs.rs/chacha20poly1305/0.9.0/chacha20poly1305/type.Key.html
|
||||
// https://docs.rs/chacha20poly1305/0.9.0/chacha20poly1305/type.XNonce.html
|
||||
const key = crypto.getRandomValues(new Uint8Array(32));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(24));
|
||||
console.log(key, nonce);
|
||||
}
|
||||
|
||||
|
||||
export { renderIndex, renderMessage, loadFromDb };
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"author": "Edward Shen <code@eddie.sh>",
|
||||
"name": "omegaupload-web",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "rimraf dist pkg && webpack",
|
||||
"start": "rimraf dist pkg && webpack-dev-server --open",
|
||||
"test": "cargo test && wasm-pack test --headless"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.1.0",
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"rimraf": "^3.0.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"webpack": "^5.60.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"hljs": "^6.2.3",
|
||||
"typescript": "^4.4.4"
|
||||
}
|
||||
}
|
33
web/src/bg_encrypt.ts
Normal file
33
web/src/bg_encrypt.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// OmegaUpload Web Frontend
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { encrypt_array_buffer } from '../pkg';
|
||||
|
||||
interface BgData {
|
||||
location: string,
|
||||
data: any
|
||||
}
|
||||
|
||||
addEventListener('message', (event: MessageEvent<BgData>) => {
|
||||
let { location, data } = event.data;
|
||||
console.log('[js-worker] Sending data to rust in a worker thread...');
|
||||
encrypt_array_buffer(location, data).then(url => {
|
||||
console.log("[js-worker] Encryption done.");
|
||||
postMessage(url);
|
||||
}).catch(e => console.error(e));
|
||||
})
|
||||
|
||||
postMessage("init");
|
|
@ -1,11 +1,35 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
// OmegaUpload Web Frontend
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gloo_console::log;
|
||||
use js_sys::{Array, Uint8Array};
|
||||
use omegaupload_common::crypto::{open_in_place, Key, Nonce};
|
||||
use omegaupload_common::crypto::{open_in_place, Error, Key};
|
||||
use omegaupload_common::secrecy::{Secret, SecretVec};
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::Blob;
|
||||
use web_sys::{Blob, BlobPropertyBag};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct ArchiveMeta {
|
||||
name: String,
|
||||
file_size: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum DecryptedData {
|
||||
|
@ -14,6 +38,7 @@ pub enum DecryptedData {
|
|||
Image(Arc<Blob>, usize),
|
||||
Audio(Arc<Blob>),
|
||||
Video(Arc<Blob>),
|
||||
Archive(Arc<Blob>, Vec<ArchiveMeta>),
|
||||
}
|
||||
|
||||
fn now() -> f64 {
|
||||
|
@ -24,82 +49,201 @@ fn now() -> f64 {
|
|||
.now()
|
||||
}
|
||||
|
||||
pub struct MimeType(pub String);
|
||||
|
||||
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 started.");
|
||||
let start = now();
|
||||
key: &Secret<Key>,
|
||||
maybe_password: Option<SecretVec<u8>>,
|
||||
name_hint: Option<&str>,
|
||||
) -> Result<(DecryptedData, MimeType), Error> {
|
||||
open_in_place(&mut container, key, maybe_password)?;
|
||||
|
||||
if let Some(password) = maybe_password {
|
||||
crate::render_message("Decrypting Stage 1...".into());
|
||||
open_in_place(container, &nonce.increment(), &password).map_err(|_| {
|
||||
crate::render_message("Unable to decrypt paste with the provided password.".into());
|
||||
PasteCompleteConstructionError::StageOneFailure
|
||||
})?;
|
||||
let mime_type = guess_mime_type(name_hint, &container);
|
||||
log!("[rs] Mime type:", mime_type);
|
||||
|
||||
log!("[rs] Blob conversion started.");
|
||||
let start = now();
|
||||
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());
|
||||
}
|
||||
log!(format!("Stage 1 completed in {}ms", now() - start));
|
||||
let mut blob_props = BlobPropertyBag::new();
|
||||
blob_props.type_(mime_type);
|
||||
let blob = Arc::new(
|
||||
Blob::new_with_u8_array_sequence_and_options(blob_chunks.dyn_ref().unwrap(), &blob_props)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
log!("Stage 2 decryption started.");
|
||||
let start = now();
|
||||
crate::render_message("Decrypting Stage 2...".into());
|
||||
open_in_place(container, &nonce, &key).map_err(|_| {
|
||||
crate::render_message(
|
||||
"Unable to decrypt paste with the provided encryption key and nonce.".into(),
|
||||
);
|
||||
PasteCompleteConstructionError::StageTwoFailure
|
||||
})?;
|
||||
log!(format!("Stage 2 completed in {}ms", now() - start));
|
||||
log!(format!(
|
||||
"[rs] Blob conversion completed in {}ms",
|
||||
now() - start
|
||||
));
|
||||
|
||||
if let Ok(decrypted) = std::str::from_utf8(container) {
|
||||
Ok(DecryptedData::String(Arc::new(decrypted.to_owned())))
|
||||
let data = match container.content_type() {
|
||||
ContentType::Text => DecryptedData::String(Arc::new(
|
||||
// SAFETY: ContentType::Text is guaranteed to be valid UTF-8.
|
||||
unsafe { String::from_utf8_unchecked(container) },
|
||||
)),
|
||||
ContentType::Image => DecryptedData::Image(blob, container.len()),
|
||||
ContentType::Audio => DecryptedData::Audio(blob),
|
||||
ContentType::Video => DecryptedData::Video(blob),
|
||||
ContentType::ZipArchive => handle_zip_archive(blob, container),
|
||||
ContentType::Gzip => handle_gzip(blob, container),
|
||||
ContentType::Unknown => DecryptedData::Blob(blob),
|
||||
};
|
||||
|
||||
Ok((data, MimeType(mime_type.to_owned())))
|
||||
}
|
||||
|
||||
fn handle_zip_archive(blob: Arc<Blob>, container: Vec<u8>) -> DecryptedData {
|
||||
let mut entries = vec![];
|
||||
let cursor = Cursor::new(container);
|
||||
if let Ok(mut zip) = zip::ZipArchive::new(cursor) {
|
||||
for i in 0..zip.len() {
|
||||
match zip.by_index(i) {
|
||||
Ok(file) => entries.push(ArchiveMeta {
|
||||
name: file.name().to_string(),
|
||||
file_size: file.size(),
|
||||
}),
|
||||
Err(err) => match err {
|
||||
zip::result::ZipError::UnsupportedArchive(s) => {
|
||||
log!("Unsupported: ", s.to_string());
|
||||
}
|
||||
_ => {
|
||||
log!(format!("Error: {err}"));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
DecryptedData::Archive(blob, entries)
|
||||
}
|
||||
|
||||
fn handle_gzip(blob: Arc<Blob>, container: Vec<u8>) -> DecryptedData {
|
||||
let mut entries = vec![];
|
||||
let cursor = Cursor::new(container);
|
||||
let gzip_dec = flate2::read::GzDecoder::new(cursor);
|
||||
let mut archive = tar::Archive::new(gzip_dec);
|
||||
if let Ok(files) = archive.entries() {
|
||||
for file in files.flatten() {
|
||||
let file_path = if let Ok(file_path) = file.path() {
|
||||
file_path.display().to_string()
|
||||
} else {
|
||||
"<Invalid utf-8 path>".to_string()
|
||||
};
|
||||
entries.push(ArchiveMeta {
|
||||
name: file_path,
|
||||
file_size: file.size(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if entries.is_empty() {
|
||||
DecryptedData::Blob(blob)
|
||||
} else {
|
||||
log!("Blob conversion started.");
|
||||
let start = now();
|
||||
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());
|
||||
DecryptedData::Archive(blob, entries)
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_mime_type(name_hint: Option<&str>, data: &[u8]) -> &'static str {
|
||||
if let Some(name) = name_hint {
|
||||
let guesses = mime_guess::from_path(name);
|
||||
if let Some(mime_type) = guesses.first_raw() {
|
||||
// Found at least one, but generally speaking this crate only
|
||||
// uses authoritative sources (RFCs), so generally speaking
|
||||
// there's only one association, and multiple are due to legacy
|
||||
// support. As a result, we can probably just get the first one.
|
||||
log!("[rs] Mime type inferred from extension.");
|
||||
return mime_type;
|
||||
}
|
||||
let blob =
|
||||
Arc::new(Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap());
|
||||
log!(format!("Blob conversion completed in {}ms", now() - start));
|
||||
log!("[rs] No mime type found for extension, falling back to introspection.");
|
||||
}
|
||||
tree_magic_mini::from_u8(data)
|
||||
}
|
||||
|
||||
let mime_type = tree_magic_mini::from_u8(container);
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum ContentType {
|
||||
Text,
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
ZipArchive,
|
||||
Gzip,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
if mime_type.starts_with("image/") || mime_type == "application/x-riff" {
|
||||
Ok(DecryptedData::Image(blob, container.len()))
|
||||
trait ContentTypeExt {
|
||||
fn mime_type(&self) -> &str;
|
||||
fn content_type(&self) -> ContentType;
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> ContentTypeExt for T {
|
||||
fn mime_type(&self) -> &str {
|
||||
tree_magic_mini::from_u8(self.as_ref())
|
||||
}
|
||||
|
||||
fn content_type(&self) -> ContentType {
|
||||
let mime_type = self.mime_type();
|
||||
// check image first; tree magic match_u8 matches SVGs as plain text
|
||||
if mime_type.starts_with("image/")
|
||||
// application/x-riff is WebP
|
||||
|| mime_type == "application/x-riff"
|
||||
{
|
||||
ContentType::Image
|
||||
} else if tree_magic_mini::match_u8("text/plain", self.as_ref()) {
|
||||
if std::str::from_utf8(self.as_ref()).is_ok() {
|
||||
ContentType::Text
|
||||
} else {
|
||||
ContentType::Unknown
|
||||
}
|
||||
} else if mime_type.starts_with("audio/") {
|
||||
Ok(DecryptedData::Audio(blob))
|
||||
} else if mime_type.starts_with("video/") || mime_type == "application/x-matroska" {
|
||||
Ok(DecryptedData::Video(blob))
|
||||
ContentType::Audio
|
||||
} else if mime_type.starts_with("video/")
|
||||
// application/x-matroska is mkv
|
||||
|| mime_type == "application/x-matroska"
|
||||
{
|
||||
ContentType::Video
|
||||
} else if mime_type == "application/zip" {
|
||||
ContentType::ZipArchive
|
||||
} else if mime_type == "application/gzip" {
|
||||
ContentType::Gzip
|
||||
} else {
|
||||
Ok(DecryptedData::Blob(blob))
|
||||
ContentType::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PasteCompleteConstructionError {
|
||||
StageOneFailure,
|
||||
StageTwoFailure,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod content_type {
|
||||
use super::*;
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
macro_rules! test_content_type {
|
||||
($($name:ident, $path:literal, $type:expr),*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
let data = include_bytes!(concat!("../../test/", $path));
|
||||
assert_eq!(data.content_type(), $type);
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_content_type!(license_is_text, "LICENSE.md", ContentType::Text);
|
||||
test_content_type!(code_is_text, "code.rs", ContentType::Text);
|
||||
test_content_type!(patch_is_text, "0000-test-patch.patch", ContentType::Text);
|
||||
test_content_type!(png_is_image, "image.png", ContentType::Image);
|
||||
test_content_type!(webp_is_image, "image.webp", ContentType::Image);
|
||||
test_content_type!(svg_is_image, "image.svg", ContentType::Image);
|
||||
test_content_type!(mp3_is_audio, "music.mp3", ContentType::Audio);
|
||||
test_content_type!(mp4_is_video, "movie.mp4", ContentType::Video);
|
||||
test_content_type!(mkv_is_video, "movie.mkv", ContentType::Video);
|
||||
test_content_type!(zip_is_zip, "archive.zip", ContentType::ZipArchive);
|
||||
test_content_type!(gzip_is_gzip, "image.png.gz", ContentType::Gzip);
|
||||
test_content_type!(binary_is_unknown, "omegaupload", ContentType::Unknown);
|
||||
test_content_type!(pgp_is_text, "text.pgp", ContentType::Text);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,22 @@
|
|||
// OmegaUpload Web Frontend
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{hint::unreachable_unchecked, marker::PhantomData};
|
||||
|
||||
use gloo_console::log;
|
||||
use js_sys::{Array, JsString, Object};
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
|
@ -21,7 +38,10 @@ impl From<IdbObject<Ready>> for Object {
|
|||
Ok(o) => o,
|
||||
// SAFETY: IdbObject maintains the invariant that it can eventually
|
||||
// be constructed into a JS object.
|
||||
_ => unsafe { unreachable_unchecked() },
|
||||
_ => {
|
||||
log!("IdbObject invariant violated?!");
|
||||
unsafe { unreachable_unchecked() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +51,10 @@ impl IdbObject<NeedsType> {
|
|||
Self(Array::new(), PhantomData)
|
||||
}
|
||||
|
||||
pub fn archive(self) -> IdbObject<NeedsExpiration> {
|
||||
self.add_tuple("type", &JsString::from("archive"))
|
||||
}
|
||||
|
||||
pub fn video(self) -> IdbObject<NeedsExpiration> {
|
||||
self.add_tuple("type", &JsString::from("video"))
|
||||
}
|
||||
|
|
10
web/src/index.html
Normal file
10
web/src/index.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Omegaupload</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
|
||||
</html>
|
6
web/src/index.js
Normal file
6
web/src/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { start } from '../pkg';
|
||||
import './main.scss';
|
||||
|
||||
start();
|
||||
|
||||
window.addEventListener("hashchange", () => location.reload());
|
421
web/src/lib.rs
421
web/src/lib.rs
|
@ -1,20 +1,39 @@
|
|||
#![warn(clippy::nursery, clippy::pedantic)]
|
||||
|
||||
// OmegaUpload Web Frontend
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use byte_unit::{n_mib_bytes, Byte};
|
||||
use decrypt::DecryptedData;
|
||||
use decrypt::{DecryptedData, MimeType};
|
||||
use gloo_console::{error, log};
|
||||
use http::uri::PathAndQuery;
|
||||
use http::{StatusCode, Uri};
|
||||
use js_sys::{JsString, Object, Uint8Array};
|
||||
use omegaupload_common::crypto::{Key, Nonce};
|
||||
use omegaupload_common::{hash, Expiration, PartialParsedUrl};
|
||||
use reqwasm::http::Request;
|
||||
use js_sys::{Array, JsString, Object};
|
||||
use omegaupload_common::base64;
|
||||
use omegaupload_common::crypto::seal_in_place;
|
||||
use omegaupload_common::crypto::{Error as CryptoError, Key};
|
||||
use omegaupload_common::fragment::Builder;
|
||||
use omegaupload_common::secrecy::{ExposeSecret, Secret, SecretString, SecretVec};
|
||||
use omegaupload_common::{Expiration, PartialParsedUrl, Url};
|
||||
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window};
|
||||
|
||||
use crate::decrypt::decrypt;
|
||||
|
@ -27,14 +46,14 @@ mod util;
|
|||
|
||||
const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500);
|
||||
|
||||
#[wasm_bindgen(raw_module = "../js/ui.js")]
|
||||
#[wasm_bindgen(raw_module = "../src/render")]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = renderIndex)]
|
||||
pub fn render_index();
|
||||
#[wasm_bindgen(js_name = loadFromDb)]
|
||||
pub fn load_from_db();
|
||||
pub fn load_from_db(mime_type: JsString, name: Option<JsString>, language: Option<JsString>);
|
||||
#[wasm_bindgen(js_name = renderMessage)]
|
||||
pub fn render_message(message: JsString);
|
||||
#[wasm_bindgen(js_name = createUploadUi)]
|
||||
pub fn create_upload_ui();
|
||||
}
|
||||
|
||||
fn window() -> Window {
|
||||
|
@ -54,185 +73,203 @@ fn open_idb() -> Result<IdbOpenDbRequest> {
|
|||
.map_err(|_| anyhow!("Failed to open idb"))
|
||||
}
|
||||
|
||||
// This is like the `main` function, except for JavaScript.
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn js_main() {
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn start() {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
|
||||
if location().pathname().unwrap() == "/" {
|
||||
render_index();
|
||||
} else {
|
||||
render_message("Loading paste...".into());
|
||||
create_upload_ui();
|
||||
return;
|
||||
}
|
||||
|
||||
let url = String::from(location().to_string());
|
||||
let request_uri = {
|
||||
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
|
||||
if let Some(parts) = uri_parts.path_and_query.as_mut() {
|
||||
*parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap();
|
||||
}
|
||||
Uri::from_parts(uri_parts).unwrap()
|
||||
};
|
||||
render_message("Loading paste...".into());
|
||||
|
||||
log!(&url);
|
||||
log!(&request_uri.to_string());
|
||||
log!(&location().pathname().unwrap());
|
||||
let (key, nonce, needs_pw) = {
|
||||
let partial_parsed_url = url
|
||||
.split_once('#')
|
||||
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
|
||||
.unwrap_or_default();
|
||||
let key = if let Some(key) = partial_parsed_url.decryption_key {
|
||||
key
|
||||
} else {
|
||||
let url = String::from(location().to_string());
|
||||
let request_uri = {
|
||||
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
|
||||
if let Some(parts) = uri_parts.path_and_query.as_mut() {
|
||||
*parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap();
|
||||
}
|
||||
Uri::from_parts(uri_parts).unwrap()
|
||||
};
|
||||
|
||||
let (
|
||||
key,
|
||||
PartialParsedUrl {
|
||||
needs_password,
|
||||
name,
|
||||
language,
|
||||
..
|
||||
},
|
||||
) = {
|
||||
let fragment = if let Some(fragment) = url.split_once('#').map(|(_, fragment)| fragment) {
|
||||
if fragment.is_empty() {
|
||||
error!("Key is missing in url; bailing.");
|
||||
render_message("Invalid paste link: Missing decryption key.".into());
|
||||
render_message("Invalid paste link: Missing metadata.".into());
|
||||
return;
|
||||
};
|
||||
let nonce = if let Some(nonce) = partial_parsed_url.nonce {
|
||||
nonce
|
||||
} else {
|
||||
error!("Nonce is missing in url; bailing.");
|
||||
render_message("Invalid paste link: Missing nonce.".into());
|
||||
return;
|
||||
};
|
||||
(key, nonce, partial_parsed_url.needs_password)
|
||||
}
|
||||
fragment
|
||||
} else {
|
||||
error!("Key is missing in url; bailing.");
|
||||
render_message("Invalid paste link: Missing metadata.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
let password = if needs_pw {
|
||||
loop {
|
||||
let pw =
|
||||
window().prompt_with_message("A password is required to decrypt this paste:");
|
||||
let mut partial_parsed_url = match PartialParsedUrl::try_from(fragment) {
|
||||
Ok(partial_parsed_url) => partial_parsed_url,
|
||||
Err(e) => {
|
||||
error!("Failed to parse text fragment; bailing.");
|
||||
render_message(format!("Invalid paste link: {e}").into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(Some(password)) = pw {
|
||||
if !password.is_empty() {
|
||||
break Some(hash(password));
|
||||
}
|
||||
let key = if let Some(key) = partial_parsed_url.decryption_key.take() {
|
||||
key
|
||||
} else {
|
||||
error!("Key is missing in url; bailing.");
|
||||
render_message("Invalid paste link: Missing decryption key.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
(key, partial_parsed_url)
|
||||
};
|
||||
|
||||
let password = if needs_password {
|
||||
loop {
|
||||
let pw = window().prompt_with_message("A password is required to decrypt this paste:");
|
||||
|
||||
match pw {
|
||||
// Ok button was entered.
|
||||
Ok(Some(password)) if !password.is_empty() => {
|
||||
break Some(SecretVec::new(password.into_bytes()));
|
||||
}
|
||||
// Empty message was entered.
|
||||
Ok(Some(_)) => (),
|
||||
// Cancel button was entered.
|
||||
Ok(None) => {
|
||||
render_message("This paste requires a password.".into());
|
||||
return;
|
||||
}
|
||||
e => {
|
||||
render_message("Internal error occurred.".into());
|
||||
error!(format!("Error occurred at pw prompt: {e:?}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
spawn_local(async move {
|
||||
if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
|
||||
log!(e.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
spawn_local(async move {
|
||||
if let Err(e) = fetch_resources(request_uri, key, password, name, language).await {
|
||||
log!(e.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::future_not_send)]
|
||||
pub async fn encrypt_array_buffer(location: String, data: Vec<u8>) -> Result<JsString, JsString> {
|
||||
do_encrypt(location, data).await.map_err(|e| {
|
||||
log!(format!("[rs] Error encrypting array buffer: {}", e));
|
||||
JsString::from(e.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::future_not_send)]
|
||||
async fn do_encrypt(location: String, mut data: Vec<u8>) -> Result<JsString> {
|
||||
let (data, key) = {
|
||||
let enc_key = seal_in_place(&mut data, None)?;
|
||||
let key = SecretString::new(base64::encode(&enc_key.expose_secret().as_ref()));
|
||||
(data, key)
|
||||
};
|
||||
|
||||
let mut url = Url::from_str(&location)?;
|
||||
let fragment = Builder::new(key);
|
||||
|
||||
let short_code = reqwest::Client::new()
|
||||
.post(url.as_ref())
|
||||
.body(data)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
url.set_path(&short_code);
|
||||
url.set_fragment(Some(fragment.build().expose_secret()));
|
||||
|
||||
Ok(JsString::from(url.as_ref()))
|
||||
}
|
||||
|
||||
#[allow(clippy::future_not_send)]
|
||||
async fn fetch_resources(
|
||||
request_uri: Uri,
|
||||
key: Key,
|
||||
nonce: Nonce,
|
||||
password: Option<Key>,
|
||||
key: Secret<Key>,
|
||||
password: Option<SecretVec<u8>>,
|
||||
name: Option<String>,
|
||||
language: Option<String>,
|
||||
) -> Result<()> {
|
||||
match Request::get(&request_uri.to_string()).send().await {
|
||||
match reqwest::Client::new()
|
||||
.get(&request_uri.to_string())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) if resp.status() == StatusCode::OK => {
|
||||
let expires = Expiration::try_from(resp.headers()).map_or_else(
|
||||
|_| "This item does not expire.".to_string(),
|
||||
|expires| expires.to_string(),
|
||||
);
|
||||
let expires = resp
|
||||
.headers()
|
||||
.get(http::header::EXPIRES)
|
||||
.and_then(|header| Expiration::try_from(header).ok())
|
||||
.map_or_else(
|
||||
|| "This item does not expire.".to_string(),
|
||||
|expires| expires.to_string(),
|
||||
);
|
||||
|
||||
let data = {
|
||||
let data_fut = resp
|
||||
.as_raw()
|
||||
.array_buffer()
|
||||
.expect("to get raw bytes from a response");
|
||||
let data = match JsFuture::from(data_fut).await {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
render_message(
|
||||
"Network failure: Failed to completely read encryption paste.".into(),
|
||||
);
|
||||
bail!(format!(
|
||||
"JsFuture returned an error while fetching resp buffer: {:?}",
|
||||
e
|
||||
));
|
||||
}
|
||||
};
|
||||
Uint8Array::new(&data).to_vec()
|
||||
};
|
||||
let data = resp
|
||||
.bytes()
|
||||
.await
|
||||
.expect("to get raw bytes from a response")
|
||||
.to_vec();
|
||||
|
||||
if data.len() as u128 > DOWNLOAD_SIZE_LIMIT {
|
||||
render_message("The paste is too large to decrypt from the web browser. You must use the CLI tool to download this paste.".into());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let decrypted = decrypt(data, key, nonce, password)?;
|
||||
let (decrypted, mimetype) = match decrypt(data, &key, password, name.as_deref()) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
let msg = match e {
|
||||
CryptoError::Password => "The provided password was incorrect.",
|
||||
CryptoError::SecretKey => "The secret key in the URL was incorrect.",
|
||||
ref e => {
|
||||
log!(format!("Bad kdf or corrupted blob: {e}"));
|
||||
"An internal error occurred."
|
||||
}
|
||||
};
|
||||
|
||||
render_message(JsString::from(msg));
|
||||
bail!(e);
|
||||
}
|
||||
};
|
||||
let db_open_req = open_idb()?;
|
||||
|
||||
// On success callback
|
||||
let on_success = Closure::once(Box::new(move |event: Event| {
|
||||
let transaction: IdbObjectStore = as_idb_db(&event)
|
||||
.transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite)
|
||||
.unwrap()
|
||||
.object_store("decrypted data")
|
||||
.unwrap();
|
||||
|
||||
let decrypted_object = match &decrypted {
|
||||
DecryptedData::String(s) => IdbObject::new()
|
||||
.string()
|
||||
.expiration_text(&expires)
|
||||
.data(&JsValue::from_str(s)),
|
||||
DecryptedData::Blob(blob) => {
|
||||
IdbObject::new().blob().expiration_text(&expires).data(blob)
|
||||
}
|
||||
DecryptedData::Image(blob, size) => IdbObject::new()
|
||||
.image()
|
||||
.expiration_text(&expires)
|
||||
.data(blob)
|
||||
.extra(
|
||||
"file_size",
|
||||
Byte::from_bytes(*size as u128)
|
||||
.get_appropriate_unit(true)
|
||||
.to_string(),
|
||||
),
|
||||
DecryptedData::Audio(blob) => IdbObject::new()
|
||||
.audio()
|
||||
.expiration_text(&expires)
|
||||
.data(blob),
|
||||
DecryptedData::Video(blob) => IdbObject::new()
|
||||
.video()
|
||||
.expiration_text(&expires)
|
||||
.data(blob),
|
||||
};
|
||||
|
||||
let put_action = transaction
|
||||
.put_with_key(
|
||||
&Object::from(decrypted_object),
|
||||
&JsString::from(location().pathname().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
put_action.set_onsuccess(Some(
|
||||
Closure::wrap(Box::new(|| {
|
||||
log!("success");
|
||||
load_from_db();
|
||||
}) as Box<dyn Fn()>)
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
put_action.set_onerror(Some(
|
||||
Closure::wrap(Box::new(|e| {
|
||||
log!(e);
|
||||
}) as Box<dyn Fn(Event)>)
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
}) as Box<dyn FnOnce(Event)>);
|
||||
let on_success = Closure::once(Box::new(move |event| {
|
||||
on_success(&event, &decrypted, mimetype, &expires, name, language);
|
||||
}));
|
||||
|
||||
db_open_req.set_onsuccess(Some(on_success.into_js_value().unchecked_ref()));
|
||||
db_open_req.set_onerror(Some(
|
||||
Closure::wrap(Box::new(|e| {
|
||||
log!(e);
|
||||
}) as Box<dyn Fn(Event)>)
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
Closure::once(Box::new(|e: Event| log!(e)))
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
let on_upgrade = Closure::wrap(Box::new(move |event: Event| {
|
||||
let on_upgrade = Closure::once(Box::new(move |event: Event| {
|
||||
let db = as_idb_db(&event);
|
||||
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().unchecked_ref()));
|
||||
}
|
||||
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
|
||||
|
@ -242,12 +279,82 @@ async fn fetch_resources(
|
|||
render_message("Invalid paste URL.".into());
|
||||
}
|
||||
Ok(err) => {
|
||||
render_message(err.status_text().into());
|
||||
render_message(err.status().as_str().into());
|
||||
}
|
||||
Err(err) => {
|
||||
render_message(format!("{}", err).into());
|
||||
render_message(format!("{err}").into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_success(
|
||||
event: &Event,
|
||||
decrypted: &DecryptedData,
|
||||
mimetype: MimeType,
|
||||
expires: &str,
|
||||
name: Option<String>,
|
||||
language: Option<String>,
|
||||
) {
|
||||
let transaction: IdbObjectStore = as_idb_db(event)
|
||||
.transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite)
|
||||
.unwrap()
|
||||
.object_store("decrypted data")
|
||||
.unwrap();
|
||||
|
||||
let decrypted_object = match decrypted {
|
||||
DecryptedData::String(s) => IdbObject::new()
|
||||
.string()
|
||||
.expiration_text(expires)
|
||||
.data(&JsValue::from_str(s)),
|
||||
DecryptedData::Blob(blob) => IdbObject::new().blob().expiration_text(expires).data(blob),
|
||||
DecryptedData::Image(blob, size) => IdbObject::new()
|
||||
.image()
|
||||
.expiration_text(expires)
|
||||
.data(blob)
|
||||
.extra(
|
||||
"file_size",
|
||||
Byte::from_bytes(*size as u128)
|
||||
.get_appropriate_unit(true)
|
||||
.to_string(),
|
||||
),
|
||||
DecryptedData::Audio(blob) => IdbObject::new().audio().expiration_text(expires).data(blob),
|
||||
DecryptedData::Video(blob) => IdbObject::new().video().expiration_text(expires).data(blob),
|
||||
DecryptedData::Archive(blob, entries) => IdbObject::new()
|
||||
.archive()
|
||||
.expiration_text(expires)
|
||||
.data(blob)
|
||||
.extra(
|
||||
"entries",
|
||||
JsValue::from(
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|x| serde_wasm_bindgen::to_value(x).ok())
|
||||
.collect::<Array>(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
let put_action = transaction
|
||||
.put_with_key(
|
||||
&Object::from(decrypted_object),
|
||||
&JsString::from(location().pathname().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
put_action.set_onsuccess(Some(
|
||||
Closure::once(Box::new(|| {
|
||||
log!("[rs] Successfully inserted encrypted item into storage.");
|
||||
let name = name.map(JsString::from);
|
||||
let language = language.map(JsString::from);
|
||||
load_from_db(JsString::from(mimetype.0), name, language);
|
||||
}))
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
put_action.set_onerror(Some(
|
||||
Closure::once(Box::new(|e: Event| log!(e)))
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
}
|
||||
|
|
144
web/src/main.scss
Normal file
144
web/src/main.scss
Normal file
|
@ -0,0 +1,144 @@
|
|||
// OmegaUpload Web Frontend
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
@use 'node_modules/highlight.js/styles/github-dark.css';
|
||||
|
||||
$padding: 1em;
|
||||
|
||||
@font-face {
|
||||
font-family: "Mplus Code";
|
||||
src: url("../vendor/MPLUS_FONTS/fonts/ttf/MPLUSCodeLatin[wdth,wght].ttf") format("truetype");
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #404040;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.unselectable {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
@extend .hljs;
|
||||
margin: $padding 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: inline-flex;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.paste {
|
||||
@extend .hljs;
|
||||
border-radius: $padding;
|
||||
margin: $padding;
|
||||
padding: 2 * $padding;
|
||||
box-shadow: 0 0 $padding black;
|
||||
min-width: 120ch;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
font-family: 'Mplus Code', sans-serif;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.hljs-ln td.hljs-ln-numbers {
|
||||
@extend .align-right;
|
||||
padding-right: $padding;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.display-anyways {
|
||||
margin-top: 4em;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img,
|
||||
audio,
|
||||
video {
|
||||
border-radius: $padding;
|
||||
margin-bottom: $padding;
|
||||
max-height: 75vh;
|
||||
max-width: 75vw;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@extend .paste;
|
||||
height: 75vh;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.primary {
|
||||
@extend .hljs;
|
||||
}
|
||||
|
||||
.archive {
|
||||
&-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-file-size {
|
||||
@extend .align-right;
|
||||
padding-left: $padding;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
@extend .hljs;
|
||||
|
||||
font-size: 16px;
|
||||
text-decoration: underline;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
@extend .button;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.text-upload {
|
||||
@extend .button;
|
||||
}
|
363
web/src/render.tsx
Normal file
363
web/src/render.tsx
Normal file
|
@ -0,0 +1,363 @@
|
|||
// OmegaUpload Web Frontend
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import ReactDom from 'react-dom';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
let hljs;
|
||||
if (typeof WorkerGlobalScope === 'undefined' || !(self instanceof WorkerGlobalScope)) {
|
||||
hljs = require('highlight.js');
|
||||
(window as any).hljs = hljs;
|
||||
require('highlightjs-line-numbers.js');
|
||||
}
|
||||
|
||||
|
||||
const FileForm = () => {
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let file = event.target.files![0];
|
||||
const fr = new FileReader();
|
||||
fr.onload = (_e) => {
|
||||
encryptMessage(new Uint8Array(fr.result as ArrayBuffer));
|
||||
}
|
||||
fr.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
return <>
|
||||
<label className="file-upload hljs-meta" >
|
||||
Select a file
|
||||
<input type="file" onChange={handleChange} />
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
|
||||
const PasteForm = () => {
|
||||
const [data, setValue] = useState("");
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (data.trim() !== "") {
|
||||
encryptMessage(new TextEncoder().encode(data));
|
||||
} else {
|
||||
console.log("[js] Not sending string because it was empty.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className='hljs centered' onSubmit={handleSubmit}>
|
||||
<textarea
|
||||
placeholder="すいちゃんは~ 今日もかわい~!!"
|
||||
value={data}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<input className="text-upload hljs-meta" type="submit" value="Submit" />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function encryptMessage(data: Uint8Array) {
|
||||
const worker = new Worker(new URL('./bg_encrypt.ts', import.meta.url));
|
||||
worker.onmessage = (event: MessageEvent<string>) => {
|
||||
console.log(event);
|
||||
if (event.data === 'init') {
|
||||
console.log("[js] Sending data to worker");
|
||||
const message = { data, location: window.location.toString() };
|
||||
worker.postMessage(message, [message.data.buffer]);
|
||||
} else {
|
||||
window.location.assign(event.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createUploadUi() {
|
||||
const html = <main className='hljs centered fullscreen'>
|
||||
<FileForm />
|
||||
<p>or paste your data below</p>
|
||||
<PasteForm />
|
||||
</main>;
|
||||
|
||||
ReactDom.render(html, document.body);
|
||||
}
|
||||
|
||||
function loadFromDb(mimeType: string, name?: string, language?: string) {
|
||||
let resolvedName: string;
|
||||
if (name) {
|
||||
resolvedName = name;
|
||||
} else {
|
||||
const pathName = window.location.pathname;
|
||||
const leafIndex = pathName.indexOf("/");
|
||||
resolvedName = pathName.slice(leafIndex + 1);
|
||||
}
|
||||
|
||||
console.log("[js] Resolved name:", resolvedName);
|
||||
console.log("[js] Got language:", language);
|
||||
console.log("[js] Got mime type:", mimeType);
|
||||
|
||||
const dbReq = window.indexedDB.open("omegaupload", 1);
|
||||
dbReq.onsuccess = (evt) => {
|
||||
const db = (evt.target as IDBRequest).result;
|
||||
const obj_store = db
|
||||
.transaction("decrypted data")
|
||||
.objectStore("decrypted data");
|
||||
const fetchReq = obj_store.get(window.location.pathname);
|
||||
fetchReq.onsuccess = (evt) => {
|
||||
const data = (evt.target as IDBRequest).result;
|
||||
switch (data.type) {
|
||||
case "string":
|
||||
console.info("[js] Rendering string UI.");
|
||||
createStringPasteUi(data, mimeType, resolvedName, language);
|
||||
break;
|
||||
case "blob":
|
||||
console.info("[js] Rendering blob UI.");
|
||||
createBlobPasteUi(data, resolvedName);
|
||||
break;
|
||||
case "image":
|
||||
console.info("[js] Rendering image UI.");
|
||||
createImagePasteUi(data, resolvedName, mimeType);
|
||||
break;
|
||||
case "audio":
|
||||
console.info("[js] Rendering audio UI.");
|
||||
createAudioPasteUi(data, resolvedName, mimeType);
|
||||
break;
|
||||
case "video":
|
||||
console.info("[js] Rendering video UI.");
|
||||
createVideoPasteUi(data, resolvedName, mimeType);
|
||||
break;
|
||||
case "archive":
|
||||
console.info("[js] Rendering archive UI.");
|
||||
createArchivePasteUi(data, resolvedName);
|
||||
break;
|
||||
default:
|
||||
console.info("[js] Rendering unknown UI.");
|
||||
renderMessage("Something went wrong. Try clearing local data.");
|
||||
break;
|
||||
}
|
||||
|
||||
// IDB was only used as a temporary medium;
|
||||
window.onbeforeunload = (_e) => {
|
||||
// See https://link.eddie.sh/NrIIq on why .commit is necessary.
|
||||
const transaction = db.transaction("decrypted data", "readwrite");
|
||||
transaction
|
||||
.objectStore("decrypted data")
|
||||
.delete(window.location.pathname);
|
||||
transaction.commit();
|
||||
transaction.oncomplete = () => {
|
||||
console.log("Item deleted from cache");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
fetchReq.onerror = (evt) => {
|
||||
console.log("err");
|
||||
console.log(evt);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function createStringPasteUi(data, mimeType: string, name: string, lang?: string, skipSyntaxHighlight?: boolean) {
|
||||
const html = <main>
|
||||
<pre className='paste'>
|
||||
<p className='unselectable centered'>{data.expiration}</p>
|
||||
<a href={getObjectUrl([data.data], mimeType)} download={name} className='hljs-meta centered'>
|
||||
Download file.
|
||||
</a>
|
||||
<hr />
|
||||
<code>
|
||||
{data.data}
|
||||
</code>
|
||||
</pre>
|
||||
</main>;
|
||||
|
||||
ReactDom.render(html, document.body);
|
||||
|
||||
if (skipSyntaxHighlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
let languages = undefined;
|
||||
|
||||
if (!hljs.getLanguage(lang)) {
|
||||
if (lang) {
|
||||
console.warn(`[js] User provided language (${lang}) is not known.`);
|
||||
} else {
|
||||
console.info(`[js] Language hint not provided.`);
|
||||
}
|
||||
} else {
|
||||
languages = [lang];
|
||||
}
|
||||
|
||||
// If a language wasn't provided, see if we can use the file extension to give
|
||||
// us a better hint for hljs
|
||||
if (!languages) {
|
||||
if (name) {
|
||||
console.log("[js] Trying to infer from file name...");
|
||||
const periodIndex = name.indexOf(".");
|
||||
if (periodIndex === -1) {
|
||||
console.warn("[js] Did not find file extension.")
|
||||
} else {
|
||||
let extension = name.slice(periodIndex + 1);
|
||||
console.info(`[js] Found extension ${extension}.`);
|
||||
if (!hljs.getLanguage(extension)) {
|
||||
console.warn(`[js] Extension was not recognized by hljs. Giving up.`);
|
||||
} else {
|
||||
console.info("[js] Successfully inferred language from file extension.");
|
||||
languages = [extension];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("[js] No file name hint provided.");
|
||||
}
|
||||
} else {
|
||||
console.info(`[js] Selecting user provided language ${languages[0]} for highlighting.`);
|
||||
}
|
||||
|
||||
// If we still haven't set languages here, then we're leaving it up to the
|
||||
// library
|
||||
if (!languages) {
|
||||
console.log("[js] Deferring to hljs inference for syntax highlighting.");
|
||||
} else {
|
||||
hljs.configure({ languages });
|
||||
}
|
||||
|
||||
hljs.highlightAll();
|
||||
|
||||
|
||||
(hljs as any).initLineNumbersOnLoad();
|
||||
}
|
||||
|
||||
function createBlobPasteUi(data, name: string) {
|
||||
const html = <main className='hljs centered fullscreen'>
|
||||
<div className='centered'>
|
||||
<p>{data.expiration}</p>
|
||||
<a href={getObjectUrl(data.data, name)} download={name} className='hljs-meta'>
|
||||
Download binary file.
|
||||
</a>
|
||||
</div>
|
||||
<p className='display-anyways hljs-comment' onClick={() => {
|
||||
data.data.text().then(text => {
|
||||
data.data = text;
|
||||
createStringPasteUi(data, "application/octet-stream", name, undefined, true);
|
||||
})
|
||||
}}>Display anyways?</p>
|
||||
</main>;
|
||||
|
||||
ReactDom.render(html, document.body);
|
||||
}
|
||||
|
||||
function createImagePasteUi({ expiration, data, file_size }, name: string, mimeType: string) {
|
||||
createMultiMediaPasteUi("img", expiration, data, name, mimeType, (downloadEle, imgEle) => {
|
||||
imgEle.onload = () => {
|
||||
const width = imgEle.naturalWidth || imgEle.width;
|
||||
const height = imgEle.naturalHeight || imgEle.height;
|
||||
downloadEle.textContent = "Download " + file_size + " \u2014 " + width + " by " + height;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createAudioPasteUi({ expiration, data }, name: string, mimeType: string) {
|
||||
createMultiMediaPasteUi("audio", expiration, data, name, mimeType, "Download");
|
||||
}
|
||||
|
||||
function createVideoPasteUi({ expiration, data }, name: string, mimeType: string) {
|
||||
createMultiMediaPasteUi("video", expiration, data, name, mimeType, "Download");
|
||||
}
|
||||
|
||||
function createArchivePasteUi({ expiration, data, entries }, name: string) {
|
||||
// Because it's a stable sort, we can first sort by name (to get all folder
|
||||
// items grouped together) and then sort by if there's a / or not.
|
||||
entries.sort((a, b) => {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
});
|
||||
|
||||
// This doesn't get sub directories and their folders, but hey it's close
|
||||
// enough
|
||||
entries.sort((a, b) => {
|
||||
return b.name.includes("/") - a.name.includes("/");
|
||||
});
|
||||
|
||||
const html = <main>
|
||||
<section className='paste'>
|
||||
<p className='centered'>{expiration}</p>
|
||||
<a href={getObjectUrl(data)} download={name} className='hljs-meta centered'>Download</a>
|
||||
<hr />
|
||||
<table className='archive-table'>
|
||||
<thead>
|
||||
<tr className='hljs-title'><th>Name</th><th className='align-right'>File Size</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
entries.map(({ name, file_size }) => {
|
||||
return <tr><td>{name}</td><td className='align-right hljs-number'>{file_size}</td></tr>;
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>;
|
||||
|
||||
ReactDom.render(html, document.body);
|
||||
|
||||
}
|
||||
|
||||
function createMultiMediaPasteUi(tag, expiration, data, name: string, mimeType: string, on_create?: Function | string) {
|
||||
const bodyEle = document.body;
|
||||
bodyEle.textContent = '';
|
||||
|
||||
const mainEle = document.createElement("main");
|
||||
mainEle.classList.add("hljs");
|
||||
mainEle.classList.add("centered");
|
||||
mainEle.classList.add("fullscreen");
|
||||
|
||||
const downloadLink = getObjectUrl(data, mimeType);
|
||||
|
||||
const expirationEle = document.createElement("p");
|
||||
expirationEle.textContent = expiration;
|
||||
mainEle.appendChild(expirationEle);
|
||||
|
||||
const mediaEle = document.createElement(tag);
|
||||
mediaEle.src = downloadLink;
|
||||
mediaEle.controls = true;
|
||||
mainEle.appendChild(mediaEle);
|
||||
|
||||
const downloadEle = document.createElement("a");
|
||||
downloadEle.href = downloadLink;
|
||||
downloadEle.download = name;
|
||||
downloadEle.classList.add("hljs-meta");
|
||||
mainEle.appendChild(downloadEle);
|
||||
|
||||
bodyEle.appendChild(mainEle);
|
||||
|
||||
if (on_create instanceof Function) {
|
||||
on_create(downloadEle, mediaEle);
|
||||
} else {
|
||||
downloadEle.textContent = on_create;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMessage(message) {
|
||||
ReactDom.render(
|
||||
<main className='hljs centered fullscreen'>
|
||||
{message}
|
||||
</main>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
function getObjectUrl(data, mimeType?: string) {
|
||||
return URL.createObjectURL(new Blob([data], { type: mimeType }));
|
||||
}
|
||||
|
||||
|
||||
export { renderMessage, createUploadUi, loadFromDb };
|
|
@ -1,3 +1,19 @@
|
|||
// OmegaUpload Web Frontend
|
||||
// Copyright (C) 2021 Edward Shen
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Event, IdbDatabase, IdbOpenDbRequest};
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Omegaupload</title>
|
||||
|
||||
<!-- <script src="highlightjs-line-numbers.min.js" defer></script> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,30 +0,0 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// This runs a unit test in native Rust, so it can only use Rust APIs.
|
||||
#[test]
|
||||
fn rust_test() {
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
|
||||
// This runs a unit test in the browser, so it can use browser APIs.
|
||||
#[wasm_bindgen_test]
|
||||
fn web_test() {
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
|
||||
// This runs a unit test in the browser, and in addition it supports asynchronous Future APIs.
|
||||
#[wasm_bindgen_test(async)]
|
||||
async fn async_test() -> Result<(), JsValue> {
|
||||
// Creates a JavaScript Promise which will asynchronously resolve with the value 42.
|
||||
let promise = js_sys::Promise::resolve(&JsValue::from(42));
|
||||
|
||||
// Converts that Promise into a Future.
|
||||
// The unit test will wait for the Future to resolve.
|
||||
JsFuture::from(promise).map(|x| {
|
||||
assert_eq!(x, 42);
|
||||
})
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"noImplicitAny": false,
|
||||
"module": "esnext",
|
||||
"target": "es5",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
}
|
||||
}
|
1
web/vendor/MPLUS_FONTS
vendored
Submodule
1
web/vendor/MPLUS_FONTS
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit a1268635894c5ee23dfdece570418ca07b66c3fc
|
|
@ -1,51 +0,0 @@
|
|||
const path = require("path");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
|
||||
|
||||
const dist = path.resolve(__dirname, "dist");
|
||||
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
entry: {
|
||||
index: "./js/index.js"
|
||||
},
|
||||
devtool: 'inline-source-map',
|
||||
output: {
|
||||
path: dist,
|
||||
filename: "[name].js"
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: dist,
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8081',
|
||||
pathRewrite: { '^/api': '' }
|
||||
}
|
||||
},
|
||||
watchFiles: ['src/**', 'js/**'],
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
path.resolve(__dirname, "static")
|
||||
]
|
||||
}),
|
||||
new WasmPackPlugin({
|
||||
crateDirectory: __dirname,
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
]
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
};
|
2356
web/yarn.lock
2356
web/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -1,43 +0,0 @@
|
|||
[package]
|
||||
name = "omegaupload-web"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
omegaupload-common = { path = "../common", features = ["wasm"] }
|
||||
# Enables wasm support
|
||||
getrandom = { version = "*", features = ["js"] }
|
||||
|
||||
anyhow = "1"
|
||||
bytes = "1"
|
||||
byte-unit = "4"
|
||||
console_error_panic_hook = "0.1"
|
||||
gloo-console = "0.1"
|
||||
http = "0.2"
|
||||
js-sys = "0.3"
|
||||
reqwasm = "0.2"
|
||||
tree_magic_mini = { version = "3", features = ["with-gpl-data"] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"TextDecoder",
|
||||
"IdbFactory",
|
||||
"IdbOpenDbRequest",
|
||||
"IdbRequest",
|
||||
"IdbDatabase",
|
||||
"IdbObjectStore",
|
||||
"IdbTransaction",
|
||||
"IdbTransactionMode",
|
||||
"IdbIndex",
|
||||
"IdbIndexParameters",
|
||||
"Event",
|
||||
"EventTarget",
|
||||
"Window",
|
||||
"Performance",
|
||||
"Location",
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
Contains the codebase used for the frontend
|
||||
|
||||
Notes on licensing:
|
||||
|
||||
https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility
|
||||
|
||||
Because there is a statically linked in dependency on `shared-mime-types`, this
|
||||
crate MUST be under a GPLv2 or later license. This has been confirmed as of
|
||||
2021-10-24.
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Omegaupload</title>
|
||||
|
||||
<link data-trunk rel="rust" data-wasm-opt="0" data-keep-debug="true" data-no-mangle="true" />
|
||||
<link data-trunk rel="copy-file" href="vendor/MPLUS_FONTS/fonts/ttf/MplusCodeLatin[wdth,wght].ttf" 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"
|
||||
dest="/" />
|
||||
<link data-trunk rel="scss" href="src/main.scss" />
|
||||
|
||||
<script src="main.js" async></script>
|
||||
<script src="highlight.min.js" defer></script>
|
||||
<script src="highlightjs-line-numbers.min.js" defer></script>
|
||||
</head>
|
||||
|
||||
</html>
|
|
@ -1,105 +0,0 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use gloo_console::log;
|
||||
use js_sys::{Array, Uint8Array};
|
||||
use omegaupload_common::crypto::{open_in_place, Key, Nonce};
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::Blob;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum DecryptedData {
|
||||
String(Arc<String>),
|
||||
Blob(Arc<Blob>),
|
||||
Image(Arc<Blob>, usize),
|
||||
Audio(Arc<Blob>),
|
||||
Video(Arc<Blob>),
|
||||
}
|
||||
|
||||
fn now() -> f64 {
|
||||
web_sys::window()
|
||||
.expect("should have a Window")
|
||||
.performance()
|
||||
.expect("should have a Performance")
|
||||
.now()
|
||||
}
|
||||
|
||||
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 started.");
|
||||
let start = now();
|
||||
|
||||
if let Some(password) = maybe_password {
|
||||
crate::render_message("Decrypting Stage 1...".into());
|
||||
open_in_place(container, &nonce.increment(), &password).map_err(|_| {
|
||||
crate::render_message("Unable to decrypt paste with the provided password.".into());
|
||||
PasteCompleteConstructionError::StageOneFailure
|
||||
})?;
|
||||
}
|
||||
log!(format!("Stage 1 completed in {}ms", now() - start));
|
||||
|
||||
log!("Stage 2 decryption started.");
|
||||
let start = now();
|
||||
crate::render_message("Decrypting Stage 2...".into());
|
||||
open_in_place(container, &nonce, &key).map_err(|_| {
|
||||
crate::render_message(
|
||||
"Unable to decrypt paste with the provided encryption key and nonce.".into(),
|
||||
);
|
||||
PasteCompleteConstructionError::StageTwoFailure
|
||||
})?;
|
||||
log!(format!("Stage 2 completed in {}ms", now() - start));
|
||||
|
||||
if let Ok(decrypted) = std::str::from_utf8(container) {
|
||||
Ok(DecryptedData::String(Arc::new(decrypted.to_owned())))
|
||||
} else {
|
||||
log!("Blob conversion started.");
|
||||
let start = now();
|
||||
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!(format!("Blob conversion completed in {}ms", now() - start));
|
||||
|
||||
let mime_type = tree_magic_mini::from_u8(container);
|
||||
|
||||
if mime_type.starts_with("image/") || mime_type == "application/x-riff" {
|
||||
Ok(DecryptedData::Image(blob, container.len()))
|
||||
} else if mime_type.starts_with("audio/") {
|
||||
Ok(DecryptedData::Audio(blob))
|
||||
} else if mime_type.starts_with("video/") || mime_type == "application/x-matroska" {
|
||||
Ok(DecryptedData::Video(blob))
|
||||
} 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
use std::{hint::unreachable_unchecked, marker::PhantomData};
|
||||
|
||||
use js_sys::{Array, JsString, Object};
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
pub struct IdbObject<State>(Array, PhantomData<State>);
|
||||
|
||||
impl<State: IdbObjectState> IdbObject<State> {
|
||||
fn add_tuple<NextState>(self, key: &str, value: &JsValue) -> IdbObject<NextState> {
|
||||
let array = Array::new();
|
||||
array.push(&JsString::from(key));
|
||||
array.push(value);
|
||||
self.0.push(&array);
|
||||
IdbObject(self.0, PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdbObject<Ready>> for Object {
|
||||
fn from(db_object: IdbObject<Ready>) -> Self {
|
||||
match Self::from_entries(db_object.as_ref()) {
|
||||
Ok(o) => o,
|
||||
// SAFETY: IdbObject maintains the invariant that it can eventually
|
||||
// be constructed into a JS object.
|
||||
_ => unsafe { unreachable_unchecked() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IdbObject<NeedsType> {
|
||||
pub fn new() -> Self {
|
||||
Self(Array::new(), PhantomData)
|
||||
}
|
||||
|
||||
pub fn video(self) -> IdbObject<NeedsExpiration> {
|
||||
self.add_tuple("type", &JsString::from("video"))
|
||||
}
|
||||
|
||||
pub fn audio(self) -> IdbObject<NeedsExpiration> {
|
||||
self.add_tuple("type", &JsString::from("audio"))
|
||||
}
|
||||
|
||||
pub fn image(self) -> IdbObject<NeedsExpiration> {
|
||||
self.add_tuple("type", &JsString::from("image"))
|
||||
}
|
||||
|
||||
pub fn blob(self) -> IdbObject<NeedsExpiration> {
|
||||
self.add_tuple("type", &JsString::from("blob"))
|
||||
}
|
||||
|
||||
pub fn string(self) -> IdbObject<NeedsExpiration> {
|
||||
self.add_tuple("type", &JsString::from("string"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IdbObject<NeedsType> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl IdbObject<NeedsExpiration> {
|
||||
pub fn expiration_text(self, expires: &str) -> IdbObject<NeedsData> {
|
||||
self.add_tuple("expiration", &JsString::from(expires))
|
||||
}
|
||||
}
|
||||
|
||||
impl IdbObject<NeedsData> {
|
||||
pub fn data(self, value: &JsValue) -> IdbObject<Ready> {
|
||||
self.add_tuple("data", value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IdbObject<Ready> {
|
||||
pub fn extra(self, key: &str, value: impl Into<JsValue>) -> Self {
|
||||
self.add_tuple(key, &value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<JsValue> for IdbObject<Ready> {
|
||||
fn as_ref(&self) -> &JsValue {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_idb_object_state {
|
||||
($($ident:ident),*) => {
|
||||
pub trait IdbObjectState {}
|
||||
$(
|
||||
pub enum $ident {}
|
||||
impl IdbObjectState for $ident {}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_idb_object_state!(NeedsType, NeedsExpiration, NeedsData, Ready);
|
|
@ -1,251 +0,0 @@
|
|||
#![warn(clippy::nursery, clippy::pedantic)]
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use byte_unit::{n_mib_bytes, Byte};
|
||||
use decrypt::DecryptedData;
|
||||
use gloo_console::{error, log};
|
||||
use http::uri::PathAndQuery;
|
||||
use http::{StatusCode, Uri};
|
||||
use js_sys::{JsString, Object, Uint8Array};
|
||||
use omegaupload_common::crypto::{Key, Nonce};
|
||||
use omegaupload_common::{hash, Expiration, PartialParsedUrl};
|
||||
use reqwasm::http::Request;
|
||||
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window};
|
||||
|
||||
use crate::decrypt::decrypt;
|
||||
use crate::idb_object::IdbObject;
|
||||
use crate::util::as_idb_db;
|
||||
|
||||
mod decrypt;
|
||||
mod idb_object;
|
||||
mod util;
|
||||
|
||||
const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500);
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = renderIndex)]
|
||||
pub fn render_index();
|
||||
#[wasm_bindgen(js_name = loadFromDb)]
|
||||
pub fn load_from_db();
|
||||
#[wasm_bindgen(js_name = renderMessage)]
|
||||
pub fn render_message(message: JsString);
|
||||
}
|
||||
|
||||
fn window() -> Window {
|
||||
web_sys::window().expect("Failed to get a reference of the window")
|
||||
}
|
||||
|
||||
fn location() -> Location {
|
||||
window().location()
|
||||
}
|
||||
|
||||
fn open_idb() -> Result<IdbOpenDbRequest> {
|
||||
window()
|
||||
.indexed_db()
|
||||
.unwrap()
|
||||
.context("Missing browser idb impl")?
|
||||
.open("omegaupload")
|
||||
.map_err(|_| anyhow!("Failed to open idb"))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
|
||||
if location().pathname().unwrap() == "/" {
|
||||
render_index();
|
||||
} else {
|
||||
render_message("Loading paste...".into());
|
||||
|
||||
let url = String::from(location().to_string());
|
||||
let request_uri = {
|
||||
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
|
||||
if let Some(parts) = uri_parts.path_and_query.as_mut() {
|
||||
*parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap();
|
||||
}
|
||||
Uri::from_parts(uri_parts).unwrap()
|
||||
};
|
||||
|
||||
log!(&url);
|
||||
log!(&request_uri.to_string());
|
||||
log!(&location().pathname().unwrap());
|
||||
let (key, nonce, needs_pw) = {
|
||||
let partial_parsed_url = url
|
||||
.split_once('#')
|
||||
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
|
||||
.unwrap_or_default();
|
||||
let key = if let Some(key) = partial_parsed_url.decryption_key {
|
||||
key
|
||||
} else {
|
||||
error!("Key is missing in url; bailing.");
|
||||
render_message("Invalid paste link: Missing decryption key.".into());
|
||||
return;
|
||||
};
|
||||
let nonce = if let Some(nonce) = partial_parsed_url.nonce {
|
||||
nonce
|
||||
} else {
|
||||
error!("Nonce is missing in url; bailing.");
|
||||
render_message("Invalid paste link: Missing nonce.".into());
|
||||
return;
|
||||
};
|
||||
(key, nonce, partial_parsed_url.needs_password)
|
||||
};
|
||||
|
||||
let password = if needs_pw {
|
||||
loop {
|
||||
let pw =
|
||||
window().prompt_with_message("A password is required to decrypt this paste:");
|
||||
|
||||
if let Ok(Some(password)) = pw {
|
||||
if !password.is_empty() {
|
||||
break Some(hash(password));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
spawn_local(async move {
|
||||
if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
|
||||
log!(e.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::future_not_send)]
|
||||
async fn fetch_resources(
|
||||
request_uri: Uri,
|
||||
key: Key,
|
||||
nonce: Nonce,
|
||||
password: Option<Key>,
|
||||
) -> Result<()> {
|
||||
match Request::get(&request_uri.to_string()).send().await {
|
||||
Ok(resp) if resp.status() == StatusCode::OK => {
|
||||
let expires = Expiration::try_from(resp.headers()).map_or_else(
|
||||
|_| "This item does not expire.".to_string(),
|
||||
|expires| expires.to_string(),
|
||||
);
|
||||
|
||||
let data = {
|
||||
let data_fut = resp
|
||||
.as_raw()
|
||||
.array_buffer()
|
||||
.expect("to get raw bytes from a response");
|
||||
let data = match JsFuture::from(data_fut).await {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
render_message(
|
||||
"Network failure: Failed to completely read encryption paste.".into(),
|
||||
);
|
||||
bail!(format!(
|
||||
"JsFuture returned an error while fetching resp buffer: {:?}",
|
||||
e
|
||||
));
|
||||
}
|
||||
};
|
||||
Uint8Array::new(&data).to_vec()
|
||||
};
|
||||
|
||||
if data.len() as u128 > DOWNLOAD_SIZE_LIMIT {
|
||||
render_message("The paste is too large to decrypt from the web browser. You must use the CLI tool to download this paste.".into());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let decrypted = decrypt(data, key, nonce, password)?;
|
||||
let db_open_req = open_idb()?;
|
||||
|
||||
// On success callback
|
||||
let on_success = Closure::once(Box::new(move |event: Event| {
|
||||
let transaction: IdbObjectStore = as_idb_db(&event)
|
||||
.transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite)
|
||||
.unwrap()
|
||||
.object_store("decrypted data")
|
||||
.unwrap();
|
||||
|
||||
let decrypted_object = match &decrypted {
|
||||
DecryptedData::String(s) => IdbObject::new()
|
||||
.string()
|
||||
.expiration_text(&expires)
|
||||
.data(&JsValue::from_str(s)),
|
||||
DecryptedData::Blob(blob) => {
|
||||
IdbObject::new().blob().expiration_text(&expires).data(blob)
|
||||
}
|
||||
DecryptedData::Image(blob, size) => IdbObject::new()
|
||||
.image()
|
||||
.expiration_text(&expires)
|
||||
.data(blob)
|
||||
.extra(
|
||||
"file_size",
|
||||
Byte::from_bytes(*size as u128)
|
||||
.get_appropriate_unit(true)
|
||||
.to_string(),
|
||||
),
|
||||
DecryptedData::Audio(blob) => IdbObject::new()
|
||||
.audio()
|
||||
.expiration_text(&expires)
|
||||
.data(blob),
|
||||
DecryptedData::Video(blob) => IdbObject::new()
|
||||
.video()
|
||||
.expiration_text(&expires)
|
||||
.data(blob),
|
||||
};
|
||||
|
||||
let put_action = transaction
|
||||
.put_with_key(
|
||||
&Object::from(decrypted_object),
|
||||
&JsString::from(location().pathname().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
put_action.set_onsuccess(Some(
|
||||
Closure::wrap(Box::new(|| {
|
||||
log!("success");
|
||||
load_from_db();
|
||||
}) as Box<dyn Fn()>)
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
put_action.set_onerror(Some(
|
||||
Closure::wrap(Box::new(|e| {
|
||||
log!(e);
|
||||
}) as Box<dyn Fn(Event)>)
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
}) as Box<dyn FnOnce(Event)>);
|
||||
|
||||
db_open_req.set_onsuccess(Some(on_success.into_js_value().unchecked_ref()));
|
||||
db_open_req.set_onerror(Some(
|
||||
Closure::wrap(Box::new(|e| {
|
||||
log!(e);
|
||||
}) as Box<dyn Fn(Event)>)
|
||||
.into_js_value()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
let on_upgrade = Closure::wrap(Box::new(move |event: Event| {
|
||||
let db = as_idb_db(&event);
|
||||
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().unchecked_ref()));
|
||||
}
|
||||
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
|
||||
render_message("Either the paste was burned or it never existed.".into());
|
||||
}
|
||||
Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {
|
||||
render_message("Invalid paste URL.".into());
|
||||
}
|
||||
Ok(err) => {
|
||||
render_message(err.status_text().into());
|
||||
}
|
||||
Err(err) => {
|
||||
render_message(format!("{}", err).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
@use '../vendor/highlight.js/src/styles/github-dark.css';
|
||||
|
||||
$padding: 1em;
|
||||
|
||||
@font-face {
|
||||
font-family: "Mplus Code";
|
||||
src: url("./MplusCodeLatin[wdth,wght].ttf") format("truetype");
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #404040;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.unselectable {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
@extend .hljs;
|
||||
margin: $padding 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: inline-flex;
|
||||
min-width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.paste {
|
||||
@extend .hljs;
|
||||
border-radius: $padding;
|
||||
margin: $padding;
|
||||
padding: 2 * $padding;
|
||||
box-shadow: 0 0 $padding black;
|
||||
min-width: 120ch;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
font-family: 'Mplus Code', sans-serif;
|
||||
}
|
||||
|
||||
.hljs-ln td.hljs-ln-numbers {
|
||||
text-align: right;
|
||||
padding-right: $padding;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.display-anyways {
|
||||
margin-top: 4em;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img, audio, video {
|
||||
border-radius: $padding;
|
||||
margin-bottom: $padding;
|
||||
max-height: 75vh;
|
||||
max-width: 75vw;
|
||||
}
|
||||
|
||||
.primary {
|
||||
@extend .hljs;
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
|
||||
window.addEventListener("hashchange", () => location.reload());
|
||||
|
||||
// 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")
|
||||
.objectStore("decrypted data");
|
||||
let fetchReq = obj_store.get(window.location.pathname);
|
||||
fetchReq.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;
|
||||
case "audio":
|
||||
createAudioPasteUi(data);
|
||||
break;
|
||||
case "video":
|
||||
createVideoPasteUi(data);
|
||||
break;
|
||||
default:
|
||||
renderMessage("Something went wrong. Try clearing local data.");
|
||||
break;
|
||||
}
|
||||
|
||||
// IDB was only used as a temporary medium;
|
||||
window.onbeforeunload = (e) => {
|
||||
// See https://link.eddie.sh/NrIIq on why .commit is necessary.
|
||||
const transaction = db.transaction("decrypted data", "readwrite");
|
||||
transaction
|
||||
.objectStore("decrypted data")
|
||||
.delete(window.location.pathname);
|
||||
transaction.commit();
|
||||
transaction.oncomplete = () => {
|
||||
console.log("Item deleted from cache");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
fetchReq.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 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);
|
||||
}
|
||||
|
||||
function createImagePasteUi({ expiration, data, file_size }) {
|
||||
createMultiMediaPasteUi("img", expiration, data, (downloadEle, imgEle) => {
|
||||
imgEle.onload = () => {
|
||||
downloadEle.textContent = "Download " + file_size + " \u2014 " + imgEle.naturalWidth + " by " + imgEle.naturalHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createAudioPasteUi({ expiration, data }) {
|
||||
createMultiMediaPasteUi("audio", expiration, data, "Download");
|
||||
}
|
||||
|
||||
function createVideoPasteUi({ expiration, data }) {
|
||||
createMultiMediaPasteUi("video", expiration, data, "Download");
|
||||
}
|
||||
|
||||
function createMultiMediaPasteUi(tag, expiration, data, on_create?) {
|
||||
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);
|
||||
|
||||
let expirationEle = document.createElement("p");
|
||||
expirationEle.textContent = expiration;
|
||||
mainEle.appendChild(expirationEle);
|
||||
|
||||
let mediaEle = document.createElement(tag);
|
||||
mediaEle.src = downloadLink;
|
||||
mediaEle.controls = true;
|
||||
mainEle.appendChild(mediaEle);
|
||||
|
||||
let downloadEle = document.createElement("a");
|
||||
downloadEle.href = downloadLink;
|
||||
downloadEle.download = window.location.pathname;
|
||||
downloadEle.classList.add("hljs-meta");
|
||||
mainEle.appendChild(downloadEle);
|
||||
|
||||
bodyEle.appendChild(mainEle);
|
||||
|
||||
if (on_create instanceof Function) {
|
||||
on_create(downloadEle, mediaEle);
|
||||
} else {
|
||||
downloadEle.textContent = on_create;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMessage(message) {
|
||||
let body = document.getElementsByTagName("body")[0];
|
||||
body.textContent = '';
|
||||
let mainEle = document.createElement("main");
|
||||
mainEle.classList.add("hljs");
|
||||
mainEle.classList.add("centered");
|
||||
mainEle.classList.add("fullscreen");
|
||||
mainEle.textContent = message;
|
||||
body.appendChild(mainEle);
|
||||
}
|
||||
|
||||
// Export to main.rs
|
||||
function renderIndex() {
|
||||
console.log("index");
|
||||
// TODO: find a way to not hard code this.
|
||||
// https://docs.rs/chacha20poly1305/0.9.0/chacha20poly1305/type.Key.html
|
||||
// https://docs.rs/chacha20poly1305/0.9.0/chacha20poly1305/type.XNonce.html
|
||||
const key = crypto.getRandomValues(new Uint8Array(32));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(24));
|
||||
console.log(key, nonce);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Event, IdbDatabase, IdbOpenDbRequest};
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if event is not an event from the IDB API.
|
||||
pub fn as_idb_db(event: &Event) -> IdbDatabase {
|
||||
let target: IdbOpenDbRequest = event.target().map(JsCast::unchecked_into).unwrap();
|
||||
target.result().map(JsCast::unchecked_into).unwrap()
|
||||
}
|
1
web_old/vendor/MPLUS_FONTS
vendored
1
web_old/vendor/MPLUS_FONTS
vendored
|
@ -1 +0,0 @@
|
|||
Subproject commit 6ee9e7ca06f40f2303d839ccac8bfb8b56d2b3cd
|
1
web_old/vendor/highlight.js
vendored
1
web_old/vendor/highlight.js
vendored
|
@ -1 +0,0 @@
|
|||
Subproject commit 257cfee803426333af25b68da17601aec2663172
|
1149
web_old/vendor/highlight.min.js
vendored
1149
web_old/vendor/highlight.min.js
vendored
File diff suppressed because one or more lines are too long
1
web_old/vendor/highlightjs-line-numbers.js
vendored
1
web_old/vendor/highlightjs-line-numbers.js
vendored
|
@ -1 +0,0 @@
|
|||
Subproject commit 8480334a29f01ad8b7fb0497c65285872781ee96
|
53
webpack.config.js
Normal file
53
webpack.config.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
|
||||
const { SourceMapDevToolPlugin } = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: './web/src/index.js',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'swc-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.scss$/i,
|
||||
use: [
|
||||
// Creates `style` nodes from JS strings
|
||||
"style-loader",
|
||||
// Translates CSS into CommonJS
|
||||
"css-loader",
|
||||
// Compiles Sass to CSS
|
||||
"sass-loader",
|
||||
// source map for debugging
|
||||
"source-map-loader"
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist/static'),
|
||||
filename: 'index.js',
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, 'web/src/index.html'),
|
||||
publicPath: "/static",
|
||||
}),
|
||||
new WasmPackPlugin({
|
||||
crateDirectory: path.resolve(__dirname, "web"),
|
||||
outDir: path.resolve(__dirname, "web/pkg"),
|
||||
}),
|
||||
new SourceMapDevToolPlugin({}),
|
||||
],
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
mode: 'development'
|
||||
};
|
Loading…
Reference in a new issue