add API support

main
Volkor 1 month ago
parent cf8f330e01
commit 8aff815ec8
Signed by: Volkor
GPG Key ID: BAD7CA8A81CC2DA5

@ -5,7 +5,7 @@ port = 8282
# Please enable this if you are using nginx (if you aren't, please do)
# This should speed up file serving a decent amount.
# TODO: benchmarks
nginx_sendfile = true
nginx_sendfile = false
[database] # DATABASE DATABASE JUST LIVING IN THE DATABASE WOOAH
# What SQL Backend to use

@ -1,27 +1,28 @@
# Ephemeral API Documentation
All results are returned in JSON Formatting.
Ephemeral uses a cool hidden input type on the upload form to say that the upload is coming from a browser.
If the "source" value is not set to "web", then the server will return json.
## File Uploading - `/`
This API Documentation is for the JSON responses.
POST - endpoint receives a file, returns the following.
This is not entirely accurate, as it includes planned features like
editing ttd, and looking at file information.
- URL
- filename
- **current** expiry
- adminkey
## File Uploading - `/`
POST - endpoint receives a URL, returns the following.
POST - endpoint receives a file, returns the following.
- URL
- 'url' name (filename but for urls)
- expiry (if enabled on server)
- adminkey
`{"file": "FAFHHSW.txt", "url": "https://cz0.au/FAFHHSW.txt", "adminurl": "https://cz0.au/edit/64f2fhdsFSGQ", "expiry": 1677123536}`
Returns 403 if the file is on the blocklist.
Returns 401 if the server requires a key to upload files.
- File: The name of the file uploaded.
- url: the URL of the file.
- adminurl: the admin URL of the file, currently, this just deletes the file.
- expiry: The expiry of the file, in unix time.
GET - Returns the webpage, so be careful.
Returns `{"error": "BlockedMimetype"}` if the Mimetype is not allowed to be uploaded (as set in config.toml)
Returns `{"error": "MissingUploadKey"}` if the server requires a key to upload files.
Returns `{"error": "InternalServerError"}` if there was something wrong with copying the file to the filestore.
Returns `{"error": "BadRequest"}` if there was a error parsing the request that was sent.
## File Serving - `/<file>`
@ -40,10 +41,12 @@ If the key is invalid returns a 401, same with all children API Endpoints
## File Deletion - `/edit/<key>` (also `/delete/<key>` for backwards compatibility.)
Accepts GET and DELETE - as long as it has the keys.
Deletes the file with the corresponding key. This is immediate and permanent.
When successful, returns `{"deletion": "success"}`
Accepts GET and DELETE - as long as it has the keys.
## File expiry Change - `/edit/<key>/ttd`
If GET, reads the expiry from the DB and responds with it.

@ -63,6 +63,7 @@ server {
location / {
# Okay, I don't know if /all/ of these are needed, but you 100% need the host header.
# And the X-Forwarded-Proto if you want urls to be generated correctly
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;

@ -95,7 +95,7 @@ pub async fn check_filename(sqlconn: &Pool<Sqlite>, filename: &String) -> bool {
}
}
// This function receives an adminkey, (and sqlpool) and returns a Some(String) with the filename corresponding to the adminkey.
// This function receives an adminkey, (and sqlpool) and returns a Some(String) containing the filename that corresponds to the adminkey.
// It returns files that haven't already been deleted, so this is a 'single-use' operation per file.
pub async fn check_adminkey(sqlconn: &Pool<Sqlite>, adminkey: String) -> Option<String> {
let result = sqlx::query!(

@ -1,7 +1,7 @@
use once_cell::sync::OnceCell;
use ramhorns::{Content, Ramhorns};
use salvo::fs::NamedFile;
use salvo::hyper::header::{HOST};
use salvo::hyper::header::HOST;
use salvo::prelude::*;
use salvo::serve_static::{StaticDir, StaticFile};
use sqlx::SqlitePool;
@ -40,8 +40,8 @@ lazy_static! {
#[derive(Content)]
struct TemplateStruct {
domain: String,
filename: String,
adminkey: String,
fileurl: String,
adminurl: String,
message1: String,
message2: String,
}
@ -64,7 +64,7 @@ async fn index(req: &mut Request, res: &mut Response) {
#[handler]
async fn serve_file(req: &mut Request, res: &mut Response) {
let headers = req.headers();
let headers = req.headers().clone();
let sqlconn = SQLITE.get().unwrap();
// Check if the filename exists in the DB
@ -72,23 +72,23 @@ async fn serve_file(req: &mut Request, res: &mut Response) {
let valid = db::check_filename(sqlconn, &filename).await;
// If the file isn't valid, show a nice 404
if !valid {
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
// Now we need to setup the templating engine.
// Yuck we need to setup a struct
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
message1: String::from("Error 404: File not Found"),
message2: String::from(
"We couldn't find that. Are you sure you know what you're looking for?",
),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::NOT_FOUND);
res.render(Text::Html(rendered));
return;
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
// Now we need to setup the templating engine.
// Yuck we need to setup a struct
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from("Error 404: File not Found"),
message2: String::from(
"We couldn't find that. Are you sure you know what you're looking for?",
),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::NOT_FOUND);
res.render(Text::Html(rendered));
return;
}
// Unsafe extension overwriting.
@ -108,14 +108,12 @@ async fn serve_file(req: &mut Request, res: &mut Response) {
mime.clone().into_string().unwrap()
);
mimetype = "text/plain".to_string();
};
}
// Add the header to the response, which /may/ or /may not/ be modified by the safety check
res.add_header("Content-Type", mimetype, false).unwrap();
// Go through all the headers and print them out, just to check for now!
tracing::debug!("response headers: {:?}", res.headers());
@ -138,7 +136,7 @@ async fn serve_file(req: &mut Request, res: &mut Response) {
.get_bool("server.nginx_sendfile")
.expect("Couldn't find 'nginx_sendfile' in config. :(");
if nginxsendfile {
if nginxsendfile {
// Add the X-Accel-Redirect header, allowing for faster file serving.
let xsend = "/files/".to_string() + &filename.to_string();
res.add_header("X-Accel-Redirect", xsend, true).unwrap();
@ -151,7 +149,7 @@ async fn serve_file(req: &mut Request, res: &mut Response) {
let file = NamedFile::open(&filepath).await.unwrap();
// Send the file to the response, and then send the response.
file.send(headers, res).await;
file.send(&headers, res).await;
res.set_status_code(StatusCode::OK);
}
}
@ -159,44 +157,64 @@ async fn serve_file(req: &mut Request, res: &mut Response) {
// This takes the adminkey in, and deletes the file that matches it in the DB.
#[handler]
async fn delete_file(req: &mut Request, res: &mut Response) {
let headers = req.headers();
let headers = req.headers().clone();
let sqlconn = SQLITE.get().unwrap();
let adminkey: &str = req.param("adminkey").unwrap();
let adminkey: &String = &req.param("adminkey").unwrap();
tracing::debug!("delete_file(adminkey): {:?}", adminkey);
// This is ugly, but this grabs the invisible form data that determines if the file was uploaded via the web.
let mut web = false;
if req.form_data().await.unwrap().fields.get("source").unwrap() == &"web".to_string() {
web = true;
}
// Checks if the adminkey is valid, and the file is active.
let filename = db::check_adminkey(sqlconn, adminkey.to_string()).await;
let filename = db::check_adminkey(sqlconn, adminkey.to_string()).await.clone();
// if the filename is None, we fail. otherwise we delete it.
if filename.is_none() {
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
message1: String::from("Error 401: Unauthorised"),
message2: String::from(
"You have entered an invalid adminkey, please enter a valid adminkey",
),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::UNAUTHORIZED);
res.render(Text::Html(rendered));
if web {
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from("Error 401: Unauthorised"),
message2: String::from(
"You have entered an invalid adminkey, please enter a valid adminkey",
),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::UNAUTHORIZED);
res.render(Text::Html(rendered));
} else {
res.set_status_code(StatusCode::UNAUTHORIZED);
res.render(Text::Json(r#"{"error": "InvalidAdminKey"}"#));
}
} else {
// Actually delete the file
let fname = filename.unwrap().clone();
db::delete_file(sqlconn, fname.clone()).await;
engine::delete_file(fname.clone()).await;
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
message1: String::from("File Deleted"),
message2: String::from("woah where'd the file go???"),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::UNAUTHORIZED);
res.render(Text::Html(rendered));
// Respond to the client.
if web {
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from("File Deleted"),
message2: String::from("woah where'd the file go???",),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::OK);
res.render(Text::Html(rendered));
} else {
res.set_status_code(StatusCode::OK);
res.render(Text::Json(r#"{"deletion": "success"}"#));
}
}
}
@ -209,6 +227,18 @@ async fn upload(req: &mut Request, res: &mut Response) {
let host = headers[HOST].to_str().unwrap();
tracing::debug!("upload(req): {:?}", req);
// Check if the request was done in https or not by seeing if "x-forwarded-proto" exists.
let mut https: String = "http".to_string();
if req.headers().contains_key("x-forwarded-proto") {
https = "https".to_string();
}
// This is ugly, but this grabs the invisible form data that determines if the file was uploaded via the web.
let mut web = false;
if req.form_data().await.unwrap().fields.get("source").is_some() {
web = true;
}
if let Some(file) = req.file("file").await {
// Generate new filename.
// TODO: Do all the checks to make sure we actually want to generate a new filename (needs config working)
@ -240,19 +270,26 @@ async fn upload(req: &mut Request, res: &mut Response) {
"Upload was blocked due to blocked extension: {:?}",
mime.clone().into_string()
);
let template_with_host =
"./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
message1: String::from("Error 403"),
message2: String::from("That filetype is not allowed."),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::FORBIDDEN);
res.render(Text::Html(rendered));
// If web is true, render html, otherwise json.
if web {
let template_with_host =
"./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from("Error 403"),
message2: String::from("That filetype is not allowed."),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::FORBIDDEN);
res.render(Text::Html(rendered));
} else {
res.set_status_code(StatusCode::FORBIDDEN);
res.render(Text::Json(r#"{"error": "BlockedFiletype"}"#));
}
}
}
@ -265,18 +302,27 @@ async fn upload(req: &mut Request, res: &mut Response) {
if let Err(e) = std::fs::copy(file.path(), Path::new(&dest)) {
tracing::error!("There was a problem uploading file: {}, error: {}", dest, e);
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
message1: String::from("Error 500"),
message2: String::from("There was a problem uploading the file, please try again."),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
res.render(Text::Html(rendered));
// If web is true, render html, otherwise json.
if web {
let template_with_host =
"./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from("Error 500"),
message2: String::from(
"There was a problem uploading the file, please try again.",
),
};
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
res.render(Text::Html(rendered));
} else {
res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
res.render(Text::Json(r#"{"error": "InternalServerError"}"#));
}
} else {
// This branch is when the file has been uploaded
@ -288,7 +334,7 @@ async fn upload(req: &mut Request, res: &mut Response) {
tracing::debug!("upload(filesize): {:?}", filesize);
let expiry = engine::calculate_expiry(sqlconn, filename.clone(), filesize).await;
// Convert that expiry to actual time and date (for display)
let dt = Utc.timestamp_opt(expiry as i64, 0).unwrap();
let expiry_dt = Utc.timestamp_opt(expiry as i64, 0).unwrap();
tracing::debug!("upload(expiry): {:?}", expiry);
// Determine what ip type it is.
@ -316,29 +362,52 @@ async fn upload(req: &mut Request, res: &mut Response) {
)
.await;
tracing::debug!("upload(addfile): {:?}", addfile.unwrap());
tracing::info!("File uploaded to {}", dest);
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(&filename),
adminkey: String::from(&adminkey),
message1: dt.to_string(),
message2: String::from(""),
};
let rendered = tpls.get("link.html").unwrap().render(&template);
res.set_status_code(StatusCode::OK);
res.render(Text::Html(rendered));
let fileurl = format!(
"{}://{}/{}",
https,
String::from(headers[HOST].to_str().unwrap()),
filename
);
let adminurl = format!(
"{}://{}/{}",
https,
String::from(headers[HOST].to_str().unwrap()),
adminkey
);
if web {
let template_with_host =
"./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
fileurl: String::from(&fileurl),
adminurl: String::from(&adminurl),
message1: expiry_dt.to_string(),
message2: String::from(""),
};
let rendered = tpls.get("link.html").unwrap().render(&template);
res.set_status_code(StatusCode::OK);
res.render(Text::Html(rendered));
} else {
res.set_status_code(StatusCode::OK);
res.render(Text::Json(format!(
r#"{{"file": "{}", "url": "{}", "adminurl": "{}", "expiry": {}}}"#,
&filename, fileurl, adminurl, expiry,
)));
}
};
} else {
} else if web {
let template_with_host = "./templates/".to_owned() + headers[HOST].to_str().unwrap();
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from("400"),
message2: String::from(
"There was a problem with the file upload request. Please try again.",
@ -347,7 +416,9 @@ async fn upload(req: &mut Request, res: &mut Response) {
let rendered = tpls.get("error.html").unwrap().render(&template);
res.set_status_code(StatusCode::BAD_REQUEST);
res.render(Text::Html(rendered));
tracing::error!("Bad Request Received on upload");
} else {
res.set_status_code(StatusCode::BAD_REQUEST);
res.render(Text::Json(r#"{"error": "BadRequest"}"#));
};
}
@ -362,8 +433,8 @@ async fn serve_static(req: &mut Request, res: &mut Response) {
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from(""),
message2: String::from(""),
};
@ -377,8 +448,8 @@ async fn serve_static(req: &mut Request, res: &mut Response) {
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from(""),
message2: String::from(""),
};
@ -392,8 +463,8 @@ async fn serve_static(req: &mut Request, res: &mut Response) {
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
fileurl: String::from(""),
adminurl: String::from(""),
message1: String::from(""),
message2: String::from(""),
};
@ -407,8 +478,8 @@ async fn serve_static(req: &mut Request, res: &mut Response) {
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
fileurl: String::from(""),
adminurl: String::from(""),
message1: rand::thread_rng().gen_range(0..1).to_string(),
message2: String::from(""),
};
@ -422,8 +493,8 @@ async fn serve_static(req: &mut Request, res: &mut Response) {
let tpls: Ramhorns = Ramhorns::from_folder(template_with_host).unwrap();
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
filename: String::from(""),
adminkey: String::from(""),
fileurl: String::from(""),
adminurl: String::from(""),
message1: rand::thread_rng().gen_range(0..1).to_string(),
message2: String::from(""),
};
@ -654,6 +725,11 @@ async fn main() {
.delete(delete_file)
.get(delete_file),
)
.push(
Router::with_path("/edit/<adminkey>")
.delete(delete_file)
.get(delete_file),
)
// Serving uploaded files, or 404.
.push(Router::with_path("<file>").get(serve_file));

@ -11,9 +11,9 @@
Jobs Done! Here's your link:
</h1>
<p style="text-align: center;">
<input readonly class="fileLink" type="url" name="link" size="55" value="http://{{domain}}/{{filename}}" autofocus="autofocus" onfocus="this.select()" onmouseover='this.select()'>
<input readonly class="fileLink" type="url" name="link" size="55" value="{{fileurl}}" autofocus="autofocus" onfocus="this.select()" onmouseover='this.select()'>
<br><br>
<input readonly class="fileLink" type="url" name="deletionlink" size="55" value="http://{{domain}}/delete/{{adminkey}}" onmouseover='this.select()'>
<input readonly class="fileLink" type="url" name="deletionlink" size="55" value="{{adminurl}}" onmouseover='this.select()'>
</p>
<div style="text-align: center;"><img height="24" src="/static/trash.svg" alt="File Expiry (UTC)">{{message1}}</div>
</div></body></html>

@ -3,21 +3,15 @@
Services of {{domain}}
</h1>
<h2>
Git server
Example Service
</h2>
<p>
We have a gitea server running at <a href="https://git.example.com">git.example.com</a>.
</p>
<h2>
Email Services
</h2>
<p>
We have email running on example.com - if you'd like an account, please considering funding the server!
We have an example service running at: <a href="https://example.com">example.com!</a>.
</p>
<h2>
Status Page
</h2>
<p>
Check the uptime of our stuff <a href="https://status.example.com">here</a>
Check the uptime of our services <a href="https://status.example.com">here!</a>
</p>
</div></body></html>
Loading…
Cancel
Save