diff --git a/src/configuration/webconfig.js b/src/configuration/webconfig.js index 607094f7..da977acd 100644 --- a/src/configuration/webconfig.js +++ b/src/configuration/webconfig.js @@ -1,5 +1,10 @@ import clone from 'clone'; +const DEFAULT_TRUSTED_PROXIES = [ + '127.0.0.1', + '::1' +]; + export default class WebConfiguration { constructor(config) { this.config = config; @@ -8,6 +13,10 @@ export default class WebConfiguration { getEmailContacts() { return clone(this.config.contacts); } + + getTrustedProxies() { + return DEFAULT_TRUSTED_PROXIES.slice(); + } } WebConfiguration.fromOldConfig = function (oldConfig) { 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 e7c8578b..8fca7352 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, @@ -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); 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/webserver.js b/src/web/webserver.js index 3b2ba345..726c7b68 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -20,30 +20,7 @@ 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._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; -} +morgan.token('real-address', function (req) { return req.realIP; }); /** * Redirects a request to HTTPS if the server supports it @@ -87,7 +64,7 @@ function handleSocketConfig(req, res) { var sioconfig = Config.get("sioconfig"); var iourl; - var ip = ipForRequest(req); + var ip = req.realIP; var ipv6 = false; if (net.isIPv6(ip)) { @@ -115,10 +92,7 @@ module.exports = { * Initializes webserver callbacks */ init: function (app, webConfig, ioConfig, clusterClient, channelIndex) { - app.use(function (req, res, next) { - 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 @@ -226,8 +200,6 @@ module.exports = { }); }, - ipForRequest: ipForRequest, - redirectHttps: redirectHttps, redirectHttp: redirectHttp