Better ui; support webp
This commit is contained in:
parent
826b995ae0
commit
c61b0b67c8
6 changed files with 122 additions and 58 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -776,6 +776,12 @@ dependencies = [
|
|||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b3b62f4e783e38afa07b51eaaa789be2fba03dbe29a05a1a906eb64feb987d"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.7.0"
|
||||
|
@ -1103,6 +1109,7 @@ dependencies = [
|
|||
"gloo-console",
|
||||
"http",
|
||||
"image",
|
||||
"imagesize",
|
||||
"js-sys",
|
||||
"omegaupload-common",
|
||||
"reqwasm",
|
||||
|
|
|
@ -18,6 +18,7 @@ gloo-console = "0.1"
|
|||
http = "0.2"
|
||||
image = "0.23"
|
||||
js-sys = "0.3"
|
||||
imagesize = "0.9"
|
||||
reqwasm = "0.2"
|
||||
tree_magic_mini = { version = "3", features = ["with-gpl-data"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::io::Cursor;
|
|||
use std::sync::Arc;
|
||||
|
||||
use gloo_console::log;
|
||||
use image::{EncodableLayout, GenericImageView, ImageDecoder};
|
||||
use image::io::Reader;
|
||||
use js_sys::{Array, Uint8Array};
|
||||
use omegaupload_common::crypto::{open_in_place, Key, Nonce};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
@ -13,7 +13,7 @@ use web_sys::Blob;
|
|||
pub enum DecryptedData {
|
||||
String(Arc<String>),
|
||||
Blob(Arc<Blob>),
|
||||
Image(Arc<Blob>, (u32, u32), usize),
|
||||
Image(Arc<Blob>, (usize, usize), usize),
|
||||
Audio(Arc<Blob>),
|
||||
Video(Arc<Blob>),
|
||||
}
|
||||
|
@ -35,16 +35,25 @@ pub fn decrypt(
|
|||
let container = &mut container;
|
||||
log!("Stage 1 decryption started.");
|
||||
let start = now();
|
||||
|
||||
if let Some(password) = maybe_password {
|
||||
open_in_place(container, &nonce.increment(), &password)
|
||||
.map_err(|_| PasteCompleteConstructionError::StageOneFailure)?;
|
||||
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();
|
||||
open_in_place(container, &nonce, &key)
|
||||
.map_err(|_| PasteCompleteConstructionError::StageTwoFailure)?;
|
||||
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) {
|
||||
|
@ -64,15 +73,18 @@ pub fn decrypt(
|
|||
|
||||
log!("Image introspection started");
|
||||
let start = now();
|
||||
let res = image::guess_format(&container);
|
||||
let dimensions = imagesize::blob_size(&container).ok();
|
||||
log!(format!(
|
||||
"Image introspection completed in {}ms",
|
||||
now() - start
|
||||
));
|
||||
// let image_reader = image::io::Reader::new(Cursor::new(container.as_bytes()));
|
||||
if let Ok(dimensions) = res {
|
||||
log!(format!("{:?}", dimensions));
|
||||
Ok(DecryptedData::Image(blob, (0, 0), container.len()))
|
||||
|
||||
if let Some(dimensions) = dimensions {
|
||||
Ok(DecryptedData::Image(
|
||||
blob,
|
||||
(dimensions.width, dimensions.height),
|
||||
container.len(),
|
||||
))
|
||||
} else {
|
||||
let mime_type = tree_magic_mini::from_u8(container);
|
||||
log!(mime_type);
|
||||
|
|
|
@ -21,7 +21,7 @@ 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 { panic!() },
|
||||
_ => unsafe { unreachable_unchecked() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
119
web/src/main.rs
119
web/src/main.rs
|
@ -2,14 +2,15 @@
|
|||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use byte_unit::Byte;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use byte_unit::{n_mib_bytes, Byte};
|
||||
use decrypt::DecryptedData;
|
||||
use gloo_console::log;
|
||||
use gloo_console::{error, log};
|
||||
use http::uri::PathAndQuery;
|
||||
use http::{StatusCode, Uri};
|
||||
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 wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
@ -24,12 +25,14 @@ mod decrypt;
|
|||
mod idb_object;
|
||||
mod util;
|
||||
|
||||
const DOWNLOAD_SIZE_LIMIT: u128 = n_mib_bytes!(500);
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = loadFromDb)]
|
||||
fn load_from_db();
|
||||
#[wasm_bindgen(js_name = createNotFoundUi)]
|
||||
fn create_not_found_ui();
|
||||
pub fn load_from_db();
|
||||
#[wasm_bindgen(js_name = renderMessage)]
|
||||
pub fn render_message(message: JsString);
|
||||
}
|
||||
|
||||
fn window() -> Window {
|
||||
|
@ -51,6 +54,9 @@ fn open_idb() -> Result<IdbOpenDbRequest> {
|
|||
|
||||
fn main() {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
|
||||
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();
|
||||
|
@ -63,10 +69,48 @@ fn main() {
|
|||
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 = 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() == "/" {
|
||||
} else {
|
||||
spawn_local(async {
|
||||
if let Err(e) = fetch_resources(request_uri, url).await {
|
||||
spawn_local(async move {
|
||||
if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
|
||||
log!(e.to_string());
|
||||
}
|
||||
});
|
||||
|
@ -74,7 +118,12 @@ fn main() {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
Ok(resp) if resp.status() == StatusCode::OK => {
|
||||
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
|
||||
.as_raw()
|
||||
.array_buffer()
|
||||
.expect("Failed to get raw bytes from response");
|
||||
let data = JsFuture::from(data_fut)
|
||||
.await
|
||||
.expect("Failed to result array buffer future");
|
||||
.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 (key, nonce) = {
|
||||
let partial_parsed_url = url
|
||||
.split_once('#')
|
||||
.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)
|
||||
};
|
||||
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, None)?;
|
||||
let decrypted = decrypt(data, key, nonce, password)?;
|
||||
let db_open_req = open_idb()?;
|
||||
|
||||
// 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()));
|
||||
}
|
||||
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(())
|
||||
|
|
|
@ -26,7 +26,7 @@ function loadFromDb() {
|
|||
createVideoPasteUi(data);
|
||||
break;
|
||||
default:
|
||||
createBrokenStateUi();
|
||||
renderMessage("Something went wrong. Try clearing local data.");
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -159,26 +159,15 @@ function createMultiMediaPasteUi(tag, expiration, data, downloadMessage) {
|
|||
bodyEle.appendChild(mainEle);
|
||||
}
|
||||
|
||||
// Exported to main.rs
|
||||
function createNotFoundUi() {
|
||||
function renderMessage(message) {
|
||||
let body = document.getElementsByTagName("body")[0];
|
||||
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");
|
||||
mainEle.classList.add("hljs");
|
||||
mainEle.classList.add("centered");
|
||||
mainEle.classList.add("fullscreen");
|
||||
mainEle.textContent = message;
|
||||
return mainEle;
|
||||
body.appendChild(mainEle);
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", () => location.reload());
|
Loading…
Reference in a new issue