require('dotenv').config();
const utils = require('./utils');
const path = require('path');
global.VIDEO_ROOT = path.join(__dirname, "../videos");
global.TEMP_UPLOAD_DIR = path.join(VIDEO_ROOT, "/temp");
const commandLineArgs = require('command-line-args');
const options = commandLineArgs([{
name: "no-captcha",
type: Boolean,
defaultValue: false
}, {
name: "no-scan",
type: Boolean,
defaultValue: false
}, {
name: "verbose",
type: Boolean,
defaultValue: false
}, {
name: "save-verbose",
type: Boolean,
defaultValue: false
}]);
console.log("CLI OPTIONS:", options);
require('./utils/clamscan').init(options["no-scan"]);
const logger = require('./utils/log');
logger.set_verbose(options);
const express = require("express");
const app = express();
const exphbs = require("express-handlebars");
const rate_limit = require("express-rate-limit");
const cookie_parser = require('cookie-parser')
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const axios = require('axios');
const query_string = require('query-string');
const time = require('./utils/time');
const mailgun = require('./utils/mailgun');
const snippets = require('./utils/snippets');
const cookies = require('./utils/cookies');
const broadcast = require('./websocket/broadcast');
const ws_utils = require('./websocket/utils');
const buildRoutes = require('./buildRoutes')(app);
const PORT = process.env.PORT || 3000;
// Enable if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB, Nginx, etc)
// see https://expressjs.com/en/guide/behind-proxies.html
app.set('trust proxy', true);
app.engine("handlebars", exphbs({
defaultLayout: "main"
}));
app.set("view engine", "handlebars");
app.use(express.json({
limit: 1000000
}));
app.use(express.urlencoded({
extended: true
}));
app.use(cookie_parser());
app.use(express.static("./dist"));
app.use(express.static("./public"));
const passport_log = logger.create_logger("Passport");
// configure passport.js to use the local strategy
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, function(email, password, done) {
utils.query("SELECT * FROM users WHERE ?", {
email: email
}).catch(utils.handle_err.sql(passport_log)).then(function([user]) {
if (!user) {
return utils.resolve([null, false, {
message: 'Unknown Email'
}]);
}
if (user.email_confirmed) {
return bcrypt.compare(password, user.password).then(function(result) {
return result ? utils.resolve([null, user]) : utils.resolve([null, false, {
message: 'Incorrect Password'
}]);
});
}
return utils.query('SELECT * FROM email_confirmations WHERE ?', {
email: email
}).catch(utils.handle_err.sql(passport_log)).then(function([confirmation]) {
if (!confirmation || confirmation.expires < Date.now()) {
// User was registered but they never got a
// token generated (registered through CLI)
// OR they haven't confirmed their email yet
// and their token has expired.
return snippets.generate_token(
"email_confirmations",
"confirmation_id",
email
).catch(utils.handle_err.sql(passport_log)).then(function(token) {
return mailgun.send_confirmation_email(email, {
query: query_string.stringify({
token: token
})
}).catch(utils.handle_err.mailgun(passport_log));
});
}
}).then(function() {
return utils.resolve([null, false, {
message: 'You need to confirm your email, please check your inbox'
}]);
});
}).then(function(data) {
done(...data);
}).catch(function(error) {
passport_log.general.error(error);
done(error);
});
}));
passport.serializeUser(function(user, done) {
done(null, user.user_id);
});
passport.deserializeUser(function(user_id, done) {
utils.query("SELECT * FROM users WHERE ?", {
user_id: user_id
}).catch(utils.handle_err.sql(passport_log)).then(function([user]) {
if (user) {
done(null, user);
} else {
// This account got deleted
done(null, false, {
message: 'Account Deleted'
});
}
}).catch(function(error) {
passport_log.general.error(error);
done(error);
});
});
const session_middleware = session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: new MySQLStore(Object.assign({}, utils.mysql_options, {
connectionLimit: 1,
clearExpired: true,
expiration: time.one_day,
checkExpirationInterval: time.one_day * 0.25,
})),
proxy: !process.env.DEBUG,
cookie: cookies.options()
});
app.use(session_middleware);
app.use(passport.initialize());
app.use(passport.session());
app.use(function(req, res, next) {
res.locals.ip = req.header("X-Forwarded-For") || "localhost";
res.locals.log = logger.create_logger(res.locals.ip, req.method, req.url);
res.locals.log.mark(req);
next();
});
// Use a map because we want to maintain order of keys
let defaultMiddleware = new Map();
defaultMiddleware.set("rate_limit", {
default: true,
func: rate_limit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
})
});
defaultMiddleware.set("harsh_rate_limit", {
default: false,
func: rate_limit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 100 // limit each IP to 100 requests per windowMs
})
});
defaultMiddleware.set("auth", {
default: true,
func: function(req, res, next) {
if (!req.isAuthenticated || !req.isAuthenticated()) {
if (req.session) {
req.session.returnTo = req.originalUrl || req.url;
}
let target = "/login";
let searchIndex = req.url.indexOf("?");
if (searchIndex >= 0) {
target += req.url.substr(req.url.indexOf("?"));
}
res.locals.log.general.warning("Client has no session, redirect to", target);
return res.redirect(target);
}
next();
}
});
function check_captcha_token(req, res, next) {
if (req.body.captcha_bypass_token) {
return utils.query("SELECT * FROM captcha_bypass WHERE ?", {
bypass_id: req.body.captcha_bypass_token
}).catch(utils.handle_error.sql(res)).then(function(rows) {
if (rows.length && rows[0].expires > Date.now()) {
utils.query("DELETE FROM captcha_bypass WHERE ?", {
bypass_id: req.body.captcha_bypass_token
}).catch(utils.handle_err.sql(res, true));
delete req.body.captcha_bypass_token;
next();
} else {
res.locals.log.general.error("Captcha bypass failed");
res.send(utils.not_ok(utils.status.error));
}
});
} else {
return axios({
method: "POST",
url: "https://www.google.com/recaptcha/api/siteverify",
data: query_string.stringify({
secret: process.env.CAPTCHA_SECRET,
response: req.body.captcha_token,
remoteip: res.locals.ip
}),
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}).then(function(response) {
/* response.data = {
"success": true | false, // whether this request was a valid reCAPTCHA token for your site
"score": number // the score for this request (0.0 - 1.0)
"action": string // the action name for this request (important to verify)
"challenge_ts": timestamp, // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
"hostname": string, // the hostname of the site where the reCAPTCHA was solved
"error-codes": [...] // optional
} */
if (response.data.success &&
response.data.action === "submit" &&
response.data.score >= 0.5
) {
res.locals.log.v("Captcha passed with score", response.data.score);
delete req.body.captcha_token;
next();
} else {
res.locals.log.v("Captcha failed with score", response.data.score);
res.locals.log.v(response.data);
return utils.reject("Captcha says you are a bot");
}
});
}
}
defaultMiddleware.set("captcha", {
default: false,
func: options["no-captcha"] ? function(req, res, next) {
res.locals.log.v("Do not check captcha");
res.locals.log.v("captcha_token:", req.body.captcha_token);
res.locals.log.v("captcha_bypass_token:", req.body.captcha_bypass_token);
if (req.body.captcha_bypass_token || req.body.captcha_token) {
delete req.body.captcha_bypass_token;
delete req.body.captcha_token;
next();
} else {
utils.handle_err.res(res)("Debug mode captcha failed - no tokens");
}
} : function(req, res, next) {
check_captcha_token(req, res, next).catch(utils.handle_err.res(res))
}
});
buildRoutes("/api/", defaultMiddleware, [
"registration",
"video",
"log"
], true);
buildRoutes("/api/account/", defaultMiddleware, [
"login",
"update",
"prefs",
"password-reset",
"delete"
], true);
buildRoutes("/", defaultMiddleware, [
"html"
], false);
// Add fallbacks for unknown requests
app.use(function(error, req, res, next) {
// res.locals.log.sus.warning();
res.render('error', {
title: " - 404"
});
});
// Make sure we have directories or everything will just fucking crash
utils.fs.mkdir_if_not_exists(logger.LOGS_ROOT).then(function() {
return logger.process_log_queue();
}).then(function() {
return utils.fs.mkdir_if_not_exists(VIDEO_ROOT);
}).then(function() {
return utils.fs.mkdir_if_not_exists(TEMP_UPLOAD_DIR);
}).then(function() {
// Wrap the express app in an http server that can upgrade
// websocket connections with authentication
require('./websocket')(app, function(req, callback) {
session_middleware(req, {}, function() {
if (req.session && req.session.passport && req.session.passport.user) {
callback(req.session.passport.user);
} else {
ws_utils.log.general.warning("Could not restore session");
callback();
}
});
}).listen(PORT, function() {
console.log("Server running on port " + PORT);
});
}).catch(console.error);
const cleanup_log = logger.create_logger("Data Cleanup");
utils.setIntervalAndTriggerImmediate(function() {
let now = Date.now();
cleanup_log.v("Clean up data on", time.toString());
utils.query("SELECT video_id, name, created_by FROM videos WHERE expires<=?", [now]).then(function(videos) {
return Promise.all(videos.map(function(video) {
return utils.rimraf(path.join(VIDEO_ROOT, video.video_id)).then(function() {
broadcast({
command: "delete-video-response",
data: utils.ok({
video_id: video.video_id,
name: video.name,
reason: "expired"
})
}, {
[video.created_by]: true
});
});
}));
}).then(function() {
return Promise.all([
"videos",
"registrations",
"password_reset",
"captcha_bypass",
"delete_account"
].map(function(table) {
return utils.query(`DELETE FROM ${table} WHERE expires<=?`, [now]);
}));
}).catch(utils.handle_err.sql(cleanup_log, true));
}, time.one_day * 0.25); // 4 times per day
process.on('SIGINT', function(code) {
console.log(`About to exit with code: ${code}`);
logger.process_log_queue().then(function() {
process.exit(0);
}).catch(function(error) {
console.error(error);
process.exit(1);
});
});