omegaupload/web/src/main.rs

256 lines
8.9 KiB
Rust
Raw Normal View History

2021-10-21 18:35:54 -07:00
#![warn(clippy::nursery, clippy::pedantic)]
2021-10-17 14:15:29 -07:00
use std::str::FromStr;
2021-10-25 02:42:20 -07:00
use anyhow::{anyhow, bail, Context, Result};
use byte_unit::{n_mib_bytes, Byte};
2021-10-24 11:40:19 -07:00
use decrypt::DecryptedData;
2021-10-25 02:42:20 -07:00
use gloo_console::{error, log};
2021-10-19 18:48:32 -07:00
use http::uri::PathAndQuery;
use http::{StatusCode, Uri};
2021-10-24 16:16:02 -07:00
use js_sys::{JsString, Object, Uint8Array};
2021-10-25 02:42:20 -07:00
use omegaupload_common::crypto::{Key, Nonce};
use omegaupload_common::{hash, Expiration, PartialParsedUrl};
2021-10-23 10:10:55 -07:00
use reqwasm::http::Request;
2021-10-24 02:25:42 -07:00
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue};
2021-10-24 12:14:55 -07:00
use wasm_bindgen_futures::{spawn_local, JsFuture};
2021-10-24 16:16:02 -07:00
use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window};
2021-10-17 14:15:29 -07:00
2021-10-24 02:25:42 -07:00
use crate::decrypt::decrypt;
2021-10-24 16:16:02 -07:00
use crate::idb_object::IdbObject;
use crate::util::as_idb_db;
2021-10-23 10:10:55 -07:00
mod decrypt;
2021-10-24 16:16:02 -07:00
mod idb_object;
mod util;
2021-10-23 10:10:55 -07:00
2021-10-25 02:42:20 -07:00
const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500);
2021-10-24 02:25:42 -07:00
#[wasm_bindgen]
extern "C" {
2021-10-24 16:16:02 -07:00
#[wasm_bindgen(js_name = loadFromDb)]
2021-10-25 02:42:20 -07:00
pub fn load_from_db();
#[wasm_bindgen(js_name = renderMessage)]
pub fn render_message(message: JsString);
2021-10-24 16:16:02 -07: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 02:25:42 -07:00
}
2021-10-24 12:14:55 -07:00
fn main() {
2021-10-24 20:54:49 -07:00
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
2021-10-25 02:42:20 -07:00
render_message("Loading paste...".into());
2021-10-24 16:16:02 -07:00
let url = String::from(location().to_string());
2021-10-24 12:14:55 -07: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 14:15:29 -07:00
}
2021-10-24 12:14:55 -07:00
Uri::from_parts(uri_parts).unwrap()
};
2021-10-17 14:15:29 -07:00
2021-10-24 16:16:02 -07:00
log!(&url);
log!(&request_uri.to_string());
log!(&location().pathname().unwrap());
2021-10-25 02:42:20 -07:00
let (key, nonce, needs_pw) = {
let partial_parsed_url = url
.split_once('#')
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
.unwrap_or_default();
let key = match partial_parsed_url.decryption_key {
Some(key) => key,
None => {
error!("Key is missing in url; bailing.");
render_message("Invalid paste link: Missing decryption key.".into());
return;
}
};
let nonce = match partial_parsed_url.nonce {
Some(nonce) => nonce,
None => {
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
};
2021-10-24 16:16:02 -07:00
if location().pathname().unwrap() == "/" {
2021-10-24 12:14:55 -07:00
} else {
2021-10-25 02:42:20 -07:00
spawn_local(async move {
if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
2021-10-24 16:16:02 -07:00
log!(e.to_string());
}
2021-10-24 13:12:14 -07:00
});
2021-10-17 14:15:29 -07:00
}
}
2021-10-24 12:14:55 -07:00
#[allow(clippy::future_not_send)]
2021-10-25 02:42:20 -07:00
async fn fetch_resources(
request_uri: Uri,
key: Key,
nonce: Nonce,
password: Option<Key>,
) -> Result<()> {
2021-10-24 12:14:55 -07:00
match Request::get(&request_uri.to_string()).send().await {
Ok(resp) if resp.status() == StatusCode::OK => {
2021-10-24 16:16:02 -07: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 12:14:55 -07:00
let data = {
2021-10-24 13:12:14 -07:00
let data_fut = resp
.as_raw()
.array_buffer()
2021-10-25 02:42:20 -07: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!(
"JsFuture returned an error while fetching resp buffer: {:?}",
e
));
}
};
2021-10-24 13:12:14 -07:00
Uint8Array::new(&data).to_vec()
2021-10-24 02:25:42 -07:00
};
2021-10-23 10:10:55 -07:00
2021-10-25 02:42:20 -07: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 02:18:33 -07:00
2021-10-25 02:42:20 -07:00
let decrypted = decrypt(data, key, nonce, password)?;
2021-10-24 16:16:02 -07:00
let db_open_req = open_idb()?;
2021-10-24 12:14:55 -07:00
// On success callback
let on_success = Closure::once(Box::new(move |event: Event| {
2021-10-24 16:16:02 -07:00
let transaction: IdbObjectStore = as_idb_db(&event)
2021-10-24 12:14:55 -07:00
.transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite)
.unwrap()
.object_store("decrypted data")
.unwrap();
2021-10-24 13:12:14 -07:00
let decrypted_object = match &decrypted {
DecryptedData::String(s) => IdbObject::new()
.string()
.expiration_text(&expires)
.data(&JsValue::from_str(s)),
2021-10-24 12:14:55 -07:00
DecryptedData::Blob(blob) => {
2021-10-24 13:12:14 -07:00
IdbObject::new().blob().expiration_text(&expires).data(blob)
2021-10-24 12:14:55 -07:00
}
2021-10-24 13:12:14 -07:00
DecryptedData::Image(blob, (width, height), size) => IdbObject::new()
.image()
.expiration_text(&expires)
.data(blob)
.extra("width", *width)
.extra("height", *height)
.extra(
"button",
&format!(
"Download {} \u{2014} {} by {}",
Byte::from_bytes(*size as u128).get_appropriate_unit(true),
width,
height,
),
),
DecryptedData::Audio(blob) => IdbObject::new()
.audio()
.expiration_text(&expires)
.data(blob),
DecryptedData::Video(blob) => IdbObject::new()
.video()
.expiration_text(&expires)
.data(blob),
};
2021-10-24 16:16:02 -07:00
let put_action = transaction
2021-10-24 12:14:55 -07:00
.put_with_key(
2021-10-24 16:16:02 -07:00
&Object::from(decrypted_object),
&JsString::from(location().pathname().unwrap()),
2021-10-24 12:14:55 -07:00
)
2021-10-24 16:16:02 -07:00
.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(),
));
2021-10-24 12:14:55 -07:00
}) as Box<dyn FnOnce(Event)>);
2021-10-24 16:16:02 -07:00
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(),
));
2021-10-24 12:14:55 -07:00
let on_upgrade = Closure::wrap(Box::new(move |event: Event| {
2021-10-24 16:16:02 -07:00
let db = as_idb_db(&event);
let _ = db.create_object_store("decrypted data").unwrap();
2021-10-24 12:14:55 -07:00
}) as Box<dyn FnMut(Event)>);
2021-10-24 16:16:02 -07:00
db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref()));
2021-10-24 12:14:55 -07:00
}
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
2021-10-25 02:42:20 -07: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) => {
render_message(format!("{}", err.status_text()).into());
}
Err(err) => {
render_message(format!("{}", err).into());
2021-10-24 12:14:55 -07:00
}
2021-10-24 13:12:14 -07:00
}
Ok(())
}