diff --git a/src/configuration/webconfig.js b/src/configuration/webconfig.js index da977acd..aee7322a 100644 --- a/src/configuration/webconfig.js +++ b/src/configuration/webconfig.js @@ -1,9 +1,9 @@ import clone from 'clone'; -const DEFAULT_TRUSTED_PROXIES = [ +const DEFAULT_TRUSTED_PROXIES = Object.freeze([ '127.0.0.1', '::1' -]; +]); export default class WebConfiguration { constructor(config) { @@ -15,7 +15,31 @@ export default class WebConfiguration { } getTrustedProxies() { - return DEFAULT_TRUSTED_PROXIES.slice(); + 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; } } @@ -32,5 +56,19 @@ WebConfiguration.fromOldConfig = function (oldConfig) { }); }); + 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/server.js b/src/server.js index 54ae6557..c387ccd3 100644 --- a/src/server.js +++ b/src/server.js @@ -46,6 +46,7 @@ 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; @@ -73,7 +74,8 @@ var Server = function () { webConfig, ioConfig, clusterClient, - channelIndex); + channelIndex, + session); // http/https/sio server init ----------------------------------------- var key = "", cert = "", ca = undefined; @@ -255,4 +257,3 @@ Server.prototype.shutdown = function () { process.exit(1); }); }; - 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/webserver.js b/src/web/webserver.js index 726c7b68..f06d109a 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -1,33 +1,37 @@ -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'; -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.realIP; }); +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; } @@ -43,7 +47,7 @@ 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; } @@ -54,36 +58,81 @@ function redirectHttp(req, res) { * 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 = 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 initializeErrorHandlers(app) { + app.use((req, res, next) => { + return next(new HTTPError(`No route for ${req.path}`, { + status: HTTPStatus.NOT_FOUND + })); + }); + + 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(); + } }); } @@ -91,113 +140,54 @@ module.exports = { /** * Initializes webserver callbacks */ - init: function (app, webConfig, ioConfig, clusterClient, channelIndex) { - require("./middleware/x-forwarded-for")(app, webConfig); + init: function (app, webConfig, ioConfig, clusterClient, channelIndex, session) { + 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'); } - require("./routes/channel")(app, ioConfig); - require("./routes/index")(app, channelIndex); - app.get("/sioconfig(.json)?", handleSocketConfig); - 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: 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((req, res, next) => { - return next(new HTTPError(`No route for ${req.path}`, { - status: HTTPStatus.NOT_FOUND - })); - }); - app.use(function (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(); - } - }); + initializeErrorHandlers(app); }, redirectHttps: redirectHttps,