From 8de83749a36038f9dd26d6b8d1e35f43a153d3c6 Mon Sep 17 00:00:00 2001 From: blek Date: Tue, 12 Dec 2023 16:28:44 +1000 Subject: [PATCH] add upload route --- filed/src/web/api.rs | 5 +- filed/src/web/api/files.rs | 36 +++++++- filed/src/web/api/files/upload.rs | 137 ++++++++++++++++++++++++++++++ filed/src/web/api/types.rs | 6 +- filed/src/web/mod.rs | 4 +- 5 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 filed/src/web/api/files/upload.rs diff --git a/filed/src/web/api.rs b/filed/src/web/api.rs index 6b958c5..71863c5 100644 --- a/filed/src/web/api.rs +++ b/filed/src/web/api.rs @@ -1,7 +1,7 @@ use warp::{reply::Reply, reject::Rejection, Filter}; use serde::{Serialize, Deserialize}; -use self::files::{get_all::get_all_f, delete::delete_f}; +use self::files::{get_all::get_all_f, delete::delete_f, upload::upload_f}; use super::state::SharedState; @@ -25,5 +25,6 @@ pub fn get_routes(state: SharedState) -> impl Filter Result<(), WithStatus> { Ok(()) } +fn is_api_pass(state: &SharedState) -> bool { + if let Some(keys) = state.config.api.apikeys.clone() { + keys.len() != 0 + } else { + false + } +} + +fn check_api_pass(state: &SharedState, key: String) -> Result<(), WithStatus> { + let mut valid = { + if let Some(keys) = state.config.api.apikeys.clone() { + keys.iter().find(|x| (**x) == key).is_some() + } else { + false + } + }; + + if key.len() == 0 { + valid = false + } + + if valid { + Ok(()) + } else { + Err( + warp::reply::with_status( + json(&ErrorMessage::new(Error::APIPasswordDenied)), + StatusCode::FORBIDDEN + ) + ) + } +} + fn function_disabled_err() -> WithStatus { warp::reply::with_status( json(&ErrorMessage::new(Error::APIFunctionDisabled)), @@ -24,4 +57,5 @@ fn function_disabled_err() -> WithStatus { } pub mod get_all; -pub mod delete; \ No newline at end of file +pub mod delete; +pub mod upload; \ No newline at end of file diff --git a/filed/src/web/api/files/upload.rs b/filed/src/web/api/files/upload.rs new file mode 100644 index 0000000..4ca0e7e --- /dev/null +++ b/filed/src/web/api/files/upload.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; +use serde::{Serialize, Deserialize}; + +use warp::{reply::{Reply, with_status, json}, http::StatusCode, reject::Rejection, Filter, filters::multipart::FormData}; + +use crate::web::{state::SharedState, forms::FormElement, rejection::HttpReject, api::types::{ErrorMessage, Error}}; + +use super::{is_api_pass, check_api_pass}; + +#[derive(Serialize, Deserialize)] +struct UploadAPIMetadata { + sha512: String, + name: Option, + pass: Option +} + +struct UploadAPIPayload { + file: Vec, + instance_pass: Option, + metadata: UploadAPIMetadata +} + +impl Default for UploadAPIPayload { + fn default() -> Self { + Self { + file: vec![], + instance_pass: None, + metadata: UploadAPIMetadata { + sha512: "".into(), + name: None, + pass: None + } + } + } +} + +impl UploadAPIPayload { + pub fn from_form(data: HashMap) -> Option { + + let mut out = Self::default(); + + let file = data.get("file"); + let instance_pass = data.get("instance_pass"); + let metadata = data.get("metadata"); + + let mut fields_set = false; + + // required fields + if let Some(file) = file { + if let Some(metadata) = metadata { + if let Some(metadata) = metadata.as_atr_or_none() { + out.file = file.data.clone(); + if let Ok(metadata) = serde_json::from_str(&metadata) { + out.metadata = metadata; + } + fields_set = true; + } + } + } + + // optional ones + if let Some(pass) = instance_pass { + if let Some(pass) = pass.as_atr_or_none() { + out.instance_pass = Some(pass); + } + } + + if ! fields_set { + None + } else { + Some(out) + } + } +} + +pub async fn upload(state: SharedState, data: FormData) -> Result, Rejection> { + + let data = FormElement::from_formdata(data) + .await; + + if let Err(err) = data { + return Ok( + Box::new( + with_status( + json( + &ErrorMessage { + error: Error::APIError, + details: Some( + format!("Error while parsing payload: {err}") + ) + } + ), + StatusCode::BAD_REQUEST + ) + ) + ) + } + + let data = data.unwrap(); // this is guaranteed to be `Ok` at this point + + let payload = UploadAPIPayload::from_form(data); + if let Some(payload) = payload { + + if is_api_pass(&state) { + if let Err(res) = check_api_pass( + &state, + match payload.instance_pass { + Some(x) => x, + None => "".into() + } + ) { + return Ok(Box::new(res)) + } + } + + } + + Ok( + Box::new( + with_status( + json(&ErrorMessage { + error: Error::APIError, + details: Some("Request payload invalid".into()) + }), + StatusCode::BAD_REQUEST + ) + ) + ) +} + +pub fn upload_f(state: SharedState) -> impl Filter + Clone { + warp::path!("api" / "files" / "upload") + .and(warp::post()) + .map(move || state.clone()) + .and(warp::multipart::form()) + .and_then(upload) +} diff --git a/filed/src/web/api/types.rs b/filed/src/web/api/types.rs index 53a86eb..5da38e0 100644 --- a/filed/src/web/api/types.rs +++ b/filed/src/web/api/types.rs @@ -4,7 +4,8 @@ use serde::{Serialize, Deserialize}; pub enum Error { APIDisabled, APIFunctionDisabled, - APIError + APIError, + APIPasswordDenied } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -19,7 +20,8 @@ impl ErrorMessage { details: match error { Error::APIDisabled => Some("API is disabled by the administrator. Please contact them for further details".into()), Error::APIFunctionDisabled => Some("This API function is disabled by the administrator. Please contact them for further details.".into()), - Error::APIError => Some("An error has occured while executing the API request".into()) + Error::APIError => Some("An error has occured while executing the API request".into()), + Error::APIPasswordDenied => Some("API password authorization has been denied.".into()) }, error, } diff --git a/filed/src/web/mod.rs b/filed/src/web/mod.rs index bbc786d..f8ac89f 100644 --- a/filed/src/web/mod.rs +++ b/filed/src/web/mod.rs @@ -20,10 +20,10 @@ use state::SharedState; pub fn routes(state: SharedState) -> impl Filter + Clone { static_dir!("static") .or(curlapi::get_routes(state.clone())) - .or(pages::get_routes(state.clone())) .or(forms::get_routes(state.clone())) .or(api::get_routes(state.clone())) - .or(uploaded::get_uploaded(state)) + .or(uploaded::get_uploaded(state.clone())) + .or(pages::get_routes(state)) } /*