2021-10-22 01:35:54 +00:00
|
|
|
#![warn(clippy::nursery, clippy::pedantic)]
|
|
|
|
|
2021-10-31 21:01:27 +00:00
|
|
|
// 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/>.
|
|
|
|
|
2021-10-17 21:15:29 +00:00
|
|
|
use std::str::FromStr;
|
|
|
|
|
2021-10-25 09:42:20 +00:00
|
|
|
use anyhow::{anyhow, bail, Context, Result};
|
|
|
|
use byte_unit::{n_mib_bytes, Byte};
|
2022-01-12 06:19:15 +00:00
|
|
|
use decrypt::{DecryptedData, MimeType};
|
2021-10-25 09:42:20 +00:00
|
|
|
use gloo_console::{error, log};
|
2021-10-20 01:48:32 +00:00
|
|
|
use http::uri::PathAndQuery;
|
|
|
|
use http::{StatusCode, Uri};
|
2021-10-31 01:38:55 +00:00
|
|
|
use js_sys::{Array, JsString, Object, Uint8Array};
|
2022-07-27 02:03:50 +00:00
|
|
|
use omegaupload_common::base64;
|
|
|
|
use omegaupload_common::crypto::seal_in_place;
|
2022-07-27 02:38:45 +00:00
|
|
|
use omegaupload_common::crypto::{Error as CryptoError, Key};
|
|
|
|
use omegaupload_common::fragment::Builder;
|
|
|
|
use omegaupload_common::secrecy::{ExposeSecret, Secret, SecretString, SecretVec};
|
2022-07-27 02:03:50 +00:00
|
|
|
use omegaupload_common::{Expiration, PartialParsedUrl, Url};
|
2021-10-23 17:10:55 +00:00
|
|
|
use reqwasm::http::Request;
|
2021-10-24 09:25:42 +00:00
|
|
|
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
|
|
|
use wasm_bindgen::{JsCast, JsValue};
|
2021-10-24 19:14:55 +00:00
|
|
|
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
2021-10-24 23:16:02 +00:00
|
|
|
use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window};
|
2021-10-17 21:15:29 +00:00
|
|
|
|
2021-10-24 09:25:42 +00:00
|
|
|
use crate::decrypt::decrypt;
|
2021-10-24 23:16:02 +00:00
|
|
|
use crate::idb_object::IdbObject;
|
|
|
|
use crate::util::as_idb_db;
|
2021-10-23 17:10:55 +00:00
|
|
|
|
|
|
|
mod decrypt;
|
2021-10-24 23:16:02 +00:00
|
|
|
mod idb_object;
|
|
|
|
mod util;
|
2021-10-23 17:10:55 +00:00
|
|
|
|
2021-10-25 09:42:20 +00:00
|
|
|
const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500);
|
|
|
|
|
2022-01-18 09:39:56 +00:00
|
|
|
#[wasm_bindgen(raw_module = "../src/render")]
|
2021-10-24 09:25:42 +00:00
|
|
|
extern "C" {
|
2021-10-24 23:16:02 +00:00
|
|
|
#[wasm_bindgen(js_name = loadFromDb)]
|
2022-01-16 06:47:56 +00:00
|
|
|
pub fn load_from_db(mime_type: JsString, name: Option<JsString>, language: Option<JsString>);
|
2021-10-25 09:42:20 +00:00
|
|
|
#[wasm_bindgen(js_name = renderMessage)]
|
|
|
|
pub fn render_message(message: JsString);
|
2022-07-27 02:03:50 +00:00
|
|
|
#[wasm_bindgen(js_name = createUploadUi)]
|
|
|
|
pub fn create_upload_ui();
|
2021-10-24 23:16:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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"))
|
2021-10-24 09:25:42 +00:00
|
|
|
}
|
|
|
|
|
2022-01-18 09:39:56 +00:00
|
|
|
#[wasm_bindgen]
|
2022-02-28 00:30:21 +00:00
|
|
|
#[allow(clippy::missing_panics_doc)]
|
2022-01-18 09:39:56 +00:00
|
|
|
pub fn start() {
|
2021-10-25 03:54:49 +00:00
|
|
|
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
2021-10-25 09:42:20 +00:00
|
|
|
|
2021-10-31 08:16:31 +00:00
|
|
|
if location().pathname().unwrap() == "/" {
|
2022-07-27 02:03:50 +00:00
|
|
|
create_upload_ui();
|
2021-10-31 08:16:31 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-10-25 09:42:20 +00:00
|
|
|
render_message("Loading paste...".into());
|
|
|
|
|
2021-10-24 23:16:02 +00:00
|
|
|
let url = String::from(location().to_string());
|
2021-10-24 19:14:55 +00:00
|
|
|
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();
|
2021-10-17 21:15:29 +00:00
|
|
|
}
|
2021-10-24 19:14:55 +00:00
|
|
|
Uri::from_parts(uri_parts).unwrap()
|
|
|
|
};
|
2021-10-17 21:15:29 +00:00
|
|
|
|
2022-01-16 06:47:56 +00:00
|
|
|
let (
|
|
|
|
key,
|
|
|
|
PartialParsedUrl {
|
|
|
|
needs_password,
|
|
|
|
name,
|
|
|
|
language,
|
|
|
|
..
|
|
|
|
},
|
|
|
|
) = {
|
2022-01-12 07:50:07 +00:00
|
|
|
let fragment = if let Some(fragment) = url.split_once('#').map(|(_, fragment)| fragment) {
|
2022-01-12 07:52:20 +00:00
|
|
|
if fragment.is_empty() {
|
|
|
|
error!("Key is missing in url; bailing.");
|
|
|
|
render_message("Invalid paste link: Missing metadata.".into());
|
|
|
|
return;
|
|
|
|
}
|
2022-01-16 00:55:47 +00:00
|
|
|
fragment
|
2022-01-12 07:50:07 +00:00
|
|
|
} else {
|
|
|
|
error!("Key is missing in url; bailing.");
|
2022-01-12 07:52:20 +00:00
|
|
|
render_message("Invalid paste link: Missing metadata.".into());
|
2022-01-12 07:50:07 +00:00
|
|
|
return;
|
2022-01-12 07:48:53 +00:00
|
|
|
};
|
|
|
|
|
2022-01-16 06:47:56 +00:00
|
|
|
let mut partial_parsed_url = match PartialParsedUrl::try_from(fragment) {
|
2022-01-12 07:48:53 +00:00
|
|
|
Ok(partial_parsed_url) => partial_parsed_url,
|
|
|
|
Err(e) => {
|
|
|
|
error!("Failed to parse text fragment; bailing.");
|
2022-01-16 08:49:42 +00:00
|
|
|
render_message(format!("Invalid paste link: {e}").into());
|
2022-01-12 07:48:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-01-16 06:47:56 +00:00
|
|
|
let key = if let Some(key) = partial_parsed_url.decryption_key.take() {
|
2021-10-26 00:31:30 +00:00
|
|
|
key
|
|
|
|
} else {
|
|
|
|
error!("Key is missing in url; bailing.");
|
|
|
|
render_message("Invalid paste link: Missing decryption key.".into());
|
|
|
|
return;
|
2021-10-25 09:42:20 +00:00
|
|
|
};
|
2022-01-12 07:48:53 +00:00
|
|
|
|
2022-01-16 06:47:56 +00:00
|
|
|
(key, partial_parsed_url)
|
2021-10-25 09:42:20 +00:00
|
|
|
};
|
|
|
|
|
2022-01-16 06:47:56 +00:00
|
|
|
let password = if needs_password {
|
2021-10-25 09:42:20 +00:00
|
|
|
loop {
|
|
|
|
let pw = window().prompt_with_message("A password is required to decrypt this paste:");
|
|
|
|
|
2021-10-31 07:57:52 +00:00
|
|
|
match pw {
|
2021-10-31 08:39:11 +00:00
|
|
|
// Ok button was entered.
|
2021-10-31 08:01:05 +00:00
|
|
|
Ok(Some(password)) if !password.is_empty() => {
|
2021-10-31 04:00:09 +00:00
|
|
|
break Some(SecretVec::new(password.into_bytes()));
|
2021-10-25 09:42:20 +00:00
|
|
|
}
|
2021-10-31 19:34:26 +00:00
|
|
|
// Empty message was entered.
|
|
|
|
Ok(Some(_)) => (),
|
2021-10-31 08:39:11 +00:00
|
|
|
// Cancel button was entered.
|
|
|
|
Ok(None) => {
|
2021-10-31 07:57:52 +00:00
|
|
|
render_message("This paste requires a password.".into());
|
|
|
|
return;
|
|
|
|
}
|
2021-10-31 08:39:11 +00:00
|
|
|
e => {
|
|
|
|
render_message("Internal error occurred.".into());
|
2022-01-16 08:49:42 +00:00
|
|
|
error!(format!("Error occurred at pw prompt: {e:?}"));
|
2021-10-31 08:39:11 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-10-25 09:42:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
2021-10-31 08:16:31 +00:00
|
|
|
spawn_local(async move {
|
2022-01-16 06:47:56 +00:00
|
|
|
if let Err(e) = fetch_resources(request_uri, key, password, name, language).await {
|
2021-10-31 08:16:31 +00:00
|
|
|
log!(e.to_string());
|
|
|
|
}
|
|
|
|
});
|
2021-10-17 21:15:29 +00:00
|
|
|
}
|
|
|
|
|
2022-07-27 02:03:50 +00:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[allow(clippy::future_not_send)]
|
|
|
|
#[allow(clippy::missing_panics_doc)]
|
|
|
|
pub fn encrypt_string(data: String) {
|
|
|
|
spawn_local(async move {
|
|
|
|
if let Err(e) = do_encrypt(data.into_bytes()).await {
|
2022-07-27 16:27:51 +00:00
|
|
|
log!(format!("[rs] Error encrypting string: {}", e));
|
2022-07-27 02:03:50 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::future_not_send)]
|
|
|
|
async fn do_encrypt(mut data: Vec<u8>) -> Result<()> {
|
|
|
|
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 s: String = location().to_string().into();
|
|
|
|
let mut url = Url::from_str(&s)?;
|
|
|
|
let fragment = Builder::new(key);
|
|
|
|
|
2022-07-27 02:38:45 +00:00
|
|
|
let js_data = Uint8Array::new_with_length(u32::try_from(data.len()).expect("Data too large"));
|
2022-07-27 02:03:50 +00:00
|
|
|
js_data.copy_from(&data);
|
|
|
|
|
2022-07-27 02:38:45 +00:00
|
|
|
let short_code = Request::post(url.as_ref())
|
|
|
|
.body(js_data)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.text()
|
|
|
|
.await?;
|
2022-07-27 02:03:50 +00:00
|
|
|
url.set_path(&short_code);
|
|
|
|
url.set_fragment(Some(fragment.build().expose_secret()));
|
2022-07-27 02:38:45 +00:00
|
|
|
location()
|
|
|
|
.set_href(url.as_ref())
|
|
|
|
.expect("Unable to navigate to encrypted upload");
|
2022-07-27 02:03:50 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-10-24 19:14:55 +00:00
|
|
|
#[allow(clippy::future_not_send)]
|
2021-10-31 04:00:09 +00:00
|
|
|
async fn fetch_resources(
|
|
|
|
request_uri: Uri,
|
|
|
|
key: Secret<Key>,
|
|
|
|
password: Option<SecretVec<u8>>,
|
2022-01-16 06:47:56 +00:00
|
|
|
name: Option<String>,
|
|
|
|
language: Option<String>,
|
2021-10-31 04:00:09 +00:00
|
|
|
) -> Result<()> {
|
2021-10-24 19:14:55 +00:00
|
|
|
match Request::get(&request_uri.to_string()).send().await {
|
|
|
|
Ok(resp) if resp.status() == StatusCode::OK => {
|
2021-10-24 23:16:02 +00:00
|
|
|
let expires = Expiration::try_from(resp.headers()).map_or_else(
|
|
|
|
|_| "This item does not expire.".to_string(),
|
|
|
|
|expires| expires.to_string(),
|
|
|
|
);
|
2021-10-24 19:14:55 +00:00
|
|
|
|
|
|
|
let data = {
|
2021-10-24 20:12:14 +00:00
|
|
|
let data_fut = resp
|
|
|
|
.as_raw()
|
|
|
|
.array_buffer()
|
2021-10-25 09:42:20 +00:00
|
|
|
.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!(
|
2022-01-16 08:49:42 +00:00
|
|
|
"JsFuture returned an error while fetching resp buffer: {e:?}",
|
2021-10-25 09:42:20 +00:00
|
|
|
));
|
|
|
|
}
|
|
|
|
};
|
2021-10-24 20:12:14 +00:00
|
|
|
Uint8Array::new(&data).to_vec()
|
2021-10-24 09:25:42 +00:00
|
|
|
};
|
2021-10-23 17:10:55 +00:00
|
|
|
|
2021-10-25 09:42:20 +00:00
|
|
|
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(());
|
|
|
|
}
|
2021-10-19 09:18:33 +00:00
|
|
|
|
2022-01-16 08:49:42 +00:00
|
|
|
let (decrypted, mimetype) = match decrypt(data, &key, password, name.as_deref()) {
|
2021-10-31 07:57:52 +00:00
|
|
|
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 => {
|
2022-01-16 08:49:42 +00:00
|
|
|
log!(format!("Bad kdf or corrupted blob: {e}"));
|
2021-10-31 07:57:52 +00:00
|
|
|
"An internal error occurred."
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
render_message(JsString::from(msg));
|
|
|
|
bail!(e);
|
|
|
|
}
|
|
|
|
};
|
2021-10-24 23:16:02 +00:00
|
|
|
let db_open_req = open_idb()?;
|
2021-10-24 19:14:55 +00:00
|
|
|
|
2021-10-31 19:40:43 +00:00
|
|
|
let on_success = Closure::once(Box::new(move |event| {
|
2022-01-16 06:47:56 +00:00
|
|
|
on_success(&event, &decrypted, mimetype, &expires, name, language);
|
2021-10-31 19:40:43 +00:00
|
|
|
}));
|
2021-10-24 19:14:55 +00:00
|
|
|
|
2021-10-24 23:16:02 +00:00
|
|
|
db_open_req.set_onsuccess(Some(on_success.into_js_value().unchecked_ref()));
|
|
|
|
db_open_req.set_onerror(Some(
|
2021-10-31 19:40:43 +00:00
|
|
|
Closure::once(Box::new(|e: Event| log!(e)))
|
|
|
|
.into_js_value()
|
|
|
|
.unchecked_ref(),
|
2021-10-24 23:16:02 +00:00
|
|
|
));
|
2021-10-31 19:40:43 +00:00
|
|
|
let on_upgrade = Closure::once(Box::new(move |event: Event| {
|
2021-10-24 23:16:02 +00:00
|
|
|
let db = as_idb_db(&event);
|
2021-10-26 00:31:30 +00:00
|
|
|
let _obj_store = db.create_object_store("decrypted data").unwrap();
|
2021-10-31 19:40:43 +00:00
|
|
|
}));
|
2021-10-24 23:16:02 +00:00
|
|
|
db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref()));
|
2021-10-24 19:14:55 +00:00
|
|
|
}
|
|
|
|
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
|
2021-10-25 09:42:20 +00:00
|
|
|
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) => {
|
2021-10-26 00:31:30 +00:00
|
|
|
render_message(err.status_text().into());
|
2021-10-25 09:42:20 +00:00
|
|
|
}
|
|
|
|
Err(err) => {
|
2022-01-16 08:49:42 +00:00
|
|
|
render_message(format!("{err}").into());
|
2021-10-24 19:14:55 +00:00
|
|
|
}
|
2021-10-24 20:12:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-10-31 19:40:43 +00:00
|
|
|
|
2022-01-16 06:47:56 +00:00
|
|
|
fn on_success(
|
|
|
|
event: &Event,
|
|
|
|
decrypted: &DecryptedData,
|
|
|
|
mimetype: MimeType,
|
|
|
|
expires: &str,
|
|
|
|
name: Option<String>,
|
|
|
|
language: Option<String>,
|
|
|
|
) {
|
2021-10-31 19:40:43 +00:00
|
|
|
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| JsValue::from_serde(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(
|
2021-10-31 19:43:44 +00:00
|
|
|
Closure::once(Box::new(|| {
|
2022-01-16 07:53:25 +00:00
|
|
|
log!("[rs] Successfully inserted encrypted item into storage.");
|
2022-01-16 06:47:56 +00:00
|
|
|
let name = name.map(JsString::from);
|
|
|
|
let language = language.map(JsString::from);
|
|
|
|
load_from_db(JsString::from(mimetype.0), name, language);
|
2021-10-31 19:43:44 +00:00
|
|
|
}))
|
2021-10-31 19:40:43 +00:00
|
|
|
.into_js_value()
|
|
|
|
.unchecked_ref(),
|
|
|
|
));
|
|
|
|
put_action.set_onerror(Some(
|
2021-10-31 19:43:44 +00:00
|
|
|
Closure::once(Box::new(|e: Event| log!(e)))
|
|
|
|
.into_js_value()
|
|
|
|
.unchecked_ref(),
|
2021-10-31 19:40:43 +00:00
|
|
|
));
|
|
|
|
}
|