add /my_files endpoint
Security audit / audit (push) Failing after 47s Details
format, check and test / cargo fmt (push) Successful in 3m38s Details
format, check and test / cargo test (push) Successful in 4m38s Details
Security audit / audit (pull_request) Failing after 47s Details
format, check and test / cargo fmt (pull_request) Successful in 2m35s Details
format, check and test / cargo test (pull_request) Successful in 1m47s Details

This shows all files uploaded by the IP accessing.
review
Volkor 2023-05-30 01:19:50 +10:00
parent 823447324b
commit 89af160a12
Signed by: Volkor
SSH Key Fingerprint: SHA256:taX3XcC6grYv7+eTzBsIUNCVFgMzh7gkVgxliSh69ek
13 changed files with 226 additions and 748 deletions

714
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,5 @@ chrono = "0.4.23"
rand = "0.8.5"
config = "0.13.3"
lazy_static = "1.4.0"
cargo-audit = "0.17.4"
no-panic = "0.1.22"
tree_magic_mini = "3.0.3"
chrono-humanize = "0.2.2"

View File

@ -22,9 +22,9 @@ url = "sqlite://ephemeral-testing.db"
# Controls the logging level of the ephemeral program, and the crates it uses.
# This is production clean with info from ephemeral and errors from the rest.
level = "ephemeral=info,sqlx=error,hyper=error,salvo_extra=error,salvo=error,mio=error"
#level = "ephemeral=info,sqlx=error,hyper=error,salvo_extra=error,salvo=error,mio=error"
# Development & Testing logging, where it shows as much logs as possible.
#level = "ephemeral=trace,sqlx=trace,hyper=debug,salvo_extra=trace,salvo=trace,mio=trace"
level = "ephemeral=trace,sqlx=trace,hyper=debug,salvo_extra=trace,salvo=trace,mio=trace"
# Enables/Disables metrics generation
metrics = true

View File

@ -1,10 +1,8 @@
# Database documentation
The database at the moment is rusqlite, but I'd like to add at least postgres support later on, since I have that running on my server for other things.
The database connection is saved in a pool, and the pool serves out a connection to any object that needs them.
r2d2 opens a connection pool to the DB, allowing multiple threads to use the same connection.
Database should store everything in 1 db, multiple 2 or 3, because easy.
At the moment, we use SQLX, with SQLite, however I'd really like to support Postgres and the rest soon, SQLite is slow, buggy and annoying.
## files.db
@ -34,28 +32,13 @@ Database should store everything in 1 db, multiple 2 or 3, because easy.
Hopefully we can move away from having a separate stats table, and use metrics generated from the files table instead.
(or maybe not for performance - might be cheaper to keep the stats table and update it every minute or so.)
## SQL Commands
### API Keys Table
-- Files are stored here!
CREATE TABLE IF NOT EXISTS "files" (
file TEXT PRIMARY KEY,
filetype TEXT,
expiry INTEGER NOT NULL,
expiry_override INTEGER NOT NULL,
views INTEGER DEFAULT '0',
isDeleted INTEGER DEFAULT '0',
adminkey TEXT NOT NULL,
accessed INTEGER NOT NULL,
filesize INTEGER NOT NULL,
IP TEXT NOT NULL,
domain TEXT NOT NULL
);
This is used for premium users.
API Keys have a few permission flags, they can be `true` or `false`.
-- QR Code Scan stats
CREATE TABLE "qrscan" (
scanid INTEGER PRIMARY KEY,
time INTEGER NOT NULL,
IP TEXT NOT NULL,
useragent TEXT NOT NULL,
version INTEGER NOT NULL
);
1. server_admin: Allows the user to generate new API Keys
2. expiry_override: Allows the user to bypass the default expiry of a file. (but not past the file_expiry_max)
3. upload_permanent: Allows the user to upload a file that's stored permanently.
4. default_name: Allows the user to upload a file, keeping the filename.
5. bypass_mimetypes: Allows the user to upload a file that normally would be banned.

View File

@ -28,6 +28,17 @@ CREATE TABLE 'qrscan' (
version INTEGER NOT NULL
);
-- Admin API Key Table
CREATE TABLE IF NOT EXISTS 'apikeys' (
key TEXT NOT NULL,
server_admin INTEGER NOT NULL,
expiry_override INTEGER NOT NULL,
upload_permanent INTEGER NOT NULL,
default_name INTEGER NOT NULL,
bypass_mimetypes INTEGER NOT NULL,
comment TEXT,
);
-- Dummy file to stop compile error, temporary fix --
-- This is some seriously wacked up formatting.
INSERT INTO files (

View File

@ -8,6 +8,7 @@ pub struct FileMetric {
pub mimetype: String,
pub views: i64,
pub expiry: i64,
pub is_deleted: bool,
}
// This struct represents a single mimetype, with a few statistics about said mimetype instance
@ -276,11 +277,12 @@ fn update_expiry_override() {
/// This function gets a array of all the files uploaded by a specific API Key.
/// This allows for the select few who hold an API key to view the information about the files they upload.
pub async fn get_my_files(sqlconn: &Pool<Sqlite>) -> Result<Vec<FileMetric>, sqlx::Error> {
// TODO: PLEASE OH PLEASE DO NOT USE THIS FUNCTION YET, this returns ALL files uploaded.
pub async fn get_my_files(sqlconn: &Pool<Sqlite>, ip: &str) -> Result<Vec<FileMetric>, sqlx::Error> {
let result = sqlx::query!(
"SELECT file, filesize, mimetype, views, expiry, expiry_override
FROM files",
"SELECT file, filesize, mimetype, views, expiry, expiry_override, ip, isDeleted
FROM files
WHERE ip = ?",
ip
)
.fetch_all(sqlconn)
.await?;
@ -288,6 +290,9 @@ pub async fn get_my_files(sqlconn: &Pool<Sqlite>) -> Result<Vec<FileMetric>, sql
let mut files: Vec<FileMetric> = Vec::new();
for row in result {
// Convert the isDeleted from a int to a bool
let is_deleted = row.isDeleted.map(|v| v != 0).unwrap_or(false);
// For each file in the result, make a new FileMetric object and add it to the list
let file = FileMetric {
filename: row.file,
@ -295,6 +300,7 @@ pub async fn get_my_files(sqlconn: &Pool<Sqlite>) -> Result<Vec<FileMetric>, sql
filesize: row.filesize,
views: row.views.unwrap(),
expiry: row.expiry,
is_deleted
};
files.push(file);
}
@ -411,10 +417,11 @@ pub async fn get_file_metrics(sqlconn: &Pool<Sqlite>) -> Option<Vec<FileMetric>>
// For each row (file), add it to the struct
let file = FileMetric {
filename: row.file,
mimetype: row.mimetype,
mimetype: row.mimetype.clone(),
filesize: row.filesize,
views: row.views.unwrap(),
expiry: row.expiry,
is_deleted: false
};
// Then add the struct to the Vec
filevec.push(file);

View File

@ -1,6 +1,8 @@
use salvo::{handler, writer::Text, Request, Response};
use salvo::{handler, Request, Response, hyper::header::HOST, prelude::StatusCode};
use crate::{db, SQLITE};
use crate::{db, SQLITE, handlers::{TemplateStruct, render_template, convert_file_size, convert_unix_timestamp}};
use super::guess_ip;
/// This file contains the handler that serves the users stats back to them.
/// We expect a API Key to be provided in the cookie or header here.
@ -10,32 +12,60 @@ pub async fn list_files(req: &mut Request, res: &mut Response) {
// Setup db pool
let sqlconn = SQLITE.get().unwrap();
// Get the headers (for Host header stuff thats needed later)
// let headers = req.headers();
// let remote_addr = &req.remote_addr().unwrap().clone();
// let ip = guess_ip(headers, remote_addr);
// Get the IP of the person viewing, so we can show their files
let headers = req.headers();
let remote_addr = &req.remote_addr().unwrap().clone();
let ip = guess_ip(headers, remote_addr);
// This returns a Vec of all the files uploaded by a specific API Key
let files = db::get_my_files(sqlconn).await;
tracing::debug!("list_files(remote_addr, ip): {:?}, {:?}", remote_addr, ip);
let mut rendered = "filename, filesize, mimetype, views, expiry\n".to_string();
// For each line in the Vec.
// This returns a Vec of all the files uploaded by a specific IP Address
let files = db::get_my_files(sqlconn, &ip).await;
let mut html = String::new();
// For each file in the list, add the html to the rendered string
for f in files.unwrap() {
// View count per file
rendered = format!(
"{}{}, {}, {}. {}, {}\n",
// REVIEW: Custom datatype + impl display.
// If you query with query_as!() you can let it do a lot of work for you
// let files: Vec<MyFile> = query_as!(MyFile, ...);
rendered,
f.filename,
f.filesize,
f.mimetype,
f.views,
f.expiry
);
}
// Change the filename to a file url
let fileurl = format!("/{}", &f.filename);
// Actually render the final metrics page
res.render(Text::Plain(rendered));
// Change the filesize to a human readable value
let human_size = convert_file_size(f.filesize);
// Change the expiry to a human readable value
let human_time = convert_unix_timestamp(f.expiry);
// If isDeleted, change colour to red, else green
let mut colour: String = String::new();
if f.is_deleted {
colour = "background-color: #ff00000a;".to_string()
} else {
colour = "background-color: #00ff100a;".to_string()
}
// Add a new file to the rendered string
html.push_str(&format!("<tr style='{}'><td><a href={}>{}</a></td><td data-sort='{}'>{}</td><td>{}</td><td>{}</td><td data-sort='{}'>{}</td></tr>",
colour,
fileurl.as_str(),
f.filename.as_str(),
f.filesize,
human_size.as_str(),
f.mimetype.as_str(),
f.views.to_owned(),
f.expiry.to_owned(),
human_time.to_owned(),
));
}
// Close the table when all files are 'rendered'
html.push_str("</table>");
let template_filename = "my_files.html";
let template = TemplateStruct {
domain: String::from(headers[HOST].to_str().unwrap()),
message1: html,
..Default::default()
};
let status_code = StatusCode::OK;
render_template(res, headers, template_filename, template, status_code).await
}

View File

@ -8,6 +8,8 @@ use salvo::{
writer::Text,
Response,
};
use chrono::{DateTime, Utc};
use chrono_humanize::Humanize;
use crate::CONFIG;
@ -168,3 +170,31 @@ where
lowercase_banned.contains(&lowercase_mimetype)
}
/// Convert the filesize to a human readable number
fn convert_file_size(file_size: i64) -> String {
let units = ["B", "KB", "MB", "GB", "TB"];
let base = 1024; // Base for conversion (1024 bytes = 1 kilobyte)
if file_size < base {
return format!("{} {}", file_size, units[0]);
}
let exponent = (file_size as f64).log(base as f64).floor() as u32;
let converted_size = (file_size as f64 / base.pow(exponent) as f64).round();
format!("{:.2} {}", converted_size, units[exponent as usize])
}
/// Convert unix timestamp to a human friendly timestamp
fn convert_unix_timestamp(unix_timestamp: i64) -> String {
let datetime: DateTime<Utc> = DateTime::from_utc(
chrono::NaiveDateTime::from_timestamp_opt(unix_timestamp, 0).unwrap(),
Utc,
);
let relative_date = datetime.humanize();
relative_date.to_string()
}

View File

@ -139,7 +139,7 @@ async fn main() {
.get(handlers::delete_file::delete_file),
)
// List of files the requester IP has uploaded.
// .push(Router::with_path("/my_files").get(handlers::list_files::list_files))
.push(Router::with_path("/my_files").get(handlers::list_files::list_files))
// Serving uploaded files, or 404.
.push(Router::with_path("<file>").get(handlers::serve_file::serve_file));
@ -155,6 +155,10 @@ async fn main() {
Server::new(TcpListener::bind(&server_url))
.serve(router)
.await;
// Close SQLite before closing
tracing::info!("Attempting to safely shut down Ephemeral.");
SQLITE.get().expect("Problem while safely closing the database :(").close().await;
}
// This spawns a tokio task to run a interval timer forever.

View File

@ -24,3 +24,8 @@ a:hover{text-decoration:underline;}
ul{list-style:none;padding-left:15px;}
ul li:before{content:"- ";}
.dmca{width:100%;height:auto;padding:10px;}
tr:hover {background-color: #33BB3329;}
table {border-collapse: separate;border-spacing: 0 10px;margin-top: -10px;}
td {border: solid 1px #000;border-style: solid none;padding: 10px;background-color: #ffffff0a;}
td:first-child {border-left-style: solid;border-top-left-radius: 10px;border-bottom-left-radius: 10px;}
td:last-child {border-right-style: solid;border-bottom-right-radius: 10px;border-top-right-radius: 10px;}

44
static/sort.css Normal file
View File

@ -0,0 +1,44 @@
.sortable th {
cursor: pointer;
}
.sortable th.no-sort {
pointer-events: none;
}
.sortable th::after, .sortable th::before {
transition: color 0.1s ease-in-out;
font-size: 1.2em;
color: transparent;
}
.sortable th::after {
margin-left: 3px;
content: "▸";
}
.sortable th:hover::after {
color: inherit;
}
.sortable th.dir-d::after {
color: inherit;
content: "▾";
}
.sortable th.dir-u::after {
color: inherit;
content: "▴";
}
.sortable th.indicator-left::after {
content: "";
}
.sortable th.indicator-left::before {
margin-right: 3px;
content: "▸";
}
.sortable th.indicator-left:hover::before {
color: inherit;
}
.sortable th.indicator-left.dir-d::before {
color: inherit;
content: "▾";
}
.sortable th.indicator-left.dir-u::before {
color: inherit;
content: "▴";
}

4
static/sort.js Normal file
View File

@ -0,0 +1,4 @@
// https://github.com/tofsjonas/sortable
document.addEventListener("click",function(c){try{function f(a,b){return a.nodeName===b?a:f(a.parentNode,b)}var x=c.shiftKey||c.altKey,d=f(c.target,"TH"),m=f(d,"TR"),g=f(m,"TABLE");function n(a,b){a.classList.remove("dir-d");a.classList.remove("dir-u");b&&a.classList.add(b)}function p(a){return x&&a.dataset.sortAlt||a.dataset.sort||a.textContent}if(g.classList.contains("sortable")){var q,e=m.cells,r=parseInt(d.dataset.sortTbr);for(c=0;c<e.length;c++)e[c]===d?q=parseInt(d.dataset.sortCol)||c:n(e[c],
"");e="dir-d";if(d.classList.contains("dir-d")||g.classList.contains("asc")&&!d.classList.contains("dir-u"))e="dir-u";n(d,e);var t="dir-u"===e,v=function(a,b,h){var u=p((t?a:b).cells[h]);a=p((t?b:a).cells[h]);b=parseFloat(u)-parseFloat(a);return isNaN(b)?u.localeCompare(a):b};for(c=0;c<g.tBodies.length;c++){var k=g.tBodies[c],w=[].slice.call(k.rows,0);w.sort(function(a,b){var h=v(a,b,q);return 0!==h||isNaN(r)?h:v(a,b,r)});var l=k.cloneNode();l.append.apply(l,w);g.replaceChild(l,k)}}}catch(f){}});

View File

@ -0,0 +1,17 @@
{{>regular.html}}
<h1>
Your Uploaded Files
</h1>
<p>
These are the files you have uploaded from this IP Address.<br>
Green = File is alive and active<br>
Red = File has been deleted
</p>
<link href="/static/sort.css" rel="stylesheet" />
<script src="/static/sort.js"></script>
<table class="sortable">
<thead><tr><th>File</th><th>Filesize</th><th>Mimetype</th><th>Views</th><th>Expiry</th></tr></thead>
<tbody>{{&message1}}</tbody>
</table>
</div></body></html>