From 50ca141f1d2e970ede144887d6c8f0e00e66f391 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Mon, 26 Oct 2015 22:56:53 -0700 Subject: [PATCH] Web refactoring --- index.js | 1 + src/errors.js | 5 ++ src/server.js | 11 ++++- src/web/csrf.js | 7 ++- src/web/httpstatus.js | 3 ++ src/web/localchannelindex.js | 14 ++++++ src/web/routes/channel.js | 25 ++++++++++ src/web/routes/index.js | 19 ++++++++ src/web/routes/socketconfig.js | 10 ++-- src/web/webserver.js | 86 +++++++++------------------------- 10 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 src/web/httpstatus.js create mode 100644 src/web/localchannelindex.js create mode 100644 src/web/routes/channel.js create mode 100644 src/web/routes/index.js 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/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 31e5bfbe..234bd57d 100644 --- a/src/server.js +++ b/src/server.js @@ -42,6 +42,9 @@ 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 NullClusterClient from './io/cluster/nullclusterclient'; var Server = function () { var self = this; @@ -60,8 +63,14 @@ var Server = function () { ChannelStore.init(); // webserver init ----------------------------------------------------- + const ioConfig = IOConfiguration.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, + ioConfig, + clusterClient, + channelIndex); // http/https/sio server init ----------------------------------------- var key = "", cert = "", ca = undefined; 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..eaba24d3 --- /dev/null +++ b/src/web/httpstatus.js @@ -0,0 +1,3 @@ +export const BAD_REQUEST = 400; +export const FORBIDDEN = 403; +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/routes/channel.js b/src/web/routes/channel.js new file mode 100644 index 00000000..4a185f3e --- /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.BAD_REQUEST }); + } + + 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/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 a15d0d47..0b4bdc0a 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -16,6 +16,8 @@ var morgan = require("morgan"); var session = require("../session"); var csrf = require("./csrf"); var XSS = require("../xss"); +import * as HTTPStatus from './httpstatus'; +import { CSRFError } 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._ip; }); @@ -71,51 +73,6 @@ function redirectHttp(req, res) { 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) @@ -185,7 +142,7 @@ module.exports = { /** * Initializes webserver callbacks */ - init: function (app) { + init: function (app, ioConfig, clusterClient, channelIndex) { app.use(function (req, res, next) { req._ip = ipForRequest(req); next(); @@ -241,10 +198,10 @@ module.exports = { Logger.syslog.log("Enabled express-minify for CSS and JS"); } - app.get("/r/:channel", handleChannel); - app.get("/", handleIndex); + require("./routes/channel")(app, ioConfig); + require("./routes/index")(app, channelIndex); app.get("/sioconfig(.json)?", handleSocketConfig); - require("./routes/socketconfig")(app); + require("./routes/socketconfig")(app, clusterClient); app.get("/useragreement", handleUserAgreement); app.get("/contact", handleContactPage); require("./auth").init(app); @@ -256,21 +213,24 @@ module.exports = { })); 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; + if (err instanceof CSRFError) { + res.status(HTTPStatus.FORBIDDEN); + return sendJade(res, 'csrferror', { path: req.path }); } - Logger.errlog.log(err.stack); - res.status(500).end(); + + let { message, status } = err; + if (!status) { + status = HTTPStatus.INTERNAL_SERVER_ERROR; + } + if (!message) { + message = 'An unknown error occurred.'; + } + + if (Math.floor(status / 100) === 5) { + Logger.errlog.log(err.stack); + } + + return res.status(status).send(message); } else { next(); }