Better ui; support webp

This commit is contained in:
Edward Shen 2021-10-25 02:42:20 -07:00
parent 826b995ae0
commit c61b0b67c8
Signed by: edward
GPG key ID: 19182661E818369F
6 changed files with 122 additions and 58 deletions

7
Cargo.lock generated
View file

@ -776,6 +776,12 @@ dependencies = [
"tiff", "tiff",
] ]
[[package]]
name = "imagesize"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b3b62f4e783e38afa07b51eaaa789be2fba03dbe29a05a1a906eb64feb987d"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.7.0" version = "1.7.0"
@ -1103,6 +1109,7 @@ dependencies = [
"gloo-console", "gloo-console",
"http", "http",
"image", "image",
"imagesize",
"js-sys", "js-sys",
"omegaupload-common", "omegaupload-common",
"reqwasm", "reqwasm",

View file

@ -18,6 +18,7 @@ gloo-console = "0.1"
http = "0.2" http = "0.2"
image = "0.23" image = "0.23"
js-sys = "0.3" js-sys = "0.3"
imagesize = "0.9"
reqwasm = "0.2" reqwasm = "0.2"
tree_magic_mini = { version = "3", features = ["with-gpl-data"] } tree_magic_mini = { version = "3", features = ["with-gpl-data"] }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"

View file

@ -3,7 +3,7 @@ use std::io::Cursor;
use std::sync::Arc; use std::sync::Arc;
use gloo_console::log; use gloo_console::log;
use image::{EncodableLayout, GenericImageView, ImageDecoder}; use image::io::Reader;
use js_sys::{Array, Uint8Array}; use js_sys::{Array, Uint8Array};
use omegaupload_common::crypto::{open_in_place, Key, Nonce}; use omegaupload_common::crypto::{open_in_place, Key, Nonce};
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
@ -13,7 +13,7 @@ use web_sys::Blob;
pub enum DecryptedData { pub enum DecryptedData {
String(Arc<String>), String(Arc<String>),
Blob(Arc<Blob>), Blob(Arc<Blob>),
Image(Arc<Blob>, (u32, u32), usize), Image(Arc<Blob>, (usize, usize), usize),
Audio(Arc<Blob>), Audio(Arc<Blob>),
Video(Arc<Blob>), Video(Arc<Blob>),
} }
@ -35,16 +35,25 @@ pub fn decrypt(
let container = &mut container; let container = &mut container;
log!("Stage 1 decryption started."); log!("Stage 1 decryption started.");
let start = now(); let start = now();
if let Some(password) = maybe_password { if let Some(password) = maybe_password {
open_in_place(container, &nonce.increment(), &password) crate::render_message("Decrypting Stage 1...".into());
.map_err(|_| PasteCompleteConstructionError::StageOneFailure)?; 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!(format!("Stage 1 completed in {}ms", now() - start));
log!("Stage 2 decryption started."); log!("Stage 2 decryption started.");
let start = now(); let start = now();
open_in_place(container, &nonce, &key) crate::render_message("Decrypting Stage 2...".into());
.map_err(|_| PasteCompleteConstructionError::StageTwoFailure)?; 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!("Stage 2 completed in {}ms", now() - start));
if let Ok(decrypted) = std::str::from_utf8(container) { if let Ok(decrypted) = std::str::from_utf8(container) {
@ -64,15 +73,18 @@ pub fn decrypt(
log!("Image introspection started"); log!("Image introspection started");
let start = now(); let start = now();
let res = image::guess_format(&container); let dimensions = imagesize::blob_size(&container).ok();
log!(format!( log!(format!(
"Image introspection completed in {}ms", "Image introspection completed in {}ms",
now() - start now() - start
)); ));
// let image_reader = image::io::Reader::new(Cursor::new(container.as_bytes()));
if let Ok(dimensions) = res { if let Some(dimensions) = dimensions {
log!(format!("{:?}", dimensions)); Ok(DecryptedData::Image(
Ok(DecryptedData::Image(blob, (0, 0), container.len())) blob,
(dimensions.width, dimensions.height),
container.len(),
))
} else { } else {
let mime_type = tree_magic_mini::from_u8(container); let mime_type = tree_magic_mini::from_u8(container);
log!(mime_type); log!(mime_type);

View file

@ -21,7 +21,7 @@ impl From<IdbObject<Ready>> for Object {
Ok(o) => o, Ok(o) => o,
// SAFETY: IdbObject maintains the invariant that it can eventually // SAFETY: IdbObject maintains the invariant that it can eventually
// be constructed into a JS object. // be constructed into a JS object.
_ => unsafe { panic!() }, _ => unsafe { unreachable_unchecked() },
} }
} }
} }

View file

@ -2,14 +2,15 @@
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use byte_unit::Byte; use byte_unit::{n_mib_bytes, Byte};
use decrypt::DecryptedData; use decrypt::DecryptedData;
use gloo_console::log; use gloo_console::{error, log};
use http::uri::PathAndQuery; use http::uri::PathAndQuery;
use http::{StatusCode, Uri}; use http::{StatusCode, Uri};
use js_sys::{JsString, Object, Uint8Array}; use js_sys::{JsString, Object, Uint8Array};
use omegaupload_common::{Expiration, PartialParsedUrl}; use omegaupload_common::crypto::{Key, Nonce};
use omegaupload_common::{hash, Expiration, PartialParsedUrl};
use reqwasm::http::Request; use reqwasm::http::Request;
use wasm_bindgen::prelude::{wasm_bindgen, Closure}; use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
@ -24,12 +25,14 @@ mod decrypt;
mod idb_object; mod idb_object;
mod util; mod util;
const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500);
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
#[wasm_bindgen(js_name = loadFromDb)] #[wasm_bindgen(js_name = loadFromDb)]
fn load_from_db(); pub fn load_from_db();
#[wasm_bindgen(js_name = createNotFoundUi)] #[wasm_bindgen(js_name = renderMessage)]
fn create_not_found_ui(); pub fn render_message(message: JsString);
} }
fn window() -> Window { fn window() -> Window {
@ -51,6 +54,9 @@ fn open_idb() -> Result<IdbOpenDbRequest> {
fn main() { fn main() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook)); std::panic::set_hook(Box::new(console_error_panic_hook::hook));
render_message("Loading paste...".into());
let url = String::from(location().to_string()); let url = String::from(location().to_string());
let request_uri = { let request_uri = {
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts(); let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
@ -63,10 +69,48 @@ fn main() {
log!(&url); log!(&url);
log!(&request_uri.to_string()); log!(&request_uri.to_string());
log!(&location().pathname().unwrap()); 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 = 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
};
if location().pathname().unwrap() == "/" { if location().pathname().unwrap() == "/" {
} else { } else {
spawn_local(async { spawn_local(async move {
if let Err(e) = fetch_resources(request_uri, url).await { if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
log!(e.to_string()); log!(e.to_string());
} }
}); });
@ -74,7 +118,12 @@ fn main() {
} }
#[allow(clippy::future_not_send)] #[allow(clippy::future_not_send)]
async fn fetch_resources(request_uri: Uri, url: String) -> Result<()> { async fn fetch_resources(
request_uri: Uri,
key: Key,
nonce: Nonce,
password: Option<Key>,
) -> Result<()> {
match Request::get(&request_uri.to_string()).send().await { match Request::get(&request_uri.to_string()).send().await {
Ok(resp) if resp.status() == StatusCode::OK => { Ok(resp) if resp.status() == StatusCode::OK => {
let expires = Expiration::try_from(resp.headers()).map_or_else( let expires = Expiration::try_from(resp.headers()).map_or_else(
@ -86,28 +135,28 @@ async fn fetch_resources(request_uri: Uri, url: String) -> Result<()> {
let data_fut = resp let data_fut = resp
.as_raw() .as_raw()
.array_buffer() .array_buffer()
.expect("Failed to get raw bytes from response"); .expect("to get raw bytes from a response");
let data = JsFuture::from(data_fut) let data = match JsFuture::from(data_fut).await {
.await Ok(data) => data,
.expect("Failed to result array buffer future"); 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() Uint8Array::new(&data).to_vec()
}; };
let (key, nonce) = { if data.len() as u128 > DOWNLOAD_SIZE_LIMIT {
let partial_parsed_url = url render_message("The paste is too large to decrypt from the web browser. You must use the CLI tool to download this paste.".into());
.split_once('#') return Ok(());
.map(|(_, fragment)| PartialParsedUrl::from(fragment)) }
.unwrap_or_default();
let key = partial_parsed_url
.decryption_key
.context("missing key should be handled in the future")?;
let nonce = partial_parsed_url
.nonce
.context("missing nonce be handled in the future")?;
(key, nonce)
};
let decrypted = decrypt(data, key, nonce, None)?; let decrypted = decrypt(data, key, nonce, password)?;
let db_open_req = open_idb()?; let db_open_req = open_idb()?;
// On success callback // On success callback
@ -189,11 +238,17 @@ async fn fetch_resources(request_uri: Uri, url: String) -> Result<()> {
db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref())); db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref()));
} }
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => { Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
create_not_found_ui(); 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());
} }
Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {}
Ok(err) => {}
Err(err) => {}
} }
Ok(()) Ok(())

View file

@ -26,7 +26,7 @@ function loadFromDb() {
createVideoPasteUi(data); createVideoPasteUi(data);
break; break;
default: default:
createBrokenStateUi(); renderMessage("Something went wrong. Try clearing local data.");
break; break;
} }
@ -159,26 +159,15 @@ function createMultiMediaPasteUi(tag, expiration, data, downloadMessage) {
bodyEle.appendChild(mainEle); bodyEle.appendChild(mainEle);
} }
// Exported to main.rs function renderMessage(message) {
function createNotFoundUi() {
let body = document.getElementsByTagName("body")[0]; let body = document.getElementsByTagName("body")[0];
body.textContent = ''; body.textContent = '';
body.appendChild(createGenericError("Either the paste has been burned or one never existed."));
}
function createBrokenStateUi() {
let body = document.getElementsByTagName("body")[0];
body.textContent = '';
body.appendChild(createGenericError("Something went wrong. Try clearing local data."));
}
function createGenericError(message) {
let mainEle = document.createElement("main"); let mainEle = document.createElement("main");
mainEle.classList.add("hljs"); mainEle.classList.add("hljs");
mainEle.classList.add("centered"); mainEle.classList.add("centered");
mainEle.classList.add("fullscreen"); mainEle.classList.add("fullscreen");
mainEle.textContent = message; mainEle.textContent = message;
return mainEle; body.appendChild(mainEle);
} }
window.addEventListener("hashchange", () => location.reload()); window.addEventListener("hashchange", () => location.reload());