From 3c3ca2b87ac9d03a6fc72b59d2fbcb1b2c57331f Mon Sep 17 00:00:00 2001 From: blek Date: Sat, 9 Dec 2023 22:57:41 +1000 Subject: [PATCH 01/16] move out the api check code --- filed/src/web/api/files.rs | 18 ++++++++++++++++++ filed/src/web/api/files/get_all.rs | 15 ++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/filed/src/web/api/files.rs b/filed/src/web/api/files.rs index 2cc462a..d2d3873 100644 --- a/filed/src/web/api/files.rs +++ b/filed/src/web/api/files.rs @@ -1 +1,19 @@ +use warp::{reply::{WithStatus, Json, json}, http::StatusCode}; + +use crate::web::state::SharedState; + +use super::types::{ErrorMessage, Error}; + +fn check_api_enabled(state: &SharedState) -> Result<(), WithStatus> { + if ! state.config.api.enabled { + return Err( + warp::reply::with_status( + json(&ErrorMessage::new(Error::APIDisabled)), + StatusCode::SERVICE_UNAVAILABLE + ) + ) + } + Ok(()) +} + pub mod get_all; \ No newline at end of file diff --git a/filed/src/web/api/files/get_all.rs b/filed/src/web/api/files/get_all.rs index 8af8395..212762a 100644 --- a/filed/src/web/api/files/get_all.rs +++ b/filed/src/web/api/files/get_all.rs @@ -1,19 +1,12 @@ -use warp::{reply::{Reply, json}, reject::Rejection, Filter, http::StatusCode}; +use warp::{reply::{Reply, json}, reject::Rejection, Filter}; use crate::web::{state::SharedState, rejection::HttpReject}; -use super::super::types::{ErrorMessage, Error}; +use super::check_api_enabled; pub async fn get_all(state: SharedState) -> Result, Rejection> { - if ! state.config.api.enabled { - return Ok( - Box::new( - warp::reply::with_status( - json(&ErrorMessage::new(Error::APIDisabled)), - StatusCode::SERVICE_UNAVAILABLE - ) - ) - ) + if let Err(res) = check_api_enabled(&state) { + return Ok(Box::new(res)); } Ok( -- 2.40.1 From 1aa8a77b7884101714a238e0aeac0f93345d081c Mon Sep 17 00:00:00 2001 From: blek Date: Sat, 9 Dec 2023 23:02:23 +1000 Subject: [PATCH 02/16] respect the config.api.METHOD config values --- filed/src/web/api/files.rs | 7 +++++++ filed/src/web/api/files/get_all.rs | 8 ++++++-- filed/src/web/api/types.rs | 4 +++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/filed/src/web/api/files.rs b/filed/src/web/api/files.rs index d2d3873..6a599d0 100644 --- a/filed/src/web/api/files.rs +++ b/filed/src/web/api/files.rs @@ -16,4 +16,11 @@ fn check_api_enabled(state: &SharedState) -> Result<(), WithStatus> { Ok(()) } +fn function_disabled_err() -> WithStatus { + warp::reply::with_status( + json(&ErrorMessage::new(Error::APIFunctionDisabled)), + StatusCode::SERVICE_UNAVAILABLE + ) +} + pub mod get_all; \ No newline at end of file diff --git a/filed/src/web/api/files/get_all.rs b/filed/src/web/api/files/get_all.rs index 212762a..7439727 100644 --- a/filed/src/web/api/files/get_all.rs +++ b/filed/src/web/api/files/get_all.rs @@ -2,11 +2,15 @@ use warp::{reply::{Reply, json}, reject::Rejection, Filter}; use crate::web::{state::SharedState, rejection::HttpReject}; -use super::check_api_enabled; +use super::{check_api_enabled, function_disabled_err}; pub async fn get_all(state: SharedState) -> Result, Rejection> { if let Err(res) = check_api_enabled(&state) { - return Ok(Box::new(res)); + return Ok(Box::new(res)) + } + + if ! state.config.api.get_all { + return Ok(Box::new(function_disabled_err())) } Ok( diff --git a/filed/src/web/api/types.rs b/filed/src/web/api/types.rs index a1ecb62..717394f 100644 --- a/filed/src/web/api/types.rs +++ b/filed/src/web/api/types.rs @@ -3,6 +3,7 @@ use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Error { APIDisabled, + APIFunctionDisabled } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -15,7 +16,8 @@ impl ErrorMessage { pub fn new(error: Error) -> ErrorMessage { ErrorMessage { details: match error { - Error::APIDisabled => Some("API is disabled by the administrator. Please contact them for further details".into()) + 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, } -- 2.40.1 From bf923244fa1b3bda57c6f2bc3d5c615947cd034f Mon Sep 17 00:00:00 2001 From: blek Date: Sat, 9 Dec 2023 23:22:33 +1000 Subject: [PATCH 03/16] implement the /api/files/delete method --- filed/src/web/api.rs | 5 +-- filed/src/web/api/files.rs | 3 +- filed/src/web/api/files/delete.rs | 57 +++++++++++++++++++++++++++++++ filed/src/web/api/types.rs | 6 ++-- 4 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 filed/src/web/api/files/delete.rs diff --git a/filed/src/web/api.rs b/filed/src/web/api.rs index d07602b..6b958c5 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; +use self::files::{get_all::get_all_f, delete::delete_f}; use super::state::SharedState; @@ -24,5 +24,6 @@ pub fn get_routes(state: SharedState) -> impl Filter WithStatus { ) } -pub mod get_all; \ No newline at end of file +pub mod get_all; +pub mod delete; \ No newline at end of file diff --git a/filed/src/web/api/files/delete.rs b/filed/src/web/api/files/delete.rs new file mode 100644 index 0000000..b5d5603 --- /dev/null +++ b/filed/src/web/api/files/delete.rs @@ -0,0 +1,57 @@ +use std::collections::HashMap; + +use warp::{reply::{Reply, json}, reject::Rejection, Filter, http::StatusCode}; +use serde::{Serialize, Deserialize}; + +use crate::web::{state::SharedState, rejection::HttpReject, api::types::{ErrorMessage, Error}}; + +use super::{function_disabled_err, check_api_enabled}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DeleteFunctionPayload { + pub fid: String +} + +pub async fn delete(state: SharedState, body: DeleteFunctionPayload) -> Result, Rejection> { + if let Err(res) = check_api_enabled(&state) { + return Ok(Box::new(res)); + } + + if state.config.api.delete { + return Ok(Box::new(function_disabled_err())) + } + + let id = body.fid; + let mut file = state.file_mgr.find_by_hash(id.clone()) + .map_err(|x| HttpReject::StringError(x.to_string()))?; + + if let None = file { + file = state.file_mgr.find_by_name(id) + .map_err(|x| HttpReject::StringError(x.to_string()))?; + } + + if let None = file { + return Ok( + Box::new( + warp::reply::with_status( + json( + &ErrorMessage { + error: Error::APIError, + details: Some("No file with that ID was found.".into()) + } + ), + StatusCode::NOT_FOUND + ) + ) + ) + } + + Ok(Box::new(json(&HashMap::<(), ()>::new()))) +} + +pub fn delete_f(state: SharedState) -> impl Filter + Clone { + warp::path!("api" / "files" / "delete") + .map(move || state.clone()) + .and(warp::body::json()) + .and_then(delete) +} \ No newline at end of file diff --git a/filed/src/web/api/types.rs b/filed/src/web/api/types.rs index 717394f..53a86eb 100644 --- a/filed/src/web/api/types.rs +++ b/filed/src/web/api/types.rs @@ -3,7 +3,8 @@ use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Error { APIDisabled, - APIFunctionDisabled + APIFunctionDisabled, + APIError } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,7 +18,8 @@ impl ErrorMessage { 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::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, } -- 2.40.1 From 400c3dc129dd95fe1a47c96877201cfe280d6fb0 Mon Sep 17 00:00:00 2001 From: blek Date: Mon, 11 Dec 2023 09:58:49 +1000 Subject: [PATCH 04/16] add file password to /api/files/upload swagger spec --- filed/docs/file_api.swagger.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/filed/docs/file_api.swagger.yml b/filed/docs/file_api.swagger.yml index 8f0818d..36311fc 100644 --- a/filed/docs/file_api.swagger.yml +++ b/filed/docs/file_api.swagger.yml @@ -125,6 +125,9 @@ paths: name: type: string description: Optional name of the file so it would be accessible like file.blek.codes/uploads/{name} + pass: + type: string + description: Optional password protection for the file responses: 200: description: File uploaded successfully -- 2.40.1 From c298a188bfdb8ad4e6bce4982a32bb6730848a1b Mon Sep 17 00:00:00 2001 From: blek Date: Tue, 12 Dec 2023 16:28:44 +1000 Subject: [PATCH 05/16] 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)) } /* -- 2.40.1 From ae90e5474b8dc5ee470597aef0c28f76a0bfff10 Mon Sep 17 00:00:00 2001 From: blek Date: Tue, 12 Dec 2023 16:30:03 +1000 Subject: [PATCH 06/16] fix any request being treated as a GUI upload form --- filed/src/web/forms.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/filed/src/web/forms.rs b/filed/src/web/forms.rs index cbfcecf..7f22aba 100644 --- a/filed/src/web/forms.rs +++ b/filed/src/web/forms.rs @@ -308,7 +308,8 @@ pub async fn upload(form: FormData, ip: Option, state: SharedState) -> R } pub fn get_routes(state: SharedState) -> impl Filter + Clone { - warp::post() + warp::path("upload") + .and(warp::post()) .and(warp::multipart::form()) .and(real_ip(vec![state.env.proxy_addr])) .and( -- 2.40.1 From 7ad7a5acf09312b84ef2407112b610480381bea8 Mon Sep 17 00:00:00 2001 From: blek Date: Tue, 12 Dec 2023 17:01:29 +1000 Subject: [PATCH 07/16] add all the functionality into the upload api --- filed/src/web/api/files/upload.rs | 105 ++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/filed/src/web/api/files/upload.rs b/filed/src/web/api/files/upload.rs index 4ca0e7e..28a9b8f 100644 --- a/filed/src/web/api/files/upload.rs +++ b/filed/src/web/api/files/upload.rs @@ -1,9 +1,12 @@ -use std::collections::HashMap; +use std::{collections::HashMap, net::IpAddr}; use serde::{Serialize, Deserialize}; +use serde_json::json; +use sha2::{Sha512, Digest, digest::FixedOutput}; use warp::{reply::{Reply, with_status, json}, http::StatusCode, reject::Rejection, Filter, filters::multipart::FormData}; +use warp_real_ip::real_ip; -use crate::web::{state::SharedState, forms::FormElement, rejection::HttpReject, api::types::{ErrorMessage, Error}}; +use crate::{web::{state::SharedState, forms::FormElement, api::types::{ErrorMessage, Error}}, files::{File, lookup::LookupKind}}; use super::{is_api_pass, check_api_pass}; @@ -16,6 +19,7 @@ struct UploadAPIMetadata { struct UploadAPIPayload { file: Vec, + file_type: String, instance_pass: Option, metadata: UploadAPIMetadata } @@ -24,6 +28,7 @@ impl Default for UploadAPIPayload { fn default() -> Self { Self { file: vec![], + file_type: "application/octet-stream".into(), instance_pass: None, metadata: UploadAPIMetadata { sha512: "".into(), @@ -56,6 +61,8 @@ impl UploadAPIPayload { fields_set = true; } } + + out.file_type = file.mime.clone(); } // optional ones @@ -73,7 +80,7 @@ impl UploadAPIPayload { } } -pub async fn upload(state: SharedState, data: FormData) -> Result, Rejection> { +pub async fn upload(state: SharedState, data: FormData, ip: Option) -> Result, Rejection> { let data = FormElement::from_formdata(data) .await; @@ -100,7 +107,6 @@ pub async fn upload(state: SharedState, data: FormData) -> Result let payload = UploadAPIPayload::from_form(data); if let Some(payload) = payload { - if is_api_pass(&state) { if let Err(res) = check_api_pass( &state, @@ -113,6 +119,93 @@ pub async fn upload(state: SharedState, data: FormData) -> Result } } + // payload is all valid and accessible at this point + + let mut hash: Sha512 = Sha512::new(); + hash.update(&payload.file); + + let hash = hex::encode(hash.finalize_fixed()); + if hash != payload.metadata.sha512 { + return Ok( + Box::new( + with_status( + json( + &ErrorMessage { + error: Error::APIError, + details: Some("Hash does not match file".into()) + } + ), + StatusCode::BAD_REQUEST + ) + ) + ) + } + + let file = File::create( + payload.file, + payload.file_type, + payload.metadata.name.clone(), + state.env, + crate::files::DeleteMode::Time, + payload.metadata.pass, + ip + ).await; + + if let Err(err) = file { + return Ok( + Box::new( + with_status( + json( + &ErrorMessage { + error: Error::APIError, + details: Some( + format!("Error while saving the file: {err}") + ) + } + ), + StatusCode::INTERNAL_SERVER_ERROR + ) + ) + ) + } + + let file = file.unwrap(); + + let saved = state.file_mgr.save( + &file, + match payload.metadata.name { + Some(_) => LookupKind::ByName, + None => LookupKind::ByHash + } + ); + + if let Err(err) = saved { + return Ok( + Box::new( + with_status( + json( + &ErrorMessage { + error: Error::APIError, + details: Some( + format!("Error while saving the file: {err}") + ) + } + ), + StatusCode::INTERNAL_SERVER_ERROR + ) + ) + ) + } + + return Ok( + Box::new( + json( + &json!({ + "status": "OK" + }) + ) + ) + ) } Ok( @@ -129,9 +222,13 @@ pub async fn upload(state: SharedState, data: FormData) -> Result } pub fn upload_f(state: SharedState) -> impl Filter + Clone { + + let proxy = state.env.proxy_addr.clone(); + warp::path!("api" / "files" / "upload") .and(warp::post()) .map(move || state.clone()) .and(warp::multipart::form()) + .and(real_ip(vec![proxy])) .and_then(upload) } -- 2.40.1 From 8d8d1b5a2e8bc77f69a6a06390078bc32759d78e Mon Sep 17 00:00:00 2001 From: blek Date: Tue, 12 Dec 2023 17:02:50 +1000 Subject: [PATCH 08/16] add bodies to responses --- filed/docs/file_api.swagger.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/filed/docs/file_api.swagger.yml b/filed/docs/file_api.swagger.yml index 36311fc..fe9e0f8 100644 --- a/filed/docs/file_api.swagger.yml +++ b/filed/docs/file_api.swagger.yml @@ -131,6 +131,13 @@ paths: responses: 200: description: File uploaded successfully + content: + application/json: + schema: + type: object + properties: + status: + example: 'OK' 401: description: |- This error code is returned if one of the two conditions are met: @@ -139,6 +146,15 @@ paths: 2. The instance requires API key for all API manipulations. 3. The provided API key is invalid. 4. API authorization is not enabled, but a key is provided + content: + application/json: + schema: + type: object + properties: + error: + example: 'APIPasswordDenied' + details: + example: 'API password authorization has been denied.' components: -- 2.40.1 From d5c49d73a9b7f759da6dffad530f1e320b932a5e Mon Sep 17 00:00:00 2001 From: blek Date: Tue, 12 Dec 2023 17:11:09 +1000 Subject: [PATCH 09/16] add config checks to api upload --- filed/src/web/api/files/upload.rs | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/filed/src/web/api/files/upload.rs b/filed/src/web/api/files/upload.rs index 28a9b8f..a2836e0 100644 --- a/filed/src/web/api/files/upload.rs +++ b/filed/src/web/api/files/upload.rs @@ -82,6 +82,38 @@ impl UploadAPIPayload { pub async fn upload(state: SharedState, data: FormData, ip: Option) -> Result, Rejection> { + if (!state.config.api.enabled) || (!state.config.api.upload) { + return Ok( + Box::new( + with_status( + json(&ErrorMessage::new(Error::APIDisabled)), + StatusCode::UNAUTHORIZED + ) + ) + ) + } + + if ! state.config.files.allow_uploads { + return Ok( + Box::new( + with_status( + json( + &ErrorMessage { + error: Error::APIDisabled, + details: Some( + match state.config.files.upload_disable_reason { + Some(reason) => format!("Uploads were disabled for the following reason: {reason}"), + None => format!("Uploads were disabled by the administrator") + } + ) + } + ), + StatusCode::UNAUTHORIZED + ) + ) + ) + } + let data = FormElement::from_formdata(data) .await; -- 2.40.1 From 86f6958a0217f9e97ab9d3c6aee08702a9c30c55 Mon Sep 17 00:00:00 2001 From: blek Date: Wed, 13 Dec 2023 00:47:47 +1000 Subject: [PATCH 10/16] improve the documentation on api upload method --- filed/docs/file_api.swagger.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/filed/docs/file_api.swagger.yml b/filed/docs/file_api.swagger.yml index fe9e0f8..23758cb 100644 --- a/filed/docs/file_api.swagger.yml +++ b/filed/docs/file_api.swagger.yml @@ -102,6 +102,8 @@ paths: security: - apikey: [ key ] requestBody: + description: |- + A multipart form content: multipart/form-data: schema: @@ -117,7 +119,18 @@ paths: description: Instance-specific password needed to upload files metadata: type: object - description: file info + description: |- + JSON object with file info: + + ``` + { + sha512: string, + name?: string, + pass?: string + } + ``` + + Note that the content type does not matter on this one. properties: sha512: type: string @@ -140,7 +153,7 @@ paths: example: 'OK' 401: description: |- - This error code is returned if one of the two conditions are met: + This error code is returned if one of the 4 conditions are met: 1. The instance does not allow API file uploads. 2. The instance requires API key for all API manipulations. @@ -155,6 +168,18 @@ paths: example: 'APIPasswordDenied' details: example: 'API password authorization has been denied.' + 403: + description: |- + This error code is returned if your request payload is malformed or the hash doesn't match the file + content: + application/json: + schema: + type: object + properties: + error: + example: 'APIError' + details: + example: 'Request payload invalid' components: -- 2.40.1 From d277fd451ed9832c3661bd06adccd3867dd930c6 Mon Sep 17 00:00:00 2001 From: blek Date: Wed, 13 Dec 2023 00:55:39 +1000 Subject: [PATCH 11/16] fix stupid mistakes in config checks --- filed/src/web/api/files/delete.rs | 2 +- filed/src/web/api/files/get_all.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/filed/src/web/api/files/delete.rs b/filed/src/web/api/files/delete.rs index b5d5603..b585d4e 100644 --- a/filed/src/web/api/files/delete.rs +++ b/filed/src/web/api/files/delete.rs @@ -17,7 +17,7 @@ pub async fn delete(state: SharedState, body: DeleteFunctionPayload) -> Result Result, Rejection> { return Ok(Box::new(res)) } - if ! state.config.api.get_all { + if (!state.config.api.get_all) || (!state.config.api.enabled) { return Ok(Box::new(function_disabled_err())) } -- 2.40.1 From d27eb2dfedcd52eeb6b5578983b3fa41ebd50cda Mon Sep 17 00:00:00 2001 From: blek Date: Wed, 13 Dec 2023 20:14:37 +1000 Subject: [PATCH 12/16] make the get_all api method actually work --- filed/src/files/lookup.rs | 30 ++++++------- filed/src/web/api/files/get_all.rs | 68 +++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/filed/src/files/lookup.rs b/filed/src/files/lookup.rs index 3e67233..736980c 100644 --- a/filed/src/files/lookup.rs +++ b/filed/src/files/lookup.rs @@ -2,6 +2,7 @@ use std::error::Error; use redis::{Client, Commands, AsyncCommands, Connection}; +use tokio::task::JoinSet; use crate::env::Env; @@ -25,23 +26,18 @@ impl FileManager { async fn find_all(self: &Self, predicate: String) -> Result, Box> { let mut conn = self.conn.get_async_connection().await?; - let found: Vec = conn.keys(predicate).await?; - let serialized: Vec = - found.iter() - .map(|x| { - let result = serde_json::from_str(&x); - match result { - Ok(x) => Some(x), - Err(err) => { - log::error!("Error while serializing {x}: {:?}", err); - None - } - } - }) - .filter(|x| x.is_some()) - .map(|x| x.unwrap()) - .collect(); - Ok(serialized) + let keys: Vec = conn.keys(predicate).await?; + + let mut data: Vec = vec![]; + + for key in keys.iter() { + let raw: String = conn.get(key.clone()).await?; + + let mut parsed: File = serde_json::from_str(raw.as_str())?; + data.push(parsed); + } + + Ok(data) } /// Filter options diff --git a/filed/src/web/api/files/get_all.rs b/filed/src/web/api/files/get_all.rs index 59735cc..dbf58b7 100644 --- a/filed/src/web/api/files/get_all.rs +++ b/filed/src/web/api/files/get_all.rs @@ -1,10 +1,13 @@ -use warp::{reply::{Reply, json}, reject::Rejection, Filter}; +use std::net::IpAddr; -use crate::web::{state::SharedState, rejection::HttpReject}; +use warp::{reply::{Reply, json, with_status}, reject::Rejection, Filter, http::StatusCode}; +use warp_real_ip::real_ip; + +use crate::web::{state::SharedState, rejection::HttpReject, api::types::{ErrorMessage, Error}}; use super::{check_api_enabled, function_disabled_err}; -pub async fn get_all(state: SharedState) -> Result, Rejection> { +pub async fn get_all(state: SharedState, ip: Option) -> Result, Rejection> { if let Err(res) = check_api_enabled(&state) { return Ok(Box::new(res)) } @@ -13,20 +16,63 @@ pub async fn get_all(state: SharedState) -> Result, Rejection> { return Ok(Box::new(function_disabled_err())) } - Ok( - Box::new( - json( - &state.file_mgr.get_all(true, true) - .await - .map_err(|x| x.to_string()) - .map_err(|x| HttpReject::StringError(x))? + let found = + state.file_mgr.get_all(true, true) + .await + .map_err(|x| x.to_string()); + + if let Err(err) = found { + return Ok( + Box::new( + with_status( + json( + &ErrorMessage { + error: Error::APIError, + details: Some( + format!("Error while getting all files: {err}") + ) + } + ), + StatusCode::INTERNAL_SERVER_ERROR + ) + ) + ); + } else { + + let mut found = found.unwrap(); + + if state.config.api.get_all_own_only { + found = found + .iter() + .filter( + |x| { + if let Some(owner) = x.uploader_ip { + if let Some(caller) = ip { + println!("{owner} {caller}"); + return owner == caller + } + } + false + } + ).map(|x| x.clone()).collect(); + } + + Ok( + Box::new( + json( + &found + ) ) ) - ) + } } pub fn get_all_f(state: SharedState) -> impl Filter + Clone { + + let proxy = state.env.proxy_addr; + warp::path!("api" / "files" / "get_all") .map(move || state.clone()) + .and(real_ip(vec![proxy])) .and_then(get_all) } \ No newline at end of file -- 2.40.1 From 36bda058256f79a46703aa26428ce4e3b277abfa Mon Sep 17 00:00:00 2001 From: blek Date: Wed, 13 Dec 2023 20:15:14 +1000 Subject: [PATCH 13/16] remove an unused import --- filed/src/web/api/files/get_all.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filed/src/web/api/files/get_all.rs b/filed/src/web/api/files/get_all.rs index dbf58b7..8ec34a7 100644 --- a/filed/src/web/api/files/get_all.rs +++ b/filed/src/web/api/files/get_all.rs @@ -3,7 +3,7 @@ use std::net::IpAddr; use warp::{reply::{Reply, json, with_status}, reject::Rejection, Filter, http::StatusCode}; use warp_real_ip::real_ip; -use crate::web::{state::SharedState, rejection::HttpReject, api::types::{ErrorMessage, Error}}; +use crate::web::{state::SharedState, api::types::{ErrorMessage, Error}}; use super::{check_api_enabled, function_disabled_err}; -- 2.40.1 From 5fab1105135d7e43a3f23255da76020ea4386db8 Mon Sep 17 00:00:00 2001 From: blek Date: Wed, 13 Dec 2023 20:49:33 +1000 Subject: [PATCH 14/16] properly implement delete method according to config --- filed/docs/file_api.swagger.yml | 5 +- filed/src/web/api/files.rs | 4 +- filed/src/web/api/files/delete.rs | 78 +++++++++++++++++++++++++++--- filed/src/web/api/files/get_all.rs | 2 +- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/filed/docs/file_api.swagger.yml b/filed/docs/file_api.swagger.yml index 23758cb..7539d45 100644 --- a/filed/docs/file_api.swagger.yml +++ b/filed/docs/file_api.swagger.yml @@ -74,7 +74,7 @@ paths: example: {} 401: description: |- - This error code is returned if one of the two conditions are met: + This error code is returned if one of these conditions are met: 1. The instance does not allow deleting files via API. 2. The file has been uploaded from another IP, which is not this one, and the API was not authorized via an API key. @@ -90,6 +90,9 @@ paths: fid: type: string example: ID or name of the file. It is the NAME in file.blek.codes/uploads/NAME + api_key: + type: string + example: '123' /api/files/upload: post: summary: Upload a file diff --git a/filed/src/web/api/files.rs b/filed/src/web/api/files.rs index 631e861..70d7522 100644 --- a/filed/src/web/api/files.rs +++ b/filed/src/web/api/files.rs @@ -49,10 +49,10 @@ fn check_api_pass(state: &SharedState, key: String) -> Result<(), WithStatus WithStatus { +fn function_disabled_err(status: StatusCode) -> WithStatus { warp::reply::with_status( json(&ErrorMessage::new(Error::APIFunctionDisabled)), - StatusCode::SERVICE_UNAVAILABLE + status ) } diff --git a/filed/src/web/api/files/delete.rs b/filed/src/web/api/files/delete.rs index b585d4e..b10b67e 100644 --- a/filed/src/web/api/files/delete.rs +++ b/filed/src/web/api/files/delete.rs @@ -1,26 +1,56 @@ -use std::collections::HashMap; +use std::{collections::HashMap, net::IpAddr}; -use warp::{reply::{Reply, json}, reject::Rejection, Filter, http::StatusCode}; +use warp::{reply::{Reply, json, with_status}, reject::Rejection, Filter, http::StatusCode}; use serde::{Serialize, Deserialize}; +use warp_real_ip::real_ip; -use crate::web::{state::SharedState, rejection::HttpReject, api::types::{ErrorMessage, Error}}; +use crate::{web::{state::SharedState, rejection::HttpReject, api::types::{ErrorMessage, Error}}, files::File}; use super::{function_disabled_err, check_api_enabled}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct DeleteFunctionPayload { - pub fid: String + pub fid: String, + pub api_key: Option } -pub async fn delete(state: SharedState, body: DeleteFunctionPayload) -> Result, Rejection> { +pub async fn delete(state: SharedState, body: DeleteFunctionPayload, ip: Option) -> Result, Rejection> { if let Err(res) = check_api_enabled(&state) { return Ok(Box::new(res)); } if (!state.config.api.delete) || (!state.config.api.enabled) { - return Ok(Box::new(function_disabled_err())) + return Ok(Box::new(function_disabled_err(StatusCode::UNAUTHORIZED))) } + let mut sudo_authorized = false; + let mut blocked = false; + + if let Some(keys) = state.config.api.apikeys.clone() { + if let Some(key) = body.api_key { + + if keys.contains(&key) { + sudo_authorized = true; + blocked = false; + } else { + sudo_authorized = false; + blocked = true; + } + + } else { + sudo_authorized = false; + blocked = true + } + } + + if ! sudo_authorized { + if ip.is_none() { // need the ip if sudo is not authorized + blocked = true // to check if the file is the own file + } + } + + let ip = ip.unwrap(); + let id = body.fid; let mut file = state.file_mgr.find_by_hash(id.clone()) .map_err(|x| HttpReject::StringError(x.to_string()))?; @@ -30,7 +60,7 @@ pub async fn delete(state: SharedState, body: DeleteFunctionPayload) -> Result Result::new()))) } pub fn delete_f(state: SharedState) -> impl Filter + Clone { + + let proxy_ip = state.env.proxy_addr; + warp::path!("api" / "files" / "delete") .map(move || state.clone()) .and(warp::body::json()) + .and(real_ip(vec![proxy_ip])) .and_then(delete) } \ No newline at end of file diff --git a/filed/src/web/api/files/get_all.rs b/filed/src/web/api/files/get_all.rs index 8ec34a7..9502139 100644 --- a/filed/src/web/api/files/get_all.rs +++ b/filed/src/web/api/files/get_all.rs @@ -13,7 +13,7 @@ pub async fn get_all(state: SharedState, ip: Option) -> Result Date: Thu, 14 Dec 2023 19:35:13 +1000 Subject: [PATCH 15/16] include changes as requested --- filed/src/files/lookup.rs | 1 - filed/src/web/api/files/delete.rs | 3 ++- filed/src/web/api/files/get_all.rs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/filed/src/files/lookup.rs b/filed/src/files/lookup.rs index 736980c..0af43d8 100644 --- a/filed/src/files/lookup.rs +++ b/filed/src/files/lookup.rs @@ -2,7 +2,6 @@ use std::error::Error; use redis::{Client, Commands, AsyncCommands, Connection}; -use tokio::task::JoinSet; use crate::env::Env; diff --git a/filed/src/web/api/files/delete.rs b/filed/src/web/api/files/delete.rs index b10b67e..05da341 100644 --- a/filed/src/web/api/files/delete.rs +++ b/filed/src/web/api/files/delete.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, net::IpAddr}; +use serde_json::json; use warp::{reply::{Reply, json, with_status}, reject::Rejection, Filter, http::StatusCode}; use serde::{Serialize, Deserialize}; use warp_real_ip::real_ip; @@ -106,7 +107,7 @@ pub async fn delete(state: SharedState, body: DeleteFunctionPayload, ip: Option< file.delete(state).await; - Ok(Box::new(json(&HashMap::<(), ()>::new()))) + Ok(Box::new(json(&json!({})))) } pub fn delete_f(state: SharedState) -> impl Filter + Clone { diff --git a/filed/src/web/api/files/get_all.rs b/filed/src/web/api/files/get_all.rs index 9502139..fb77e72 100644 --- a/filed/src/web/api/files/get_all.rs +++ b/filed/src/web/api/files/get_all.rs @@ -48,7 +48,6 @@ pub async fn get_all(state: SharedState, ip: Option) -> Result Date: Thu, 14 Dec 2023 19:37:18 +1000 Subject: [PATCH 16/16] fix compile warnings --- filed/src/web/api/files/delete.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/filed/src/web/api/files/delete.rs b/filed/src/web/api/files/delete.rs index 05da341..3ed354b 100644 --- a/filed/src/web/api/files/delete.rs +++ b/filed/src/web/api/files/delete.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, net::IpAddr}; +use std::net::IpAddr; use serde_json::json; use warp::{reply::{Reply, json, with_status}, reject::Rejection, Filter, http::StatusCode}; @@ -105,7 +105,22 @@ pub async fn delete(state: SharedState, body: DeleteFunctionPayload, ip: Option< ) } - file.delete(state).await; + let res = file.delete(state).await; + if let Err(err) = res { + return Ok( + Box::new( + with_status( + json( + &ErrorMessage { + error: Error::APIError, + details: Some(format!("Couldn't delete file: {}", err)) + } + ), + StatusCode::INTERNAL_SERVER_ERROR + ) + ) + ) + } Ok(Box::new(json(&json!({})))) } -- 2.40.1