diff --git a/index.js b/index.js index 0cebe046..2cda75be 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ try { } catch (err) { console.error('FATAL: Failed to require() lib/server.js'); console.error('Have you run `npm run build-server` yet to generate it?'); + console.error(err.stack); process.exit(1); } var Config = require("./lib/config"); diff --git a/package.json b/package.json index 16de6baa..7cb574c4 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "bluebird": "^2.10.1", "body-parser": "^1.14.0", "cheerio": "^0.19.0", + "clone": "^1.0.2", "compression": "^1.5.2", "cookie-parser": "^1.4.0", "create-error": "^0.3.1", diff --git a/src/configuration/webconfig.js b/src/configuration/webconfig.js new file mode 100644 index 00000000..aee7322a --- /dev/null +++ b/src/configuration/webconfig.js @@ -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); +}; diff --git a/src/errors.js b/src/errors.js index d8ea077b..f5b45089 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,4 +1,9 @@ import createError from 'create-error'; +import * as HTTPStatus from './web/httpstatus'; export const ChannelStateSizeError = createError('ChannelStateSizeError'); export const ChannelNotFoundError = createError('ChannelNotFoundError'); +export const CSRFError = createError('CSRFError'); +export const HTTPError = createError('HTTPError', { + status: HTTPStatus.INTERNAL_SERVER_ERROR +}); diff --git a/src/server.js b/src/server.js index 6419fee6..91a39322 100644 --- a/src/server.js +++ b/src/server.js @@ -42,6 +42,11 @@ var $util = require("./utilities"); var db = require("./database"); var Flags = require("./flags"); 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 self = this; @@ -60,8 +65,17 @@ var Server = function () { ChannelStore.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(); - require("./web/webserver").init(self.express); + require("./web/webserver").init(self.express, + webConfig, + ioConfig, + clusterClient, + channelIndex, + session); // http/https/sio server init ----------------------------------------- var key = "", cert = "", ca = undefined; @@ -241,4 +255,3 @@ Server.prototype.shutdown = function () { process.exit(1); }); }; - diff --git a/src/web/account.js b/src/web/account.js index 19d61fff..627d341e 100644 --- a/src/web/account.js +++ b/src/web/account.js @@ -92,7 +92,7 @@ function handleChangePassword(req, res) { return; } - Logger.eventlog.log("[account] " + webserver.ipForRequest(req) + + Logger.eventlog.log("[account] " + req.realIP + " changed password for " + name); db.users.getUser(name, function (err, user) { @@ -172,7 +172,7 @@ function handleChangeEmail(req, res) { }); return; } - Logger.eventlog.log("[account] " + webserver.ipForRequest(req) + + Logger.eventlog.log("[account] " + req.realIP + " changed email for " + name + " to " + email); sendJade(res, "account-edit", { @@ -269,7 +269,7 @@ function handleNewChannel(req, res) { db.channels.register(name, req.user.name, function (err, channel) { if (!err) { Logger.eventlog.log("[channel] " + req.user.name + "@" + - webserver.ipForRequest(req) + + req.realIP + " registered channel " + name); var sv = Server.getServer(); if (sv.isChannelLoaded(name)) { @@ -336,7 +336,7 @@ function handleDeleteChannel(req, res) { db.channels.drop(name, function (err) { if (!err) { Logger.eventlog.log("[channel] " + req.user.name + "@" + - webserver.ipForRequest(req) + " deleted channel " + + req.realIP + " deleted channel " + name); } var sv = Server.getServer(); @@ -498,7 +498,7 @@ function handlePasswordReset(req, res) { var hash = $util.sha1($util.randomSalt(64)); // 24-hour expiration var expire = Date.now() + 86400000; - var ip = webserver.ipForRequest(req); + var ip = req.realIP; db.addPasswordReset({ ip: ip, @@ -575,7 +575,7 @@ function handlePasswordRecover(req, res) { return; } - var ip = webserver.ipForRequest(req); + var ip = req.realIP; db.lookupPasswordReset(hash, function (err, row) { if (err) { diff --git a/src/web/acp.js b/src/web/acp.js index 707c6a8b..d418417b 100644 --- a/src/web/acp.js +++ b/src/web/acp.js @@ -15,7 +15,7 @@ function checkAdmin(cb) { if (req.user.global_rank < 255) { res.send(403); Logger.eventlog.log("[acp] Attempted GET "+req.path+" from non-admin " + - user.name + "@" + webserver.ipForRequest(req)); + user.name + "@" + req.realIP); return; } diff --git a/src/web/auth.js b/src/web/auth.js index 09c7384e..5e639db8 100644 --- a/src/web/auth.js +++ b/src/web/auth.js @@ -54,7 +54,7 @@ function handleLogin(req, res) { if (err) { if (err === "Invalid username/password combination") { Logger.eventlog.log("[loginfail] Login failed (bad password): " + name - + "@" + webserver.ipForRequest(req)); + + "@" + req.realIP); } sendJade(res, "login", { loggedIn: false, @@ -127,7 +127,7 @@ function handleLogout(req, res) { res.clearCookie("auth"); req.user = res.user = null; // 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; var host = req.hostname; @@ -173,7 +173,7 @@ function handleRegister(req, res) { if (typeof email !== "string") { email = ""; } - var ip = webserver.ipForRequest(req); + var ip = req.realIP; if (typeof name !== "string" || typeof password !== "string") { res.sendStatus(400); @@ -234,7 +234,7 @@ module.exports = { init: function (app) { app.get("/login", handleLoginPage); app.post("/login", handleLogin); - app.get("/logout", handleLogout); + app.post("/logout", handleLogout); app.get("/register", handleRegisterPage); app.post("/register", handleRegister); } diff --git a/src/web/csrf.js b/src/web/csrf.js index 688370ca..2fd6fa66 100644 --- a/src/web/csrf.js +++ b/src/web/csrf.js @@ -2,8 +2,9 @@ * Adapted from https://github.com/expressjs/csurf */ +import { CSRFError } from '../errors'; + var csrf = require("csrf"); -var createError = require("http-errors"); var tokens = csrf(); @@ -39,8 +40,6 @@ exports.verify = function csrfVerify(req) { var token = req.body._csrf || req.query._csrf; if (!tokens.verify(secret, token)) { - throw createError(403, 'invalid csrf token', { - code: 'EBADCSRFTOKEN' - }); + throw new CSRFError('Invalid CSRF token'); } }; diff --git a/src/web/httpstatus.js b/src/web/httpstatus.js new file mode 100644 index 00000000..b2e2430d --- /dev/null +++ b/src/web/httpstatus.js @@ -0,0 +1,4 @@ +export const BAD_REQUEST = 400; +export const FORBIDDEN = 403; +export const NOT_FOUND = 404; +export const INTERNAL_SERVER_ERROR = 500; diff --git a/src/web/localchannelindex.js b/src/web/localchannelindex.js new file mode 100644 index 00000000..6dc73c32 --- /dev/null +++ b/src/web/localchannelindex.js @@ -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)); + } +} diff --git a/src/web/middleware/authorize.js b/src/web/middleware/authorize.js new file mode 100644 index 00000000..e4fbba4c --- /dev/null +++ b/src/web/middleware/authorize.js @@ -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(); + }); + } + }); +} diff --git a/src/web/middleware/x-forwarded-for.js b/src/web/middleware/x-forwarded-for.js new file mode 100644 index 00000000..28b93098 --- /dev/null +++ b/src/web/middleware/x-forwarded-for.js @@ -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(); + }); +} diff --git a/src/web/routes/channel.js b/src/web/routes/channel.js new file mode 100644 index 00000000..457565d9 --- /dev/null +++ b/src/web/routes/channel.js @@ -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` + }); + }); +} diff --git a/src/web/routes/contact.js b/src/web/routes/contact.js new file mode 100644 index 00000000..32907469 --- /dev/null +++ b/src/web/routes/contact.js @@ -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 + }); + }); +} diff --git a/src/web/routes/index.js b/src/web/routes/index.js new file mode 100644 index 00000000..86aaf9b9 --- /dev/null +++ b/src/web/routes/index.js @@ -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 + }); + }); + }); +} diff --git a/src/web/routes/socketconfig.js b/src/web/routes/socketconfig.js index 836a6916..b6f04d87 100644 --- a/src/web/routes/socketconfig.js +++ b/src/web/routes/socketconfig.js @@ -1,16 +1,12 @@ -import IOConfiguration from '../../configuration/ioconfig'; -import NullClusterClient from '../../io/cluster/nullclusterclient'; import Config from '../../config'; import CyTubeUtil from '../../utilities'; import Logger from '../../logger'; +import * as HTTPStatus from '../httpstatus'; -export default function initialize(app) { - const ioConfig = IOConfiguration.fromOldConfig(Config); - const clusterClient = new NullClusterClient(ioConfig); - +export default function initialize(app, clusterClient) { app.get('/socketconfig/:channel.json', (req, res) => { 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.` }); } diff --git a/src/web/webserver.js b/src/web/webserver.js index c83c3e74..5416feaa 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -1,55 +1,38 @@ -var path = require("path"); -var fs = require("fs"); -var net = require("net"); -var express = require("express"); -var webroot = path.join(__dirname, "..", "www"); -var sendJade = require("./jade").sendJade; -var Server = require("../server"); -var $util = require("../utilities"); -var Logger = require("../logger"); -var Config = require("../config"); -var db = require("../database"); -var bodyParser = require("body-parser"); -var cookieParser = require("cookie-parser"); -var serveStatic = require("serve-static"); -var morgan = require("morgan"); -var session = require("../session"); -var csrf = require("./csrf"); -var XSS = require("../xss"); +import fs from 'fs'; +import path from 'path'; +import net from 'net'; +import express from 'express'; +import { sendJade } from './jade'; +import Logger from '../logger'; +import Config from '../config'; +import bodyParser from 'body-parser'; +import cookieParser from 'cookie-parser'; +import serveStatic from 'serve-static'; +import morgan from 'morgan'; +import csrf from './csrf'; +import * as HTTPStatus from './httpstatus'; +import { CSRFError, HTTPError } from '../errors'; import counters from "../counters"; -const LOG_FORMAT = ':real-address - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'; -morgan.token('real-address', function (req) { return req._ip; }); - -/** - * Extracts an IP address from a request. Uses X-Forwarded-For if the IP is localhost - */ -function ipForRequest(req) { - var ip = req.ip; - if (ip === "127.0.0.1" || ip === "::1") { - var xforward = req.header("x-forwarded-for"); - 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; +function initializeLog(app) { + 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, { + flags: 'a', // append to existing file + encoding: 'utf8' + }); + morgan.token('real-address', req => req.realIP); + app.use(morgan(logFormat, { + stream: outputStream + })); } /** * Redirects a request to HTTPS if the server supports it */ function redirectHttps(req, res) { - if (!req.secure && Config.get("https.enabled") && Config.get("https.redirect")) { - var ssldomain = Config.get("https.full-address"); + if (!req.secure && Config.get('https.enabled') && Config.get('https.redirect')) { + var ssldomain = Config.get('https.full-address'); if (ssldomain.indexOf(req.hostname) < 0) { return false; } @@ -65,120 +48,92 @@ function redirectHttps(req, res) { */ function redirectHttp(req, res) { if (req.secure) { - var domain = Config.get("http.full-address"); + var domain = Config.get('http.full-address'); res.redirect(domain + req.path); return true; } 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 * /socketconfig/.json (see ./routes/socketconfig.js) */ -function handleSocketConfig(req, res) { +function handleLegacySocketConfig(req, res) { if (/\.json$/.test(req.path)) { - res.json(Config.get("sioconfigjson")); + res.json(Config.get('sioconfigjson')); return; } - res.type("application/javascript"); + res.type('application/javascript'); - var sioconfig = Config.get("sioconfig"); + var sioconfig = Config.get('sioconfig'); var iourl; - var ip = ipForRequest(req); + var ip = req.realIP; var ipv6 = false; if (net.isIPv6(ip)) { - iourl = Config.get("io.ipv6-default"); + iourl = Config.get('io.ipv6-default'); ipv6 = true; } if (!iourl) { - iourl = Config.get("io.ipv4-default"); + iourl = Config.get('io.ipv4-default'); } - sioconfig += "var IO_URL='" + iourl + "';"; - sioconfig += "var IO_V6=" + ipv6 + ";"; + sioconfig += 'var IO_URL=\'' + iourl + '\';'; + sioconfig += 'var IO_V6=' + ipv6 + ';'; res.send(sioconfig); } function handleUserAgreement(req, res) { - sendJade(res, "tos", { - domain: Config.get("http.domain") + sendJade(res, 'tos', { + domain: Config.get('http.domain') }); } -function handleContactPage(req, res) { - // Make a copy to prevent messing with the original - var contacts = Config.get("contacts").map(function (c) { - return { - name: c.name, - email: c.email, - title: c.title - }; +function initializeErrorHandlers(app) { + app.use((req, res, next) => { + return next(new HTTPError(`No route for ${req.path}`, { + status: HTTPStatus.NOT_FOUND + })); }); - // Rudimentary hiding of email addresses to prevent spambots - contacts.forEach(function (c) { - c.emkey = $util.randomSalt(16) - var email = new Array(c.email.length); - for (var i = 0; i < c.email.length; i++) { - email[i] = String.fromCharCode( - c.email.charCodeAt(i) ^ c.emkey.charCodeAt(i % c.emkey.length) - ); + app.use((err, req, res, next) => { + if (err) { + if (err instanceof CSRFError) { + res.status(HTTPStatus.FORBIDDEN); + return sendJade(res, 'csrferror', { + path: req.path, + 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 */ - init: function (app) { - app.use(function (req, res, next) { + init: function (app, webConfig, ioConfig, clusterClient, channelIndex, session) { + app.use((req, res, next) => { counters.add("http:request", 1); req._ip = ipForRequest(req); next(); }); + require('./middleware/x-forwarded-for')(app, webConfig); app.use(bodyParser.urlencoded({ extended: false, limit: '1kb' // No POST data should ever exceed this size under normal usage })); - if (Config.get("http.cookie-secret") === "change-me") { - Logger.errlog.log("YOU SHOULD CHANGE THE VALUE OF cookie-secret IN config.yaml"); + if (webConfig.getCookieSecret() === 'change-me') { + 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(csrf.init(Config.get("http.root-domain-dotted"))); - app.use(morgan(LOG_FORMAT, { - stream: require("fs").createWriteStream(path.join(__dirname, "..", "..", - "http.log"), { - flags: "a", - encoding: "utf-8" - }) - })); + app.use(cookieParser(webConfig.getCookieSecret())); + app.use(csrf.init(webConfig.getCookieDomain())); + initializeLog(app); + require('./middleware/authorize')(app, session); - app.use(function (req, res, next) { - if (req.path.match(/^\/(css|js|img|boop).*$/)) { - return next(); - } - - 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 (webConfig.getEnableGzip()) { + app.use(require('compression')({ + threshold: webConfig.getGzipThreshold() + })); + Logger.syslog.log('Enabled gzip compression'); } - if (Config.get("http.minify")) { - var cache = path.join(__dirname, "..", "..", "www", "cache") + if (webConfig.getEnableMinification()) { + const cacheDir = path.join(__dirname, '..', '..', 'www', 'cache'); if (!fs.existsSync(cache)) { fs.mkdirSync(cache); } - app.use(require("express-minify")({ - cache: cache + app.use(require('express-minify')({ + 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); - app.get("/", handleIndex); - app.get("/sioconfig(.json)?", handleSocketConfig); - require("./routes/socketconfig")(app); - app.get("/useragreement", handleUserAgreement); - app.get("/contact", handleContactPage); - require("./auth").init(app); - require("./account").init(app); - require("./acp").init(app); - require("../google2vtt").attach(app); - app.use(serveStatic(path.join(__dirname, "..", "..", "www"), { - maxAge: Config.get("http.max-age") || Config.get("http.cache-ttl") + require('./routes/channel')(app, ioConfig); + require('./routes/index')(app, channelIndex); + app.get('/sioconfig(.json)?', handleLegacySocketConfig); + require('./routes/socketconfig')(app, clusterClient); + app.get('/useragreement', handleUserAgreement); + require('./routes/contact')(app, webConfig); + require('./auth').init(app); + require('./account').init(app); + require('./acp').init(app); + require('../google2vtt').attach(app); + app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), { + 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, diff --git a/templates/csrferror.jade b/templates/csrferror.jade index b67bd428..a3a8add3 100644 --- a/templates/csrferror.jade +++ b/templates/csrferror.jade @@ -24,7 +24,8 @@ html(lang="en") li A malicious user has attempted to tamper with your session li Your browser does not support cookies, or they are not enabled | 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 mixin footer() diff --git a/templates/httperror.jade b/templates/httperror.jade new file mode 100644 index 00000000..56909bc9 --- /dev/null +++ b/templates/httperror.jade @@ -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 #{path} +mixin genericerror() + h1 Oops + p Your request could not be processed. Status code: #{status}, message: #{message} +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() diff --git a/templates/nav.jade b/templates/nav.jade index 8068c9b3..3645e99d 100644 --- a/templates/nav.jade +++ b/templates/nav.jade @@ -67,8 +67,10 @@ mixin navloginform(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  ·  - a#logout.navbar-link(href="/logout?dest=#{encodeURIComponent(baseUrl + redirect)}&_csrf=#{csrfToken}") Logout + input#logout.navbar-link(type="submit", value="Logout") diff --git a/www/css/cytube.css b/www/css/cytube.css index 840f6f4a..0f9b4f24 100644 --- a/www/css/cytube.css +++ b/www/css/cytube.css @@ -639,3 +639,13 @@ li.vjs-menu-item.vjs-selected { .video-js video::-webkit-media-text-track-container { bottom: 50px; } + +input#logout[type="submit"] { + background: none; + border: none; + padding: 0; +} + +input#logout[type="submit"]:hover { + text-decoration: underline; +}