From 17dd44c8ccdc93ad0682ab09e01026364e069f93 Mon Sep 17 00:00:00 2001 From: Edward Shen Date: Fri, 22 Oct 2021 19:15:23 -0700 Subject: [PATCH] image support --- Cargo.lock | 257 ++++++++++++++++++++++++++++++++++++++++++ common/src/lib.rs | 2 + web/Cargo.toml | 6 +- web/index.html | 50 +-------- web/src/main.rs | 277 ++++++++++++++++++++++++++++++---------------- web/src/main.scss | 73 ++++++++++++ 6 files changed, 517 insertions(+), 148 deletions(-) create mode 100644 web/src/main.scss diff --git a/Cargo.lock b/Cargo.lock index cd7fc60..58bd9ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aead" version = "0.4.3" @@ -176,6 +188,18 @@ version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.1.0" @@ -305,6 +329,12 @@ dependencies = [ "syn", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -324,6 +354,69 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + [[package]] name = "digest" version = "0.9.0" @@ -339,6 +432,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "encoding_rs" version = "0.8.28" @@ -458,6 +557,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "glob" version = "0.3.0" @@ -682,6 +791,25 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + [[package]] name = "indexmap" version = "1.7.0" @@ -713,6 +841,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.55" @@ -805,12 +942,40 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.7.13" @@ -875,6 +1040,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -953,8 +1140,12 @@ dependencies = [ "getrandom", "gloo-console", "http", + "image", + "js-sys", "omegaupload-common", "reqwest", + "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", "yew", "yew-router", @@ -1023,6 +1214,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + [[package]] name = "poly1305" version = "0.7.2" @@ -1140,6 +1343,31 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + [[package]] name = "regex" version = "1.5.4" @@ -1252,6 +1480,18 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sct" version = "0.6.1" @@ -1472,6 +1712,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + [[package]] name = "time" version = "0.1.43" @@ -1869,6 +2120,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "weezl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" + [[package]] name = "winapi" version = "0.3.9" diff --git a/common/src/lib.rs b/common/src/lib.rs index c3c3bba..924638f 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -266,6 +266,8 @@ impl Header for Expiration { impl From<&Expiration> for HeaderValue { fn from(expiration: &Expiration) -> Self { + // SAFETY: All possible values of `Expiration` are valid header values, + // so we don't need the extra check. unsafe { Self::from_maybe_shared_unchecked(match expiration { Expiration::BurnAfterReading => Bytes::from_static(b"0"), diff --git a/web/Cargo.toml b/web/Cargo.toml index 34de8bc..c5926c1 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,8 +15,12 @@ bytes = "1" downcast-rs = "1" gloo-console = "0.1" http = "0.2" +image = "0.23" +js-sys = "0.3" reqwest = { version = "0.11", default_features = false, features = ["tokio-rustls"] } -web-sys = { version = "0.3" } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = ["TextDecoder"] } yew = { version = "0.18", features = ["wasm-bindgen-futures"] } yew-router = "0.15" yewtil = "0.4" \ No newline at end of file diff --git a/web/index.html b/web/index.html index 7e956e4..8430d6f 100644 --- a/web/index.html +++ b/web/index.html @@ -10,60 +10,12 @@ - + - - \ No newline at end of file diff --git a/web/src/main.rs b/web/src/main.rs index bf1133c..6eb3047 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -1,17 +1,22 @@ #![warn(clippy::nursery, clippy::pedantic)] -use std::fmt::Debug; +use std::fmt::{Debug, Display, Formatter}; use std::str::FromStr; -use anyhow::{anyhow, bail}; +use anyhow::{anyhow, bail, Context}; use bytes::Bytes; use downcast_rs::{impl_downcast, Downcast}; +use gloo_console::log; use http::header::EXPIRES; use http::uri::PathAndQuery; use http::{StatusCode, Uri}; +use js_sys::{Array, ArrayBuffer, Uint8Array}; use omegaupload_common::crypto::{open, Key, Nonce}; use omegaupload_common::{Expiration, PartialParsedUrl}; -use yew::format::Nothing; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::TextDecoder; +use web_sys::{Blob, Url}; use yew::utils::window; use yew::Properties; use yew::{html, Component, ComponentLink, Html, ShouldRender}; @@ -98,25 +103,27 @@ impl Component for Paste { .headers() .get(EXPIRES) .and_then(|v| Expiration::try_from(v).ok()); - let partial = match resp.bytes().await { - Ok(bytes) => PastePartial::new( - bytes, - expires, - &url.split_once('#') - .map(|(_, fragment)| PartialParsedUrl::from(fragment)) - .unwrap_or_default(), - link_clone, - ), + let bytes = match resp.bytes().await { + Ok(bytes) => bytes, Err(e) => { return Box::new(PasteError(anyhow!("Got {}.", e))) as Box } }; - if let Ok(completed) = PasteComplete::try_from(partial.clone()) { - Box::new(completed) as Box + let info = url + .split_once('#') + .map(|(_, fragment)| PartialParsedUrl::from(fragment)) + .unwrap_or_default(); + let key = info.decryption_key.unwrap(); + let nonce = info.nonce.unwrap(); + + if let Ok(completed) = decrypt(bytes, key, nonce, None) { + Box::new(PasteComplete::new(link_clone, completed, expires)) + as Box } else { - Box::new(partial) as Box + todo!() + // Box::new(partial) as Box } } Ok(resp) if resp.status() == StatusCode::NOT_FOUND => { @@ -154,7 +161,7 @@ impl Component for Paste { if self.state.is::() { return html! { -
+

{ "Either the paste has been burned or one never existed." }

}; @@ -162,7 +169,7 @@ impl Component for Paste { if self.state.is::() { return html! { -
+

{ "Bad Request. Is this a valid paste URL?" }

}; @@ -170,7 +177,7 @@ impl Component for Paste { if let Some(error) = self.state.downcast_ref::() { return html! { -

{ error.0.to_string() }

+

{ error.0.to_string() }

}; } @@ -203,11 +210,16 @@ struct PastePartial { #[derive(Properties, Clone)] struct PasteComplete { - data: Bytes, + parent: ComponentLink, + decrypted: DecryptedData, expires: Option, - key: Key, - nonce: Nonce, - password: Option, +} + +#[derive(Clone)] +enum DecryptedData { + String(String), + Blob(Blob), + Image(Blob), } trait PasteState: Downcast {} @@ -276,16 +288,18 @@ impl Component for PastePartial { if (self.needs_pw && maybe_password.is_some()) || (!self.needs_pw && maybe_password.is_none()) => { + let parent = self.parent.clone(); let data = self.data.clone(); let expires = self.expires; - self.parent.callback_once(move |Nothing| { - Box::new(PasteComplete::new( - data, - expires, - key, - nonce, - maybe_password, - )) as Box + + self.parent.send_future(async move { + match decrypt(data, key, nonce, maybe_password) { + Ok(decrypted) => Box::new(PasteComplete::new(parent, decrypted, expires)) + as Box, + Err(e) => { + todo!() + } + } }); } _ => (), @@ -307,92 +321,159 @@ impl Component for PastePartial { } } -impl TryFrom for PasteComplete { - type Error = anyhow::Error; +fn decrypt( + encrypted: Bytes, + key: Key, + nonce: Nonce, + maybe_password: Option, +) -> Result { + let stage_one = maybe_password.map_or_else( + || Ok(encrypted.to_vec()), + |password| open(&encrypted, &nonce.increment(), &password), + ); - fn try_from(partial: PastePartial) -> Result { - match partial { - PastePartial { - data, - key: Some(key), - expires, - nonce: Some(nonce), - password: Some(password), - needs_pw: true, - .. - } => Ok(Self { - data, - expires, - key, - nonce, - password: Some(password), - }), - PastePartial { - data, - key: Some(key), - expires, - nonce: Some(nonce), - needs_pw: false, - .. - } => Ok(Self { - data, - key, - expires, - nonce, - password: None, - }), - _ => bail!("missing field"), + let stage_one = stage_one.map_err(|_| PasteCompleteConstructionError::StageOneFailure)?; + + let stage_two = open(&stage_one, &nonce, &key) + .map_err(|_| PasteCompleteConstructionError::StageTwoFailure)?; + + if let Ok(decrypted) = std::str::from_utf8(&stage_two) { + Ok(DecryptedData::String(decrypted.to_owned())) + } else { + let blob_chunks = Array::new_with_length(stage_two.chunks(65536).len().try_into().unwrap()); + for (i, chunk) in stage_two.chunks(65536).enumerate() { + let array = Uint8Array::new_with_length(chunk.len().try_into().unwrap()); + array.copy_from(&chunk); + blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap()); + } + let blob = Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap(); + + if image::guess_format(&stage_two).is_ok() { + Ok(DecryptedData::Image(blob)) + } else { + Ok(DecryptedData::Blob(blob)) + } + } +} + +#[derive(Debug)] +enum PasteCompleteConstructionError { + StageOneFailure, + StageTwoFailure, +} + +impl std::error::Error for PasteCompleteConstructionError {} + +impl Display for PasteCompleteConstructionError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PasteCompleteConstructionError::StageOneFailure => { + write!(f, "Failed to decrypt stage one.") + } + PasteCompleteConstructionError::StageTwoFailure => { + write!(f, "Failed to decrypt stage two.") + } } } } impl PasteComplete { fn new( - data: Bytes, + parent: ComponentLink, + decrypted: DecryptedData, expires: Option, - key: Key, - nonce: Nonce, - password: Option, ) -> Self { Self { - data, + parent, + decrypted, expires, - key, - nonce, - password, } } fn view(&self) -> Html { - let stage_one = self.password.map_or_else( - || self.data.to_vec(), - |password| open(&self.data, &self.nonce.increment(), &password).unwrap(), - ); - let decrypted = open(&stage_one, &self.nonce, &self.key).unwrap(); + match &self.decrypted { + DecryptedData::String(decrypted) => html! { + html! { + <> +
+                            
+ { + self.expires.as_ref().map(ToString::to_string).unwrap_or_else(|| + "This paste will not expire.".to_string() + ) + } +
+
+ {decrypted} +
- if let Ok(str) = String::from_utf8(decrypted) { - html! { - <> -
-                    
- { - self.expires.as_ref().map(ToString::to_string).unwrap_or_else(|| - "This paste will not expire.".to_string() - ) + + } -
-
- {str} -
- - - + }, + DecryptedData::Blob(decrypted) => { + let object_url = Url::create_object_url_with_blob(decrypted); + if let Ok(object_url) = object_url { + let file_name = window().location().pathname().unwrap_or("file".to_string()); + let mut cloned = self.clone(); + let decrypted_cloned = decrypted.clone(); + let display_anyways_callback = + self.parent.callback_future_once(|_| async move { + let array_buffer: ArrayBuffer = + JsFuture::from(decrypted_cloned.array_buffer()) + .await + .unwrap() + .dyn_into() + .unwrap(); + let decoder = TextDecoder::new().unwrap(); + cloned.decrypted = decoder + .decode_with_buffer_source(&array_buffer) + .map(DecryptedData::String) + .unwrap(); + Box::new(cloned) as Box + }); + html! { +
+
+

{ "Found a binary file." }

+ {"Download"} +
+

{ "Display anyways?" }

+
+ } + } else { + // This branch really shouldn't happen, but might as well + // try and give a user-friendly error message. + html! { +
+

{ "Failed to create an object URL for the decrypted file. Try reloading the page?" }

+
+ } + } + } + DecryptedData::Image(decrypted) => { + let object_url = Url::create_object_url_with_blob(decrypted); + if let Ok(object_url) = object_url { + let file_name = window().location().pathname().unwrap_or("file".to_string()); + html! { +
+ + {"Download"} +
+ } + } else { + // This branch really shouldn't happen, but might as well + // try and give a user-friendly error message. + html! { +
+

{ "Failed to create an object URL for the decrypted file. Try reloading the page?" }

+
+ } + } } - } else { - html! { "binary" } } } } diff --git a/web/src/main.scss b/web/src/main.scss new file mode 100644 index 0000000..7d2074e --- /dev/null +++ b/web/src/main.scss @@ -0,0 +1,73 @@ +@font-face { + font-family: "Mplus Code"; + src: url("./MplusCodeLatin[wdth,wght].ttf") format("truetype"); +} + +body { + background-color: #404040; + font-family: 'Mplus Code', sans-serif; + margin: 0; +} + +pre header { + user-select: none; + margin: 1em; +} + +hr { + margin: 1em; +} + +main { + display: inline-flex; + min-width: 100%; + justify-content: center; +} + +.paste { + border-radius: 1em; + margin: 1em; + padding: 1em; + background-color: #0d1117; + box-shadow: 0 0 1em black; + min-width: 120ch; +} + +.hljs { + font-family: 'Mplus Code', sans-serif; +} + +.hljs-ln td.hljs-ln-numbers { + text-align: right; + padding-right: 1em; +} + +.centered { + height: 100vh; + width: 100vw; + margin: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.display-anyways { + margin-bottom: 4em; + text-decoration: underline; +} + +img { + margin-bottom: 4em; +} + +.primary { + background-color: #0d1117; +} + +.image { + display: block; + + a { + } +} \ No newline at end of file