This repository has been archived on 2024-10-31. You can view files and clone it, but cannot push or open issues or pull requests.
Files
2021-08-18 00:12:37 -07:00

310 lines
13 KiB
JavaScript

/* eslint-disable consistent-return */
const express = require('express');
const fs = require('fs-extra');
const app = express();
const bodyParser = require('body-parser');
const Eris = require('eris');
const path = require('path');
const utils = require(`${__dirname}/../util`);
const routes = require(`${__dirname}/routes`);
const https = require('https');
const events = require(`${__dirname}/../bot/events`);
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');
const adapter = new FileSync('db.json');
const db = low(adapter);
const helmet = require('helmet');
/** Express Webserver Class */
class ShareXAPI {
/**
* Starting server and bot, handling routing, and middleware
* @param {object} c - configuration json file
*/
constructor(c) {
this.db = db;
/** Setting LowDB Defaults */
db.defaults({
files: [],
bans: [],
visitors: [],
trafficTotal: [],
passwordUploads: [],
})
.write();
/** Defintions */
this.utils = utils;
this.log = utils.log;
this.auth = utils.auth;
this.randomToken = utils.randomToken;
this.mimeType = utils.mimeType;
this.c = c;
this.monitorChannel = null;
this.checkMonth();
this.c.discordToken && this.c.discordToken !== undefined && this.c.discrdToken !== null
? this.runDiscordBot()
: this.log.verbose('No Discord Token provided...\nContinuing without Discord connection...');
this.app = app;
this.app.set('view engine', 'ejs');
this.app.set('views', path.join(__dirname, '/views'));
this.app.use(helmet());
this.app.use(bodyParser.text());
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({
extended: true,
}));
/* Don't allow access if not accessed with configured domain */
this.app.use((req, res, next) => {
if(this.c.domain === '*') {
next();
} else if(req.headers.host !== this.c.domain.toLowerCase() && !this.c.domain.includes('*')) {
res.statusCode = 401;
res.write('Error 401: Unauthorized Domain');
return res.end();
} else if(this.c.domain.includes('*')) {
let reqParts = req.headers.host.toLowerCase().split('.');
let domainParts = this.c.domain.toLowerCase().split('.')
if(reqParts[1] === domainParts[1] && reqParts[2] === domainParts[2]) {
next();
} else {
res.statusCode = 401;
res.write('Error 401: Unauthorized Domain');
return res.end();
}
} else {
next();
}
});
/** Checking to see if IP is banned */
this.app.use((req, res, next) => {
const userIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress;
const exists = this.db.get('bans').find({ ip: userIP }).value();
if (exists === undefined) { // if a ban was not found, then it will move on
next();
} else {
res.statusCode = 401;
res.render('unauthorized');
return res.end();
}
});
/** Set to place IPs in temporarily for ratelimiting uploads */
const ratelimited = new Set();
this.app.use((req, res, next) => {
if (req.method === 'POST') {
const userIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress;
if (ratelimited.has(userIP)) {
res.statusCode = 429;
res.write('Error 429: Ratelimited');
return res.end();
}
next(); // Move on if IP is not in ratelimited set
ratelimited.add(userIP);
// delete IP from ratelimit set after time specified in config.json
setTimeout(() => ratelimited.delete(userIP), c.ratelimit);
} else {
next(); // move on if request type is not POST
}
});
this.app.use((req, res, next) => {
if (req.method === 'GET') {
const userIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress;
let file = req.path;
// Not ignoring these files causes bloat in the db
const ignored = ['/favicon.ico', '/assets/css/styles.min.css', '/highlight.pack.js', '/highlightjs-line-numbers.min.js', '/paste.css', '/atom-one-dark.css'];
let exists = this.db.get('files').find({ path: file }).value();
// making sure ignored files aren't included
if (ignored.includes(file)) exists = true;
if (exists === undefined) {
next(); // Move on if it doesn't exist, then input data into db
this.db.get('files')
.push({ path: file, ip: 'Unknown', views: 0 })
// Set IP to unknown in case a file is visited, and not uploaded using /api/files
.write();
if (!ignored.includes(file)) {
this.db.get('visitors')
.push({ date: new Date(), ip: userIP, path: file })
.write(); // Sets correct information for files uploaded with /api/files
}
} else {
next(); // Move on if the file already exists
const trafficPeriod = this.trafficPeriod(); // Gets month and year for tracking
let viewCount;
let trafficCount;
const filesExist = this.db.get('files').find({ path: file }).value(); // traffic exists for this file
const trafficExists = this.db.get('trafficTotal').find({ month: trafficPeriod }).value(); // traffic exists for this month and year
const visitors = this.db.get('visitors').value();
// Resetting visitors in the DB every 100 requests so the DB doesn't get bloated
if (visitors.length > 100) {
this.db.set('visitors', [])
.write();
}
filesExist === undefined
? viewCount = 0
: viewCount = filesExist.views + 1;
trafficExists === undefined
? trafficCount = 0
: trafficCount = trafficExists.total + 1;
this.db.get('files')
.find({ path: file })
.assign({ views: viewCount })
.write(); // Setting viewcount for file
if (!ignored.includes(file)) {
this.db.get('visitors')
.push({ date: new Date(), ip: userIP, path: file })
.write(); // Adding vsitor information to DB
}
if (!ignored.includes(file)) {
this.db.get('trafficTotal')
.find({ month: trafficPeriod })
.assign({ total: trafficCount })
.write(); // if request isn't to an ignored file, take request into total traffic
}
}
} else {
next();
}
});
// All files in /uploads/ are publicly accessible via http
this.app.use(express.static(`${__dirname}/uploads/`, {
extensions: this.c.admin.allowed.includes("*") ? null : this.c.admin.allowed,
}));
this.app.use(express.static(`${__dirname}/views/`, {
extensions: ['css'],
}));
// routing
this.app.get('/', routes.upload.bind(this));
this.app.get('/gallery', routes.gallery.get.bind(this));
this.app.get('/short', routes.short.get.bind(this));
this.app.get('/upload', routes.upload.bind(this));
this.app.get('/ERR_FILE_TOO_BIG', routes.fileTooBig.bind(this));
this.app.get('/ERR_ILLEGAL_FILE_TYPE', routes.illegalFileType.bind(this));
this.app.get('*', routes.err404.bind(this));
this.app.post('/api/shortener', routes.shortener.bind(this));
this.app.post('/short', routes.short.post.bind(this));
this.app.post('/gallery', routes.gallery.post.bind(this));
this.app.post('/pupload', routes.pupload.bind(this));
this.app.post('/api/paste', routes.paste.bind(this));
this.app.post('/api/files', routes.files.bind(this));
// Begin server
this.startServer();
}
/** Booting up the Discord Bot
* @returns {void}
*/
async runDiscordBot() {
this.bot = new Eris(this.c.discordToken, {
maxShards: 'auto',
});
this.log.verbose('Connecting to Discord...');
this.commands = [];
this.loadCommands();
this.bot
.on('messageCreate', events.messageCreate.bind(this))
.on('ready', events.ready.bind(this));
this.bot.connect();
}
/** Loads the commands for the discord bot to use in /bot/commands
* into an array defined before the calling of this function
* @returns {void}
*/
async loadCommands() {
fs.readdir(`${__dirname}/../bot/commands`, (err, files) => {
/** Commands are pushed to an array */
files.forEach(file => {
if (file.toString().includes('.js')) {
// eslint-disable-next-line global-require
this.commands.push(require(`${__dirname}/../bot/commands/${file.toString()}`));
this.log.verbose(`Loaded Command: ${file.toString()}`);
}
});
});
}
/** Start's the Express server
* @returns {void}
*/
async startServer() {
if (this.c.secure) {
/** if the secure option is set to true in config,
* it will boot in https so long as it detects
* key.pem and cert.pem in the src directory
*/
if (fs.existsSync(`${__dirname}/../key.pem`) && fs.existsSync(`${__dirname}/../cert.pem`)) {
const privateKey = fs.readFileSync(`${__dirname}/../key.pem`);
const certificate = fs.readFileSync(`${__dirname}/../cert.pem`);
https.createServer({
key: privateKey,
cert: certificate,
}, this.app).listen(this.c.securePort, '0.0.0.0');
} else {
// CF Flexible SSL
/** if no key & cert pem files are detected,
* it will still run in secure mode (returning urls with https)
* so that it's compatible with CF flexible SSL
* and SSL configurations via a reverse proxy */
this.app.listen(this.c.securePort, '0.0.0.0', () => {
this.log.warning('Server using flexible SSL secure setting\nTo run a full SSL setting, ensure key.pem and cert.pem are in the /src folder');
});
}
this.log.success(`Secure server listening on port ${this.c.securePort}`);
} else {
this.app.listen(this.c.port, '0.0.0.0', () => {
this.log.success(`Server listening on port ${this.c.port}`);
});
}
}
/** Checks to see if any DB entry is available for this month and year
* Then inserts a new object into the array if no data is available for
* that month/year
* @returns {void}
*/
async checkMonth() {
const trafficPeriod = this.trafficPeriod();
const dbMonth = this.db.get('trafficTotal').find({ month: trafficPeriod }).value();
if (dbMonth === undefined) {
this.db.get('trafficTotal')
.push({ month: trafficPeriod, total: 0 })
.write();
}
}
/** Gets the current month, and the current year
* then combines the two into a string
* this string is inserted into the database to be used
* for collecting traffic data on a per month basis
* @returns {string} 4/2019
*/
// eslint-disable-next-line class-methods-use-this
trafficPeriod() {
const date = new Date();
const currentMonth = date.getMonth() + 1;
const currentYear = date.getFullYear();
return `${currentMonth}/${currentYear}`;
}
/** Checks to see if server administrator wants to return http or https
* Using this function instead of req.secure because of
* Certain possible SSL configurations (CF Flexible SSL)
* @returns {string} http OR https
*/
protocol() {
if (this.c.secure) {
return 'https';
}
return 'http';
}
}
module.exports = ShareXAPI;