diff --git a/filed/config/filed.toml.example b/filed/config/filed.toml.example index dd3d161..9ab705d 100644 --- a/filed/config/filed.toml.example +++ b/filed/config/filed.toml.example @@ -84,3 +84,7 @@ sudo_delete=false # Whether /api/upload is enabled # It is not recommended to enable it if API key auth is not enabled upload=false + +# Whether curlapi is enabled +# curl {url}/curlapi/help for more info +curlapi=true diff --git a/filed/src/config/types.rs b/filed/src/config/types.rs index 878230d..050a1d2 100644 --- a/filed/src/config/types.rs +++ b/filed/src/config/types.rs @@ -112,6 +112,10 @@ pub struct APISettings { /// Whether /api/upload is enabled #[serde(default)] pub upload: bool, + + /// Whether curlapi is enabled + #[serde(default)] + pub curlapi: bool } impl Default for APISettings { @@ -123,7 +127,8 @@ impl Default for APISettings { get_all_own_only: true, delete: false, sudo_delete: false, - upload: false + upload: false, + curlapi: true } } } diff --git a/filed/src/web/curlapi.rs b/filed/src/web/curlapi.rs new file mode 100644 index 0000000..8538802 --- /dev/null +++ b/filed/src/web/curlapi.rs @@ -0,0 +1,11 @@ +use warp::{Filter, reply::Reply, reject::Rejection}; + +use super::state::SharedState; + +mod upload; +mod help; + +pub fn get_routes(state: SharedState) -> impl Filter + Clone { + upload::get_routes(state.clone()) + .or(help::get_routes(state)) +} \ No newline at end of file diff --git a/filed/src/web/curlapi/help.rs b/filed/src/web/curlapi/help.rs new file mode 100644 index 0000000..3bc652b --- /dev/null +++ b/filed/src/web/curlapi/help.rs @@ -0,0 +1,85 @@ +use askama::Template; +use warp::{Filter, reply::{Reply, html}, reject::Rejection}; + +use crate::web::{state::SharedState, pages::CurlHelpPage, rejection::HttpReject}; + +pub async fn help(state: SharedState, ua: String) -> Result, Rejection> { + + if ! ua.starts_with("curl/") { + let page = CurlHelpPage { conf: state.config.clone(), env: state.env.clone() }; + return Ok( + Box::new( + html(page.render().map_err(|x| HttpReject::AskamaError(x))?) + ) + ) + } + + let brand = format!( + "{} \x1b[1m{}\x1b[0m {}", + state.config.brand.instance_emoji, + state.config.brand.instance_name, + { + if state.config.brand.instance_name != "blek! File" { + "\n\x1b[90mPowered by blek! File\x1b[0m" + } else { "" } + } + ); + + let mut warns: String = String::new(); + if ! state.config.api.curlapi { + warns += "\x1b[1;31mWarning: curl API is disabled on this instance.\nYou can use the web UI to upload files.\x1b[0m\n\n" + } + if ! state.config.files.allow_uploads { + warns += { + format!( + "\x1b[1;31mWarning: all uploads are disabled on this instance{}\x1b[0m", + { + if let Some(reason) = state.config.files.upload_disable_reason { + format!(" for this reason:\n\"{}\"", reason) + } else { ".".to_string() } + } + ).as_str() + } + } + + let instance = state.env.instanceurl; + let help = +format!( +"To upload a new file, you can POST it like this: + \x1b[32mcurl\x1b[0m \x1b[33m-X POST\x1b[0m \x1b[34m{instance}/curlapi/upload\x1b[0m \x1b[33m-F'file=@file.txt'\x1b[0m \x1b[33m-F'tos_consent=on'\x1b[0m +You can also add a password: + \x1b[32mcurl\x1b[0m \x1b[33m-X POST\x1b[0m \x1b[34m{instance}/curlapi/upload\x1b[0m \x1b[33m-F'file=@file.txt'\x1b[0m \x1b[33m-F'filename=uwu'\x1b[0m \x1b[33m-F'tos_consent=on'\x1b[0m \x1b[33m-F'named=on'\x1b[0m +The `named=on` switch is neede because this API is basically +the HTML used at the regular web UI form wrapped into this URL + +\x1b[1;32mIMPORTANT:\x1b[0m Read the terms of service \x1b[1mbefore\x1b[0m uploading the file! +The ToS can be found here: \x1b[34m{instance}/tos\x1b[0m . + +{warns}" +); + + Ok( + Box::new( +format!(" + \x1b[31m┓ ╻\x1b[0m \x1b[35m┏┓•┓ \x1b[0m + \x1b[32m┣┓┃\x1b[0m \x1b[95m┣ ┓┃┏┓\x1b[0m + \x1b[34m┗┛•\x1b[0m \x1b[35m┻ ┗┗┗━\x1b[0m + +{brand} +{help} +").to_string()) +) +} + +pub fn get_routes(state: SharedState) -> impl Filter + Clone { + warp::any() + .and(warp::path!("curlapi" / "help")) + .and( + warp::any() + .map(move || state.clone()) + ) + .and( + warp::header::("user-agent") + ) + .and_then(help) +} \ No newline at end of file diff --git a/filed/src/web/curlapi/upload.rs b/filed/src/web/curlapi/upload.rs new file mode 100644 index 0000000..8658cf5 --- /dev/null +++ b/filed/src/web/curlapi/upload.rs @@ -0,0 +1,139 @@ +use std::net::IpAddr; + +use warp::{filters::multipart::FormData, reply::{Reply, with_status}, reject::Rejection, Filter}; +use warp_real_ip::real_ip; + +use crate::{web::{state::SharedState, forms::{FormElement, UploadFormData}, rejection::HttpReject}, files::File}; + +pub async fn upload(form: FormData, ip: Option, state: SharedState) -> Result, Rejection> { + if ! state.config.files.allow_uploads { + return Ok( + Box::new( + with_status( + match state.config.files.upload_disable_reason { + Some(reason) => format!("Uploads are disabled for the following reason:\n{reason}"), + None => "Uploads are disabled.".into() + }, + warp::http::StatusCode::SERVICE_UNAVAILABLE + ) + ) + ) + } + + let params = FormElement::from_formdata(form) + .await + .map_err(|x| HttpReject::WarpError(x))?; + + if let Some(consent) = params.get("tos_consent") { + if consent.data != "on".bytes().collect::>() { + return Ok( + Box::new( + format!("You need to agree to the ToS to upload a file.\nSee {}/curlapi/help for details\n\nTo agree to the ToS, add a -F'tos_consent=on'\n", state.env.instanceurl) + ) + ) + } + } else { + return Ok( + Box::new( + format!("You need to agree to the ToS to upload a file.\nSee {}/curlapi/help for details\n\nTo agree to the ToS, add a -F'tos_consent=on'\n", state.env.instanceurl) + ) + ) + } + + let formdata = UploadFormData::from_formdata(params, true); + if let Some(formdata) = formdata { + + let mut breaks_conf = false; + if (!state.config.files.allow_custom_names) && formdata.filename.is_some() { + breaks_conf = true; + } + if (!state.config.files.allow_pass_protection) && formdata.password.is_some() { + breaks_conf = true; + } + + if breaks_conf { + return Ok( + Box::new( + with_status( + "Attempt to set name or password when they are disabled".to_string(), + warp::http::StatusCode::BAD_REQUEST + ) + ) + ); + } + + if let Some(pass) = state.config.files.upload_pass { + let pass_valid: bool; + if let Some(upass) = formdata.instancepass { + pass_valid = upass == pass; + } else { + pass_valid = false + } + + if ! pass_valid { + return Ok( + Box::new( + with_status( + "Invalid instance password".to_string(), + warp::http::StatusCode::BAD_REQUEST + ) + ) + ); + } + } + + let file = File::create( + formdata.file, + formdata.mime, + formdata.filename.clone(), + state.env.clone(), + formdata.delmode, + formdata.password, + ip + ).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()))?; + + return Ok( + Box::new( + format!( + concat!( + "File uploaded successfully.\n", + "It is available via this link:\n\n", + + "{}/upload/{}\n" + ), + state.env.instanceurl, + urlencoding::encode( + match formdata.filename { + Some(name) => name, + None => file.hash() + }.as_str() + ) + ) + ) + ); + + } else { + Ok( + Box::new( + with_status( + "Invalid form".to_string(), + warp::http::StatusCode::BAD_REQUEST + ) + ) + ) + } +} + +pub fn get_routes(state: SharedState) -> impl Filter + Clone { + warp::post() + .and(warp::path!("curlapi" / "upload")) + .and(warp::multipart::form()) + .and(real_ip(vec![state.env.proxy_addr])) + .and( + warp::any() + .map(move || state.clone()) + ) + .and_then(upload) +} \ No newline at end of file diff --git a/filed/src/web/forms.rs b/filed/src/web/forms.rs index 2d43aae..cbfcecf 100644 --- a/filed/src/web/forms.rs +++ b/filed/src/web/forms.rs @@ -17,9 +17,9 @@ use crate::files::{File, lookup::LookupKind, DeleteMode}; use super::{state::SharedState, pages::{UploadSuccessPage, ErrorPage}, rejection::HttpReject}; #[derive(Debug, Serialize, Clone)] -struct FormElement { - data: Vec, - mime: String +pub struct FormElement { + pub data: Vec, + pub mime: String } impl FormElement { @@ -58,15 +58,15 @@ impl FormElement { } } -struct UploadFormData { - filename: Option, - password: Option, - instancepass: Option, - lookup_kind: LookupKind, - delmode: DeleteMode, - file: Vec, - mime: String, - tos_consent: bool +pub struct UploadFormData { + pub filename: Option, + pub password: Option, + pub instancepass: Option, + pub lookup_kind: LookupKind, + pub delmode: DeleteMode, + pub file: Vec, + pub mime: String, + pub tos_consent: bool } impl Default for UploadFormData { @@ -86,7 +86,7 @@ impl Default for UploadFormData { impl UploadFormData { - pub fn from_formdata(data: HashMap) -> Option { + pub fn from_formdata(data: HashMap, use_defaults: bool) -> Option { let mut out = Self::default(); // Add a name @@ -125,7 +125,9 @@ impl UploadFormData { } }, None => { - return None + if ! use_defaults { + return None + } } } @@ -169,7 +171,7 @@ pub async fn upload(form: FormData, ip: Option, state: SharedState) -> R } let params: HashMap = FormElement::from_formdata(form).await.map_err(|x| HttpReject::WarpError(x))?; - let formdata = UploadFormData::from_formdata(params.clone()); + let formdata = UploadFormData::from_formdata(params.clone(), false); if let Some(formdata) = formdata { diff --git a/filed/src/web/mod.rs b/filed/src/web/mod.rs index 2fc92a3..bbc786d 100644 --- a/filed/src/web/mod.rs +++ b/filed/src/web/mod.rs @@ -8,16 +8,18 @@ use warp::{Filter, reply::Reply, reject::Rejection}; use crate::{env::Env, files::lookup::FileManager, config::types::Config}; mod pages; -mod forms; +pub mod forms; pub mod state; mod rejection; mod api; mod uploaded; +mod curlapi; 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())) diff --git a/filed/src/web/pages.rs b/filed/src/web/pages.rs index a0faf97..6b634e8 100644 --- a/filed/src/web/pages.rs +++ b/filed/src/web/pages.rs @@ -88,6 +88,14 @@ pub struct ErrorPage { pub link_text: Option } +#[derive(Template)] +#[template( path = "curlapi_help.html" )] +#[allow(dead_code)] +pub struct CurlHelpPage { + pub env: Env, + pub conf: Config +} + pub async fn uploaded(query: HashMap, state: SharedState) -> Result, Rejection> { if ! query.contains_key("file") { diff --git a/filed/static/assets/alert.css b/filed/static/assets/alert.css index 55bc69d..2444755 100644 --- a/filed/static/assets/alert.css +++ b/filed/static/assets/alert.css @@ -24,4 +24,7 @@ .alert.blue .alert-title { background: #203050; +} +.alert.green .alert-title { + background: #205030; } \ No newline at end of file diff --git a/filed/static/assets/code.css b/filed/static/assets/code.css new file mode 100644 index 0000000..6ccfdfc --- /dev/null +++ b/filed/static/assets/code.css @@ -0,0 +1,23 @@ +.code { + display: block; + padding: 1em; + border: 1px solid var(--header-sec-color); + border-radius: 12px; + overflow-x: auto; +} +.code .inner { + width: max-content; + display: block; +} +.code-inline { + display: inline; + background: #00000010; + border: 1px solid #c2c4c210; + border-radius: 4px; + padding: 2px 4px; +} + +.code, .code-inline { + font-family: monospace; + line-height: 1.5em; +} \ No newline at end of file diff --git a/filed/templates/curlapi_help.html b/filed/templates/curlapi_help.html new file mode 100644 index 0000000..d8ef7cf --- /dev/null +++ b/filed/templates/curlapi_help.html @@ -0,0 +1,114 @@ + +{% extends "base.html" %} + +{% block head %} + + + + +{% endblock %} + +{% block body %} + +
+

Curl API

+

+ blek! File has an API for uploading files via cURL. + To upload a file via cURL, follow these instructions: +

+

+ To upload a file, POST it like this: + + Copy! + +

+
+ + curl + -X POST + {{env.instanceurl}}/curlapi/upload + -F'file=@file.txt' + -F'tos_consent=on' + +
+

+ To add a password, do it like this: + + Copy! + +

+
+ + curl + -X POST + {{env.instanceurl}}/curlapi/upload + -F'file=@file.txt' + -F'tos_consent=on' + -F'named=on' + +
+

+ Note that the + named=on + switch is required. + Its needed because the curl API is basically a wrapper of + this + HTML form. +

+ +
+

+ Important +

+

+ Read the + Terms of Service + before + uploading a file. +
+ You agree to them by adding the + tos_consent=on + switch. +

+
+ +
+

Web UI

+
+

+ Hey, it looks like you are viewing this page from a browser!
+ You can use the Web UI as well to upload a file! +

+

+ + Go to the web UI + +

+
+
+
+ +{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file