initial commit
This commit is contained in:
commit
7ed2992f64
18 changed files with 3245 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/database
|
1932
Cargo.lock
generated
Normal file
1932
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"cli",
|
||||||
|
"common",
|
||||||
|
"server",
|
||||||
|
"web",
|
||||||
|
]
|
17
cli/Cargo.toml
Normal file
17
cli/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "omegaupload-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# 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"] }
|
||||||
|
sodiumoxide = "0.2"
|
||||||
|
url = "2"
|
213
cli/src/main.rs
Normal file
213
cli/src/main.rs
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use atty::Stream;
|
||||||
|
use clap::Clap;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use secrecy::{ExposeSecret, SecretString};
|
||||||
|
use sodiumoxide::base64;
|
||||||
|
use sodiumoxide::base64::Variant::UrlSafe;
|
||||||
|
use sodiumoxide::crypto::hash::sha256;
|
||||||
|
use sodiumoxide::crypto::secretbox::{gen_key, gen_nonce, open, seal, Key, Nonce, KEYBYTES};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clap)]
|
||||||
|
struct Opts {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
action: Action,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clap)]
|
||||||
|
enum Action {
|
||||||
|
Upload {
|
||||||
|
url: Url,
|
||||||
|
#[clap(short, long)]
|
||||||
|
password: Option<SecretString>,
|
||||||
|
},
|
||||||
|
Download {
|
||||||
|
url: ParsedUrl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
sodiumoxide::init().map_err(|_| anyhow!("Failed to init sodiumoxide"))?;
|
||||||
|
let opts = Opts::parse();
|
||||||
|
|
||||||
|
match opts.action {
|
||||||
|
Action::Upload { url, password } => handle_upload(url, password),
|
||||||
|
Action::Download { url } => handle_download(url),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> {
|
||||||
|
url.set_fragment(None);
|
||||||
|
|
||||||
|
if atty::is(Stream::Stdin) {
|
||||||
|
bail!("This tool requires non interactive CLI. Pipe something in!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let (data, nonce, key, pw_used) = {
|
||||||
|
let enc_key = gen_key();
|
||||||
|
let nonce = gen_nonce();
|
||||||
|
let mut container = Vec::new();
|
||||||
|
std::io::stdin().read_to_end(&mut container)?;
|
||||||
|
let mut enc = seal(&container, &nonce, &enc_key);
|
||||||
|
|
||||||
|
let pw_used = if let Some(password) = password {
|
||||||
|
assert_eq!(sha256::DIGESTBYTES, KEYBYTES);
|
||||||
|
let pw_hash = sha256::hash(password.expose_secret().as_bytes());
|
||||||
|
let pw_key = Key::from_slice(pw_hash.as_ref()).expect("to succeed");
|
||||||
|
enc = seal(&enc, &nonce.increment_le(), &pw_key);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = base64::encode(&enc_key, UrlSafe);
|
||||||
|
let nonce = base64::encode(&nonce, UrlSafe);
|
||||||
|
|
||||||
|
(enc, nonce, key, pw_used)
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = Client::new()
|
||||||
|
.post(url.as_ref())
|
||||||
|
.body(data)
|
||||||
|
.send()
|
||||||
|
.context("Request to server failed")?;
|
||||||
|
|
||||||
|
if res.status() != StatusCode::OK {
|
||||||
|
bail!("Upload failed. Got HTTP error {}", res.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
url.path_segments_mut()
|
||||||
|
.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
url.set_fragment(Some(&fragment));
|
||||||
|
|
||||||
|
println!("{}", url);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_download(url: ParsedUrl) -> Result<()> {
|
||||||
|
let res = Client::new()
|
||||||
|
.get(url.sanitized_url)
|
||||||
|
.send()
|
||||||
|
.context("Failed to get data")?;
|
||||||
|
|
||||||
|
if res.status() != StatusCode::OK {
|
||||||
|
bail!("Got bad response from server: {}", res.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = res.bytes()?.as_ref().to_vec();
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
assert_eq!(sha256::DIGESTBYTES, KEYBYTES);
|
||||||
|
let pw_hash = sha256::hash(input.as_bytes());
|
||||||
|
let pw_key = Key::from_slice(pw_hash.as_ref()).expect("to succeed");
|
||||||
|
|
||||||
|
data = open(&data, &url.nonce.increment_le(), &pw_key)
|
||||||
|
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect password?"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = open(&data, &url.nonce, &url.decryption_key)
|
||||||
|
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect decryption key?"))?;
|
||||||
|
|
||||||
|
if atty::is(Stream::Stdout) {
|
||||||
|
if let Ok(data) = String::from_utf8(data) {
|
||||||
|
std::io::stdout().write_all(data.as_bytes())?;
|
||||||
|
} else {
|
||||||
|
bail!("Binary output detected. Please pipe to a file.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::io::stdout().write_all(&data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParsedUrl {
|
||||||
|
sanitized_url: Url,
|
||||||
|
decryption_key: Key,
|
||||||
|
nonce: Nonce,
|
||||||
|
needs_password: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ParsedUrl {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut url = Url::from_str(s)?;
|
||||||
|
let fragment = url
|
||||||
|
.fragment()
|
||||||
|
.context("Missing fragment. The decryption key is part of the fragment.")?;
|
||||||
|
if fragment.is_empty() {
|
||||||
|
bail!("Empty fragment. The decryption key is part of the fragment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = fragment.split('!').filter_map(|kv| {
|
||||||
|
let (k, v) = {
|
||||||
|
let mut iter = kv.split(':');
|
||||||
|
(iter.next(), iter.next())
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((k?, v))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut decryption_key = None;
|
||||||
|
let mut needs_password = false;
|
||||||
|
let mut nonce = None;
|
||||||
|
|
||||||
|
for (key, value) in args {
|
||||||
|
match (key, value) {
|
||||||
|
("key", Some(value)) => {
|
||||||
|
let key = base64::decode(value, UrlSafe)
|
||||||
|
.map_err(|_| anyhow!("Failed to decode key"))?;
|
||||||
|
let key = Key::from_slice(&key).context("Failed to parse key")?;
|
||||||
|
decryption_key = Some(key);
|
||||||
|
}
|
||||||
|
("pw", _) => {
|
||||||
|
needs_password = true;
|
||||||
|
}
|
||||||
|
("nonce", Some(value)) => {
|
||||||
|
nonce = Some(
|
||||||
|
Nonce::from_slice(
|
||||||
|
&base64::decode(value, UrlSafe)
|
||||||
|
.map_err(|_| anyhow!("Failed to decode nonce"))?,
|
||||||
|
)
|
||||||
|
.context("Invalid nonce provided")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url.set_fragment(None);
|
||||||
|
Ok(Self {
|
||||||
|
sanitized_url: url,
|
||||||
|
decryption_key: decryption_key.context("Missing decryption key")?,
|
||||||
|
needs_password,
|
||||||
|
nonce: nonce.context("Missing nonce")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
8
common/Cargo.toml
Normal file
8
common/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "omegaupload-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
7
common/src/lib.rs
Normal file
7
common/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
assert_eq!(2 + 2, 4);
|
||||||
|
}
|
||||||
|
}
|
23
server/Cargo.toml
Normal file
23
server/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "omegaupload-server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
axum = { version = "0.2", features = ["http2", "headers"] }
|
||||||
|
bincode = "1"
|
||||||
|
# We don't care about which version (We want to match with axum), we just need
|
||||||
|
# to enable the feature
|
||||||
|
bytes = { version = "*", features= ["serde"] }
|
||||||
|
# We just need to pull in whatever axum is pulling in
|
||||||
|
headers = "*"
|
||||||
|
lazy_static = "1"
|
||||||
|
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"
|
231
server/src/main.rs
Normal file
231
server/src/main.rs
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
#![warn(clippy::nursery, clippy::pedantic)]
|
||||||
|
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use paste::Expiration;
|
||||||
|
use rand::prelude::StdRng;
|
||||||
|
use rand::{Rng, SeedableRng};
|
||||||
|
use rocksdb::IteratorMode;
|
||||||
|
use rocksdb::WriteBatch;
|
||||||
|
use rocksdb::{Options, DB};
|
||||||
|
use short_code::ShortCode;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::body::Bytes;
|
||||||
|
use axum::extract::{Extension, Path, TypedHeader};
|
||||||
|
use axum::handler::{get, post};
|
||||||
|
use axum::{AddExtensionLayer, Router};
|
||||||
|
use tokio::task;
|
||||||
|
use tracing::warn;
|
||||||
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
|
use crate::paste::Paste;
|
||||||
|
use crate::time::FIVE_MINUTES;
|
||||||
|
|
||||||
|
mod paste;
|
||||||
|
mod short_code;
|
||||||
|
mod time;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
const DB_PATH: &str = "database";
|
||||||
|
const SHORT_CODE_SIZE: usize = 12;
|
||||||
|
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let db = Arc::new(DB::open_default(DB_PATH)?);
|
||||||
|
|
||||||
|
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||||
|
task::spawn(cleanup(Arc::clone(&stop_signal), Arc::clone(&db)));
|
||||||
|
|
||||||
|
axum::Server::bind(&"0.0.0.0:8080".parse()?)
|
||||||
|
.serve(
|
||||||
|
Router::new()
|
||||||
|
.route("/", post(upload::<SHORT_CODE_SIZE>))
|
||||||
|
.route(
|
||||||
|
"/:code",
|
||||||
|
get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
|
||||||
|
)
|
||||||
|
.layer(AddExtensionLayer::new(db))
|
||||||
|
.layer(AddExtensionLayer::new(StdRng::from_entropy()))
|
||||||
|
.into_make_service(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
stop_signal.store(true, Ordering::Release);
|
||||||
|
// Must be called for correct shutdown
|
||||||
|
DB::destroy(&Options::default(), DB_PATH)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(db), err)]
|
||||||
|
async fn upload<const N: usize>(
|
||||||
|
Extension(db): Extension<Arc<DB>>,
|
||||||
|
Extension(mut rng): Extension<StdRng>,
|
||||||
|
maybe_expires: Option<TypedHeader<Expiration>>,
|
||||||
|
body: Bytes,
|
||||||
|
) -> Result<Vec<u8>, StatusCode> {
|
||||||
|
if body.is_empty() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let paste = Paste::new(maybe_expires.map(|v| v.0).unwrap_or_default(), body);
|
||||||
|
let mut new_key = None;
|
||||||
|
|
||||||
|
// Try finding a code; give up after 1000 attempts
|
||||||
|
// Statistics show that this is very unlikely to happen
|
||||||
|
for _ in 0..1000 {
|
||||||
|
let code: ShortCode<N> = rng.sample(short_code::Generator);
|
||||||
|
let db = Arc::clone(&db);
|
||||||
|
let key = code.as_bytes();
|
||||||
|
let query = task::spawn_blocking(move || db.key_may_exist(key)).await;
|
||||||
|
if matches!(query, Ok(false)) {
|
||||||
|
new_key = Some(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = if let Some(key) = new_key {
|
||||||
|
key
|
||||||
|
} else {
|
||||||
|
error!("Failed to generate a valid shortcode");
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = if let Ok(v) = bincode::serialize(&paste) {
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
error!("Failed to serialize paste?!");
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
};
|
||||||
|
|
||||||
|
match task::spawn_blocking(move || db.put(key, value)).await {
|
||||||
|
Ok(Ok(_)) => (),
|
||||||
|
e => {
|
||||||
|
error!("Failed to insert paste into db: {:?}", e);
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Vec::from(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(db), err)]
|
||||||
|
async fn paste<const N: usize>(
|
||||||
|
Extension(db): Extension<Arc<DB>>,
|
||||||
|
Path(url): Path<ShortCode<N>>,
|
||||||
|
) -> Result<Bytes, StatusCode> {
|
||||||
|
let key = url.as_bytes();
|
||||||
|
|
||||||
|
let parsed: Paste = {
|
||||||
|
// not sure if perf of get_pinned is better than spawn_blocking
|
||||||
|
let query_result = db.get_pinned(key).map_err(|e| {
|
||||||
|
error!("Failed to fetch initial query: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let data = match query_result {
|
||||||
|
Some(data) => data,
|
||||||
|
None => return Err(StatusCode::NOT_FOUND),
|
||||||
|
};
|
||||||
|
|
||||||
|
bincode::deserialize(&data).map_err(|_| {
|
||||||
|
error!("Failed to deserialize data?!");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
if parsed.expired() {
|
||||||
|
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 delete expired paste: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Err(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.is_burn_after_read() {
|
||||||
|
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);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(parsed.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(db))]
|
||||||
|
async fn delete<const N: usize>(
|
||||||
|
Extension(db): Extension<Arc<DB>>,
|
||||||
|
Path(url): Path<ShortCode<N>>,
|
||||||
|
) -> StatusCode {
|
||||||
|
match task::spawn_blocking(move || db.delete(url.as_bytes())).await {
|
||||||
|
Ok(Ok(_)) => StatusCode::OK,
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Periodic clean-up task that deletes expired entries.
|
||||||
|
async fn cleanup(stop_signal: Arc<AtomicBool>, db: Arc<DB>) {
|
||||||
|
while !stop_signal.load(Ordering::Acquire) {
|
||||||
|
tokio::time::sleep(*FIVE_MINUTES).await;
|
||||||
|
let mut batch = WriteBatch::default();
|
||||||
|
for (key, value) in db.snapshot().iterator(IteratorMode::Start) {
|
||||||
|
// TODO: only partially decode struct for max perf
|
||||||
|
let join_handle = task::spawn_blocking(move || {
|
||||||
|
bincode::deserialize::<Paste>(&value)
|
||||||
|
.as_ref()
|
||||||
|
.map(Paste::expired)
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let should_delete = match join_handle {
|
||||||
|
Ok(should_delete) => should_delete,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to join thread?! {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_delete {
|
||||||
|
batch.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = Arc::clone(&db);
|
||||||
|
let join_handle = task::spawn_blocking(move || db.write(batch)).await;
|
||||||
|
let db_op_res = match join_handle {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to join handle?! {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = db_op_res {
|
||||||
|
warn!("Failed to cleanup db: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
server/src/paste.rs
Normal file
91
server/src/paste.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use axum::body::Bytes;
|
||||||
|
use headers::{Header, HeaderName, HeaderValue};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::time::{FIVE_MINUTES, ONE_DAY, ONE_HOUR, TEN_MINUTES};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Paste {
|
||||||
|
expiration: Option<Expiration>,
|
||||||
|
pub bytes: Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paste {
|
||||||
|
pub fn new(expiration: impl Into<Option<Expiration>>, bytes: Bytes) -> Self {
|
||||||
|
Self {
|
||||||
|
expiration: expiration.into(),
|
||||||
|
bytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expired(&self) -> bool {
|
||||||
|
self.expiration
|
||||||
|
.map(|expires| match expires {
|
||||||
|
Expiration::BurnAfterReading => false,
|
||||||
|
Expiration::UnixTime(expiration) => {
|
||||||
|
let now = time_since_unix();
|
||||||
|
expiration < now
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_burn_after_read(&self) -> bool {
|
||||||
|
matches!(self.expiration, Some(Expiration::BurnAfterReading))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Expiration {
|
||||||
|
BurnAfterReading,
|
||||||
|
UnixTime(Duration),
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref EXPIRATION_HEADER_NAME: HeaderName = HeaderName::from_static("burn-after");
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header for Expiration {
|
||||||
|
fn name() -> &'static HeaderName {
|
||||||
|
&*EXPIRATION_HEADER_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
I: Iterator<Item = &'i HeaderValue>,
|
||||||
|
{
|
||||||
|
let now = time_since_unix();
|
||||||
|
match values
|
||||||
|
.next()
|
||||||
|
.ok_or_else(headers::Error::invalid)?
|
||||||
|
.as_bytes()
|
||||||
|
{
|
||||||
|
b"read" => Ok(Self::BurnAfterReading),
|
||||||
|
b"5m" => Ok(Self::UnixTime(now + *FIVE_MINUTES)),
|
||||||
|
b"10m" => Ok(Self::UnixTime(now + *TEN_MINUTES)),
|
||||||
|
b"1h" => Ok(Self::UnixTime(now + *ONE_HOUR)),
|
||||||
|
b"1d" => Ok(Self::UnixTime(now + *ONE_DAY)),
|
||||||
|
_ => Err(headers::Error::invalid()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode<E: Extend<HeaderValue>>(&self, _: &mut E) {
|
||||||
|
unimplemented!("This shouldn't need implementation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Expiration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::UnixTime(time_since_unix() + *ONE_DAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time_since_unix() -> Duration {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("time since epoch to always work")
|
||||||
|
}
|
130
server/src/short_code.rs
Normal file
130
server/src/short_code.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
use rand::prelude::Distribution;
|
||||||
|
use serde::de::{Unexpected, Visitor};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
pub struct ShortCode<const N: usize>([ShortCodeChar; N]);
|
||||||
|
|
||||||
|
impl<const N: usize> ShortCode<N> {
|
||||||
|
pub fn as_bytes(&self) -> [u8; N] {
|
||||||
|
self.0.map(|v| v.0 as u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Debug for ShortCode<N> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let short_code = String::from_iter(self.0.map(|v| v.0));
|
||||||
|
f.debug_tuple("ShortCode").field(&short_code).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, const N: usize> Deserialize<'de> for ShortCode<N> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct ShortCodeVisitor<const N: usize>;
|
||||||
|
impl<'de, const N: usize> Visitor<'de> for ShortCodeVisitor<N> {
|
||||||
|
type Value = ShortCode<N>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a valid shortcode")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
if v.len() != N {
|
||||||
|
return Err(E::invalid_length(v.len(), &"a 12 character value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !v.is_ascii() {
|
||||||
|
return Err(E::invalid_value(Unexpected::Str(v), &"ascii only"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is fine, it'll get overwritten anyways.
|
||||||
|
let mut output = [ShortCodeChar('\0'); N];
|
||||||
|
for (i, c) in v.char_indices() {
|
||||||
|
output[i] = c.try_into().map_err(|_| {
|
||||||
|
E::invalid_value(Unexpected::Char(c), &"a valid short code character")
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ShortCode(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_str(ShortCodeVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `ShortCodeChar` uses the Word-safe alphabet, a Base32 extension of the Open
|
||||||
|
/// Location Code Base20 alphabet.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct ShortCodeChar(char);
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ShortCodeChar {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct ShortCodeCharVisitor;
|
||||||
|
impl<'de> Visitor<'de> for ShortCodeCharVisitor {
|
||||||
|
type Value = ShortCodeChar;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a valid short code char")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_char<E>(self, v: char) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
v.try_into().map_err(|_| {
|
||||||
|
E::invalid_value(Unexpected::Char(v as char), &"a valid short code character")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_char(ShortCodeCharVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<char> for ShortCodeChar {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn try_from(v: char) -> Result<Self, Self::Error> {
|
||||||
|
if v.is_ascii() && ALPHABET.contains(&(v as u8)) {
|
||||||
|
Ok(Self(v))
|
||||||
|
} else {
|
||||||
|
Err("a valid short code character")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let value = rng.gen_range(0..32);
|
||||||
|
assert!(value < 32);
|
||||||
|
ShortCodeChar(ALPHABET[value] as char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Distribution<ShortCode<N>> for Generator {
|
||||||
|
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> ShortCode<N> {
|
||||||
|
let mut arr = [ShortCodeChar('\0'); N];
|
||||||
|
|
||||||
|
for c in arr.iter_mut() {
|
||||||
|
*c = self.sample(rng);
|
||||||
|
}
|
||||||
|
ShortCode(arr)
|
||||||
|
}
|
||||||
|
}
|
10
server/src/time.rs
Normal file
10
server/src/time.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref FIVE_MINUTES: Duration = Duration::from_secs(5 * 60);
|
||||||
|
pub static ref TEN_MINUTES: Duration = Duration::from_secs(5 * 60);
|
||||||
|
pub static ref ONE_HOUR: Duration = Duration::from_secs(5 * 60);
|
||||||
|
pub static ref ONE_DAY: Duration = Duration::from_secs(5 * 60);
|
||||||
|
}
|
10
web/Cargo.toml
Normal file
10
web/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "omegaupload-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chacha20poly1305 = "0.9"
|
||||||
|
yew = "0.18"
|
477
web/dist/index-cb70f5af99403c29.js
vendored
Normal file
477
web/dist/index-cb70f5af99403c29.js
vendored
Normal file
|
@ -0,0 +1,477 @@
|
||||||
|
|
||||||
|
let wasm;
|
||||||
|
|
||||||
|
const heap = new Array(32).fill(undefined);
|
||||||
|
|
||||||
|
heap.push(undefined, null, true, false);
|
||||||
|
|
||||||
|
function getObject(idx) { return heap[idx]; }
|
||||||
|
|
||||||
|
let heap_next = heap.length;
|
||||||
|
|
||||||
|
function addHeapObject(obj) {
|
||||||
|
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||||
|
const idx = heap_next;
|
||||||
|
heap_next = heap[idx];
|
||||||
|
|
||||||
|
heap[idx] = obj;
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||||
|
|
||||||
|
cachedTextDecoder.decode();
|
||||||
|
|
||||||
|
let cachegetUint8Memory0 = null;
|
||||||
|
function getUint8Memory0() {
|
||||||
|
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
|
||||||
|
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachegetUint8Memory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFromWasm0(ptr, len) {
|
||||||
|
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropObject(idx) {
|
||||||
|
if (idx < 36) return;
|
||||||
|
heap[idx] = heap_next;
|
||||||
|
heap_next = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeObject(idx) {
|
||||||
|
const ret = getObject(idx);
|
||||||
|
dropObject(idx);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugString(val) {
|
||||||
|
// primitive types
|
||||||
|
const type = typeof val;
|
||||||
|
if (type == 'number' || type == 'boolean' || val == null) {
|
||||||
|
return `${val}`;
|
||||||
|
}
|
||||||
|
if (type == 'string') {
|
||||||
|
return `"${val}"`;
|
||||||
|
}
|
||||||
|
if (type == 'symbol') {
|
||||||
|
const description = val.description;
|
||||||
|
if (description == null) {
|
||||||
|
return 'Symbol';
|
||||||
|
} else {
|
||||||
|
return `Symbol(${description})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == 'function') {
|
||||||
|
const name = val.name;
|
||||||
|
if (typeof name == 'string' && name.length > 0) {
|
||||||
|
return `Function(${name})`;
|
||||||
|
} else {
|
||||||
|
return 'Function';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// objects
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const length = val.length;
|
||||||
|
let debug = '[';
|
||||||
|
if (length > 0) {
|
||||||
|
debug += debugString(val[0]);
|
||||||
|
}
|
||||||
|
for(let i = 1; i < length; i++) {
|
||||||
|
debug += ', ' + debugString(val[i]);
|
||||||
|
}
|
||||||
|
debug += ']';
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
// Test for built-in
|
||||||
|
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||||
|
let className;
|
||||||
|
if (builtInMatches.length > 1) {
|
||||||
|
className = builtInMatches[1];
|
||||||
|
} else {
|
||||||
|
// Failed to match the standard '[object ClassName]'
|
||||||
|
return toString.call(val);
|
||||||
|
}
|
||||||
|
if (className == 'Object') {
|
||||||
|
// we're a user defined class or Object
|
||||||
|
// JSON.stringify avoids problems with cycles, and is generally much
|
||||||
|
// easier than looping through ownProperties of `val`.
|
||||||
|
try {
|
||||||
|
return 'Object(' + JSON.stringify(val) + ')';
|
||||||
|
} catch (_) {
|
||||||
|
return 'Object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// errors
|
||||||
|
if (val instanceof Error) {
|
||||||
|
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||||
|
}
|
||||||
|
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
|
let cachedTextEncoder = new TextEncoder('utf-8');
|
||||||
|
|
||||||
|
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||||
|
? function (arg, view) {
|
||||||
|
return cachedTextEncoder.encodeInto(arg, view);
|
||||||
|
}
|
||||||
|
: function (arg, view) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
view.set(buf);
|
||||||
|
return {
|
||||||
|
read: arg.length,
|
||||||
|
written: buf.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
|
|
||||||
|
if (realloc === undefined) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
const ptr = malloc(buf.length);
|
||||||
|
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||||
|
WASM_VECTOR_LEN = buf.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = arg.length;
|
||||||
|
let ptr = malloc(len);
|
||||||
|
|
||||||
|
const mem = getUint8Memory0();
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (; offset < len; offset++) {
|
||||||
|
const code = arg.charCodeAt(offset);
|
||||||
|
if (code > 0x7F) break;
|
||||||
|
mem[ptr + offset] = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset !== len) {
|
||||||
|
if (offset !== 0) {
|
||||||
|
arg = arg.slice(offset);
|
||||||
|
}
|
||||||
|
ptr = realloc(ptr, len, len = offset + arg.length * 3);
|
||||||
|
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
|
||||||
|
const ret = encodeString(arg, view);
|
||||||
|
|
||||||
|
offset += ret.written;
|
||||||
|
}
|
||||||
|
|
||||||
|
WASM_VECTOR_LEN = offset;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachegetInt32Memory0 = null;
|
||||||
|
function getInt32Memory0() {
|
||||||
|
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
|
||||||
|
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachegetInt32Memory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMutClosure(arg0, arg1, dtor, f) {
|
||||||
|
const state = { a: arg0, b: arg1, cnt: 1, dtor };
|
||||||
|
const real = (...args) => {
|
||||||
|
// First up with a closure we increment the internal reference
|
||||||
|
// count. This ensures that the Rust closure environment won't
|
||||||
|
// be deallocated while we're invoking it.
|
||||||
|
state.cnt++;
|
||||||
|
const a = state.a;
|
||||||
|
state.a = 0;
|
||||||
|
try {
|
||||||
|
return f(a, state.b, ...args);
|
||||||
|
} finally {
|
||||||
|
if (--state.cnt === 0) {
|
||||||
|
wasm.__wbindgen_export_2.get(state.dtor)(a, state.b);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
state.a = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
real.original = state;
|
||||||
|
|
||||||
|
return real;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stack_pointer = 32;
|
||||||
|
|
||||||
|
function addBorrowedObject(obj) {
|
||||||
|
if (stack_pointer == 1) throw new Error('out of js stack');
|
||||||
|
heap[--stack_pointer] = obj;
|
||||||
|
return stack_pointer;
|
||||||
|
}
|
||||||
|
function __wbg_adapter_16(arg0, arg1, arg2) {
|
||||||
|
try {
|
||||||
|
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6f95cc4b17d3592d(arg0, arg1, addBorrowedObject(arg2));
|
||||||
|
} finally {
|
||||||
|
heap[stack_pointer++] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikeNone(x) {
|
||||||
|
return x === undefined || x === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(f, args) {
|
||||||
|
try {
|
||||||
|
return f.apply(this, args);
|
||||||
|
} catch (e) {
|
||||||
|
wasm.__wbindgen_exn_store(addHeapObject(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load(module, imports) {
|
||||||
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
|
try {
|
||||||
|
return await WebAssembly.instantiateStreaming(module, imports);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (module.headers.get('Content-Type') != 'application/wasm') {
|
||||||
|
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await module.arrayBuffer();
|
||||||
|
return await WebAssembly.instantiate(bytes, imports);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const instance = await WebAssembly.instantiate(module, imports);
|
||||||
|
|
||||||
|
if (instance instanceof WebAssembly.Instance) {
|
||||||
|
return { instance, module };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init(input) {
|
||||||
|
if (typeof input === 'undefined') {
|
||||||
|
input = new URL('index-cb70f5af99403c29_bg.wasm', import.meta.url);
|
||||||
|
}
|
||||||
|
const imports = {};
|
||||||
|
imports.wbg = {};
|
||||||
|
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
||||||
|
var ret = getObject(arg0);
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_undefined = function(arg0) {
|
||||||
|
var ret = getObject(arg0) === undefined;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||||
|
var ret = getStringFromWasm0(arg0, arg1);
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||||
|
const obj = takeObject(arg0).original;
|
||||||
|
if (obj.cnt-- == 1) {
|
||||||
|
obj.a = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var ret = false;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_error_09919627ac0992f5 = function(arg0, arg1) {
|
||||||
|
try {
|
||||||
|
console.error(getStringFromWasm0(arg0, arg1));
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(arg0, arg1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_693216e109162396 = function() {
|
||||||
|
var ret = new Error();
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_stack_0ddaca5d1abfb52f = function(arg0, arg1) {
|
||||||
|
var ret = getObject(arg1).stack;
|
||||||
|
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len0 = WASM_VECTOR_LEN;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 1] = len0;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||||
|
takeObject(arg0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_Window_c4b70662a0d2c5ec = function(arg0) {
|
||||||
|
var ret = getObject(arg0) instanceof Window;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_document_1c64944725c0d81d = function(arg0) {
|
||||||
|
var ret = getObject(arg0).document;
|
||||||
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_createElement_86c152812a141a62 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
var ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_createElementNS_ae12b8681c3957a3 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||||
|
var ret = getObject(arg0).createElementNS(arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_createTextNode_365db3bc3d0523ab = function(arg0, arg1, arg2) {
|
||||||
|
var ret = getObject(arg0).createTextNode(getStringFromWasm0(arg1, arg2));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_querySelector_b92a6c73bcfe671b = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
var ret = getObject(arg0).querySelector(getStringFromWasm0(arg1, arg2));
|
||||||
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_instanceof_HtmlTextAreaElement_c2f3b4bd6871d5ad = function(arg0) {
|
||||||
|
var ret = getObject(arg0) instanceof HTMLTextAreaElement;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_value_686b2a68422cb88d = function(arg0, arg1) {
|
||||||
|
var ret = getObject(arg1).value;
|
||||||
|
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len0 = WASM_VECTOR_LEN;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 1] = len0;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setvalue_0a07023245efa3cc = function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_HtmlButtonElement_54060a3d8d49c8a6 = function(arg0) {
|
||||||
|
var ret = getObject(arg0) instanceof HTMLButtonElement;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_settype_bd9da7e07b7cb217 = function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0).type = getStringFromWasm0(arg1, arg2);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_HtmlInputElement_8cafe5f30dfdb6bc = function(arg0) {
|
||||||
|
var ret = getObject(arg0) instanceof HTMLInputElement;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setchecked_206243371da58f6a = function(arg0, arg1) {
|
||||||
|
getObject(arg0).checked = arg1 !== 0;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_settype_6a7d0ca3b1b6d0c2 = function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0).type = getStringFromWasm0(arg1, arg2);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_value_0627d4b1c27534e6 = function(arg0, arg1) {
|
||||||
|
var ret = getObject(arg1).value;
|
||||||
|
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len0 = WASM_VECTOR_LEN;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 1] = len0;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setvalue_2459f62386b6967f = function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_addEventListener_09e11fbf8b4b719b = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||||
|
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_removeEventListener_24d5a7c12c3f3c39 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||||
|
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_namespaceURI_f4cd665d07463337 = function(arg0, arg1) {
|
||||||
|
var ret = getObject(arg1).namespaceURI;
|
||||||
|
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len0 = WASM_VECTOR_LEN;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 1] = len0;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_removeAttribute_eea03ed128669b8f = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
getObject(arg0).removeAttribute(getStringFromWasm0(arg1, arg2));
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_setAttribute_1b533bf07966de55 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||||
|
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_lastChild_ca5bac177ef353f6 = function(arg0) {
|
||||||
|
var ret = getObject(arg0).lastChild;
|
||||||
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setnodeValue_702374ad3d0ec3df = function(arg0, arg1, arg2) {
|
||||||
|
getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_appendChild_d318db34c4559916 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
var ret = getObject(arg0).appendChild(getObject(arg1));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_insertBefore_5b314357408fbec1 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
var ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_removeChild_d3ca7b53e537867e = function() { return handleError(function (arg0, arg1) {
|
||||||
|
var ret = getObject(arg0).removeChild(getObject(arg1));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_newnoargs_be86524d73f67598 = function(arg0, arg1) {
|
||||||
|
var ret = new Function(getStringFromWasm0(arg0, arg1));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_call_888d259a5fefc347 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
var ret = getObject(arg0).call(getObject(arg1));
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_is_0f5efc7977a2c50b = function(arg0, arg1) {
|
||||||
|
var ret = Object.is(getObject(arg0), getObject(arg1));
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_0b83d3df67ecb33e = function() {
|
||||||
|
var ret = new Object();
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_globalThis_3f735a5746d41fbd = function() { return handleError(function () {
|
||||||
|
var ret = globalThis.globalThis;
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_self_c6fbdfc2918d5e58 = function() { return handleError(function () {
|
||||||
|
var ret = self.self;
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_window_baec038b5ab35c54 = function() { return handleError(function () {
|
||||||
|
var ret = window.window;
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_global_1bc0b39582740e95 = function() { return handleError(function () {
|
||||||
|
var ret = global.global;
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_set_82a4e8a85e31ac42 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
var ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2));
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||||
|
var ret = debugString(getObject(arg1));
|
||||||
|
var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len0 = WASM_VECTOR_LEN;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 1] = len0;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||||
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper1071 = function(arg0, arg1, arg2) {
|
||||||
|
var ret = makeMutClosure(arg0, arg1, 25, __wbg_adapter_16);
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
|
||||||
|
input = fetch(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const { instance, module } = await load(await input, imports);
|
||||||
|
|
||||||
|
wasm = instance.exports;
|
||||||
|
init.__wbindgen_wasm_module = module;
|
||||||
|
wasm.__wbindgen_start();
|
||||||
|
return wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default init;
|
||||||
|
|
BIN
web/dist/index-cb70f5af99403c29_bg.wasm
vendored
Normal file
BIN
web/dist/index-cb70f5af99403c29_bg.wasm
vendored
Normal file
Binary file not shown.
34
web/dist/index.html
vendored
Normal file
34
web/dist/index.html
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<!DOCTYPE html><html><head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Yew App</title>
|
||||||
|
|
||||||
|
<link rel="preload" href="/index-cb70f5af99403c29_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||||
|
<link rel="modulepreload" href="/index-cb70f5af99403c29.js"></head>
|
||||||
|
|
||||||
|
<body><script type="module">import init from '/index-cb70f5af99403c29.js';init('/index-cb70f5af99403c29_bg.wasm');</script><script>(function () {
|
||||||
|
var url = 'ws://' + window.location.host + '/_trunk/ws';
|
||||||
|
var poll_interval = 5000;
|
||||||
|
var reload_upon_connect = () => {
|
||||||
|
window.setTimeout(
|
||||||
|
() => {
|
||||||
|
// when we successfully reconnect, we'll force a
|
||||||
|
// reload (since we presumably lost connection to
|
||||||
|
// trunk due to it being killed, so it will have
|
||||||
|
// rebuilt on restart)
|
||||||
|
var ws = new WebSocket(url);
|
||||||
|
ws.onopen = () => window.location.reload();
|
||||||
|
ws.onclose = reload_upon_connect;
|
||||||
|
},
|
||||||
|
poll_interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
var ws = new WebSocket(url);
|
||||||
|
ws.onmessage = (ev) => {
|
||||||
|
const msg = JSON.parse(ev.data);
|
||||||
|
if (msg.reload) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onclose = reload_upon_connect;
|
||||||
|
})()
|
||||||
|
</script></body></html>
|
9
web/index.html
Normal file
9
web/index.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Yew App</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
</html>
|
44
web/src/main.rs
Normal file
44
web/src/main.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use yew::prelude::*;
|
||||||
|
enum Msg {
|
||||||
|
AddOne,
|
||||||
|
}
|
||||||
|
struct Model {
|
||||||
|
// `ComponentLink` is like a reference to a component.
|
||||||
|
// It can be used to send messages to the component
|
||||||
|
link: ComponentLink<Self>,
|
||||||
|
value: i64,
|
||||||
|
}
|
||||||
|
impl Component for Model {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = ();
|
||||||
|
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||||
|
Self { link, value: 0 }
|
||||||
|
}
|
||||||
|
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||||
|
match msg {
|
||||||
|
Msg::AddOne => {
|
||||||
|
self.value += 1;
|
||||||
|
// the value has changed so we need to
|
||||||
|
// re-render for it to appear on the page
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||||
|
// Should only return "true" if new properties are different to
|
||||||
|
// previously received properties.
|
||||||
|
// This component has no properties so we will always return "false".
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<button onclick=self.link.callback(|_| Msg::AddOne)>{ "+1" }</button>
|
||||||
|
<p>{ self.value }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn main() {
|
||||||
|
yew::start_app::<Model>();
|
||||||
|
}
|
Loading…
Reference in a new issue