diff --git a/src/io/ioserver.js b/src/io/ioserver.js index 9077f891..603f5b18 100644 --- a/src/io/ioserver.js +++ b/src/io/ioserver.js @@ -15,6 +15,7 @@ var crypto = require("crypto"); var isTorExit = require("../tor").isTorExit; var session = require("../session"); import counters from '../counters'; +import { verifyIPSessionCookie } from '../web/middleware/ipsessioncookie'; var CONNECT_RATE = { burst: 5, @@ -25,33 +26,53 @@ var ipThrottle = {}; // Keep track of number of connections per IP var ipCount = {}; +function parseCookies(socket, accept) { + var req = socket.request; + if (req.headers.cookie) { + cookieParser(req, null, () => { + accept(null, true); + }); + } else { + req.cookies = {}; + req.signedCookies = {}; + accept(null, true); + } +} /** * Called before an incoming socket.io connection is accepted. */ function handleAuth(socket, accept) { - var data = socket.request; - socket.user = false; - if (data.headers.cookie) { - cookieParser(data, null, function () { - var auth = data.signedCookies.auth; - if (!auth) { - return accept(null, true); - } - - session.verifySession(auth, function (err, user) { - if (!err) { - socket.user = { - name: user.name, - global_rank: user.global_rank - }; - } - accept(null, true); - }); - }); - } else { - accept(null, true); + var auth = socket.request.signedCookies.auth; + if (!auth) { + return accept(null, true); } + + session.verifySession(auth, function (err, user) { + if (!err) { + socket.user = { + name: user.name, + global_rank: user.global_rank + }; + } + accept(null, true); + }); +} + +function handleIPSessionCookie(socket, accept) { + var cookie = socket.request.signedCookies['ip-session']; + if (!cookie) { + socket.ipSessionFirstSeen = new Date(); + return accept(null, true); + } + + var sessionMatch = verifyIPSessionCookie(socket._realip, cookie); + if (sessionMatch) { + socket.ipSessionFirstSeen = sessionMatch.date; + } else { + socket.ipSessionFirstSeen = new Date(); + } + accept(null, true); } function throttleIP(sock) { @@ -247,8 +268,10 @@ module.exports = { }; var io = sio.instance = sio(); - io.use(handleAuth); io.use(ipForwardingMiddleware(webConfig)); + io.use(parseCookies); + io.use(handleIPSessionCookie); + io.use(handleAuth); io.on("connection", handleConnection); Config.get("listen").forEach(function (bind) { diff --git a/src/server.js b/src/server.js index 593e21b4..3a5c328a 100644 --- a/src/server.js +++ b/src/server.js @@ -18,6 +18,11 @@ module.exports = { exists || fs.mkdir(chandumppath); }); + var statepath = path.join(__dirname, "../state"); + fs.exists(statepath, function (exists) { + exists || fs.mkdir(statepath); + }); + var gdvttpath = path.join(__dirname, "../google-drive-subtitles"); fs.exists(gdvttpath, function (exists) { exists || fs.mkdir(gdvttpath); diff --git a/src/web/middleware/ipsessioncookie.js b/src/web/middleware/ipsessioncookie.js new file mode 100644 index 00000000..c2cd849b --- /dev/null +++ b/src/web/middleware/ipsessioncookie.js @@ -0,0 +1,76 @@ +import path from 'path'; +import fs from 'fs'; +import crypto from 'crypto'; + +const SALT_PATH = path.resolve(__dirname, '..', '..', '..', 'state', 'ipsessionsalt.json'); + +var SALT; +try { + SALT = require(SALT_PATH); +} catch (error) { + SALT = crypto.randomBytes(32).toString('base64'); + fs.writeFileSync(SALT_PATH, SALT); +} + +function sha256(input) { + var hash = crypto.createHash("sha256"); + hash.update(input); + return hash.digest("base64"); +} + +export function createIPSessionCookie(ip, date) { + const hashInput = [ + ip, + date.getTime(), + SALT + ].join(':'); + + return [ + date.getTime(), + sha256(hashInput) + ].join(':'); +} + +export function verifyIPSessionCookie(ip, cookie) { + const parts = cookie.split(':'); + if (parts.length !== 2) { + return false; + } + + const timestamp = parseInt(parts[0], 10); + if (isNaN(timestamp)) { + return false; + } + + const date = new Date(timestamp); + const expected = createIPSessionCookie(ip, date); + if (expected !== cookie) { + return false; + } + + return { + date: date, + }; +} + +export function ipSessionCookieMiddleware(req, res, next) { + var firstSeen = new Date(); + var hasSession = false; + if (req.signedCookies && req.signedCookies['ip-session']) { + var sessionMatch = verifyIPSessionCookie(req.realIP, req.signedCookies['ip-session']); + if (sessionMatch) { + hasSession = true; + firstSeen = sessionMatch.date; + } + } + + if (!hasSession) { + res.cookie('ip-session', createIPSessionCookie(req.realIP, firstSeen), { + signed: true, + httpOnly: true + }); + } + + req.ipSessionFirstSeen = firstSeen; + next(); +} diff --git a/src/web/webserver.js b/src/web/webserver.js index b428517f..e34a2caf 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -146,6 +146,7 @@ module.exports = { } app.use(cookieParser(webConfig.getCookieSecret())); app.use(csrf.init(webConfig.getCookieDomain())); + app.use('/r/:channel', require('./middleware/ipsessioncookie').ipSessionCookieMiddleware); initializeLog(app); require('./middleware/authorize')(app, session);