From 89aeb6ba2a899a122c30ee6013558677269ceb2a Mon Sep 17 00:00:00 2001 From: Edward Shen Date: Sat, 23 Oct 2021 10:10:55 -0700 Subject: [PATCH] Partial work --- Cargo.lock | 64 +++++++++++++- Trunk.toml | 8 ++ cli/src/main.rs | 14 +-- common/src/lib.rs | 23 ++++- server/src/main.rs | 15 +++- web/Cargo.toml | 4 +- web/index.html | 2 +- web/src/decrypt.rs | 138 ++++++++++++++++++++++++++++++ web/src/main.rs | 208 ++++++++++++++++++++++++--------------------- web/src/main.scss | 46 +++++----- 10 files changed, 386 insertions(+), 136 deletions(-) create mode 100644 Trunk.toml create mode 100644 web/src/decrypt.rs diff --git a/Cargo.lock b/Cargo.lock index 58bd9ee..c00fffc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,15 @@ version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +[[package]] +name = "byte-unit" +version = "4.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ffc5b0ec7d7a6949e3f21fd63ba5af4cffdc2ba1e0b7bf62b481458c4ae7f" +dependencies = [ + "utf8-width", +] + [[package]] name = "bytemuck" version = "1.7.2" @@ -469,6 +478,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" +[[package]] +name = "futures" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.17" @@ -476,6 +500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -484,6 +509,17 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +[[package]] +name = "futures-executor" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.17" @@ -522,9 +558,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ "autocfg", + "futures-channel", "futures-core", "futures-io", "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1135,6 +1173,7 @@ name = "omegaupload-web" version = "0.1.0" dependencies = [ "anyhow", + "byte-unit", "bytes", "downcast-rs", "getrandom", @@ -1143,7 +1182,7 @@ dependencies = [ "image", "js-sys", "omegaupload-common", - "reqwest", + "reqwasm", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1394,6 +1433,23 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "reqwasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34bf31941fb867ae9386a4b443b388e6713574944e6517136ee21a6a93cf996" +dependencies = [ + "anyhow", + "futures", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reqwest" version = "0.11.5" @@ -1995,6 +2051,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8-width" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Trunk.toml b/Trunk.toml new file mode 100644 index 0000000..28a1ae7 --- /dev/null +++ b/Trunk.toml @@ -0,0 +1,8 @@ + +[build] +target = "web/index.html" +release = true + +[[proxy]] +backend = "http://localhost:8081" +rewrite = "/api/" diff --git a/cli/src/main.rs b/cli/src/main.rs index 101a6ca..96b19bc 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -6,7 +6,7 @@ use std::io::{Read, Write}; use anyhow::{anyhow, bail, Context, Result}; use atty::Stream; use clap::Clap; -use omegaupload_common::crypto::{gen_key_nonce, open, seal, Key}; +use omegaupload_common::crypto::{gen_key_nonce, open_in_place, seal_in_place, Key}; use omegaupload_common::{base64, hash, Expiration, ParsedUrl, Url}; use reqwest::blocking::Client; use reqwest::header::EXPIRES; @@ -53,13 +53,13 @@ fn handle_upload(mut url: Url, password: Option) -> Result<()> { let (enc_key, nonce) = gen_key_nonce(); let mut container = Vec::new(); std::io::stdin().read_to_end(&mut container)?; - let mut enc = - seal(&container, &nonce, &enc_key).map_err(|_| anyhow!("Failed to encrypt data"))?; + seal_in_place(&mut container, &nonce, &enc_key) + .map_err(|_| anyhow!("Failed to encrypt data"))?; let pw_used = if let Some(password) = password { let pw_hash = hash(password.expose_secret().as_bytes()); let pw_key = Key::from_slice(pw_hash.as_ref()); - enc = seal(&enc, &nonce.increment(), pw_key) + seal_in_place(&mut container, &nonce.increment(), pw_key) .map_err(|_| anyhow!("Failed to encrypt data"))?; true } else { @@ -69,7 +69,7 @@ fn handle_upload(mut url: Url, password: Option) -> Result<()> { let key = base64::encode(&enc_key); let nonce = base64::encode(&nonce); - (enc, nonce, key, pw_used) + (container, nonce, key, pw_used) }; let res = Client::new() @@ -131,11 +131,11 @@ fn handle_download(url: ParsedUrl) -> Result<()> { let pw_hash = hash(input.as_bytes()); let pw_key = Key::from_slice(pw_hash.as_ref()); - data = open(&data, &url.nonce.increment(), pw_key) + open_in_place(&mut data, &url.nonce.increment(), pw_key) .map_err(|_| anyhow!("Failed to decrypt data. Incorrect password?"))?; } - data = open(&data, &url.nonce, &url.decryption_key) + open_in_place(&mut data, &url.nonce, &url.decryption_key) .map_err(|_| anyhow!("Failed to decrypt data. Incorrect decryption key?"))?; if atty::is(Stream::Stdout) { diff --git a/common/src/lib.rs b/common/src/lib.rs index 924638f..a18c67e 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -39,7 +39,7 @@ pub mod crypto { use std::ops::{Deref, DerefMut}; use chacha20poly1305::aead::generic_array::GenericArray; - use chacha20poly1305::aead::{Aead, Error, NewAead}; + use chacha20poly1305::aead::{Aead, AeadInPlace, Buffer, Error, NewAead}; use chacha20poly1305::XChaCha20Poly1305; use chacha20poly1305::XNonce; use rand::{thread_rng, Rng}; @@ -62,11 +62,21 @@ pub mod crypto { cipher.encrypt(nonce, plaintext) } + pub fn seal_in_place(buffer: &mut impl Buffer, nonce: &Nonce, key: &Key) -> Result<(), Error> { + let cipher = XChaCha20Poly1305::new(key); + cipher.encrypt_in_place(nonce, &[], buffer) + } + pub fn open(encrypted: &[u8], nonce: &Nonce, key: &Key) -> Result, Error> { let cipher = XChaCha20Poly1305::new(key); cipher.decrypt(nonce, encrypted) } + pub fn open_in_place(buffer: &mut impl Buffer, nonce: &Nonce, key: &Key) -> Result<(), Error> { + let cipher = XChaCha20Poly1305::new(key); + cipher.decrypt_in_place(nonce, &[], buffer) + } + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Nonce(XNonce); @@ -291,7 +301,16 @@ impl TryFrom<&HeaderValue> for Expiration { fn try_from(value: &HeaderValue) -> Result { value .to_str() - .map_err(|_| ParseHeaderValueError)? + .map_err(|_| ParseHeaderValueError) + .and_then(Self::try_from) + } +} + +impl TryFrom<&str> for Expiration { + type Error = ParseHeaderValueError; + + fn try_from(value: &str) -> Result { + value .parse::>() .map_err(|_| ParseHeaderValueError) .map(Self::UnixTime) diff --git a/server/src/main.rs b/server/src/main.rs index b83c66b..a97ecec 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -17,7 +17,7 @@ use rand::Rng; use rocksdb::IteratorMode; use rocksdb::{Options, DB}; use tokio::task; -use tracing::{error, instrument}; +use tracing::{error, instrument, trace}; use tracing::{info, warn}; use crate::paste::Paste; @@ -109,7 +109,7 @@ fn set_up_expirations(db: Arc) { info!("Cleanup timers have been initialized."); } -#[instrument(skip(db), err)] +#[instrument(skip(db, body), err)] async fn upload( Extension(db): Extension>, maybe_expires: Option>, @@ -127,25 +127,30 @@ async fn upload( let paste = Paste::new(maybe_expires.map(|v| v.0).unwrap_or_default(), body); let mut new_key = None; + trace!("Generating short code..."); + // Try finding a code; give up after 1000 attempts // Statistics show that this is very unlikely to happen - for _ in 0..1000 { + for i in 0..1000 { let code: ShortCode = thread_rng().sample(short_code::Generator); let db = Arc::clone(&db); let key = code.as_bytes(); let query = task::spawn_blocking(move || db.key_may_exist(key)).await; if matches!(query, Ok(false)) { new_key = Some(key); + trace!("Found new key after {} attempts.", i); + break; } } let key = if let Some(key) = new_key { key } else { - error!("Failed to generate a valid shortcode"); + error!("Failed to generate a valid short code!"); return Err(StatusCode::INTERNAL_SERVER_ERROR); }; + trace!("Serializing paste..."); let value = if let Ok(v) = bincode::serialize(&paste) { v } else { @@ -153,6 +158,8 @@ async fn upload( return Err(StatusCode::INTERNAL_SERVER_ERROR); }; + trace!("Finished serializing paste."); + let db_ref = Arc::clone(&db); match task::spawn_blocking(move || db_ref.put(key, value)).await { Ok(Ok(_)) => { diff --git a/web/Cargo.toml b/web/Cargo.toml index c5926c1..e54f1b9 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -12,12 +12,14 @@ getrandom = { version = "*", features = ["js"] } anyhow = "1" bytes = "1" +byte-unit = "4" 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"] } +# reqwest = { version = "0.11", default_features = false, features = ["tokio-rustls"] } +reqwasm = "0.2" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3", features = ["TextDecoder"] } diff --git a/web/index.html b/web/index.html index 8430d6f..7d80d50 100644 --- a/web/index.html +++ b/web/index.html @@ -5,12 +5,12 @@ Omegaupload + - diff --git a/web/src/decrypt.rs b/web/src/decrypt.rs new file mode 100644 index 0000000..a4335d8 --- /dev/null +++ b/web/src/decrypt.rs @@ -0,0 +1,138 @@ +use std::{collections::HashSet, sync::Arc}; + +use gloo_console::log; +use image::GenericImageView; +use js_sys::{Array, Uint8Array}; +use omegaupload_common::{ + crypto::{open_in_place, Key, Nonce}, + Expiration, +}; +use wasm_bindgen::JsCast; +use web_sys::Blob; +use yew::worker::{Agent, AgentLink, Context, HandlerId}; +use yew::{html::Scope, worker::Public}; + +use crate::{DecryptedData, Paste, PasteCompleteConstructionError}; + +#[derive(Clone)] +pub struct DecryptionAgent { + link: AgentLink, +} + +impl Agent for DecryptionAgent { + type Reach = Public; + + type Message = (); + + type Input = DecryptionAgentMessage; + + type Output = Result<(DecryptedData, PasteContext), PasteCompleteConstructionError>; + + fn create(link: AgentLink) -> Self { + Self { link } + } + + fn update(&mut self, _: Self::Message) {} + + fn handle_input( + &mut self, + DecryptionAgentMessage { context, params }: Self::Input, + id: HandlerId, + ) { + let DecryptionParams { + data, + key, + nonce, + maybe_password, + } = params; + + self.link.respond( + id, + decrypt(data, key, nonce, maybe_password).map(|res| (res, context)), + ) + } +} + +pub struct DecryptionAgentMessage { + context: PasteContext, + params: DecryptionParams, +} + +impl DecryptionAgentMessage { + pub fn new(context: PasteContext, params: DecryptionParams) -> Self { + Self { context, params } + } +} + +pub struct PasteContext { + pub link: Scope, + pub expires: Option, +} + +impl PasteContext { + pub fn new(link: Scope, expires: Option) -> Self { + Self { link, expires } + } +} + +pub struct DecryptionParams { + data: Vec, + key: Key, + nonce: Nonce, + maybe_password: Option, +} + +impl DecryptionParams { + pub fn new(data: Vec, key: Key, nonce: Nonce, maybe_password: Option) -> Self { + Self { + data, + key, + nonce, + maybe_password, + } + } +} + +fn decrypt( + mut container: Vec, + key: Key, + nonce: Nonce, + maybe_password: Option, +) -> Result { + let container = &mut container; + log!("stage 1 decryption start"); + if let Some(password) = maybe_password { + open_in_place(container, &nonce.increment(), &password) + .map_err(|_| PasteCompleteConstructionError::StageOneFailure)?; + } + + log!("stage 2 decryption start"); + open_in_place(container, &nonce, &key) + .map_err(|_| PasteCompleteConstructionError::StageTwoFailure)?; + + log!("stage 2 decryption end"); + if let Ok(decrypted) = std::str::from_utf8(&container) { + Ok(DecryptedData::String(Arc::new(decrypted.to_owned()))) + } else { + log!("blob conversion start"); + let blob_chunks = Array::new_with_length(container.chunks(65536).len().try_into().unwrap()); + for (i, chunk) in container.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 = + Arc::new(Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap()); + log!("blob conversion end"); + + if let Ok(image) = image::load_from_memory(&container) { + Ok(DecryptedData::Image( + blob, + image.dimensions(), + container.len(), + )) + } else { + Ok(DecryptedData::Blob(blob)) + } + } +} diff --git a/web/src/main.rs b/web/src/main.rs index 6eb3047..a60de24 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -2,28 +2,39 @@ use std::fmt::{Debug, Display, Formatter}; use std::str::FromStr; +use std::sync::Arc; use anyhow::{anyhow, bail, Context}; +use byte_unit::Byte; use bytes::Bytes; +use decrypt::DecryptionAgent; use downcast_rs::{impl_downcast, Downcast}; use gloo_console::log; use http::header::EXPIRES; use http::uri::PathAndQuery; use http::{StatusCode, Uri}; +use image::GenericImageView; use js_sys::{Array, ArrayBuffer, Uint8Array}; -use omegaupload_common::crypto::{open, Key, Nonce}; +use omegaupload_common::crypto::{open, open_in_place, Key, Nonce}; use omegaupload_common::{Expiration, PartialParsedUrl}; +use reqwasm::http::Request; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::TextDecoder; use web_sys::{Blob, Url}; +use yew::agent::Dispatcher; use yew::utils::window; -use yew::Properties; -use yew::{html, Component, ComponentLink, Html, ShouldRender}; +use yew::worker::Agent; +use yew::{html, Bridge, Bridged, Component, ComponentLink, Html, ShouldRender}; +use yew::{Dispatched, Properties}; use yew_router::router::Router; use yew_router::Switch; use yewtil::future::LinkFuture; +use crate::decrypt::{DecryptionAgentMessage, DecryptionParams, PasteContext}; + +mod decrypt; + fn main() { yew::start_app::(); } @@ -76,8 +87,9 @@ fn render_route(route: Route) -> Html { } } -struct Paste { +pub struct Paste { state: Box, + _listener: Box>, } impl Component for Paste { @@ -95,20 +107,38 @@ impl Component for Paste { Uri::from_parts(uri_parts).unwrap() }; + let handle_decryption_result = |res: ::Output| { + log!("Got decryption result back!"); + match res { + Ok((decrypted, context)) => { + Box::new(PasteComplete::new(context.link, decrypted, context.expires)) + as Box + } + Err(e) => Box::new(PasteError(anyhow!("wtf"))) as Box, + } + }; + + let listener = DecryptionAgent::bridge(link.callback(handle_decryption_result)); + let link_clone = link.clone(); link.send_future(async move { - match reqwest::get(&request_uri.to_string()).await { + match Request::get(&request_uri.to_string()).send().await { Ok(resp) if resp.status() == StatusCode::OK => { let expires = resp .headers() - .get(EXPIRES) + .get(EXPIRES.as_str()) + .ok() + .flatten() + .as_deref() .and_then(|v| Expiration::try_from(v).ok()); - let bytes = match resp.bytes().await { - Ok(bytes) => bytes, - Err(e) => { - return Box::new(PasteError(anyhow!("Got {}.", e))) - as Box - } + + let data = { + Uint8Array::new( + &JsFuture::from(resp.as_raw().array_buffer().unwrap()) + .await + .unwrap(), + ) + .to_vec() }; let info = url @@ -118,13 +148,12 @@ impl Component for Paste { 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 { - todo!() - // Box::new(partial) as Box - } + let mut decryption_agent = DecryptionAgent::dispatcher(); + + let params = DecryptionParams::new(data, key, nonce, None); + let ctx = PasteContext::new(link_clone, expires); + decryption_agent.send(DecryptionAgentMessage::new(ctx, params)); + Box::new(PasteDecrypting(decryption_agent)) as Box } Ok(resp) if resp.status() == StatusCode::NOT_FOUND => { Box::new(PasteNotFound) as Box @@ -138,8 +167,10 @@ impl Component for Paste { Err(err) => Box::new(PasteError(anyhow!("Got {}.", err))) as Box, } }); + Self { state: Box::new(PasteLoading), + _listener: listener, } } @@ -159,6 +190,12 @@ impl Component for Paste { }; } + if self.state.is::() { + return html! { + "decrypting" + }; + } + if self.state.is::() { return html! {
@@ -197,9 +234,10 @@ impl Component for Paste { struct PasteError(anyhow::Error); -#[derive(Properties, Clone, Debug)] +#[derive(Debug)] struct PastePartial { parent: ComponentLink, + dispatcher: Dispatcher, data: Bytes, expires: Option, key: Option, @@ -216,13 +254,13 @@ struct PasteComplete { } #[derive(Clone)] -enum DecryptedData { - String(String), - Blob(Blob), - Image(Blob), +pub enum DecryptedData { + String(Arc), + Blob(Arc), + Image(Arc, (u32, u32), usize), } -trait PasteState: Downcast {} +pub trait PasteState: Downcast {} impl_downcast!(PasteState); impl PasteState for PasteError {} @@ -242,6 +280,10 @@ macro_rules! impl_paste_type_state { impl_paste_type_state!(PasteLoading, PasteNotFound, PasteBadRequest); +struct PasteDecrypting(Dispatcher); + +impl PasteState for PasteDecrypting {} + impl PastePartial { fn new( data: Bytes, @@ -251,6 +293,7 @@ impl PastePartial { ) -> Self { Self { parent, + dispatcher: DecryptionAgent::dispatcher(), data, expires, key: partial_parsed_url.decryption_key, @@ -270,10 +313,10 @@ enum PartialPasteMessage { impl Component for PastePartial { type Message = PartialPasteMessage; - type Properties = Self; + type Properties = (); - fn create(props: Self::Properties, _: ComponentLink) -> Self { - props + fn create(_: Self::Properties, _: ComponentLink) -> Self { + unimplemented!() } fn update(&mut self, msg: Self::Message) -> ShouldRender { @@ -289,18 +332,11 @@ impl Component for PastePartial { || (!self.needs_pw && maybe_password.is_none()) => { let parent = self.parent.clone(); - let data = self.data.clone(); + let mut data = self.data.to_vec(); let expires = self.expires; - 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!() - } - } - }); + // self.dispatcher.send((data, key, nonce, maybe_password)); + todo!() } _ => (), } @@ -321,43 +357,8 @@ impl Component for PastePartial { } } -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), - ); - - 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 { +pub enum PasteCompleteConstructionError { StageOneFailure, StageTwoFailure, } @@ -395,22 +396,24 @@ impl PasteComplete { DecryptedData::String(decrypted) => html! { html! { <> -
-                            
- { - self.expires.as_ref().map(ToString::to_string).unwrap_or_else(|| - "This paste will not expire.".to_string() - ) - } -
-
- {decrypted} -
+
+                                
+ { + self.expires.as_ref().map(ToString::to_string).unwrap_or_else(|| + "This paste will not expire.".to_string() + ) + } +
+
+ {decrypted} +
- + } }, @@ -419,11 +422,11 @@ impl PasteComplete { 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 decrypted_ref = Arc::clone(&decrypted); let display_anyways_callback = self.parent.callback_future_once(|_| async move { let array_buffer: ArrayBuffer = - JsFuture::from(decrypted_cloned.array_buffer()) + JsFuture::from(decrypted_ref.array_buffer()) .await .unwrap() .dyn_into() @@ -431,15 +434,16 @@ impl PasteComplete { let decoder = TextDecoder::new().unwrap(); cloned.decrypted = decoder .decode_with_buffer_source(&array_buffer) + .map(Arc::new) .map(DecryptedData::String) .unwrap(); Box::new(cloned) as Box }); html! { -
+

{ "Found a binary file." }

- {"Download"} + {"Download"}

{ "Display anyways?" }

@@ -454,21 +458,29 @@ impl PasteComplete { } } } - DecryptedData::Image(decrypted) => { + DecryptedData::Image(decrypted, (width, height), size) => { 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"} +
+ + + { + format!( + "Download {} \u{2014} {} by {}", + Byte::from_bytes(*size as u128).get_appropriate_unit(true), + width, height, + ) + } +
} } 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?" }

} diff --git a/web/src/main.scss b/web/src/main.scss index 7d2074e..1e35126 100644 --- a/web/src/main.scss +++ b/web/src/main.scss @@ -1,3 +1,7 @@ +@use '../vendor/highlight.js/src/styles/github-dark.css'; + +$padding: 1em; + @font-face { font-family: "Mplus Code"; src: url("./MplusCodeLatin[wdth,wght].ttf") format("truetype"); @@ -5,17 +9,16 @@ body { background-color: #404040; - font-family: 'Mplus Code', sans-serif; margin: 0; } -pre header { - user-select: none; - margin: 1em; +.unselectable { + user-select: none; } hr { - margin: 1em; + @extend .hljs; + margin: $padding 0; } main { @@ -25,11 +28,11 @@ main { } .paste { - border-radius: 1em; - margin: 1em; - padding: 1em; - background-color: #0d1117; - box-shadow: 0 0 1em black; + @extend .hljs; + border-radius: $padding; + margin: $padding; + padding: 2 * $padding; + box-shadow: 0 0 $padding black; min-width: 120ch; } @@ -39,19 +42,22 @@ main { .hljs-ln td.hljs-ln-numbers { text-align: right; - padding-right: 1em; + padding-right: $padding; } .centered { - height: 100vh; - width: 100vw; - margin: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; } +.fullscreen { + min-height: 100vh; + min-width: 100vw; + margin: 0; +} + .display-anyways { margin-bottom: 4em; text-decoration: underline; @@ -59,15 +65,11 @@ main { img { margin-bottom: 4em; + max-height: 75vh; + max-width: 75vw; + border-radius: $padding; } .primary { - background-color: #0d1117; + @extend .hljs; } - -.image { - display: block; - - a { - } -} \ No newline at end of file