mirror of https://github.com/calzoneman/sync.git
Merge branch 'web-refactoring' into 3.0
This commit is contained in:
commit
de9b963d38
1
index.js
1
index.js
|
@ -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");
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
|
@ -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
|
||||||
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const BAD_REQUEST = 400;
|
||||||
|
export const FORBIDDEN = 403;
|
||||||
|
export const NOT_FOUND = 404;
|
||||||
|
export const INTERNAL_SERVER_ERROR = 500;
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
|
@ -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`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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.`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
|
@ -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 ·
|
span ·
|
||||||
a#logout.navbar-link(href="/logout?dest=#{encodeURIComponent(baseUrl + redirect)}&_csrf=#{csrfToken}") Logout
|
input#logout.navbar-link(type="submit", value="Logout")
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue