WIP: Rewrite Start
This commit is contained in:
parent
c89d3db1ed
commit
17b48c92a4
18 changed files with 2407 additions and 683 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1 @@
|
||||||
testing/
|
node_modules/
|
|
@ -1,76 +0,0 @@
|
||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
|
||||||
contributors and maintainers pledge to making participation in our project and
|
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
|
||||||
level of experience, education, socio-economic status, nationality, personal
|
|
||||||
appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
|
||||||
include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic
|
|
||||||
address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
|
||||||
when an individual is representing the project or its community. Examples of
|
|
||||||
representing a project or community include using an official project e-mail
|
|
||||||
address, posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event. Representation of a project may be
|
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported by contacting the project team at eusprojects@mail.com. All
|
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
|
||||||
members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see
|
|
||||||
https://www.contributor-covenant.org/faq
|
|
550
EUS.js
550
EUS.js
|
@ -1,550 +0,0 @@
|
||||||
const config = require("../config/config.json"), crypto = require("crypto"), emoji = require("../misc/emoji_list.json"), fs = require("fs");
|
|
||||||
|
|
||||||
// Defines the function of this module
|
|
||||||
const MODULE_FUNCTION = "handle_requests",
|
|
||||||
|
|
||||||
// Base path for module folder creation and navigation
|
|
||||||
BASE_PATH = "/EUS",
|
|
||||||
API_CACHE_LIFESPAN = 3600000;
|
|
||||||
|
|
||||||
let node_modules = {};
|
|
||||||
|
|
||||||
let eusConfig = {},
|
|
||||||
useUploadKey = true,
|
|
||||||
cacheJSON = "",
|
|
||||||
timeSinceLastCache = Date.now();
|
|
||||||
|
|
||||||
class Database {
|
|
||||||
constructor(databaseAddress, databasePort = 3306, databaseUsername, databasePassword, databaseName) {
|
|
||||||
this.connectionPool = node_modules["mysql2"].createPool({
|
|
||||||
connectionLimit: 128,
|
|
||||||
host: databaseAddress,
|
|
||||||
port: databasePort,
|
|
||||||
user: databaseUsername,
|
|
||||||
password: databasePassword,
|
|
||||||
database: databaseName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dataReceived(resolveCallback, data, limited = false) {
|
|
||||||
if (limited) resolveCallback(data[0]);
|
|
||||||
else resolveCallback(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
query(query = "", data) {
|
|
||||||
const limited = query.includes("LIMIT 1");
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.connectionPool.getConnection((err, connection) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
try { connection.release();}
|
|
||||||
catch (e) {
|
|
||||||
console.error("Failed to release mysql connection", err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use old query
|
|
||||||
if (data == null) {
|
|
||||||
connection.query(query, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
connection.release();
|
|
||||||
} else {
|
|
||||||
this.dataReceived(resolve, data, limited);
|
|
||||||
connection.release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Use new prepared statements w/ placeholders
|
|
||||||
else {
|
|
||||||
connection.execute(query, data, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
connection.release();
|
|
||||||
} else {
|
|
||||||
this.dataReceived(resolve, data, limited);
|
|
||||||
connection.release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let dbConnection;
|
|
||||||
function init() {
|
|
||||||
// Require node modules
|
|
||||||
node_modules["dyetty"] = require("dyetty");
|
|
||||||
node_modules["busboy"] = require("connect-busboy");
|
|
||||||
node_modules["randomstring"] = require("randomstring");
|
|
||||||
node_modules["streamMeter"] = require("stream-meter");
|
|
||||||
node_modules["mysql2"] = require("mysql2");
|
|
||||||
|
|
||||||
// Only ran on startup so using sync functions is fine
|
|
||||||
|
|
||||||
// Makes the folder for files of the module
|
|
||||||
if (!fs.existsSync(__dirname + BASE_PATH)) {
|
|
||||||
fs.mkdirSync(__dirname + BASE_PATH);
|
|
||||||
console.log(`[EUS] Made EUS module folder`);
|
|
||||||
}
|
|
||||||
// Makes the folder for frontend files
|
|
||||||
if (!fs.existsSync(__dirname + BASE_PATH + "/files")) {
|
|
||||||
fs.mkdirSync(__dirname + BASE_PATH + "/files");
|
|
||||||
console.log(`[EUS] Made EUS web files folder`);
|
|
||||||
}
|
|
||||||
// Makes the folder for images
|
|
||||||
if (!fs.existsSync(__dirname + BASE_PATH + "/i")) {
|
|
||||||
fs.mkdirSync(__dirname + BASE_PATH + "/i");
|
|
||||||
console.log(`[EUS] Made EUS images folder`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync("/tmp/EUS_UPLOADS")) {
|
|
||||||
fs.mkdirSync("/tmp/EUS_UPLOADS");
|
|
||||||
console.log("[EUS] Made EUS temp upload folder");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Makes the config file
|
|
||||||
if (!fs.existsSync(__dirname + BASE_PATH + "/config.json")) {
|
|
||||||
// Config doesn't exist, make it.
|
|
||||||
fs.writeFileSync(`${__dirname}${BASE_PATH}/config.json`, '{\n\t"baseURL":"http://example.com/",\n\t"acceptedTypes": [\n\t\t".png",\n\t\t".jpg",\n\t\t".jpeg",\n\t\t".gif"\n\t],\n\t"uploadKey": "",\n\t"database": {\n\t\t"databaseAddress": "127.0.0.1",\n\t\t"databasePort": 3306,\n\t\t"databaseUsername": "root",\n\t\t"databasePassword": "password",\n\t\t"databaseName": "EUS"\n\t}\n}');
|
|
||||||
console.log("[EUS] Made EUS config File!");
|
|
||||||
console.log("[EUS] Please edit the EUS Config file before restarting.");
|
|
||||||
// Config has been made, close framework.
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
eusConfig = require(`${__dirname}${BASE_PATH}/config.json`);
|
|
||||||
if (validateConfig(eusConfig)) console.log("[EUS] EUS config passed all checks");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!eusConfig["noUpload"]) {
|
|
||||||
dbConnection = new Database(eusConfig["database"]["databaseAddress"], eusConfig["database"]["databasePort"], eusConfig["database"]["databaseUsername"], eusConfig["database"]["databasePassword"], eusConfig["database"]["databaseName"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[EUS] Finished loading.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache for the file count and space usage, this takes a while to do so it's best to cache the result
|
|
||||||
let cacheIsReady = false;
|
|
||||||
function cacheFilesAndSpace() {
|
|
||||||
timeSinceLastCache = Date.now();
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
const startCacheTime = Date.now();
|
|
||||||
cacheIsReady = false;
|
|
||||||
let cachedFilesAndSpace = {
|
|
||||||
fileCounts: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const dbData = await dbConnection.query(`SELECT imageType, COUNT(imageType) AS "count" FROM images GROUP BY imageType`);
|
|
||||||
let totalFiles = 0;
|
|
||||||
dbData.forEach(fileType => {
|
|
||||||
cachedFilesAndSpace["fileCounts"][fileType.imageType] = fileType.count;
|
|
||||||
totalFiles += fileType.count;
|
|
||||||
});
|
|
||||||
cachedFilesAndSpace["filesTotal"] = totalFiles;
|
|
||||||
|
|
||||||
cachedFilesAndSpace["size"] = {};
|
|
||||||
|
|
||||||
const dbSize = (await dbConnection.query(`SELECT SUM(fileSize) FROM images LIMIT 1`))["SUM(fileSize)"];
|
|
||||||
const totalSizeBytes = dbSize == null ? 0 : dbSize;
|
|
||||||
const mbSize = totalSizeBytes / 1024 / 1024;
|
|
||||||
cachedFilesAndSpace["size"]["mb"] = parseFloat(mbSize.toFixed(4));
|
|
||||||
cachedFilesAndSpace["size"]["gb"] = parseFloat((mbSize / 1024).toFixed(4));
|
|
||||||
cachedFilesAndSpace["size"]["string"] = await spaceToLowest(totalSizeBytes, true);
|
|
||||||
|
|
||||||
resolve(cachedFilesAndSpace);
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.folder, `Stats api cache took ${Date.now() - startCacheTime}ms`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateConfig(json) {
|
|
||||||
let performShutdownAfterValidation = false;
|
|
||||||
// URL Tests
|
|
||||||
if (!json["noUpload"]) {
|
|
||||||
if (json["baseURL"] == null) {
|
|
||||||
console.error("EUS baseURL property does not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
} else {
|
|
||||||
if (json["baseURL"] == "") console.warn("EUS baseURL property is blank");
|
|
||||||
const bURL = `${json["baseURL"]}`.split("");
|
|
||||||
if (bURL.length > 1) {
|
|
||||||
if (bURL[bURL.length-1] != "/") console.warn("EUS baseURL property doesn't have a / at the end, this can lead to unpredictable results!");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (json["baseURL"] != "http://" || json["baseURL"] != "https://") console.warn("EUS baseURL property is possibly invalid!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// acceptedTypes checks
|
|
||||||
if (!json["noUpload"]) {
|
|
||||||
if (json["acceptedTypes"] == null) {
|
|
||||||
console.error("EUS acceptedTypes list does not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
} else {
|
|
||||||
if (json["acceptedTypes"].length < 1) console.warn("EUS acceptedTypes array has no extentions in it, users will not be able to upload images!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// uploadKey checks
|
|
||||||
if (!json["noUpload"]) {
|
|
||||||
if (json["uploadKey"] == null) {
|
|
||||||
console.error("EUS uploadKey property does not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
} else {
|
|
||||||
if (json["uploadKey"] == "") useUploadKey = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// database checks
|
|
||||||
if (!json["noUpload"]) {
|
|
||||||
if (json["database"] == null) {
|
|
||||||
console.error("EUS database properties do not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
} else {
|
|
||||||
// databaseAddress
|
|
||||||
if (json["database"]["databaseAddress"] == null) {
|
|
||||||
console.error("EUS database.databaseAddress property does not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
}
|
|
||||||
// databasePort
|
|
||||||
if (json["database"]["databasePort"] == null) {
|
|
||||||
console.error("EUS database.databasePort property does not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
}
|
|
||||||
// databaseUsername
|
|
||||||
if (json["database"]["databaseUsername"] == null) {
|
|
||||||
console.error("EUS database.databaseUsername property does not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
}
|
|
||||||
// databasePassword
|
|
||||||
if (json["database"]["databasePassword"] == null) {
|
|
||||||
console.error("EUS database.databasePassword property does not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
}
|
|
||||||
// databaseName
|
|
||||||
if (json["database"]["databaseName"] == null) {
|
|
||||||
console.error("EUS database.databaseName property does not exist!");
|
|
||||||
performShutdownAfterValidation = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if server needs to be shutdown
|
|
||||||
if (performShutdownAfterValidation) {
|
|
||||||
console.error("EUS config properties are missing, refer to example config on GitHub (https://github.com/tgpholly/EUS)");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
else return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let existanceCache = {};
|
|
||||||
|
|
||||||
function fileExists(file) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
fs.access(file, (error) => {
|
|
||||||
resolve(!error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendFile(req, res, path, startTime) {
|
|
||||||
// File does exist, send it back to the client.
|
|
||||||
res.sendFile(path);
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: ${node_modules.dyetty.green("[200]")} ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function regularFile(req, res, urs = "", startTime) {
|
|
||||||
if (req.url === "/") { urs = "/index.html" } else if (!req.url.includes(".") && !req.url.endsWith("/")) { urs = `${req.url}/index.html` } else { urs = req.url }
|
|
||||||
|
|
||||||
if (await fileExists(`${__dirname}${BASE_PATH}/files${urs}`)) {
|
|
||||||
sendFile(req, res, `${__dirname}${BASE_PATH}/files${decodeURIComponent(urs)}`, startTime);
|
|
||||||
} else {
|
|
||||||
let isSuccess = false;
|
|
||||||
if (!urs.endsWith(".html")) {
|
|
||||||
if (await fileExists(`${__dirname}${BASE_PATH}/files${decodeURIComponent(urs)}.html`)) {
|
|
||||||
isSuccess = true;
|
|
||||||
sendFile(req, res, `${__dirname}${BASE_PATH}/files${decodeURIComponent(urs)}.html`, startTime);
|
|
||||||
}
|
|
||||||
} else if (urs.endsWith("/index.html")) {
|
|
||||||
const path = `${__dirname}${BASE_PATH}/files${decodeURIComponent(req.url)}.html`;
|
|
||||||
if (await fileExists(path)) {
|
|
||||||
isSuccess = true;
|
|
||||||
sendFile(req, res, path, startTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSuccess) {
|
|
||||||
// Doesn't exist, send a 404 to the client.
|
|
||||||
await error404Page(res);
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.cross, `${req.method}: ${node_modules.dyetty.red("[404]")} ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function imageFile(req, res, file, startTime = 0) {
|
|
||||||
res.sendFile(`${__dirname}${BASE_PATH}/i/${file}`);
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: ${node_modules.dyetty.green("[200]")} (ImageReq) ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doImageLookup(req, res, urs, startTime) {
|
|
||||||
// Check if we even need to query the DB
|
|
||||||
const dbAble = (!/[^0-9A-Za-z]/.test(urs)) && urs != "" && urs != "index" && (req.url.split("/").length == 2);
|
|
||||||
|
|
||||||
if (dbAble) {
|
|
||||||
if (urs in existanceCache) {
|
|
||||||
const cachedFile = existanceCache[urs];
|
|
||||||
imageFile(req, res, `${cachedFile.fileHash}.${cachedFile.fileType}`, startTime);
|
|
||||||
} else {
|
|
||||||
// Try to get what we think is an image's details from the DB
|
|
||||||
const dbEntry = await dbConnection.query(`SELECT hash, imageType FROM images WHERE imageId = ? LIMIT 1`, [urs]);
|
|
||||||
|
|
||||||
// There's an entry in the DB for this, send the file back.
|
|
||||||
if (dbEntry != null) {
|
|
||||||
existanceCache[urs] = {
|
|
||||||
fileHash: dbEntry.hash,
|
|
||||||
fileType: dbEntry.imageType
|
|
||||||
};
|
|
||||||
imageFile(req, res, `${dbEntry.hash}.${dbEntry.imageType}`, startTime);
|
|
||||||
}
|
|
||||||
// There's no entry, so treat this as a regular file.
|
|
||||||
else regularFile(req, res, urs, startTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We can still serve files if they are not dbable
|
|
||||||
// since we don't need to check the db
|
|
||||||
else regularFile(req, res, urs, startTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
const PATH_404 = `${__dirname}${BASE_PATH}/files/404.html`;
|
|
||||||
async function error404Page(res) {
|
|
||||||
if (await fileExists(PATH_404)) {
|
|
||||||
res.status(404).sendFile(PATH_404);
|
|
||||||
} else {
|
|
||||||
res.status(404).send("404!<hr>EUS");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let getHandler = (req, res, urs, startTime) => res.send("EUS is starting up, please try again in a few seconds.");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init: init,
|
|
||||||
extras:async function() {
|
|
||||||
if (!eusConfig["noUpload"]) {
|
|
||||||
// Setup express to use busboy
|
|
||||||
global.app.use(node_modules.busboy());
|
|
||||||
getHandler = doImageLookup;
|
|
||||||
cacheJSON = JSON.stringify(await cacheFilesAndSpace());
|
|
||||||
cacheIsReady = true;
|
|
||||||
} else {
|
|
||||||
getHandler = regularFile;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get:async function(req, res) {
|
|
||||||
/*
|
|
||||||
req - Request from client
|
|
||||||
res - Response from server
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Set some headers
|
|
||||||
res.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
||||||
res.set("X-XSS-Protection", "1; mode=block");
|
|
||||||
res.set("Permissions-Policy", "microphone=(), geolocation=(), magnetometer=(), camera=(), payment=(), usb=(), accelerometer=(), gyroscope=()");
|
|
||||||
res.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
||||||
res.set("Content-Security-Policy", "block-all-mixed-content;frame-ancestors 'self'");
|
|
||||||
res.set("X-Frame-Options", "SAMEORIGIN");
|
|
||||||
res.set("X-Content-Type-Options", "nosniff");
|
|
||||||
|
|
||||||
req.url = decodeURIComponent(req.url.split("?")[0]);
|
|
||||||
|
|
||||||
// Auto 404 any attempted php / wp access and log it.
|
|
||||||
// TODO: IP ban based on mass access. Very clearly a scan.
|
|
||||||
if (req.url.includes(".php") || req.url.includes("/wp-")) {
|
|
||||||
global.modules.consoleHelper.printWarn(emoji.globe_europe, `${req.method}: SUSSY ${req.headers["cf-connecting-ip"]} ${req.url}`);
|
|
||||||
return await error404Page(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.url.includes("/api/")) {
|
|
||||||
return handleAPI(req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const urs = req.url.split("/")[1];
|
|
||||||
getHandler(req, res, urs, startTime);
|
|
||||||
},
|
|
||||||
post:async function(req, res) {
|
|
||||||
/*
|
|
||||||
req - Request from client
|
|
||||||
res - Response from server
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (eusConfig["noUpload"]) return res.end("");
|
|
||||||
if (req.url != "/upload") return res.end("");
|
|
||||||
|
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
|
||||||
|
|
||||||
if (useUploadKey && eusConfig["uploadKey"] != req.header("key")) return res.end("Incorrect key provided for upload");
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
req.pipe(req.busboy);
|
|
||||||
req.busboy.on('file', function (fieldname, file, info) {
|
|
||||||
fileOutName = node_modules.randomstring.generate(14);
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.fast_up, `${req.method}: Upload of ${fileOutName} started.`);
|
|
||||||
// Check the file is within the accepted file types
|
|
||||||
let fileType = info.filename.split(".").slice(-1);
|
|
||||||
if (info.filename === "blob") {
|
|
||||||
fileType = info.mimeType.split("/")[1];
|
|
||||||
}
|
|
||||||
var thefe = "";
|
|
||||||
if (eusConfig.acceptedTypes.includes(`.${fileType}`)) {
|
|
||||||
// File is accepted, set the extention of the file in thefe for later use.
|
|
||||||
thefe = fileType;
|
|
||||||
} else {
|
|
||||||
// File isn't accepted, send response back to client stating so.
|
|
||||||
res.status(403).end("This file type isn't accepted.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Create a write stream for the file
|
|
||||||
let fstream = fs.createWriteStream("/tmp/EUS_UPLOADS/" + fileOutName);
|
|
||||||
file.pipe(fstream);
|
|
||||||
|
|
||||||
// Get all file data for the file MD5
|
|
||||||
let fileData = [];
|
|
||||||
file.on("data", (chunk) => {
|
|
||||||
fileData.push(chunk);
|
|
||||||
});
|
|
||||||
|
|
||||||
fstream.on('close', async () => {
|
|
||||||
let md5Buffer = Buffer.concat(fileData);
|
|
||||||
// bye
|
|
||||||
fileData = null;
|
|
||||||
|
|
||||||
// Create MD5 hash of file
|
|
||||||
const hash = crypto.createHash("md5");
|
|
||||||
hash.setEncoding("hex");
|
|
||||||
hash.write(md5Buffer);
|
|
||||||
hash.end();
|
|
||||||
|
|
||||||
const fileHash = hash.read();
|
|
||||||
const dataOnHash = await dbConnection.query("SELECT imageId FROM images WHERE hash = ? LIMIT 1", [fileHash]);
|
|
||||||
if (dataOnHash !== undefined)
|
|
||||||
{
|
|
||||||
fs.unlink(`/tmp/EUS_UPLOADS/${fileOutName}`, () => {});
|
|
||||||
res.end(`${eusConfig.baseURL}${dataOnHash.imageId}`);
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: Hash matched! Sending ${dataOnHash.imageId} instead. Took ${Date.now() - startTime}ms`);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
fs.rename(`/tmp/EUS_UPLOADS/${fileOutName}`, `${__dirname}${BASE_PATH}/i/${fileHash}.${thefe[0]}`, async () => {
|
|
||||||
// Add to the existance cache
|
|
||||||
existanceCache[fileOutName] = {
|
|
||||||
fileHash: fileHash,
|
|
||||||
fileType: thefe[0]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store image data in db
|
|
||||||
await dbConnection.query(`INSERT INTO images (id, imageId, imageType, hash, fileSize) VALUES (NULL, ?, ?, ?, ?)`, [fileOutName, thefe[0], fileHash, md5Buffer.length]);
|
|
||||||
|
|
||||||
// Send URL of the uploaded image to the client
|
|
||||||
res.end(eusConfig.baseURL + fileOutName);
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: Upload of ${fileOutName} finished. Took ${Date.now() - startTime}ms`);
|
|
||||||
|
|
||||||
// Update cached files & space
|
|
||||||
if ((Date.now() - timeSinceLastCache) >= API_CACHE_LIFESPAN) {
|
|
||||||
cacheJSON = JSON.stringify(await cacheFilesAndSpace());
|
|
||||||
} else {
|
|
||||||
const tempJson = JSON.parse(cacheJSON);
|
|
||||||
tempJson.fileCounts[thefe[0]]++;
|
|
||||||
cacheJSON = JSON.stringify(tempJson);
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.folder, `Skiped api cache`);
|
|
||||||
}
|
|
||||||
cacheIsReady = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAPI(req, res) {
|
|
||||||
const startTime = Date.now();
|
|
||||||
let jsonaa = {}, filesaa = 0, spaceaa = 0;
|
|
||||||
switch (req.url.split("?")[0]) {
|
|
||||||
// Status check to see the online status of EUS
|
|
||||||
// Used by ESL to make sure EUS is online
|
|
||||||
case "/api/get-server-status":
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: ${node_modules.dyetty.green("[200]")} (APIReq) ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
return res.end('{"status":1,"version":"'+global.internals.version+'"}');
|
|
||||||
|
|
||||||
/* Stats api endpoint
|
|
||||||
Query inputs
|
|
||||||
f : Values [0,1]
|
|
||||||
s : Values [0,1]
|
|
||||||
*/
|
|
||||||
case "/api/get-stats":
|
|
||||||
filesaa = req.query["f"];
|
|
||||||
spaceaa = req.query["s"];
|
|
||||||
if (!cacheIsReady) return res.end("Cache is not ready");
|
|
||||||
jsonaa = JSON.parse(cacheJSON);
|
|
||||||
// If total files is asked for
|
|
||||||
if (filesaa == 1) {
|
|
||||||
// If getting the space used on the server isn't required send the json
|
|
||||||
if (spaceaa != 1) {
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: ${node_modules.dyetty.green("[200]")} (APIReq) ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
delete jsonaa["space"];
|
|
||||||
return res.end(JSON.stringify(jsonaa));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Getting space is required
|
|
||||||
if (spaceaa == 1) {
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: ${node_modules.dyetty.green("[200]")} (APIReq) ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
if (filesaa != 1) delete jsonaa["files"];
|
|
||||||
return res.end(JSON.stringify(jsonaa));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filesaa != 1 && spaceaa != 1) {
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: ${node_modules.dyetty.green("[200]")} (APIReq) ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
return res.end("Please add f and or s to your queries to get the files and space");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Information API
|
|
||||||
case "/api/get-info":
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: ${node_modules.dyetty.green("[200]")} (APIReq) ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
return res.end(JSON.stringify({
|
|
||||||
version: global.internals.version,
|
|
||||||
instance: config["server"]["instance_type"]
|
|
||||||
}));
|
|
||||||
|
|
||||||
default:
|
|
||||||
global.modules.consoleHelper.printInfo(emoji.heavy_check, `${req.method}: ${node_modules.dyetty.green("[200]")} (APIReq) ${req.url} ${Date.now() - startTime}ms`);
|
|
||||||
return res.send(`
|
|
||||||
<h2>All currently avaliable api endpoints</h2>
|
|
||||||
<a href="/api/get-server-status">/api/get-server-status</a>
|
|
||||||
<a href="/api/get-stats">/api/get-stats</a>
|
|
||||||
<a href="/api/get-info">/api/get-info</a>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const spaceValues = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]; // Futureproofing™
|
|
||||||
|
|
||||||
async function spaceToLowest(spaceValue, includeStringValue) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Converts space values to lower values e.g MB, GB, TB etc depending on the size of the number
|
|
||||||
let i1 = 1;
|
|
||||||
// Loop through until value is at it's lowest
|
|
||||||
while (spaceValue >= 1024) {
|
|
||||||
spaceValue = spaceValue / 1024;
|
|
||||||
if (spaceValue >= 1024) i1++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeStringValue) resolve(`${spaceValue.toFixed(2)} ${spaceValues[i1]}`);
|
|
||||||
else resolve(spaceValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.MOD_FUNC = MODULE_FUNCTION;
|
|
||||||
|
|
||||||
module.exports.REQUIRED_NODE_MODULES = [
|
|
||||||
"dyetty", "connect-busboy", "randomstring",
|
|
||||||
"stream-meter", "mysql2"
|
|
||||||
];
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 Holly Stubbs (tgpholly)
|
Copyright (c) 2024 Holly Stubbs (tgpholly)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
36
README.md
36
README.md
|
@ -1,36 +0,0 @@
|
||||||
<p align="center">
|
|
||||||
<img width="150" height="150" src="https://eusv.net/images/EUSLossless.webp">
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
|
||||||
EUS is my public screenshot server built using <a href="https://github.com/tgpholly/Revolution">Revolution</a><br>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://www.codefactor.io/repository/github/tgpethan/eus/overview/master"><img src="https://www.codefactor.io/repository/github/tgpholly/eus/badge/master" alt="CodeFactor" /></a>
|
|
||||||
<a src="https://discord.gg/BV8QGn6"><img src="https://img.shields.io/discord/477024246959308810?color=7289da&label=Discord&logo=discord&logoColor=ffffff"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
EUS has extra dependencies other than those of [Revolution](https://github.com/tgpethan/Revolution), the server EUS is made on, of which include:
|
|
||||||
- [connect-busboy](https://www.npmjs.com/package/connect-busboy)
|
|
||||||
- [randomstring](https://www.npmjs.com/package/randomstring)
|
|
||||||
- [diskusage](https://www.npmjs.com/package/diskusage)
|
|
||||||
- [stream-meter](https://www.npmjs.com/package/stream-meter)
|
|
||||||
- [mysql](https://www.npmjs.com/package/mysql)
|
|
||||||
|
|
||||||
Simply drop the EUS.js into a Revolution instance's modules folder **(If you still have [example_request_handler.js](https://github.com/tgpethan/Revolution/blob/master/modules/example_request_handler.js) be sure to delete it!)** and the extra required modules should be automatically installed.
|
|
||||||
|
|
||||||
## Config
|
|
||||||
On first startup EUS will create a new config file in the **modules/EUS/** folder, some of these values may need to be changed depending on your use case.
|
|
||||||
|
|
||||||
The value of **baseURL** will need to be changed to what you access the server from, for example if the server's ip is 192.168.1.100 and you are not planning to use EUS at a url you would change the value to **http://192.168.1.100/**. **baseURL** is used to construct the response url for file uploads, for example the value of **baseURL** on my instance of EUS is **https://eusv.net/**.
|
|
||||||
|
|
||||||
If you want to expand the files that the server allows to be sent to it this can be done in the **allowedTypes** array. By default the array contains **png, jpg and gif**.
|
|
||||||
|
|
||||||
The value of **uploadKey** is used to restrict who can upload to your server, set this to something and the server will restrict who can upload depending on if they provided the key or not. If this field is left blank EUS will asume you don't want an upload key and uploads to it will be unrestricted
|
|
||||||
|
|
||||||
## API
|
|
||||||
EUS has 3 api endpoints, they are **[/api/get-stats](https://eusv.net/api/get-stats)**, **[/api/get-info](https://eusv.net/api/get-info)** and **[/api/get-server-status](https://eusv.net/api/get-server-status)**
|
|
||||||
|
|
||||||
## Websites that use EUS
|
|
||||||
[EUS](https://eusv.net)
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"baseURL":"https://example.com/",
|
|
||||||
"acceptedTypes": [
|
|
||||||
".png",
|
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".gif",
|
|
||||||
".mp4"
|
|
||||||
],
|
|
||||||
"uploadKey":"",
|
|
||||||
"noUpload": false,
|
|
||||||
"database": {
|
|
||||||
"databaseAddress": "127.0.0.1",
|
|
||||||
"databasePort": 3306,
|
|
||||||
"databaseUsername": "root",
|
|
||||||
"databasePassword": "password",
|
|
||||||
"databaseName": "EUS"
|
|
||||||
}
|
|
||||||
}
|
|
141
controllers/Controller.ts
Normal file
141
controllers/Controller.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
import { Console } from "hsconsole";
|
||||||
|
import Session from "../objects/Session";
|
||||||
|
import SessionUser from "../objects/SessionUser";
|
||||||
|
import RequestCtx from "../objects/RequestCtx";
|
||||||
|
import UserType from "../enums/UserType";
|
||||||
|
|
||||||
|
// prepare for ts-ignore :3
|
||||||
|
// TODO: figure out some runtime field / type checking so
|
||||||
|
// can auto badRequest on missing stuff.
|
||||||
|
export default abstract class Controller {
|
||||||
|
public static FastifyInstance:FastifyInstance;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
|
||||||
|
const rawControllerParts = this.constructor.name.split("_");
|
||||||
|
const controllerName = rawControllerParts.splice(0, 1)[0].replace("Controller", "").toLowerCase();
|
||||||
|
const controllerAuthLevels: Array<UserType> = [];
|
||||||
|
const actionAuthLevels: { [ key : string ]: Array<UserType> } = {};
|
||||||
|
|
||||||
|
for (const prop of rawControllerParts) {
|
||||||
|
if (prop.startsWith("Auth")) {
|
||||||
|
const userLevel = prop.split("$")[1];
|
||||||
|
// @ts-ignore
|
||||||
|
controllerAuthLevels.push(UserLevel[userLevel]);
|
||||||
|
Console.printInfo(`Set Auth level requirement for ${this.constructor.name} to ${userLevel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const method of methods) {
|
||||||
|
if (method === "constructor" || method[0] !== method[0].toUpperCase()) { // * Anything that starts with lowercase we'll consider "private"
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = method.split("_");
|
||||||
|
const methodNameRaw = params.splice(0, 1)[0];
|
||||||
|
const methodName = methodNameRaw.toLowerCase();
|
||||||
|
const doAuth = !params.includes("AllowAnonymous");
|
||||||
|
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const controllerRequestHandler = this[method];
|
||||||
|
const requestHandler = (req:FastifyRequest, res:FastifyReply) => {
|
||||||
|
let session = Session.CheckValiditiy(req.cookies);
|
||||||
|
if (doAuth && session === undefined) {
|
||||||
|
return res.redirect(`/account/login?returnTo=${encodeURIComponent(req.url)}`);
|
||||||
|
}
|
||||||
|
const methodAuthCheck = actionAuthLevels[`${controllerName}_${methodName}_${req.method.toLowerCase()}`];
|
||||||
|
let wasMethodMatch = false;
|
||||||
|
if (methodAuthCheck && session !== undefined) {
|
||||||
|
for (const auth of methodAuthCheck) {
|
||||||
|
if (auth === session.userType) {
|
||||||
|
wasMethodMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!wasMethodMatch && session !== undefined && controllerAuthLevels.length > 0) {
|
||||||
|
let hasLevelMatch = false;
|
||||||
|
for (const level of controllerAuthLevels) {
|
||||||
|
if (level === session.userType) {
|
||||||
|
hasLevelMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasLevelMatch) {
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.header("X-Powered-By", "MultiProbe");
|
||||||
|
if (controllerName !== "api") {
|
||||||
|
res.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
res.header("X-XSS-Protection", "1; mode=block");
|
||||||
|
res.header("Permissions-Policy", "microphone=(), geolocation=(), magnetometer=(), camera=(), payment=(), usb=(), accelerometer=(), gyroscope=()");
|
||||||
|
res.header("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
|
res.header("Content-Security-Policy", "block-all-mixed-content;frame-ancestors 'self'");
|
||||||
|
res.header("X-Frame-Options", "SAMEORIGIN");
|
||||||
|
res.header("X-Content-Type-Options", "nosniff");
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestCtx = new RequestCtx(req, res, controllerName, methodName, session);
|
||||||
|
controllerRequestHandler.bind(requestCtx)(req.method === "GET" ? req.query : req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
let funcMethods:Array<string> = [];
|
||||||
|
let thisMethodHttpMethod = "";
|
||||||
|
for (const param of params) {
|
||||||
|
if (param === "Get" || param === "Post" || param === "Put") {
|
||||||
|
funcMethods.push(param);
|
||||||
|
thisMethodHttpMethod = param.toLowerCase();
|
||||||
|
// @ts-ignore
|
||||||
|
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName === "index" ? "" : methodName}`, requestHandler);
|
||||||
|
Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName === "index" ? "" : methodName}" as ${param}`);
|
||||||
|
if (methodName === "index") {
|
||||||
|
// @ts-ignore
|
||||||
|
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName}`, requestHandler);
|
||||||
|
Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName}" as ${param}`);
|
||||||
|
// @ts-ignore
|
||||||
|
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}`, requestHandler);
|
||||||
|
Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}" as ${param}`);
|
||||||
|
}
|
||||||
|
} else if (param.startsWith("Auth")) {
|
||||||
|
const nameWithMethod = `${controllerName}_${methodName}_${thisMethodHttpMethod}`;
|
||||||
|
const userLevel = param.split("$")[1];
|
||||||
|
if (!(nameWithMethod in actionAuthLevels)) {
|
||||||
|
actionAuthLevels[nameWithMethod] = [];
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
actionAuthLevels[nameWithMethod].push(UserLevel[userLevel]);
|
||||||
|
Console.printInfo(`Set Auth level requirement for ${this.constructor.name}.${method} to ${userLevel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllerName === "home" && methodName === "index") {
|
||||||
|
for (const httpMethod of funcMethods) {
|
||||||
|
Console.printInfo(`Registered ${this.constructor.name}.${method} to "/" as ${httpMethod}`);
|
||||||
|
// @ts-ignore
|
||||||
|
Controller.FastifyInstance[httpMethod.toLowerCase()](`/`, requestHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not real, these RequestCtx so they autocomplete :)
|
||||||
|
// yeah, i know. this is terrible.
|
||||||
|
|
||||||
|
// Fields
|
||||||
|
// @ts-ignore
|
||||||
|
public session:SessionUser;
|
||||||
|
// @ts-ignore
|
||||||
|
public req: FastifyRequest;
|
||||||
|
// @ts-ignore
|
||||||
|
public res: FastifyReply;
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
view(view?:string | Object, model?: Object) {}
|
||||||
|
redirectToAction(action:string, controller?:string) {}
|
||||||
|
ok(message?:string) {}
|
||||||
|
badRequest(message?:string) {}
|
||||||
|
unauthorised(message?:string) {}
|
||||||
|
forbidden(message?:string) {}
|
||||||
|
}
|
0
controllers/HomeController.ts
Normal file
0
controllers/HomeController.ts
Normal file
7
enums/UserType.ts
Normal file
7
enums/UserType.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
enum UserType {
|
||||||
|
Unknown = 0,
|
||||||
|
User = 10,
|
||||||
|
Admin = 999
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserType;
|
62
index.ts
Normal file
62
index.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import Fastify from "fastify";
|
||||||
|
import FastifyFormBody from "@fastify/formbody";
|
||||||
|
import FastifyMultipart from "@fastify/multipart";
|
||||||
|
import FastifyCookie from "@fastify/cookie";
|
||||||
|
import FastifyView from "@fastify/view";
|
||||||
|
import FastifyStatic from "@fastify/static";
|
||||||
|
import Config from "./objects/Config";
|
||||||
|
import EJS from "ejs";
|
||||||
|
import { Console } from "hsconsole";
|
||||||
|
import Controller from "./controllers/Controller";
|
||||||
|
import HomeController from "./controllers/HomeController";
|
||||||
|
import Database from "./objects/Database";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
Console.customHeader(`EUS Hosting Panel server started at ${new Date()}`);
|
||||||
|
|
||||||
|
const fastify = Fastify({
|
||||||
|
logger: false
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.register(FastifyView, {
|
||||||
|
engine: {
|
||||||
|
ejs: EJS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.register(FastifyFormBody);
|
||||||
|
|
||||||
|
fastify.register(FastifyMultipart);
|
||||||
|
|
||||||
|
fastify.register(FastifyCookie, {
|
||||||
|
secret: Config.session.secret,
|
||||||
|
parseOptions: {
|
||||||
|
path: "/",
|
||||||
|
secure: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.register(FastifyStatic, {
|
||||||
|
root: join(__dirname, "wwwroot"),
|
||||||
|
prefix: `${Config.ports.webroot}/static/`
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.setNotFoundHandler(async (_req, res) => {
|
||||||
|
return res.status(404).view("views/404.ejs", { });
|
||||||
|
});
|
||||||
|
|
||||||
|
new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name);
|
||||||
|
|
||||||
|
Controller.FastifyInstance = fastify;
|
||||||
|
new HomeController();
|
||||||
|
new AccountController();
|
||||||
|
new FileController();
|
||||||
|
|
||||||
|
fastify.listen({ port: Config.ports.http, host: "127.0.0.1" }, (err, address) => {
|
||||||
|
if (err) {
|
||||||
|
Console.printError(`Error occured while spinning up fastify:\n${err}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.printInfo(`Fastify listening at ${address.replace("0.0.0.0", "localhost").replace("127.0.0.1", "localhost")}`);
|
||||||
|
});
|
3
objects/Config.ts
Normal file
3
objects/Config.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default class Config {
|
||||||
|
|
||||||
|
}
|
102
objects/Database.ts
Normal file
102
objects/Database.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import { Console } from "hsconsole";
|
||||||
|
import { createPool, Pool, RowDataPacket } from "mysql2";
|
||||||
|
|
||||||
|
export type DBInDataType = string | number | null | undefined;
|
||||||
|
|
||||||
|
export default class Database {
|
||||||
|
private connectionPool:Pool;
|
||||||
|
private static readonly CONNECTION_LIMIT = 128;
|
||||||
|
|
||||||
|
public connected:boolean = false;
|
||||||
|
|
||||||
|
public static Instance:Database;
|
||||||
|
|
||||||
|
public constructor(databaseAddress:string, databasePort:number = 3306, databaseUsername:string, databasePassword:string, databaseName:string) {
|
||||||
|
this.connectionPool = createPool({
|
||||||
|
connectionLimit: Database.CONNECTION_LIMIT,
|
||||||
|
host: databaseAddress,
|
||||||
|
port: databasePort,
|
||||||
|
user: databaseUsername,
|
||||||
|
password: databasePassword,
|
||||||
|
database: databaseName
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.printInfo(`DB connection pool created. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`);
|
||||||
|
|
||||||
|
Database.Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public execute(query:string, data?:Array<DBInDataType>) {
|
||||||
|
return new Promise<boolean>((resolve, reject) => {
|
||||||
|
this.connectionPool.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
connection.execute(query, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
connection.release();
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result !== undefined);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
connection.execute(query, data, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
connection.release();
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result !== undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public query(query:string, data?:Array<DBInDataType>) {
|
||||||
|
return new Promise<RowDataPacket[]>((resolve, reject) => {
|
||||||
|
this.connectionPool.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
} else {
|
||||||
|
// Use old query
|
||||||
|
if (data == null) {
|
||||||
|
connection.query<RowDataPacket[]>(query, (err, rows) => {
|
||||||
|
connection.release();
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
connection.release();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Use new prepared statements w/ placeholders
|
||||||
|
else {
|
||||||
|
connection.execute<RowDataPacket[]>(query, data, (err, rows) => {
|
||||||
|
connection.release();
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
connection.release();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async querySingle(query:string, data?:Array<DBInDataType>) {
|
||||||
|
const dbData = await this.query(query, data);
|
||||||
|
if (dbData != null && dbData.length > 0) {
|
||||||
|
return dbData[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
67
objects/RequestCtx.ts
Normal file
67
objects/RequestCtx.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
import SessionUser from "./SessionUser";
|
||||||
|
import UserType from "../enums/UserType";
|
||||||
|
|
||||||
|
export default class RequestCtx {
|
||||||
|
public controllerName:string;
|
||||||
|
public actionName:string;
|
||||||
|
public session?:SessionUser;
|
||||||
|
public req: FastifyRequest;
|
||||||
|
public res: FastifyReply;
|
||||||
|
|
||||||
|
public constructor(req: FastifyRequest, res: FastifyReply, controllerName:string, actionName:string, sessionUser?:SessionUser) {
|
||||||
|
this.session = sessionUser;
|
||||||
|
this.req = req;
|
||||||
|
this.res = res;
|
||||||
|
this.controllerName = controllerName;
|
||||||
|
this.actionName = actionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
view(view?:string | Object, model?: Object) {
|
||||||
|
let viewName: string = this.actionName;
|
||||||
|
let viewModel: Object = {};
|
||||||
|
if (typeof(view) === "string") {
|
||||||
|
viewName = view;
|
||||||
|
} else if (typeof(view) === "object") {
|
||||||
|
viewModel = view;
|
||||||
|
}
|
||||||
|
if (typeof(model) === "object") {
|
||||||
|
viewModel = model;
|
||||||
|
}
|
||||||
|
// @ts-ignore inject session
|
||||||
|
viewModel["session"] = this.session;
|
||||||
|
// @ts-ignore inject enums
|
||||||
|
viewModel["UserLevel"] = UserLevel;
|
||||||
|
return this.res.view(`views/${this.controllerName}/${viewName}.ejs`, viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: query params
|
||||||
|
redirectToAction(action:string, controller?:string) {
|
||||||
|
const controllerName = controller ?? this.controllerName;
|
||||||
|
if (action === "index") {
|
||||||
|
if (controllerName === "home") {
|
||||||
|
return this.res.redirect(`/`, 302);
|
||||||
|
} else {
|
||||||
|
return this.res.redirect(`/${controllerName}`, 302);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.res.redirect(`/${controllerName}/${action}`, 302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(message?:string) {
|
||||||
|
return this.res.status(200).send(message ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
badRequest(message?:string) {
|
||||||
|
return this.res.status(400).send(message ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
unauthorised(message?:string) {
|
||||||
|
return this.res.status(401).send(message ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
forbidden(message?:string) {
|
||||||
|
return this.res.status(403).send(message ?? "");
|
||||||
|
}
|
||||||
|
}
|
67
objects/Session.ts
Normal file
67
objects/Session.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
import SessionUser from "./SessionUser";
|
||||||
|
import UserType from "../enums/UserType";
|
||||||
|
|
||||||
|
export default class RequestCtx {
|
||||||
|
public controllerName:string;
|
||||||
|
public actionName:string;
|
||||||
|
public session?:SessionUser;
|
||||||
|
public req: FastifyRequest;
|
||||||
|
public res: FastifyReply;
|
||||||
|
|
||||||
|
public constructor(req: FastifyRequest, res: FastifyReply, controllerName:string, actionName:string, sessionUser?:SessionUser) {
|
||||||
|
this.session = sessionUser;
|
||||||
|
this.req = req;
|
||||||
|
this.res = res;
|
||||||
|
this.controllerName = controllerName;
|
||||||
|
this.actionName = actionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
view(view?:string | Object, model?: Object) {
|
||||||
|
let viewName: string = this.actionName;
|
||||||
|
let viewModel: Object = {};
|
||||||
|
if (typeof(view) === "string") {
|
||||||
|
viewName = view;
|
||||||
|
} else if (typeof(view) === "object") {
|
||||||
|
viewModel = view;
|
||||||
|
}
|
||||||
|
if (typeof(model) === "object") {
|
||||||
|
viewModel = model;
|
||||||
|
}
|
||||||
|
// @ts-ignore inject session
|
||||||
|
viewModel["session"] = this.session;
|
||||||
|
// @ts-ignore inject enums
|
||||||
|
viewModel["UserLevel"] = UserLevel;
|
||||||
|
return this.res.view(`views/${this.controllerName}/${viewName}.ejs`, viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: query params
|
||||||
|
redirectToAction(action:string, controller?:string) {
|
||||||
|
const controllerName = controller ?? this.controllerName;
|
||||||
|
if (action === "index") {
|
||||||
|
if (controllerName === "home") {
|
||||||
|
return this.res.redirect(`/`, 302);
|
||||||
|
} else {
|
||||||
|
return this.res.redirect(`/${controllerName}`, 302);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.res.redirect(`/${controllerName}/${action}`, 302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(message?:string) {
|
||||||
|
return this.res.status(200).send(message ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
badRequest(message?:string) {
|
||||||
|
return this.res.status(400).send(message ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
unauthorised(message?:string) {
|
||||||
|
return this.res.status(401).send(message ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
forbidden(message?:string) {
|
||||||
|
return this.res.status(403).send(message ?? "");
|
||||||
|
}
|
||||||
|
}
|
13
objects/SessionUser.ts
Normal file
13
objects/SessionUser.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import UserType from "../enums/UserType";
|
||||||
|
|
||||||
|
export default class SessionUser {
|
||||||
|
public readonly userId:number;
|
||||||
|
public readonly userType:UserType;
|
||||||
|
public readonly validityPeriod:Date;
|
||||||
|
|
||||||
|
constructor(userId:number, userType: UserType, validityPeriod:Date) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.userType = userType;
|
||||||
|
this.validityPeriod = validityPeriod;
|
||||||
|
}
|
||||||
|
}
|
1898
package-lock.json
generated
Normal file
1898
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
33
package.json
Normal file
33
package.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "eus",
|
||||||
|
"description": "EUS is my public screenshot server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.eusv.net/tgpholly/EUS"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "tgpholly",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "commonjs",
|
||||||
|
"scripts": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ejs": "^3.1.5",
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"@vercel/ncc": "^0.38.3",
|
||||||
|
"check-outdated": "^2.12.0",
|
||||||
|
"nodemon": "^3.1.9",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.1",
|
||||||
|
"@fastify/formbody": "^8.0.1",
|
||||||
|
"@fastify/multipart": "^9.0.1",
|
||||||
|
"@fastify/static": "^8.0.3",
|
||||||
|
"@fastify/view": "^10.0.1",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"fastify": "^5.2.0",
|
||||||
|
"hsconsole": "^1.0.2"
|
||||||
|
}
|
||||||
|
}
|
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "ES2022",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"rootDir": "./",
|
||||||
|
"outDir": "./build",
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue