diff --git a/filed/src/web/forms.rs b/filed/src/web/forms.rs index c992213..eea511c 100644 --- a/filed/src/web/forms.rs +++ b/filed/src/web/forms.rs @@ -6,14 +6,14 @@ use std::collections::HashMap; use askama::Template; -use warp::{Filter, reply::Reply, reject::Rejection, filters::multipart::FormData, http::StatusCode}; +use warp::{Filter, reply::{Reply, json, with_status, html}, reject::Rejection, filters::multipart::FormData, http::StatusCode}; use futures_util::TryStreamExt; use bytes::BufMut; use serde::Serialize; use crate::files::{File, lookup::LookupKind, DeleteMode}; -use super::{state::SharedState, pages::{BadActionReq, UploadSuccessPage, self}, rejection::HttpReject}; +use super::{state::SharedState, pages::{BadActionReq, UploadSuccessPage, self, ErrorPage}, rejection::HttpReject}; #[derive(Debug, Serialize, Clone)] struct FormElement { @@ -24,6 +24,125 @@ impl FormElement { pub fn as_str_or_reject(self: &Self) -> Result { Ok(String::from_utf8(self.data.clone()).map_err(|err| warp::reject::custom(HttpReject::FromUtf8Error(err)))?) } + + pub fn as_atr_or_none(self: &Self) -> Option { + if let Ok(res) = String::from_utf8(self.data.clone()) { + Some(res) + } else { + None + } + } + + pub fn is_checked(self: &Self) -> bool { + if self.data.len() != 2 { + return false + } + let data = self.data.clone(); + let on = "on".bytes().collect::>(); + data == on + } + + pub async fn from_formdata(form: FormData) -> Result, warp::Error> { + form.and_then(|mut field| async move { + let mut bytes: Vec = vec![]; + while let Some(byte) = field.data().await { + bytes.put(byte.unwrap()) + } + + Ok((field.name().into(), FormElement { data: bytes, mime: field.content_type().unwrap_or("text/plain").to_string() })) + }).try_collect() + .await + } +} + +struct UploadFormData { + filename: Option, + password: Option, + lookup_kind: LookupKind, + delmode: DeleteMode, + file: Vec, + mime: String, + tos_consent: bool +} + +impl Default for UploadFormData { + fn default() -> Self { + UploadFormData { + filename: None, + password: None, + lookup_kind: LookupKind::ByHash, + delmode: DeleteMode::Time, + file: vec![], + mime: "application/x-octet-stream".into(), + tos_consent: false + } + } +} + +impl UploadFormData { + + pub fn from_formdata(data: HashMap) -> Option { + let mut out = Self::default(); + + // Add a name + match data.get("named") { + Some(val) => { + if val.is_checked() { + let name = data.get("filename")?; + out.filename = Some(name.as_atr_or_none()?); + out.lookup_kind = LookupKind::ByHash + } + }, + None => () + } + + // Add a password + match data.get("passworded") { + Some(val) => { + if val.is_checked() { + let pass = data.get("password")?; + out.password = Some(pass.as_atr_or_none()?); + out.lookup_kind = LookupKind::ByName + } + }, + None => () + } + + // Delete mode + match data.get("delmode") { + Some(val) => { + let val = val.data.clone(); + let is_30 = val == "30".bytes().collect::>(); + if is_30 { + out.delmode = DeleteMode::Time + } else { + out.delmode = DeleteMode::TimeOrDownload + } + }, + None => { + return None + } + } + + let file = data.get("file")?; + out.file = file.data.clone(); + out.mime = file.mime.clone(); + out.tos_consent = match data.get("tos_consent") { + Some(v) => v.is_checked(), + None => false + }; + + Some(out) + } +} + +fn bad_req_html(data: String) -> Box { + Box::new( + with_status( + html(data), + StatusCode::BAD_REQUEST + ) + ) } pub async fn upload(form: FormData, state: SharedState) -> Result, Rejection> { @@ -34,141 +153,75 @@ pub async fn upload(form: FormData, state: SharedState) -> Result ) } - let params: HashMap = form.and_then(|mut field| async move { - let mut bytes: Vec = vec![]; - while let Some(byte) = field.data().await { - bytes.put(byte.unwrap()) - } - - Ok((field.name().into(), FormElement { data: bytes, mime: field.content_type().unwrap_or("text/plain").to_string() })) - }).try_collect() - .await - .map_err(|err| warp::reject::custom(HttpReject::WarpError(err.into())))?; + let params: HashMap = FormElement::from_formdata(form).await.map_err(|x| HttpReject::WarpError(x))?; + let formdata = UploadFormData::from_formdata(params.clone()); + + if let Some(formdata) = formdata { - // check that required fields exist - let mut all_exist = true; - let _ = vec!["delmode", "file", "filename", "password"].iter().for_each(|x| { - let field = x.to_string(); - if ! params.contains_key(&field) { - all_exist = false; - } - }); + if ! formdata.tos_consent { + let error = ErrorPage { + env: state.env, + conf: state.config, + error_text: "You must agree to the ToS".into(), + link: None, + link_text: None + }; - if ! all_exist { - return Ok(Box::new( - warp::reply::with_status( - warp::reply::html( - BadActionReq { - env: state.env.clone(), - conf: state.config.clone() - } - .render() - .map_err(|err| warp::reject::custom(HttpReject::AskamaError(err.into())))? - ), - StatusCode::BAD_REQUEST + return Ok( + bad_req_html( + error.render() + .map_err(|x| HttpReject::AskamaError(x))? + ) ) - )) - } - - let check_off = FormElement { data: "off".as_bytes().to_vec(), mime: "text/plain".into() }; - - let data = params.get("file").unwrap(); - let delmode = params.get("delmode").unwrap(); - let named = params.get("named"); - let filename = params.get("filename").unwrap(); - let tos_check = match params.get("tos_consent") { - Some(v) => (*v).clone(), - None => check_off.clone() - }; - - let protected = params.get("passworded").unwrap_or(&check_off.clone()).as_str_or_reject()?; - let protected = protected == "on"; - let password: Option = { - if ! state.config.files.allow_pass_protection { - None - } else { - let pass = params.get("password"); - if protected && pass.is_some() { - Some(pass.unwrap().as_str_or_reject()?) - } else { - None - } } - }; - let mut is_named = named.is_none(); - let tos_check = tos_check.as_str_or_reject()?; - if tos_check != "on" { + let file = File::create( + formdata.file, + formdata.mime, + formdata.filename.clone(), + state.env.clone(), + formdata.delmode, + formdata.password + ).await + .map_err(|x| HttpReject::StringError(x.to_string())) + ?; + + state.file_mgr.save(&file, formdata.lookup_kind) + .map_err(|x| HttpReject::StringError(x.to_string()))?; + + let page = UploadSuccessPage { + env: state.env, + conf: state.config, + link: match formdata.filename { + Some(v) => v, + None => file.hash() + } + }; + return Ok( Box::new( - warp::reply::html( - pages::ErrorPage { - env: state.env, - conf: state.config.clone(), - error_text: "You must consent to the terms and conditions!".into(), - link: Some("/".into()), - link_text: Some("Go back".into()) - } - .render() - .map_err( - |err| - warp::reject::custom(HttpReject::AskamaError(err)) - )? + html( + page.render() + .map_err(|x| HttpReject::AskamaError(x))? ) ) ) } - let delmode = delmode.as_str_or_reject()?; - if delmode != "30" && delmode != "dl" { - return Err(warp::reject::custom(HttpReject::StringError("delmode is neither 30 or dl!".into()))); - } - - if named.is_some() { - if state.config.files.allow_custom_names { - is_named = false; - } else { - is_named = named.unwrap().as_str_or_reject()? == "on"; - } - } - - let file = File::create( - data.data.clone(), - data.mime.clone(), - { - if is_named { - Some(filename.as_str_or_reject()?) - } else { - None - } - }, - state.env.clone(), - { - if delmode == "30" { - DeleteMode::Time - } else { - DeleteMode::TimeOrDownload - } - }, - password - ).await - .map_err(|err| warp::reject::custom(HttpReject::StringError(err.to_string())))?; - - state.file_mgr.save(&file, { - if is_named { - LookupKind::ByName - } else { - LookupKind::ByHash - } - }).map_err(|err| warp::reject::custom(HttpReject::StringError(err.to_string())))?; - - let uploaded = UploadSuccessPage { - env: state.env.clone(), - conf: state.config.clone(), - link: file.leftmost_link() + let error = ErrorPage { + env: state.env, + conf: state.config, + error_text: "Form is malformed".into(), + link: None, + link_text: None }; - Ok(Box::new(warp::reply::html(uploaded.render().unwrap()))) + return Ok( + bad_req_html( + error.render() + .map_err(|x| HttpReject::AskamaError(x))? + ) + ) }