This commit is contained in:
MDMCK10 2024-03-29 21:41:01 +01:00
commit d0538907ab
15 changed files with 1864 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
files/
node_modules/
tmpfiles.db
tmpfiles.db-shm
tmpfiles.db-wal

262
index.js Normal file
View File

@ -0,0 +1,262 @@
import { rename, readFile, unlink, mkdir, mkdtemp, rmdir } from 'fs/promises';
import { createWriteStream } from 'fs';
import { tmpdir } from 'node:os';
import { promisify } from 'util';
import { pipeline } from 'stream';
import filenamify from "filenamify";
import Fastify from 'fastify'
import FastifyMultipartPlugin from '@fastify/multipart'
import FastifyStaticPlugin from '@fastify/static'
import Database from 'better-sqlite3';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import {fileTypeFromFile} from 'file-type';
import {filesize} from "filesize";
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime.js'
dayjs.extend(relativeTime, {
thresholds: [
{ l: 's', r: 1 },
{ l: 'm', r: 1 },
{ l: 'mm', r: 59, d: 'minute' },
{ l: 'h', r: 1 },
{ l: 'hh', r: 23, d: 'hour' },
{ l: 'd', r: 1 },
{ l: 'dd', r: 29, d: 'day' },
{ l: 'M', r: 1 },
{ l: 'MM', r: 11, d: 'month' },
{ l: 'y', r: 1 },
{ l: 'yy', d: 'year' }
]
});
const fastify = Fastify({logger: true, bodyLimit: 104857600, trustProxy: '127.0.0.1' });
const db = new Database('tmpfiles.db', { verbose: console.log });
db.pragma('journal_mode = WAL');
db.exec("CREATE TABLE IF NOT EXISTS tmpfiles (filename CHAR, size INT, filetype CHAR, visits INT, visitsMax INT, expiry TEXT)");
const __dirname = dirname(fileURLToPath(import.meta.url));
const pump = promisify(pipeline);
const notfound = await readFile(join(__dirname, "./templates/404.html"));
const statement = db.prepare(`INSERT INTO tmpfiles(filename, size, filetype, visits, visitsMax, expiry) VALUES(?, ?, ?, ?, ?, ?)`);
const download = await readFile(join(__dirname, "/templates/download.html"));
const api = await readFile(join(__dirname, "/templates/api.html"));
fastify.register(FastifyMultipartPlugin);
fastify.register(FastifyStaticPlugin, {
root: join(__dirname, 'public'),
wildcard: false
});
function makeDLTemplate(file, id, hostname) {
var response = download.toString()
.replace("__DLHOST__", hostname)
.replaceAll("__DLURL__", file.filename)
.replace("__DSIZE__", filesize(file.size))
.replaceAll("__DID__", id)
.replace("__DTIME__", dayjs.unix(file.expiry).fromNow(true));
if(file.filetype.startsWith("image/")) {
response = response.replace("__DPREV__", `<img id="img_preview" src="/dl/${id}/${file.filename}">`);
}else{
response = response.replace("__DPREV__", "");
}
return response;
}
fastify.get('/api.html', (request, reply) => {
return reply.status(200).type("text/html").send(api.toString().replaceAll("__APIHOST__", `${request.protocol}://${request.hostname}`))
})
fastify.get('/:id/:filename', (request, reply) => {
if(!/^\d+$/.test(request.params["id"])) {
reply.code(400);
return reply.send("Bad Request")
}
const id = request.params["id"];
const filename = filenamify(request.params["filename"]);
const file = db.prepare("SELECT * FROM tmpfiles WHERE rowid = ? AND filename = ?").get(id, filename);
if(typeof file === "undefined") {
return reply.status(404).type("text/html").send(notfound);
}
const expired = dayjs() > dayjs.unix(file.expiry) || (file.visits >= file.visitsMax && file.visitsMax);
if(expired) {
return reply.status(404).type("text/html").send(notfound);
}
return reply.status(200).type("text/html").send(makeDLTemplate(file, id, `${request.protocol}://${request.hostname}`));
});
fastify.get('/dl/:id/:filename', (request, reply) => {
if(!/^\d+$/.test(request.params["id"])) {
reply.code(400);
return reply.send("Bad Request")
}
const id = request.params["id"];
const filename = filenamify(request.params["filename"]);
const file = db.prepare("SELECT * FROM tmpfiles WHERE rowid = ? AND filename = ?").get(id, filename);
if(typeof file === "undefined") {
return reply.status(404).type("text/html").send(notfound);
}
const expired = dayjs() > dayjs.unix(file.expiry) || (file.visits >= file.visitsMax && file.visitsMax);
if(expired) {
return reply.status(404).type("text/html").send(notfound);
}
db.prepare("UPDATE tmpfiles SET visits = ? WHERE rowid = ? AND filename = ?").run(file.visits + 1, id, filename);
return reply.header("Content-Disposition", "attachment").sendFile(filename, join(__dirname, "files", id));
});
fastify.post('/api/v1/upload', async (req, reply) => {
if(!req.headers['content-length'] || !req.isMultipart) {
reply.code(400);
return reply.send({status: "error", message: "Bad Request, make sure your request matches the API documentation."});
}else if(Number(req.headers['content-length']) > req.routeOptions.bodyLimit) {
reply.code(413);
return reply.send({status: "error", message: "Request Entity Too Large"});
}
const data = await req.file();
if(data == null) {
reply.code(400);
return reply.send({status: "error", message: "Bad Request, the file field is required."});
}
const filename = filenamify(data.filename);
const tempdir = await mkdtemp(join(tmpdir(), 'tmpfiles-'));
const pending = join(tempdir, filename);
await pump(data.file, createWriteStream(pending));
if (data.file.truncated) {
// This shouldn't happen given the check above
await unlink(pending);
reply.code(413);
return reply.send({status: "error", message: "Request Entity Too Large"});
}else{
var exp;
try {
exp = Number(data.fields["max_time"].value);
}catch{
exp = 60;
};
if(Number.isNaN(exp) || exp < 1 || exp > 120) {
await unlink(pending);
reply.code(400);
return reply.send({status: "error", message: "Bad Request, max_views must be between 1 and 120."});
}
var max;
try {
max = Number(data.fields["max_views"].value);
}catch{
max = 60;
};
if(Number.isNaN(max) || max < 0 || max > 1000) {
await unlink(pending);
reply.code(400);
return reply.send({status: "error", message: "Bad Request, max_views must be between 0 and 1000."});
}
var mime;
const filetype = await fileTypeFromFile(pending);
if(typeof filetype === "undefined") {
mime = "application/octet-stream";
}else{
mime = filetype.mime;
}
const expiry = dayjs().add(exp, "minutes").unix();
const record = statement.run(filename, Number(req.headers['content-length']), mime, 0, max, Number(expiry));
if(record.changes) {
await mkdir(join(__dirname, "files", record.lastInsertRowid.toString()));
await rename(pending, join(__dirname, "files", record.lastInsertRowid.toString(), filename));
await rmdir(dirname(pending));
return reply.send({status: "success", data: { url: `${req.protocol}://${req.hostname}/${record.lastInsertRowid}/${filename}`}});
}else{
return reply.status(500).type("text/plain").send({status: "error", message: "Internal Server Error"});
}
}
});
fastify.post('/', async (req, reply) => {
if(!req.headers['content-length'] || !req.isMultipart) {
reply.code(400);
return reply.send("Bad Request")
}else if(Number(req.headers['content-length']) > req.routeOptions.bodyLimit) {
reply.code(413);
return reply.send("Request Entity Too Large");
}
const data = await req.file();
if(data == null) {
reply.code(400);
return reply.send("Bad Request");
}
const filename = filenamify(data.filename);
const tempdir = await mkdtemp(join(tmpdir(), 'tmpfiles-'));
const pending = join(tempdir, filename);
await pump(data.file, createWriteStream(pending));
if (data.file.truncated) {
// This shouldn't happen given the check above
await unlink(pending);
reply.code(413);
return reply.send("Request Entity Too Large");
}else{
if(!data.fields["max_time"] || !data.fields["max_views"]) {
await unlink(pending);
reply.code(400);
return reply.send("Bad Request")
}
var exp = Number(data.fields["max_time"].value);
if(Number.isNaN(exp) || exp < 1 || exp > 120) {
await unlink(pending);
reply.code(400);
return reply.send("Bad Request");
}
var max = Number(data.fields["max_views"].value);
if(Number.isNaN(max) || max < 0 || max > 1000) {
await unlink(pending);
reply.code(400);
return reply.send("Bad Request");
}
var mime;
const filetype = await fileTypeFromFile(pending);
if(typeof filetype === "undefined") {
mime = "application/octet-stream";
}else{
mime = filetype.mime;
}
const expiry = dayjs().add(exp, "minutes").unix();
const record = statement.run(filename, Number(req.headers['content-length']), mime, 0, max, Number(expiry));
if(record.changes) {
await mkdir(join(__dirname, "files", record.lastInsertRowid.toString()));
await rename(pending, join(__dirname, "files", record.lastInsertRowid.toString(), filename));
await rmdir(dirname(pending));
return reply.redirect(302, `${req.protocol}://${req.hostname}/${record.lastInsertRowid}/${filename}`);
}else{
return reply.status(500).type("text/plain").send("Internal Server Error");
}
}
});
fastify.setNotFoundHandler((_, res) => {
res.status(404).type("text/html").send(notfound);
});
fastify.setErrorHandler((_, __, res) => {
res.status(500).type("text/plain").send("Internal Server Error");
});
fastify.listen({ port: 8080 }, (err, address) => {
if (err) throw err;
console.log(`Server is now listening on ${address}`);
})

1375
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "tmpfilesclone",
"version": "1.0.0",
"description": "",
"main": "index.js",
"keywords": [],
"author": "MDMCK10 (backend), ecovector3 (frontend)",
"type": "module",
"license": "ISC",
"dependencies": {
"@fastify/multipart": "^7.7.3",
"@fastify/static": "^6.10.2",
"better-sqlite3": "^8.5.0",
"dayjs": "^1.11.9",
"fastify": "^4.21.0",
"file-type": "^18.5.0",
"filenamify": "^6.0.0",
"filesize": "^10.0.12"
}
}

31
public/about.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>/tmp/files 2.0 - About</title>
<meta http-equiv="content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />
<link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700' rel='stylesheet' type='text/css' />
</head>
<body>
<div id="container">
<header>
<h1><a href="/">/tmp/files</a></h1>
<h2>Because Sometimes, Older Is Better</h2>
</header>
<section>
<p>Suggestions/Questions/Abuse: <a href="mailto:abuse@illegalcybercri.me">abuse@illegalcybercri.me</a></p><br>
<p>Are there any restrictions on what files can be uploaded? Yes! The following are NOT allowed: Malware, CSAM, Copyrighted content, spam, etc...</p><br>
</section>
<footer>
<ul>
<li><a href="/">Upload</a></li>
<li><a href="/api.html">API</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="https://computernewb.com/collab-vm/">CollabVM</a></li>
<li><a href="https://up.kevinthe.horse/">KevinUpload</a></li>
</ul>
</footer>
</div>
</body>
</html>

1
public/css/style.css Normal file
View File

@ -0,0 +1 @@
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}*{margin:0 auto;padding:0;font-family:'firasans-regular', sans-serif;-webkit-font-smoothing:antialiased}@font-face{font-family:'firasans-light';src:url("/font/FiraSans-Light.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:'firasans-regular';src:url("/font/FiraSans-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:'firasans-medium';src:url("/font/FiraSans-Medium.ttf") format("truetype");font-weight:400;font-style:normal}body{background:#f08080}a{text-decoration:none;color:#fff}a.download{background:#fff;color:#f08080;padding:8px 14px 6px 14px;display:inline-block;margin-top:6px;transition:box-shadow .3s ease-in}a.download:hover{box-shadow:0 0 12px #ff0000}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#fff}::-webkit-scrollbar-thumb{background:#ff0000}#container{width:360px;margin:62px;padding:24px;text-align:left}#container header h1,#container header h2,#container header h3{line-height:1.3;text-transform:uppercase;color:#fff}#container header h1{font-size:26px;font-family:'firasans-light', sans-serif}#container header h2{font-size:16px;font-family:'firasans-regular', sans-serif}#container section{margin-top:32px}#container section img{border:2px solid #fff;max-width:360px}#container section p{font-family:'firasans-regular', sans-serif;font-size:12px;color:#fff}#container section p.error{color:#ffde42;font-family:'firasans-medium', sans-serif}#container section p.success{color:#3fb663;font-family:'firasans-medium', sans-serif}#container section form div{display:block}form{margin:0}label{display:block;font-size:12px;color:#fff;padding:0;margin:0;margin-top:24px;width:auto;text-align:left;font-family:'firasans-regular', sans-serif}input{width:auto;max-width:220px;padding:4px;padding-left:0;margin:0;outline:none;font-size:14px;border:none;border-bottom:1px solid #fad2d2;border-right:none;border-radius:0;color:#fff;font-weight:300;-webkit-font-smoothing:antialiased;display:block;background:#f08080}input[type="text"]{max-width:320px}input[type="number"]{max-width:100px;width:auto}input[type="submit"]{background:#fff;color:#f08080;padding:8px 14px 6px 14px;border:none;margin:0 auto;margin-top:24px;width:auto;max-width:450px;text-align:center;cursor:pointer;display:inline-block;transition:box-shadow .3s ease-in}input[type="submit"]:hover{box-shadow:0 0 12px #ff0000}footer{margin-top:48px;margin-bottom:24px}footer ul li{display:inline-block}footer ul li a{color:#fff;display:block;padding:6px 12px;font-family:'firasans-regular', sans-serif;font-size:12px}footer ul li a:hover{text-decoration:underline}footer ul li:first-child a{padding-left:0 !important}@media (max-width: 800px){#container{width:auto;margin:0;padding:24px;text-align:left}#container section img{max-width:100%}}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

46
public/index.html Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>/tmp/files 2.0 - Because Sometimes, Older Is Better</title>
<meta http-equiv="content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />
<link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700' rel='stylesheet' type='text/css' />
</head>
<body>
<div id="container">
<header>
<h1><a href="/">/tmp/files 2.0</a></h1>
<h2>Because Sometimes, Older Is Better</h2>
</header>
<section>
<form action="/" method="post" enctype="multipart/form-data">
<div>
<label>Select a file (max 100 MB)</label>
<input type="file" name="file" required>
</div>
<div>
<label>Download limit (0 = unlimited)</label>
<input type="number" name="max_views" value="0" min="0" max="1000" required>
</div>
<div>
<label>Time limit (in minutes, max 120)</label>
<input type="number" name="max_time" value="60" min="1" max="120" required>
</div>
<div>
<input type="submit" value="Upload" name="upload">
</div>
</form>
</section>
<footer>
<ul>
<li><a href="/">Upload</a></li>
<li><a href="/api.html">API</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="https://computernewb.com/collab-vm/">CollabVM</a></li>
<li><a href="https://up.kevinthe.horse/">KevinUpload</a></li>
</ul>
</footer>
</div>
</body>
</html>

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: archive.org_bot
Disallow: /

33
templates/404.html Normal file

File diff suppressed because one or more lines are too long

40
templates/api.html Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>/tmp/files 2.0 - API</title>
<meta http-equiv="content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />
<link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700' rel='stylesheet' type='text/css' />
</head>
<body>
<div id="container">
<header>
<h1><a href="/">/tmp/files</a></h1>
<h2>Because Sometimes, Older Is Better</h2>
</header>
<section>
<p>You can use our API to automate file uploads.</p>
<br>
<p>Method: POST</p>
<p>Params: file=/path/to/test.jpg; max_views=[1 to 1000]; max_time=[1 to 120];</p>
<p>Only the file param is required, the rest, if not specified, will be set to the following default values:</p>
<p>max_views=0; max_time=60</p>
<p>URL: __APIHOST__/api/v1/upload</p>
<br>
<p>Example with CURL:</p>
<p>curl -F "file=@/Users/myuser/test.jpg" -F "max_views=0" -F "max_time=60" __APIHOST__/api/v1/upload</p>
<p>This file has no download limit, and will expire in 1 hour.</p>
</section>
<footer>
<ul>
<li><a href="/">Upload</a></li>
<li><a href="/api.html">API</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="https://computernewb.com/collab-vm/">CollabVM</a></li>
<li><a href="https://up.kevinthe.horse/">KevinUpload</a></li>
</ul>
</footer>
</div>
</body>
</html>

49
templates/download.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<title>/tmp/files 2.0 - __DLURL__</title>
<meta http-equiv="content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />
<link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700' rel='stylesheet' type='text/css' />
</head>
<body>
<div id="container">
<header>
<h1><a href="/">/tmp/files 2.0</a></h1>
<h2>Because Sometimes, Older Is Better</h2>
</header>
<section>
<table style="color: #ffffff; font-size: 12px;">
<tr>
<th style="width: 80px;">Filename</th>
<td>__DLURL__</td>
</tr>
<tr>
<th>Size</th>
<td>__DSIZE__</td>
</tr>
<tr>
<th>URL</th>
<td><a target="_blank" href="/dl/__DID__/__DLURL__">__DLHOST__/dl/__DID__/__DLURL__</a></td>
</tr>
<tr>
<th>Deleted in</th>
<td>__DTIME__</td>
</tr>
</table>
<br>__DPREV__
<p><a class="download" href="/dl/__DID__/__DLURL__">Download</a></p>
</section>
<footer>
<ul>
<li><a href="/">Upload</a></li>
<li><a href="/api.html">API</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="https://computernewb.com/collab-vm/">CollabVM</a></li>
<li><a href="https://up.kevinthe.horse/">KevinUpload</a></li>
</ul>
</footer>
</div>
</body>
</html>