Merge branch 'web-refactoring' into 3.0

This commit is contained in:
calzoneman 2015-11-02 21:08:14 -08:00
commit de9b963d38
22 changed files with 418 additions and 224 deletions

View File

@ -3,6 +3,7 @@ try {
} catch (err) { } catch (err) {
console.error('FATAL: Failed to require() lib/server.js'); console.error('FATAL: Failed to require() lib/server.js');
console.error('Have you run `npm run build-server` yet to generate it?'); console.error('Have you run `npm run build-server` yet to generate it?');
console.error(err.stack);
process.exit(1); process.exit(1);
} }
var Config = require("./lib/config"); var Config = require("./lib/config");

View File

@ -13,6 +13,7 @@
"bluebird": "^2.10.1", "bluebird": "^2.10.1",
"body-parser": "^1.14.0", "body-parser": "^1.14.0",
"cheerio": "^0.19.0", "cheerio": "^0.19.0",
"clone": "^1.0.2",
"compression": "^1.5.2", "compression": "^1.5.2",
"cookie-parser": "^1.4.0", "cookie-parser": "^1.4.0",
"create-error": "^0.3.1", "create-error": "^0.3.1",

View File

@ -0,0 +1,74 @@
import clone from 'clone';
const DEFAULT_TRUSTED_PROXIES = Object.freeze([
'127.0.0.1',
'::1'
]);
export default class WebConfiguration {
constructor(config) {
this.config = config;
}
getEmailContacts() {
return clone(this.config.contacts);
}
getTrustedProxies() {
return DEFAULT_TRUSTED_PROXIES;
}
getCookieSecret() {
return this.config.authCookie.cookieSecret;
}
getCookieDomain() {
return this.config.authCookie.cookieDomain;
}
getEnableGzip() {
return this.config.gzip.enabled;
}
getGzipThreshold() {
return this.config.gzip.threshold;
}
getEnableMinification() {
return this.config.enableMinification;
}
getCacheTTL() {
return this.config.cacheTTL;
}
}
WebConfiguration.fromOldConfig = function (oldConfig) {
const config = {
contacts: []
};
oldConfig.get('contacts').forEach(contact => {
config.contacts.push({
name: contact.name,
email: contact.email,
title: contact.title
});
});
config.gzip = {
enabled: oldConfig.get('http.gzip'),
threshold: oldConfig.get('http.gzip-threshold')
};
config.authCookie = {
cookieSecret: oldConfig.get('http.cookie-secret'),
cookieDomain: oldConfig.get('http.root-domain-dotted')
};
config.enableMinification = oldConfig.get('http.minify');
config.cacheTTL = oldConfig.get('http.max-age');
return new WebConfiguration(config);
};

View File

@ -1,4 +1,9 @@
import createError from 'create-error'; import createError from 'create-error';
import * as HTTPStatus from './web/httpstatus';
export const ChannelStateSizeError = createError('ChannelStateSizeError'); export const ChannelStateSizeError = createError('ChannelStateSizeError');
export const ChannelNotFoundError = createError('ChannelNotFoundError'); export const ChannelNotFoundError = createError('ChannelNotFoundError');
export const CSRFError = createError('CSRFError');
export const HTTPError = createError('HTTPError', {
status: HTTPStatus.INTERNAL_SERVER_ERROR
});

View File

@ -42,6 +42,11 @@ var $util = require("./utilities");
var db = require("./database"); var db = require("./database");
var Flags = require("./flags"); var Flags = require("./flags");
var sio = require("socket.io"); var sio = require("socket.io");
import LocalChannelIndex from './web/localchannelindex';
import IOConfiguration from './configuration/ioconfig';
import WebConfiguration from './configuration/webconfig';
import NullClusterClient from './io/cluster/nullclusterclient';
import session from './session';
var Server = function () { var Server = function () {
var self = this; var self = this;
@ -60,8 +65,17 @@ var Server = function () {
ChannelStore.init(); ChannelStore.init();
// webserver init ----------------------------------------------------- // webserver init -----------------------------------------------------
const ioConfig = IOConfiguration.fromOldConfig(Config);
const webConfig = WebConfiguration.fromOldConfig(Config);
const clusterClient = new NullClusterClient(ioConfig);
const channelIndex = new LocalChannelIndex();
self.express = express(); self.express = express();
require("./web/webserver").init(self.express); require("./web/webserver").init(self.express,
webConfig,
ioConfig,
clusterClient,
channelIndex,
session);
// http/https/sio server init ----------------------------------------- // http/https/sio server init -----------------------------------------
var key = "", cert = "", ca = undefined; var key = "", cert = "", ca = undefined;
@ -241,4 +255,3 @@ Server.prototype.shutdown = function () {
process.exit(1); process.exit(1);
}); });
}; };

View File

@ -92,7 +92,7 @@ function handleChangePassword(req, res) {
return; return;
} }
Logger.eventlog.log("[account] " + webserver.ipForRequest(req) + Logger.eventlog.log("[account] " + req.realIP +
" changed password for " + name); " changed password for " + name);
db.users.getUser(name, function (err, user) { db.users.getUser(name, function (err, user) {
@ -172,7 +172,7 @@ function handleChangeEmail(req, res) {
}); });
return; return;
} }
Logger.eventlog.log("[account] " + webserver.ipForRequest(req) + Logger.eventlog.log("[account] " + req.realIP +
" changed email for " + name + " changed email for " + name +
" to " + email); " to " + email);
sendJade(res, "account-edit", { sendJade(res, "account-edit", {
@ -269,7 +269,7 @@ function handleNewChannel(req, res) {
db.channels.register(name, req.user.name, function (err, channel) { db.channels.register(name, req.user.name, function (err, channel) {
if (!err) { if (!err) {
Logger.eventlog.log("[channel] " + req.user.name + "@" + Logger.eventlog.log("[channel] " + req.user.name + "@" +
webserver.ipForRequest(req) + req.realIP +
" registered channel " + name); " registered channel " + name);
var sv = Server.getServer(); var sv = Server.getServer();
if (sv.isChannelLoaded(name)) { if (sv.isChannelLoaded(name)) {
@ -336,7 +336,7 @@ function handleDeleteChannel(req, res) {
db.channels.drop(name, function (err) { db.channels.drop(name, function (err) {
if (!err) { if (!err) {
Logger.eventlog.log("[channel] " + req.user.name + "@" + Logger.eventlog.log("[channel] " + req.user.name + "@" +
webserver.ipForRequest(req) + " deleted channel " + req.realIP + " deleted channel " +
name); name);
} }
var sv = Server.getServer(); var sv = Server.getServer();
@ -498,7 +498,7 @@ function handlePasswordReset(req, res) {
var hash = $util.sha1($util.randomSalt(64)); var hash = $util.sha1($util.randomSalt(64));
// 24-hour expiration // 24-hour expiration
var expire = Date.now() + 86400000; var expire = Date.now() + 86400000;
var ip = webserver.ipForRequest(req); var ip = req.realIP;
db.addPasswordReset({ db.addPasswordReset({
ip: ip, ip: ip,
@ -575,7 +575,7 @@ function handlePasswordRecover(req, res) {
return; return;
} }
var ip = webserver.ipForRequest(req); var ip = req.realIP;
db.lookupPasswordReset(hash, function (err, row) { db.lookupPasswordReset(hash, function (err, row) {
if (err) { if (err) {

View File

@ -15,7 +15,7 @@ function checkAdmin(cb) {
if (req.user.global_rank < 255) { if (req.user.global_rank < 255) {
res.send(403); res.send(403);
Logger.eventlog.log("[acp] Attempted GET "+req.path+" from non-admin " + Logger.eventlog.log("[acp] Attempted GET "+req.path+" from non-admin " +
user.name + "@" + webserver.ipForRequest(req)); user.name + "@" + req.realIP);
return; return;
} }

View File

@ -54,7 +54,7 @@ function handleLogin(req, res) {
if (err) { if (err) {
if (err === "Invalid username/password combination") { if (err === "Invalid username/password combination") {
Logger.eventlog.log("[loginfail] Login failed (bad password): " + name Logger.eventlog.log("[loginfail] Login failed (bad password): " + name
+ "@" + webserver.ipForRequest(req)); + "@" + req.realIP);
} }
sendJade(res, "login", { sendJade(res, "login", {
loggedIn: false, loggedIn: false,
@ -127,7 +127,7 @@ function handleLogout(req, res) {
res.clearCookie("auth"); res.clearCookie("auth");
req.user = res.user = null; req.user = res.user = null;
// Try to find an appropriate redirect // Try to find an appropriate redirect
var dest = req.query.dest || req.header("referer"); var dest = req.body.dest || req.header("referer");
dest = dest && dest.match(/login|logout|account/) ? null : dest; dest = dest && dest.match(/login|logout|account/) ? null : dest;
var host = req.hostname; var host = req.hostname;
@ -173,7 +173,7 @@ function handleRegister(req, res) {
if (typeof email !== "string") { if (typeof email !== "string") {
email = ""; email = "";
} }
var ip = webserver.ipForRequest(req); var ip = req.realIP;
if (typeof name !== "string" || typeof password !== "string") { if (typeof name !== "string" || typeof password !== "string") {
res.sendStatus(400); res.sendStatus(400);
@ -234,7 +234,7 @@ module.exports = {
init: function (app) { init: function (app) {
app.get("/login", handleLoginPage); app.get("/login", handleLoginPage);
app.post("/login", handleLogin); app.post("/login", handleLogin);
app.get("/logout", handleLogout); app.post("/logout", handleLogout);
app.get("/register", handleRegisterPage); app.get("/register", handleRegisterPage);
app.post("/register", handleRegister); app.post("/register", handleRegister);
} }

View File

@ -2,8 +2,9 @@
* Adapted from https://github.com/expressjs/csurf * Adapted from https://github.com/expressjs/csurf
*/ */
import { CSRFError } from '../errors';
var csrf = require("csrf"); var csrf = require("csrf");
var createError = require("http-errors");
var tokens = csrf(); var tokens = csrf();
@ -39,8 +40,6 @@ exports.verify = function csrfVerify(req) {
var token = req.body._csrf || req.query._csrf; var token = req.body._csrf || req.query._csrf;
if (!tokens.verify(secret, token)) { if (!tokens.verify(secret, token)) {
throw createError(403, 'invalid csrf token', { throw new CSRFError('Invalid CSRF token');
code: 'EBADCSRFTOKEN'
});
} }
}; };

4
src/web/httpstatus.js Normal file
View File

@ -0,0 +1,4 @@
export const BAD_REQUEST = 400;
export const FORBIDDEN = 403;
export const NOT_FOUND = 404;
export const INTERNAL_SERVER_ERROR = 500;

View File

@ -0,0 +1,14 @@
import Promise from 'bluebird';
import Server from '../server';
var SERVER = null;
export default class LocalChannelIndex {
listPublicChannels() {
if (SERVER === null) {
SERVER = require('../server').getServer();
}
return Promise.resolve(SERVER.packChannelList(true));
}
}

View File

@ -0,0 +1,19 @@
const STATIC_RESOURCE = /\..+$/;
export default function initialize(app, session) {
app.use((req, res, next) => {
if (STATIC_RESOURCE.test(req.path)) {
return next();
} else if (!req.signedCookies || !req.signedCookies.auth) {
return nuext();
} else {
session.verifySession(req.signedCookies.auth, (err, account) => {
if (!err) {
req.user = res.user = account;
}
next();
});
}
});
}

View File

@ -0,0 +1,32 @@
import net from 'net';
export default function initialize(app, webConfig) {
function isTrustedProxy(ip) {
return webConfig.getTrustedProxies().indexOf(ip) >= 0;
}
function getForwardedIP(req) {
const xForwardedFor = req.header('x-forwarded-for');
if (!xForwardedFor) {
return req.ip;
}
const ipList = xForwardedFor.split(',');
for (let i = 0; i < ipList.length; i++) {
const ip = ipList[i].trim();
if (net.isIP(ip)) {
return ip;
}
}
return req.ip;
}
app.use((req, res, next) => {
if (isTrustedProxy(req.ip)) {
req.realIP = getForwardedIP(req);
}
next();
});
}

25
src/web/routes/channel.js Normal file
View File

@ -0,0 +1,25 @@
import CyTubeUtil from '../../utilities';
import { sanitizeText } from '../../xss';
import { sendJade } from '../jade';
import * as HTTPStatus from '../httpstatus';
import { HTTPError } from '../../errors';
export default function initialize(app, ioConfig) {
app.get('/r/:channel', (req, res) => {
if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) {
throw new HTTPError(`"${sanitizeText(req.params.channel)}" is not a valid ` +
'channel name.', { status: HTTPStatus.NOT_FOUND });
}
const endpoints = ioConfig.getSocketEndpoints();
if (endpoints.length === 0) {
throw new HTTPError('No socket.io endpoints configured');
}
const socketBaseURL = endpoints[0].url;
sendJade(res, 'channel', {
channelName: req.params.channel,
sioSource: `${socketBaseURL}/socket.io/socket.io.js`
});
});
}

26
src/web/routes/contact.js Normal file
View File

@ -0,0 +1,26 @@
import CyTubeUtil from '../../utilities';
import { sendJade } from '../jade';
export default function initialize(app, webConfig) {
app.get('/contact', (req, res) => {
// Basic obfuscation of email addresses to prevent spambots
// from picking them up. Not real encryption.
// Deobfuscated by clientside JS.
const contacts = webConfig.getEmailContacts().map(contact => {
const emkey = CyTubeUtil.randomSalt(16);
let email = new Array(contact.email.length);
for (let i = 0; i < contact.email.length; i++) {
email[i] = String.fromCharCode(
contact.email.charCodeAt(i) ^ emkey.charCodeAt(i % emkey.length)
);
}
contact.email = escape(email.join(""));
contact.emkey = escape(emkey);
return contact;
});
return sendJade(res, 'contact', {
contacts: contacts
});
});
}

19
src/web/routes/index.js Normal file
View File

@ -0,0 +1,19 @@
import { sendJade } from '../jade';
export default function initialize(app, channelIndex) {
app.get('/', (req, res) => {
channelIndex.listPublicChannels().then((channels) => {
channels.sort((a, b) => {
if (a.usercount === b.usercount) {
return a.uniqueName > b.uniqueName ? -1 : 1;
}
return b.usercount - a.usercount;
});
sendJade(res, 'index', {
channels: channels
});
});
});
}

View File

@ -1,16 +1,12 @@
import IOConfiguration from '../../configuration/ioconfig';
import NullClusterClient from '../../io/cluster/nullclusterclient';
import Config from '../../config'; import Config from '../../config';
import CyTubeUtil from '../../utilities'; import CyTubeUtil from '../../utilities';
import Logger from '../../logger'; import Logger from '../../logger';
import * as HTTPStatus from '../httpstatus';
export default function initialize(app) { export default function initialize(app, clusterClient) {
const ioConfig = IOConfiguration.fromOldConfig(Config);
const clusterClient = new NullClusterClient(ioConfig);
app.get('/socketconfig/:channel.json', (req, res) => { app.get('/socketconfig/:channel.json', (req, res) => {
if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) { if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) {
return res.status(404).json({ return res.status(HTTPStatus.NOT_FOUND).json({
error: `Channel "${req.params.channel}" does not exist.` error: `Channel "${req.params.channel}" does not exist.`
}); });
} }

View File

@ -1,55 +1,38 @@
var path = require("path"); import fs from 'fs';
var fs = require("fs"); import path from 'path';
var net = require("net"); import net from 'net';
var express = require("express"); import express from 'express';
var webroot = path.join(__dirname, "..", "www"); import { sendJade } from './jade';
var sendJade = require("./jade").sendJade; import Logger from '../logger';
var Server = require("../server"); import Config from '../config';
var $util = require("../utilities"); import bodyParser from 'body-parser';
var Logger = require("../logger"); import cookieParser from 'cookie-parser';
var Config = require("../config"); import serveStatic from 'serve-static';
var db = require("../database"); import morgan from 'morgan';
var bodyParser = require("body-parser"); import csrf from './csrf';
var cookieParser = require("cookie-parser"); import * as HTTPStatus from './httpstatus';
var serveStatic = require("serve-static"); import { CSRFError, HTTPError } from '../errors';
var morgan = require("morgan");
var session = require("../session");
var csrf = require("./csrf");
var XSS = require("../xss");
import counters from "../counters"; import counters from "../counters";
const LOG_FORMAT = ':real-address - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'; function initializeLog(app) {
morgan.token('real-address', function (req) { return req._ip; }); const logFormat = ':real-address - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"';
const logPath = path.join(__dirname, '..', '..', 'http.log');
/** const outputStream = fs.createWriteStream(logPath, {
* Extracts an IP address from a request. Uses X-Forwarded-For if the IP is localhost flags: 'a', // append to existing file
*/ encoding: 'utf8'
function ipForRequest(req) { });
var ip = req.ip; morgan.token('real-address', req => req.realIP);
if (ip === "127.0.0.1" || ip === "::1") { app.use(morgan(logFormat, {
var xforward = req.header("x-forwarded-for"); stream: outputStream
if (typeof xforward !== "string") { }));
xforward = [];
} else {
xforward = xforward.split(",");
}
for (var i = 0; i < xforward.length; i++) {
if (net.isIP(xforward[i])) {
return xforward[i];
}
}
return ip;
}
return ip;
} }
/** /**
* Redirects a request to HTTPS if the server supports it * Redirects a request to HTTPS if the server supports it
*/ */
function redirectHttps(req, res) { function redirectHttps(req, res) {
if (!req.secure && Config.get("https.enabled") && Config.get("https.redirect")) { if (!req.secure && Config.get('https.enabled') && Config.get('https.redirect')) {
var ssldomain = Config.get("https.full-address"); var ssldomain = Config.get('https.full-address');
if (ssldomain.indexOf(req.hostname) < 0) { if (ssldomain.indexOf(req.hostname) < 0) {
return false; return false;
} }
@ -65,120 +48,92 @@ function redirectHttps(req, res) {
*/ */
function redirectHttp(req, res) { function redirectHttp(req, res) {
if (req.secure) { if (req.secure) {
var domain = Config.get("http.full-address"); var domain = Config.get('http.full-address');
res.redirect(domain + req.path); res.redirect(domain + req.path);
return true; return true;
} }
return false; return false;
} }
/**
* Handles a GET request for /r/:channel - serves channel.html
*/
function handleChannel(req, res) {
if (!$util.isValidChannelName(req.params.channel)) {
res.status(404);
res.send("Invalid channel name '" + XSS.sanitizeText(req.params.channel) + "'");
return;
}
var sio;
if (net.isIPv6(ipForRequest(req))) {
sio = Config.get("io.ipv6-default");
}
if (!sio) {
sio = Config.get("io.ipv4-default");
}
sio += "/socket.io/socket.io.js";
sendJade(res, "channel", {
channelName: req.params.channel,
sioSource: sio
});
}
/**
* Handles a request for the index page
*/
function handleIndex(req, res) {
var channels = Server.getServer().packChannelList(true);
channels.sort(function (a, b) {
if (a.usercount === b.usercount) {
return a.uniqueName > b.uniqueName ? -1 : 1;
}
return b.usercount - a.usercount;
});
sendJade(res, "index", {
channels: channels
});
}
/** /**
* Legacy socket.io configuration endpoint. This is being migrated to * Legacy socket.io configuration endpoint. This is being migrated to
* /socketconfig/<channel name>.json (see ./routes/socketconfig.js) * /socketconfig/<channel name>.json (see ./routes/socketconfig.js)
*/ */
function handleSocketConfig(req, res) { function handleLegacySocketConfig(req, res) {
if (/\.json$/.test(req.path)) { if (/\.json$/.test(req.path)) {
res.json(Config.get("sioconfigjson")); res.json(Config.get('sioconfigjson'));
return; return;
} }
res.type("application/javascript"); res.type('application/javascript');
var sioconfig = Config.get("sioconfig"); var sioconfig = Config.get('sioconfig');
var iourl; var iourl;
var ip = ipForRequest(req); var ip = req.realIP;
var ipv6 = false; var ipv6 = false;
if (net.isIPv6(ip)) { if (net.isIPv6(ip)) {
iourl = Config.get("io.ipv6-default"); iourl = Config.get('io.ipv6-default');
ipv6 = true; ipv6 = true;
} }
if (!iourl) { if (!iourl) {
iourl = Config.get("io.ipv4-default"); iourl = Config.get('io.ipv4-default');
} }
sioconfig += "var IO_URL='" + iourl + "';"; sioconfig += 'var IO_URL=\'' + iourl + '\';';
sioconfig += "var IO_V6=" + ipv6 + ";"; sioconfig += 'var IO_V6=' + ipv6 + ';';
res.send(sioconfig); res.send(sioconfig);
} }
function handleUserAgreement(req, res) { function handleUserAgreement(req, res) {
sendJade(res, "tos", { sendJade(res, 'tos', {
domain: Config.get("http.domain") domain: Config.get('http.domain')
}); });
} }
function handleContactPage(req, res) { function initializeErrorHandlers(app) {
// Make a copy to prevent messing with the original app.use((req, res, next) => {
var contacts = Config.get("contacts").map(function (c) { return next(new HTTPError(`No route for ${req.path}`, {
return { status: HTTPStatus.NOT_FOUND
name: c.name, }));
email: c.email,
title: c.title
};
}); });
// Rudimentary hiding of email addresses to prevent spambots app.use((err, req, res, next) => {
contacts.forEach(function (c) { if (err) {
c.emkey = $util.randomSalt(16) if (err instanceof CSRFError) {
var email = new Array(c.email.length); res.status(HTTPStatus.FORBIDDEN);
for (var i = 0; i < c.email.length; i++) { return sendJade(res, 'csrferror', {
email[i] = String.fromCharCode( path: req.path,
c.email.charCodeAt(i) ^ c.emkey.charCodeAt(i % c.emkey.length) referer: req.header('referer')
); });
}
let { message, status } = err;
if (!status) {
status = HTTPStatus.INTERNAL_SERVER_ERROR;
}
if (!message) {
message = 'An unknown error occurred.';
} else if (/\.(jade|js)/.test(message)) {
// Prevent leakage of stack traces
message = 'An internal error occurred.';
}
// Log 5xx (server) errors
if (Math.floor(status / 100) === 5) {
Logger.errlog.log(err.stack);
}
res.status(status);
return sendJade(res, 'httperror', {
path: req.path,
status: status,
message: message
});
} else {
next();
} }
c.email = escape(email.join(""));
c.emkey = escape(c.emkey);
});
sendJade(res, "contact", {
contacts: contacts
}); });
} }
@ -186,100 +141,60 @@ module.exports = {
/** /**
* Initializes webserver callbacks * Initializes webserver callbacks
*/ */
init: function (app) { init: function (app, webConfig, ioConfig, clusterClient, channelIndex, session) {
app.use(function (req, res, next) { app.use((req, res, next) => {
counters.add("http:request", 1); counters.add("http:request", 1);
req._ip = ipForRequest(req); req._ip = ipForRequest(req);
next(); next();
}); });
require('./middleware/x-forwarded-for')(app, webConfig);
app.use(bodyParser.urlencoded({ app.use(bodyParser.urlencoded({
extended: false, extended: false,
limit: '1kb' // No POST data should ever exceed this size under normal usage limit: '1kb' // No POST data should ever exceed this size under normal usage
})); }));
if (Config.get("http.cookie-secret") === "change-me") { if (webConfig.getCookieSecret() === 'change-me') {
Logger.errlog.log("YOU SHOULD CHANGE THE VALUE OF cookie-secret IN config.yaml"); Logger.errlog.log('WARNING: The configured cookie secret was left as the ' +
'default of "change-me".');
} }
app.use(cookieParser(Config.get("http.cookie-secret"))); app.use(cookieParser(webConfig.getCookieSecret()));
app.use(csrf.init(Config.get("http.root-domain-dotted"))); app.use(csrf.init(webConfig.getCookieDomain()));
app.use(morgan(LOG_FORMAT, { initializeLog(app);
stream: require("fs").createWriteStream(path.join(__dirname, "..", "..", require('./middleware/authorize')(app, session);
"http.log"), {
flags: "a",
encoding: "utf-8"
})
}));
app.use(function (req, res, next) { if (webConfig.getEnableGzip()) {
if (req.path.match(/^\/(css|js|img|boop).*$/)) { app.use(require('compression')({
return next(); threshold: webConfig.getGzipThreshold()
} }));
Logger.syslog.log('Enabled gzip compression');
if (!req.signedCookies || !req.signedCookies.auth) {
return next();
}
session.verifySession(req.signedCookies.auth, function (err, account) {
if (!err) {
req.user = res.user = account;
}
next();
});
});
if (Config.get("http.gzip")) {
app.use(require("compression")({ threshold: Config.get("http.gzip-threshold") }));
Logger.syslog.log("Enabled gzip compression");
} }
if (Config.get("http.minify")) { if (webConfig.getEnableMinification()) {
var cache = path.join(__dirname, "..", "..", "www", "cache") const cacheDir = path.join(__dirname, '..', '..', 'www', 'cache');
if (!fs.existsSync(cache)) { if (!fs.existsSync(cache)) {
fs.mkdirSync(cache); fs.mkdirSync(cache);
} }
app.use(require("express-minify")({ app.use(require('express-minify')({
cache: cache cache: cacheDir
})); }));
Logger.syslog.log("Enabled express-minify for CSS and JS"); Logger.syslog.log('Enabled express-minify for CSS and JS');
} }
app.get("/r/:channel", handleChannel); require('./routes/channel')(app, ioConfig);
app.get("/", handleIndex); require('./routes/index')(app, channelIndex);
app.get("/sioconfig(.json)?", handleSocketConfig); app.get('/sioconfig(.json)?', handleLegacySocketConfig);
require("./routes/socketconfig")(app); require('./routes/socketconfig')(app, clusterClient);
app.get("/useragreement", handleUserAgreement); app.get('/useragreement', handleUserAgreement);
app.get("/contact", handleContactPage); require('./routes/contact')(app, webConfig);
require("./auth").init(app); require('./auth').init(app);
require("./account").init(app); require('./account').init(app);
require("./acp").init(app); require('./acp').init(app);
require("../google2vtt").attach(app); require('../google2vtt').attach(app);
app.use(serveStatic(path.join(__dirname, "..", "..", "www"), { app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), {
maxAge: Config.get("http.max-age") || Config.get("http.cache-ttl") maxAge: webConfig.getCacheTTL()
})); }));
app.use(function (err, req, res, next) {
if (err) {
if (err.message && err.message.match(/failed to decode param/i)) {
return res.status(400).send("Malformed path: " + req.path);
} else if (err.message && err.message.match(/range not satisfiable/i)) {
return res.status(416).end();
} else if (err.message && err.message.match(/request entity too large/i)) {
return res.status(413).end();
} else if (err.message && err.message.match(/bad request/i)) {
return res.status(400).end("Bad Request");
} else if (err.message && err.message.match(/invalid csrf token/i)) {
res.status(403);
sendJade(res, 'csrferror', { path: req.path });
return;
}
Logger.errlog.log(err.stack);
res.status(500).end();
} else {
next();
}
});
},
ipForRequest: ipForRequest, initializeErrorHandlers(app);
},
redirectHttps: redirectHttps, redirectHttps: redirectHttps,

View File

@ -24,7 +24,8 @@ html(lang="en")
li A malicious user has attempted to tamper with your session li A malicious user has attempted to tamper with your session
li Your browser does not support cookies, or they are not enabled li Your browser does not support cookies, or they are not enabled
| If the problem persists, please contact an administrator. | If the problem persists, please contact an administrator.
a(href=path) Return to previous page if referer
a(href=referer) Return to previous page
include footer include footer
mixin footer() mixin footer()

38
templates/httperror.jade Normal file
View File

@ -0,0 +1,38 @@
mixin notfound()
h1 Not Found
p The page you were looking for doesn't seem to exist. Please check that you typed the URL correctly.
if message
p Reason: #{message}
mixin forbidden()
h1 Forbidden
p You don't have permission to access <code>#{path}</code>
mixin genericerror()
h1 Oops
p Your request could not be processed. Status code: <code>#{status}</code>, message: <code>#{message}</code>
doctype html
html(lang="en")
head
include head
mixin head()
body
#wrap
nav.navbar.navbar-inverse.navbar-fixed-top(role="navigation")
include nav
mixin navheader()
#nav-collapsible.collapse.navbar-collapse
ul.nav.navbar-nav
mixin navdefaultlinks(path)
mixin navloginlogout(path)
section#mainpage.container
.col-md-12
.alert.alert-danger
if status == 404
mixin notfound()
else if status == 403
mixin forbidden()
else
mixin genericerror()
include footer
mixin footer()

View File

@ -67,8 +67,10 @@ mixin navloginform(redirect)
mixin navlogoutform(redirect) mixin navlogoutform(redirect)
p#logoutform.navbar-text.pull-right form#logoutform.navbar-text.pull-right(action="/logout", method="post")
input(type="hidden", name="dest", value=baseUrl + redirect)
input(type="hidden", name="_csrf", value=csrfToken)
span#welcome Welcome, #{loginName} span#welcome Welcome, #{loginName}
span &nbsp;&middot;&nbsp; span &nbsp;&middot;&nbsp;
a#logout.navbar-link(href="/logout?dest=#{encodeURIComponent(baseUrl + redirect)}&_csrf=#{csrfToken}") Logout input#logout.navbar-link(type="submit", value="Logout")

View File

@ -639,3 +639,13 @@ li.vjs-menu-item.vjs-selected {
.video-js video::-webkit-media-text-track-container { .video-js video::-webkit-media-text-track-container {
bottom: 50px; bottom: 50px;
} }
input#logout[type="submit"] {
background: none;
border: none;
padding: 0;
}
input#logout[type="submit"]:hover {
text-decoration: underline;
}