Partial work

This commit is contained in:
Edward Shen 2021-10-23 10:10:55 -07:00
parent 17dd44c8cc
commit 89aeb6ba2a
Signed by: edward
GPG key ID: 19182661E818369F
10 changed files with 386 additions and 136 deletions

64
Cargo.lock generated
View file

@ -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"

8
Trunk.toml Normal file
View file

@ -0,0 +1,8 @@
[build]
target = "web/index.html"
release = true
[[proxy]]
backend = "http://localhost:8081"
rewrite = "/api/"

View file

@ -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<SecretString>) -> 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<SecretString>) -> 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) {

View file

@ -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<Vec<u8>, 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<Self, Self::Error> {
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<Self, Self::Error> {
value
.parse::<DateTime<Utc>>()
.map_err(|_| ParseHeaderValueError)
.map(Self::UnixTime)

View file

@ -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<DB>) {
info!("Cleanup timers have been initialized.");
}
#[instrument(skip(db), err)]
#[instrument(skip(db, body), err)]
async fn upload<const N: usize>(
Extension(db): Extension<Arc<DB>>,
maybe_expires: Option<TypedHeader<Expiration>>,
@ -127,25 +127,30 @@ async fn upload<const N: usize>(
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<N> = 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<const N: usize>(
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(_)) => {

View file

@ -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"] }

View file

@ -5,12 +5,12 @@
<meta charset="utf-8" />
<title>Omegaupload</title>
<link data-trunk rel="rust" data-wasm-opt="0" data-keep-debug data-no-mangle />
<link data-trunk rel="copy-file" href="vendor/MPLUS_FONTS/fonts/ttf/MplusCodeLatin[wdth,wght].ttf" dest="/" />
<link data-trunk rel="copy-file" href="vendor/highlight.min.js" dest="/" />
<link data-trunk rel="copy-file" href="vendor/highlightjs-line-numbers.js/dist/highlightjs-line-numbers.min.js"
dest="/" />
<link data-trunk rel="copy-file" href="src/reload_on_hash_change.js" dest="/" />
<link data-trunk rel="css" href="vendor/highlight.js/src/styles/github-dark.css" />
<link data-trunk rel="scss" href="src/main.scss" />
<script src="reload_on_hash_change.js" async></script>

138
web/src/decrypt.rs Normal file
View file

@ -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<Self>,
}
impl Agent for DecryptionAgent {
type Reach = Public<Self>;
type Message = ();
type Input = DecryptionAgentMessage;
type Output = Result<(DecryptedData, PasteContext), PasteCompleteConstructionError>;
fn create(link: AgentLink<Self>) -> 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<Paste>,
pub expires: Option<Expiration>,
}
impl PasteContext {
pub fn new(link: Scope<Paste>, expires: Option<Expiration>) -> Self {
Self { link, expires }
}
}
pub struct DecryptionParams {
data: Vec<u8>,
key: Key,
nonce: Nonce,
maybe_password: Option<Key>,
}
impl DecryptionParams {
pub fn new(data: Vec<u8>, key: Key, nonce: Nonce, maybe_password: Option<Key>) -> Self {
Self {
data,
key,
nonce,
maybe_password,
}
}
}
fn decrypt(
mut container: Vec<u8>,
key: Key,
nonce: Nonce,
maybe_password: Option<Key>,
) -> Result<DecryptedData, PasteCompleteConstructionError> {
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))
}
}
}

View file

@ -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::<App>();
}
@ -76,8 +87,9 @@ fn render_route(route: Route) -> Html {
}
}
struct Paste {
pub struct Paste {
state: Box<dyn PasteState>,
_listener: Box<dyn Bridge<DecryptionAgent>>,
}
impl Component for Paste {
@ -95,20 +107,38 @@ impl Component for Paste {
Uri::from_parts(uri_parts).unwrap()
};
let handle_decryption_result = |res: <DecryptionAgent as Agent>::Output| {
log!("Got decryption result back!");
match res {
Ok((decrypted, context)) => {
Box::new(PasteComplete::new(context.link, decrypted, context.expires))
as Box<dyn PasteState>
}
Err(e) => Box::new(PasteError(anyhow!("wtf"))) as Box<dyn PasteState>,
}
};
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<dyn PasteState>
}
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<dyn PasteState>
} else {
todo!()
// Box::new(partial) as Box<dyn PasteState>
}
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<dyn PasteState>
}
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
Box::new(PasteNotFound) as Box<dyn PasteState>
@ -138,8 +167,10 @@ impl Component for Paste {
Err(err) => Box::new(PasteError(anyhow!("Got {}.", err))) as Box<dyn PasteState>,
}
});
Self {
state: Box::new(PasteLoading),
_listener: listener,
}
}
@ -159,6 +190,12 @@ impl Component for Paste {
};
}
if self.state.is::<PasteDecrypting>() {
return html! {
"decrypting"
};
}
if self.state.is::<PasteNotFound>() {
return html! {
<section class={"hljs centered"}>
@ -197,9 +234,10 @@ impl Component for Paste {
struct PasteError(anyhow::Error);
#[derive(Properties, Clone, Debug)]
#[derive(Debug)]
struct PastePartial {
parent: ComponentLink<Paste>,
dispatcher: Dispatcher<DecryptionAgent>,
data: Bytes,
expires: Option<Expiration>,
key: Option<Key>,
@ -216,13 +254,13 @@ struct PasteComplete {
}
#[derive(Clone)]
enum DecryptedData {
String(String),
Blob(Blob),
Image(Blob),
pub enum DecryptedData {
String(Arc<String>),
Blob(Arc<Blob>),
Image(Arc<Blob>, (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<DecryptionAgent>);
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>) -> Self {
props
fn create(_: Self::Properties, _: ComponentLink<Self>) -> 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<dyn PasteState>,
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<Key>,
) -> Result<DecryptedData, PasteCompleteConstructionError> {
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! {
<>
<pre class={"paste"}>
<header class={"hljs"}>
{
self.expires.as_ref().map(ToString::to_string).unwrap_or_else(||
"This paste will not expire.".to_string()
)
}
</header>
<hr class={"hljs"} />
<code>{decrypted}</code>
</pre>
<pre class="paste">
<header class="unselectable">
{
self.expires.as_ref().map(ToString::to_string).unwrap_or_else(||
"This paste will not expire.".to_string()
)
}
</header>
<hr />
<code>{decrypted}</code>
</pre>
<script>{"
hljs.highlightAll();
hljs.initLineNumbersOnLoad();
"}</script>
<script>
{"
hljs.highlightAll();
hljs.initLineNumbersOnLoad();
"}
</script>
</>
}
},
@ -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<dyn PasteState>
});
html! {
<section class="hljs centered">
<section class="hljs fullscreen centered">
<div class="centered">
<p>{ "Found a binary file." }</p>
<a href={object_url} download=file_name class="hljs-meta">{"Download"}</a>
<a href=object_url download=file_name class="hljs-meta">{"Download"}</a>
</div>
<p onclick=display_anyways_callback class="display-anyways hljs-meta">{ "Display anyways?" }</p>
</section>
@ -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! {
<section class="centered">
<img src={object_url.clone()} />
<a href={object_url} download=file_name class="hljs-meta">{"Download"}</a>
<section class="hljs fullscreen centered">
<img src=object_url.clone() />
<a href=object_url download=file_name class="hljs-meta">
{
format!(
"Download {} \u{2014} {} by {}",
Byte::from_bytes(*size as u128).get_appropriate_unit(true),
width, height,
)
}
</a>
</section>
}
} else {
// This branch really shouldn't happen, but might as well
// try and give a user-friendly error message.
html! {
<section class="hljs centered">
<section class="hljs fullscreen centered">
<p>{ "Failed to create an object URL for the decrypted file. Try reloading the page?" }</p>
</section>
}

View file

@ -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;
}
.image {
display: block;
a {
}
@extend .hljs;
}