omegaupload/web/src/main.rs

480 lines
15 KiB
Rust
Raw Normal View History

2021-10-21 18:35:54 -07:00
#![warn(clippy::nursery, clippy::pedantic)]
2021-10-22 19:15:23 -07:00
use std::fmt::{Debug, Display, Formatter};
2021-10-17 14:15:29 -07:00
use std::str::FromStr;
2021-10-22 19:15:23 -07:00
use anyhow::{anyhow, bail, Context};
2021-10-19 18:48:32 -07:00
use bytes::Bytes;
2021-10-19 02:18:33 -07:00
use downcast_rs::{impl_downcast, Downcast};
2021-10-22 19:15:23 -07:00
use gloo_console::log;
2021-10-19 23:53:22 -07:00
use http::header::EXPIRES;
2021-10-19 18:48:32 -07:00
use http::uri::PathAndQuery;
use http::{StatusCode, Uri};
2021-10-22 19:15:23 -07:00
use js_sys::{Array, ArrayBuffer, Uint8Array};
2021-10-19 02:18:33 -07:00
use omegaupload_common::crypto::{open, Key, Nonce};
2021-10-19 23:53:22 -07:00
use omegaupload_common::{Expiration, PartialParsedUrl};
2021-10-22 19:15:23 -07:00
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::TextDecoder;
use web_sys::{Blob, Url};
2021-10-17 14:15:29 -07:00
use yew::utils::window;
2021-10-19 02:18:33 -07:00
use yew::Properties;
2021-10-17 14:15:29 -07:00
use yew::{html, Component, ComponentLink, Html, ShouldRender};
use yew_router::router::Router;
use yew_router::Switch;
2021-10-19 18:48:32 -07:00
use yewtil::future::LinkFuture;
2021-10-17 14:15:29 -07:00
fn main() {
yew::start_app::<App>();
}
struct App;
impl Component for App {
type Message = ();
type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
Self
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<Router<Route> render={Router::render(render_route)} />
}
}
}
#[derive(Clone, Debug, Switch)]
enum Route {
#[to = "/!"]
Index,
#[rest]
Path(String),
}
2021-10-21 18:35:54 -07:00
#[allow(clippy::needless_pass_by_value)]
2021-10-17 14:15:29 -07:00
fn render_route(route: Route) -> Html {
match route {
Route::Index => html! {
<main>
<p>{ "Hello world" }</p>
</main>
},
Route::Path(_) => html! {
<main>
<Paste/>
</main>
},
}
}
struct Paste {
2021-10-19 02:18:33 -07:00
state: Box<dyn PasteState>,
2021-10-17 14:15:29 -07:00
}
impl Component for Paste {
2021-10-19 02:18:33 -07:00
type Message = Box<dyn PasteState>;
2021-10-17 14:15:29 -07:00
2021-10-16 09:50:11 -07:00
type Properties = ();
2021-10-17 14:15:29 -07:00
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let url = String::from(window().location().to_string());
let request_uri = {
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
2021-10-21 18:35:54 -07:00
if let Some(parts) = uri_parts.path_and_query.as_mut() {
*parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap();
}
2021-10-17 14:15:29 -07:00
Uri::from_parts(uri_parts).unwrap()
};
2021-10-19 02:18:33 -07:00
let link_clone = link.clone();
2021-10-19 18:48:32 -07:00
link.send_future(async move {
match reqwest::get(&request_uri.to_string()).await {
Ok(resp) if resp.status() == StatusCode::OK => {
2021-10-19 23:53:22 -07:00
let expires = resp
.headers()
.get(EXPIRES)
.and_then(|v| Expiration::try_from(v).ok());
2021-10-22 19:15:23 -07:00
let bytes = match resp.bytes().await {
Ok(bytes) => bytes,
2021-10-19 18:48:32 -07:00
Err(e) => {
2021-10-21 18:35:54 -07:00
return Box::new(PasteError(anyhow!("Got {}.", e)))
2021-10-19 18:48:32 -07:00
as Box<dyn PasteState>
}
};
2021-10-19 02:18:33 -07:00
2021-10-22 19:15:23 -07:00
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<dyn PasteState>
2021-10-19 02:18:33 -07:00
} else {
2021-10-22 19:15:23 -07:00
todo!()
// Box::new(partial) as Box<dyn PasteState>
2021-10-19 02:18:33 -07:00
}
}
2021-10-21 18:35:54 -07:00
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
Box::new(PasteNotFound) as Box<dyn PasteState>
}
Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {
Box::new(PasteBadRequest) as Box<dyn PasteState>
2021-10-19 02:18:33 -07:00
}
2021-10-21 18:35:54 -07:00
Ok(err) => {
Box::new(PasteError(anyhow!("Got {}.", err.status()))) as Box<dyn PasteState>
}
Err(err) => Box::new(PasteError(anyhow!("Got {}.", err))) as Box<dyn PasteState>,
2021-10-19 18:48:32 -07:00
}
});
Self {
state: Box::new(PasteLoading),
2021-10-17 14:15:29 -07:00
}
2021-10-16 09:50:11 -07:00
}
2021-10-17 14:15:29 -07:00
2021-10-19 02:18:33 -07:00
fn update(&mut self, msg: Self::Message) -> ShouldRender {
self.state = msg;
true
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
if self.state.is::<PasteLoading>() {
return html! {
<p>{ "loading" }</p>
};
}
if self.state.is::<PasteNotFound>() {
return html! {
2021-10-22 19:15:23 -07:00
<section class={"hljs centered"}>
2021-10-21 18:35:54 -07:00
<p>{ "Either the paste has been burned or one never existed." }</p>
</section>
};
}
if self.state.is::<PasteBadRequest>() {
return html! {
2021-10-22 19:15:23 -07:00
<section class={"hljs centered"}>
2021-10-21 18:35:54 -07:00
<p>{ "Bad Request. Is this a valid paste URL?" }</p>
</section>
2021-10-19 02:18:33 -07:00
};
}
if let Some(error) = self.state.downcast_ref::<PasteError>() {
return html! {
2021-10-22 19:15:23 -07:00
<section class={"hljs centered"}><p>{ error.0.to_string() }</p></section>
2021-10-19 02:18:33 -07:00
};
}
if let Some(partial_paste) = self.state.downcast_ref::<PastePartial>() {
return partial_paste.view();
}
if let Some(paste) = self.state.downcast_ref::<PasteComplete>() {
return paste.view();
}
html! {
"An internal error occurred: client is in unknown state!"
}
}
}
struct PasteError(anyhow::Error);
#[derive(Properties, Clone, Debug)]
struct PastePartial {
parent: ComponentLink<Paste>,
2021-10-19 23:53:22 -07:00
data: Bytes,
expires: Option<Expiration>,
2021-10-19 02:18:33 -07:00
key: Option<Key>,
nonce: Option<Nonce>,
password: Option<Key>,
needs_pw: bool,
}
#[derive(Properties, Clone)]
struct PasteComplete {
2021-10-22 19:15:23 -07:00
parent: ComponentLink<Paste>,
decrypted: DecryptedData,
2021-10-19 23:53:22 -07:00
expires: Option<Expiration>,
2021-10-22 19:15:23 -07:00
}
#[derive(Clone)]
enum DecryptedData {
String(String),
Blob(Blob),
Image(Blob),
2021-10-19 02:18:33 -07:00
}
trait PasteState: Downcast {}
impl_downcast!(PasteState);
2021-10-21 18:35:54 -07:00
2021-10-19 02:18:33 -07:00
impl PasteState for PasteError {}
impl PasteState for PastePartial {}
impl PasteState for PasteComplete {}
2021-10-21 18:35:54 -07:00
macro_rules! impl_paste_type_state {
(
$($state:ident),* $(,)?
) => {
$(
struct $state;
impl PasteState for $state {}
)*
};
}
impl_paste_type_state!(PasteLoading, PasteNotFound, PasteBadRequest);
2021-10-19 02:18:33 -07:00
impl PastePartial {
fn new(
2021-10-19 23:53:22 -07:00
data: Bytes,
expires: Option<Expiration>,
2021-10-21 18:35:54 -07:00
partial_parsed_url: &PartialParsedUrl,
2021-10-19 02:18:33 -07:00
parent: ComponentLink<Paste>,
) -> Self {
Self {
parent,
2021-10-19 23:53:22 -07:00
data,
expires,
2021-10-19 02:18:33 -07:00
key: partial_parsed_url.decryption_key,
nonce: partial_parsed_url.nonce,
password: None,
needs_pw: partial_parsed_url.needs_password,
}
}
}
enum PartialPasteMessage {
DecryptionKey(Key),
Nonce(Nonce),
Password(Key),
}
impl Component for PastePartial {
type Message = PartialPasteMessage;
type Properties = Self;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
props
}
2021-10-16 09:50:11 -07:00
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
2021-10-19 02:18:33 -07:00
PartialPasteMessage::DecryptionKey(key) => self.key = Some(key),
PartialPasteMessage::Nonce(nonce) => self.nonce = Some(nonce),
PartialPasteMessage::Password(password) => self.password = Some(password),
2021-10-16 09:50:11 -07:00
}
2021-10-19 02:18:33 -07:00
2021-10-19 23:53:22 -07:00
match (self.key, self.nonce, self.password) {
(Some(key), Some(nonce), maybe_password)
if (self.needs_pw && maybe_password.is_some())
|| (!self.needs_pw && maybe_password.is_none()) =>
{
2021-10-22 19:15:23 -07:00
let parent = self.parent.clone();
2021-10-19 23:53:22 -07:00
let data = self.data.clone();
2021-10-21 18:35:54 -07:00
let expires = self.expires;
2021-10-22 19:15:23 -07:00
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!()
}
}
2021-10-19 02:18:33 -07:00
});
}
_ => (),
}
2021-10-21 18:35:54 -07:00
// parent should re-render so this element should be dropped; no point
// in saying this needs to be re-rendered.
2021-10-19 02:18:33 -07:00
false
2021-10-16 09:50:11 -07:00
}
2021-10-17 14:15:29 -07:00
2021-10-16 09:50:11 -07:00
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
2021-10-17 14:15:29 -07:00
2021-10-16 09:50:11 -07:00
fn view(&self) -> Html {
2021-10-19 02:18:33 -07:00
html! {
"got partial data"
}
}
}
2021-10-22 19:15:23 -07:00
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 {
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.")
}
2021-10-19 02:18:33 -07:00
}
}
}
impl PasteComplete {
2021-10-19 23:53:22 -07:00
fn new(
2021-10-22 19:15:23 -07:00
parent: ComponentLink<Paste>,
decrypted: DecryptedData,
2021-10-19 23:53:22 -07:00
expires: Option<Expiration>,
) -> Self {
2021-10-19 02:18:33 -07:00
Self {
2021-10-22 19:15:23 -07:00
parent,
decrypted,
2021-10-19 23:53:22 -07:00
expires,
2021-10-19 02:18:33 -07:00
}
}
fn view(&self) -> Html {
2021-10-22 19:15:23 -07:00
match &self.decrypted {
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>
<script>{"
hljs.highlightAll();
hljs.initLineNumbersOnLoad();
"}</script>
</>
}
},
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<dyn PasteState>
});
html! {
<section class="hljs centered">
<div class="centered">
<p>{ "Found a binary file." }</p>
<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>
}
} else {
// This branch really shouldn't happen, but might as well
// try and give a user-friendly error message.
html! {
<section class="hljs centered">
<p>{ "Failed to create an object URL for the decrypted file. Try reloading the page?" }</p>
</section>
2021-10-19 23:53:22 -07:00
}
2021-10-22 19:15:23 -07:00
}
}
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! {
<section class="centered">
<img src={object_url.clone()} />
<a href={object_url} download=file_name class="hljs-meta">{"Download"}</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">
<p>{ "Failed to create an object URL for the decrypted file. Try reloading the page?" }</p>
</section>
}
}
2021-10-17 14:15:29 -07:00
}
2021-10-16 09:50:11 -07:00
}
}
}