diff --git a/Cargo.lock b/Cargo.lock index 58840ea..881748b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1051,12 +1051,14 @@ dependencies = [ "chacha20poly1305", "chrono", "headers", + "http", "lazy_static", "rand", "serde", "sha2", "thiserror", "url", + "web-sys", ] [[package]] diff --git a/common/Cargo.toml b/common/Cargo.toml index 11ef53a..84fa933 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -17,3 +17,9 @@ serde = { version = "1", features = ["derive"] } sha2 = "0.9" thiserror = "1" url = "2" + +web-sys = { version = "0.3", features = ["Headers"], optional = true } +http = { version = "0.2", optional = true } + +[features] +wasm = ["web-sys", "http"] \ No newline at end of file diff --git a/common/src/lib.rs b/common/src/lib.rs index aef345b..2cefdc2 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -293,6 +293,21 @@ impl From for HeaderValue { } } +#[cfg(feature = "wasm")] +impl TryFrom for Expiration { + type Error = ParseHeaderValueError; + + fn try_from(headers: web_sys::Headers) -> Result { + headers + .get(http::header::EXPIRES.as_str()) + .ok() + .flatten() + .as_deref() + .and_then(|v| Expiration::try_from(v).ok()) + .ok_or(ParseHeaderValueError) + } +} + pub struct ParseHeaderValueError; impl TryFrom<&HeaderValue> for Expiration { diff --git a/web/Cargo.toml b/web/Cargo.toml index 488605a..53758bc 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -omegaupload-common = { path = "../common" } +omegaupload-common = { path = "../common", features = ["wasm"] } # Enables wasm support getrandom = { version = "*", features = ["js"] } diff --git a/web/src/idb_object.rs b/web/src/idb_object.rs new file mode 100644 index 0000000..d8fb0fe --- /dev/null +++ b/web/src/idb_object.rs @@ -0,0 +1,89 @@ +use std::{hint::unreachable_unchecked, marker::PhantomData}; + +use js_sys::{Array, JsString, Object}; +use wasm_bindgen::JsValue; + +pub struct IdbObject(Array, PhantomData); + +impl IdbObject { + fn add_tuple(self, key: &str, value: &JsValue) -> IdbObject { + let array = Array::new(); + array.push(&JsString::from(key)); + array.push(value); + self.0.push(&array); + IdbObject(self.0, PhantomData) + } +} + +impl From> for Object { + fn from(db_object: IdbObject) -> Self { + match Object::from_entries(db_object.as_ref()) { + Ok(o) => o, + // SAFETY: IdbObject maintains the invariant that it can eventually + // be constructed into a JS object. + _ => unsafe { panic!() }, + } + } +} + +impl IdbObject { + pub fn new() -> Self { + Self(Array::new(), PhantomData) + } + + pub fn video(self) -> IdbObject { + self.add_tuple("type", &JsString::from("video")) + } + + pub fn audio(self) -> IdbObject { + self.add_tuple("type", &JsString::from("audio")) + } + + pub fn image(self) -> IdbObject { + self.add_tuple("type", &JsString::from("image")) + } + + pub fn blob(self) -> IdbObject { + self.add_tuple("type", &JsString::from("blob")) + } + + pub fn string(self) -> IdbObject { + self.add_tuple("type", &JsString::from("string")) + } +} + +impl IdbObject { + pub fn expiration_text(self, expires: &str) -> IdbObject { + self.add_tuple("expiration", &JsString::from(expires)) + } +} + +impl IdbObject { + pub fn data(self, value: &JsValue) -> IdbObject { + self.add_tuple("data", value) + } +} + +impl IdbObject { + pub fn extra(self, key: &str, value: impl Into) -> Self { + self.add_tuple(key, &value.into()) + } +} + +impl AsRef for IdbObject { + fn as_ref(&self) -> &JsValue { + self.0.as_ref() + } +} + +macro_rules! impl_idb_object_state { + ($($ident:ident),*) => { + pub trait IdbObjectState {} + $( + pub enum $ident {} + impl IdbObjectState for $ident {} + )* + }; +} + +impl_idb_object_state!(NeedsType, NeedsExpiration, NeedsData, Ready); diff --git a/web/src/main.rs b/web/src/main.rs index b0a408d..356302f 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -1,36 +1,56 @@ #![warn(clippy::nursery, clippy::pedantic)] -use std::marker::PhantomData; use std::str::FromStr; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use byte_unit::Byte; use decrypt::DecryptedData; use gloo_console::log; -use http::header::EXPIRES; use http::uri::PathAndQuery; use http::{StatusCode, Uri}; -use js_sys::{Array, JsString, Object, Uint8Array}; +use js_sys::{JsString, Object, Uint8Array}; use omegaupload_common::{Expiration, PartialParsedUrl}; use reqwasm::http::Request; use wasm_bindgen::prelude::{wasm_bindgen, Closure}; use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen_futures::{spawn_local, JsFuture}; -use web_sys::{window, Event, IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode}; +use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window}; use crate::decrypt::decrypt; +use crate::idb_object::IdbObject; +use crate::util::as_idb_db; mod decrypt; +mod idb_object; +mod util; #[wasm_bindgen] extern "C" { - fn loadFromDb(); - fn createNotFoundUi(); + #[wasm_bindgen(js_name = loadFromDb)] + fn load_from_db(); + #[wasm_bindgen(js_name = createNotFoundUi)] + fn create_not_found_ui(); +} + +fn window() -> Window { + web_sys::window().expect("Failed to get a reference of the window") +} + +fn location() -> Location { + window().location() +} + +fn open_idb() -> Result { + window() + .indexed_db() + .unwrap() + .context("Missing browser idb impl")? + .open("omegaupload") + .map_err(|_| anyhow!("Failed to open idb")) } fn main() { - let window = window().unwrap(); - let url = String::from(window.location().to_string()); + let url = String::from(location().to_string()); let request_uri = { let mut uri_parts = url.parse::().unwrap().into_parts(); if let Some(parts) = uri_parts.path_and_query.as_mut() { @@ -39,30 +59,27 @@ fn main() { Uri::from_parts(uri_parts).unwrap() }; - if window.location().pathname().unwrap() == "/" { + log!(&url); + log!(&request_uri.to_string()); + log!(&location().pathname().unwrap()); + if location().pathname().unwrap() == "/" { } else { spawn_local(async { - a(request_uri, url).await; + if let Err(e) = fetch_resources(request_uri, url).await { + log!(e.to_string()); + } }); } } #[allow(clippy::future_not_send)] -async fn a(request_uri: Uri, url: String) -> Result<()> { +async fn fetch_resources(request_uri: Uri, url: String) -> Result<()> { match Request::get(&request_uri.to_string()).send().await { Ok(resp) if resp.status() == StatusCode::OK => { - let expires = resp - .headers() - .get(EXPIRES.as_str()) - .ok() - .flatten() - .as_deref() - .and_then(|v| Expiration::try_from(v).ok()) - .as_ref() - .map_or_else( - || "This item does not expire.".to_string(), - Expiration::to_string, - ); + let expires = Expiration::try_from(resp.headers()).map_or_else( + |_| "This item does not expire.".to_string(), + |expires| expires.to_string(), + ); let data = { let data_fut = resp @@ -75,44 +92,33 @@ async fn a(request_uri: Uri, url: String) -> Result<()> { Uint8Array::new(&data).to_vec() }; - let info = url - .split_once('#') - .map(|(_, fragment)| PartialParsedUrl::from(fragment)) - .unwrap_or_default(); - let key = info - .decryption_key - .expect("missing key should be handled in the future"); - let nonce = info.nonce.expect("missing nonce be handled in the future"); - - let result = decrypt(data, key, nonce, None); - - let decrypted = match result { - Ok(decrypted) => decrypted, - Err(err) => { - // log!("decryption error: {}", err); - // return Box::new(PasteError(err)); - unimplemented!() - } + 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) }; - let db_open_req = window() - .unwrap() - .indexed_db() - .unwrap() - .unwrap() - .open("omegaupload") - .unwrap(); + let decrypted = decrypt(data, key, nonce, None)?; + let db_open_req = open_idb()?; // On success callback let on_success = Closure::once(Box::new(move |event: Event| { - let target: IdbOpenDbRequest = event.target().unwrap().dyn_into().unwrap(); - let db: IdbDatabase = target.result().unwrap().dyn_into().unwrap(); - let transaction: IdbObjectStore = db + let transaction: IdbObjectStore = as_idb_db(&event) .transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite) .unwrap() .object_store("decrypted data") .unwrap(); + log!(line!()); + let decrypted_object = match &decrypted { DecryptedData::String(s) => IdbObject::new() .string() @@ -146,37 +152,49 @@ async fn a(request_uri: Uri, url: String) -> Result<()> { .data(blob), }; - let db_entry = Object::from_entries(decrypted_object.as_ref()).unwrap(); - transaction + log!(line!()); + let put_action = transaction .put_with_key( - &db_entry, - &JsString::from(window().unwrap().location().pathname().unwrap()), + &Object::from(decrypted_object), + &JsString::from(location().pathname().unwrap()), ) - .unwrap() - .set_onsuccess(Some( - Closure::once(Box::new(|| { - log!("success"); - loadFromDb(); - }) as Box) - .into_js_value() - .dyn_ref() - .unwrap(), - )); + .unwrap(); + put_action.set_onsuccess(Some( + Closure::wrap(Box::new(|| { + log!("success"); + load_from_db(); + }) as Box) + .into_js_value() + .unchecked_ref(), + )); + put_action.set_onerror(Some( + Closure::wrap(Box::new(|e| { + log!(line!()); + log!(e); + }) as Box) + .into_js_value() + .unchecked_ref(), + )); }) as Box); - db_open_req.set_onsuccess(Some(on_success.into_js_value().dyn_ref().unwrap())); - - // On upgrade callback + 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!(line!()); + log!(e); + }) as Box) + .into_js_value() + .unchecked_ref(), + )); let on_upgrade = Closure::wrap(Box::new(move |event: Event| { - let target: IdbOpenDbRequest = event.target().unwrap().dyn_into().unwrap(); - let db: IdbDatabase = target.result().unwrap().dyn_into().unwrap(); - let _obj_store = db.create_object_store("decrypted data").unwrap(); + let db = as_idb_db(&event); + let _ = db.create_object_store("decrypted data").unwrap(); }) as Box); - - db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().dyn_ref().unwrap())); + db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref())); + log!(&db_open_req); } Ok(resp) if resp.status() == StatusCode::NOT_FOUND => { - createNotFoundUi(); + create_not_found_ui(); } Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {} Ok(err) => {} @@ -185,79 +203,3 @@ async fn a(request_uri: Uri, url: String) -> Result<()> { Ok(()) } - -struct IdbObject(Array, PhantomData); - -impl IdbObject { - fn add_tuple(self, key: &str, value: &JsValue) -> IdbObject { - let array = Array::new(); - array.push(&JsString::from(key)); - array.push(value); - self.0.push(&array); - IdbObject(self.0, PhantomData) - } -} - -impl IdbObject { - fn new() -> Self { - Self(Array::new(), PhantomData) - } - - fn video(self) -> IdbObject { - self.add_tuple("type", &JsString::from("video")) - } - - fn audio(self) -> IdbObject { - self.add_tuple("type", &JsString::from("audio")) - } - - fn image(self) -> IdbObject { - self.add_tuple("type", &JsString::from("image")) - } - - fn blob(self) -> IdbObject { - self.add_tuple("type", &JsString::from("blob")) - } - - fn string(self) -> IdbObject { - self.add_tuple("type", &JsString::from("string")) - } -} - -impl IdbObject { - fn expiration_text(self, expires: &str) -> IdbObject { - self.add_tuple("expiration", &JsString::from(expires)) - } -} - -impl IdbObject { - fn data(self, value: &JsValue) -> IdbObject { - self.add_tuple("data", value) - } -} - -impl IdbObject { - fn extra(self, key: &str, value: impl Into) -> Self { - self.add_tuple(key, &value.into()) - } -} - -impl AsRef for IdbObject { - fn as_ref(&self) -> &JsValue { - self.0.as_ref() - } -} - -trait IdbObjectState {} - -enum NeedsType {} -impl IdbObjectState for NeedsType {} - -enum NeedsExpiration {} -impl IdbObjectState for NeedsExpiration {} - -enum NeedsData {} -impl IdbObjectState for NeedsData {} - -enum Ready {} -impl IdbObjectState for Ready {} diff --git a/web/src/util.rs b/web/src/util.rs new file mode 100644 index 0000000..3897827 --- /dev/null +++ b/web/src/util.rs @@ -0,0 +1,13 @@ +use js_sys::Function; +use wasm_bindgen::closure::WasmClosure; +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::{Event, IdbDatabase, IdbOpenDbRequest}; + +/// # Panics +/// +/// This will panic if event is not an event from the IDB API. +pub fn as_idb_db(event: &Event) -> IdbDatabase { + let target: IdbOpenDbRequest = event.target().map(JsCast::unchecked_into).unwrap(); + target.result().map(JsCast::unchecked_into).unwrap() +}