├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── cookieParser.js ├── csrf.js ├── index.js ├── logger.js └── session.js ├── package.json └── test ├── cookieParser.js ├── csrf.js ├── logger.js ├── session.js └── support └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | Makefile 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 0.11 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Naoyuki Kanezawa 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Socket.IO-bundle 2 | [![Build Status](https://travis-ci.org/nkzawa/socket.io-bundle.png?branch=master)](https://travis-ci.org/nkzawa/socket.io-bundle) 3 | 4 | This is a collection of commonly used middlewares for [Socket.IO](https://github.com/LearnBoost/socket.io), which I wish Socket.IO was bundled with. 5 | Socket.IO-bundle is based on [Express](https://github.com/visionmedia/express) middlewares, so that you can easily integrate with Express and Connect. 6 | 7 | ```js 8 | var bundle = require('socket.io-bundle'); 9 | var server = require('http').Server(); 10 | var io = require('socket.io')(server); 11 | 12 | io.use(bundle.cookieParser()); 13 | io.use(bundle.session({secret: 'my secret here'})); 14 | io.use(bundle.csrf()); 15 | 16 | server.listen(3000); 17 | ``` 18 | 19 | Arguments for each middlewares are completely the same with Express's ones. 20 | You must be aware of that `session` middleware can’t set cookies to clients due to the behavior of Socket.I. 21 | 22 | ### CSRF 23 | 24 | Csrf tokens will be supplied to browsers via Express/Connect, and be sent to a Socket.IO server as a query parameter. 25 | 26 | ```js 27 | // client 28 | var socket = io('http://localhost:3000?_csrf=' + encodeURIComponent(_csrf)); 29 | ``` 30 | 31 | ## License 32 | MIT 33 | 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/'); 2 | -------------------------------------------------------------------------------- /lib/cookieParser.js: -------------------------------------------------------------------------------- 1 | 2 | var _cookieParser = require('cookie-parser'); 3 | 4 | 5 | module.exports = function cookieParser(secret) { 6 | var fn = _cookieParser(secret); 7 | 8 | return function cookieParser(socket, next) { 9 | var req = socket.request; 10 | var res = req.res; 11 | 12 | return fn(req, res, next); 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/csrf.js: -------------------------------------------------------------------------------- 1 | 2 | var _csrf = require('csurf'); 3 | 4 | 5 | /** 6 | * CSRF protection middleware. 7 | */ 8 | 9 | module.exports = function csrf(options) { 10 | var fn = _csrf(options); 11 | 12 | return function csrf(socket, next) { 13 | var req = socket.request; 14 | var res = req.res; 15 | var method = req.method; 16 | 17 | // A pseudo value to pass through the method check. 18 | req.method = 'POST'; 19 | 20 | if (!req.query) req.query = req._query; 21 | 22 | return fn(req, res, function(err) { 23 | // put back 24 | req.method = method; 25 | delete req.query; 26 | 27 | next(err); 28 | }); 29 | }; 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path'); 3 | 4 | 5 | var _filename = path.basename(__filename); 6 | 7 | fs.readdirSync(__dirname).forEach(function(filename) { 8 | if (filename == _filename) return; 9 | if (!/\.js$/.test(filename)) return; 10 | 11 | var name = path.basename(filename, '.js'); 12 | exports.__defineGetter__(name, function() { 13 | return require('./' + name); 14 | }); 15 | }); 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 2 | var _logger = require('morgan'); 3 | 4 | 5 | exports = module.exports = logger; 6 | 7 | /** 8 | * Expose properties. 9 | */ 10 | 11 | for (var key in _logger) { 12 | exports[key] = _logger[key]; 13 | } 14 | 15 | /** 16 | * Logger 17 | */ 18 | 19 | function logger(options) { 20 | if ('object' == typeof options) { 21 | options = options || {}; 22 | } else if (options) { 23 | options = { format: options }; 24 | } else { 25 | options = {}; 26 | } 27 | options.immediate = true; 28 | 29 | var fn = _logger(options); 30 | 31 | return function logger(socket, next) { 32 | var req = socket.request; 33 | var res = req.res; 34 | 35 | req.originalUrl = req.originalUrl || req.url; 36 | 37 | return fn(req, res, next); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var _session = require('express-session'); 6 | var debug = require('debug')('socket.io-bundle:session'); 7 | 8 | 9 | exports = module.exports = session; 10 | 11 | /** 12 | * Expose constructors. 13 | */ 14 | 15 | for (var key in _session) { 16 | exports[key] = _session[key]; 17 | } 18 | 19 | function session(options) { 20 | var fn = _session(options); 21 | 22 | return function session(socket, next) { 23 | var req = socket.request; 24 | var res = req.res; 25 | 26 | req.originalUrl = req.originalUrl || req.url; 27 | 28 | // proxy `onconnect` to commit the session. 29 | socket.onconnect = persist(socket.onconnect, req); 30 | 31 | return fn(req, res, next); 32 | }; 33 | } 34 | 35 | /** 36 | * Decorator to save the given req's session. 37 | * 38 | * @param {Function} fn 39 | * @param {ServerRequest} req 40 | * @return {Function} 41 | * @api private 42 | */ 43 | 44 | function persist(fn, req) { 45 | return function() { 46 | if (!req.session) return fn.apply(this, arguments); 47 | 48 | var self = this; 49 | var args = arguments; 50 | 51 | debug('saving'); 52 | req.session.resetMaxAge(); 53 | req.session.save(function(err) { 54 | if (err) console.error(err.stack); 55 | debug('saved'); 56 | fn.apply(self, args); 57 | }); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.io-bundle", 3 | "version": "0.1.2", 4 | "description": "A collection of middlewares for Socket.IO", 5 | "author": "Naoyuki Kanezawa ", 6 | "keywords": [ 7 | "middleware", 8 | "rack", 9 | "socket.io", 10 | "connect" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/nkzawa/socket.io-bundle.git" 15 | }, 16 | "scripts": { 17 | "test": "mocha --reporter dot" 18 | }, 19 | "dependencies": { 20 | "cookie-parser": "1.1.0", 21 | "csurf": "1.2.0", 22 | "morgan": "1.1.1", 23 | "express-session": "1.2.1", 24 | "debug": "1.0.0" 25 | }, 26 | "devDependencies": { 27 | "socket.io": "1.0.4", 28 | "socket.io-client": "1.0.4", 29 | "connect": "~2.19.3", 30 | "cookie-signature": "~1.0.3", 31 | "mocha": "*", 32 | "chai": "*" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/cookieParser.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var signature = require('cookie-signature'); 3 | var bundle = require('../'); 4 | var support = require('./support'); 5 | var client = support.client; 6 | 7 | 8 | describe('cookieParser', function() { 9 | beforeEach(function(done) { 10 | var self = this; 11 | 12 | support.startServer(this, function(e) { 13 | self.io.use(bundle.cookieParser('keyboard cat')); 14 | self.io.on('connection', function(socket) { 15 | socket.send(socket.request.cookies); 16 | }); 17 | self.io.of('/signed').on('connection', function(socket) { 18 | socket.send(socket.request.signedCookies); 19 | }); 20 | done(e); 21 | }); 22 | }); 23 | 24 | afterEach(function(done) { 25 | support.stopServer(this, done); 26 | }); 27 | 28 | describe('when no cookies are sent', function() { 29 | it('should default req.cookies to {}', function(done) { 30 | var socket = client(); 31 | socket.on('message', function(cookies) { 32 | expect(cookies).to.eql({}); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should default req.signedCookies to {}', function(done) { 38 | var socket = client('/signed'); 39 | socket.on('message', function(signedCookies) { 40 | expect(signedCookies).to.eql({}); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('when cookies are sent', function() { 47 | it('should populate req.cookies', function(done){ 48 | var socket = client('/', {headers: {cookie: 'foo=bar; bar=baz'}}); 49 | socket.on('message', function(cookies) { 50 | expect(cookies).to.eql({foo: 'bar', bar: 'baz'}); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('when a secret is given', function() { 57 | var val = signature.sign('foobarbaz', 'keyboard cat'); 58 | 59 | it('should populate req.signedCookies', function(done) { 60 | var socket = client('/signed', {headers: {cookie: 'foo=s:' + val}}); 61 | socket.on('message', function(signedCookies) { 62 | expect(signedCookies).to.eql({foo: 'foobarbaz'}); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should remove the signed value from req.cookies', function(done) { 68 | var socket = client('/', {headers: {cookie: 'foo=s:' + val}}); 69 | socket.on('message', function(cookies) { 70 | expect(cookies).to.eql({}); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('should omit invalid signatures', function(done) { 76 | var socket = client('/signed', {headers: {cookie: 'foo=' + val + '3'}}); 77 | socket.on('message', function(signedCookies) { 78 | expect(signedCookies).to.eql({}); 79 | 80 | socket = client('/', {headers: {cookie: 'foo=' + val + '3'}}); 81 | socket.on('message', function(cookies) { 82 | expect(cookies).to.eql({foo: 'foobarbaz.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3'}); 83 | done(); 84 | }); 85 | }); 86 | }); 87 | }); 88 | }); 89 | 90 | 91 | -------------------------------------------------------------------------------- /test/csrf.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var http = require('http'); 3 | var connect = require('connect'); 4 | var bundle = require('../'); 5 | var support = require('./support'); 6 | var client = support.client; 7 | 8 | 9 | describe('csrf', function() { 10 | var io, cookie, csrfToken; 11 | 12 | beforeEach(function(done) { 13 | var self = this; 14 | 15 | support.startServer(this, function() { 16 | var store = new bundle.session.MemoryStore(); 17 | 18 | var app = self.app; 19 | app.use(connect.cookieParser()); 20 | app.use(connect.session({secret: 'greg', store: store})); 21 | app.use(connect.csrf()); 22 | app.use(function(req, res) { 23 | res.end(req.csrfToken() || 'none'); 24 | }); 25 | 26 | io = self.io; 27 | io.use(bundle.cookieParser()) 28 | io.use(bundle.session({secret: 'greg', store: store})) 29 | io.use(bundle.csrf()) 30 | 31 | http.request('http://localhost:8888', function(res) { 32 | cookie = res.headers['set-cookie'][0]; 33 | csrfToken = ''; 34 | 35 | res.on('data', function(chunk) { 36 | csrfToken += chunk; 37 | }); 38 | 39 | res.on('end', done); 40 | }).end(); 41 | }); 42 | }); 43 | 44 | afterEach(function(done) { 45 | support.stopServer(this, done); 46 | }); 47 | 48 | it('should work with a valid token', function(done) { 49 | var socket = client('/', {headers: {Cookie: cookie, 'X-CSRF-Token': csrfToken}}); 50 | socket.on('connect', done); 51 | }); 52 | 53 | it('should work with a valid token via query', function(done) { 54 | var socket = client('/?_csrf=' + encodeURIComponent(csrfToken), 55 | {headers: {Cookie: cookie}}); 56 | socket.on('connect', done); 57 | }); 58 | 59 | it('should fail with an invalid token', function(done) { 60 | var socket = client('/?_csrf=42', {headers: {Cookie: cookie}}); 61 | socket.once('error', function(err) { 62 | expect(err).to.eql('invalid csrf token'); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should fail with no token', function(done){ 68 | var socket = client('/', {headers: {Cookie: cookie}}); 69 | socket.once('error', function(err) { 70 | expect(err).to.eql('invalid csrf token'); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('should keep the original method value', function(done) { 76 | io.on('connect', function(socket) { 77 | expect(socket.request.method).to.eql('GET'); 78 | done(); 79 | }); 80 | 81 | client('/?_csrf=' + encodeURIComponent(csrfToken), 82 | {headers: {Cookie: cookie}}); 83 | }); 84 | }); 85 | 86 | 87 | -------------------------------------------------------------------------------- /test/logger.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var bundle = require('../'); 3 | var support = require('./support'); 4 | var client = support.client; 5 | 6 | 7 | function MockStream() { 8 | this.data = []; 9 | } 10 | 11 | MockStream.prototype.write = function(str) { 12 | this.data.push(str); 13 | }; 14 | 15 | /** 16 | * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions 17 | */ 18 | 19 | function escapeRegExp(str) { 20 | return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); 21 | } 22 | 23 | function formatToRegExp(format) { 24 | var re = escapeRegExp(format).replace(/\\:[a-z-]+(\\\[[a-z-]+\\\])?/g, '(.+)'); 25 | return new RegExp(re); 26 | } 27 | 28 | describe('logger', function() { 29 | beforeEach(function(done) { 30 | support.startServer(this, done); 31 | }); 32 | 33 | afterEach(function(done) { 34 | support.stopServer(this, done); 35 | }); 36 | 37 | it('should write a log on connection', function(done) { 38 | var stream = new MockStream(); 39 | this.io.use(bundle.logger({stream: stream})); 40 | this.io.on('connection', function(socket) { 41 | expect(stream.data.length).to.eql(1); 42 | expect(stream.data[0]).to.match(formatToRegExp(bundle.logger.default)); 43 | done(); 44 | }); 45 | client(); 46 | }); 47 | }); 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/session.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var signature = require('cookie-signature'); 3 | var bundle = require('../'); 4 | var support = require('./support'); 5 | var client = support.client; 6 | var sessionCookie = support.sessionCookie; 7 | 8 | 9 | var min = 60 * 1000; 10 | 11 | function sid(val) { 12 | if (!val) return ''; 13 | return decodeURIComponent(/^connect\.sid=([^;]+);/.exec(val)[1]); 14 | } 15 | 16 | describe('session', function() { 17 | it('should export constructors', function(){ 18 | expect(bundle.session.Session).to.be.a('function'); 19 | expect(bundle.session.Store).to.be.a('function'); 20 | expect(bundle.session.MemoryStore).to.be.a('function'); 21 | expect(bundle.session.Cookie).to.be.a('function'); 22 | }) 23 | 24 | describe('acceptance', function() { 25 | beforeEach(function(done) { 26 | support.startServer(this, done); 27 | }); 28 | 29 | afterEach(function(done) { 30 | support.stopServer(this, done); 31 | }); 32 | 33 | describe('req.session', function() { 34 | it('should persist', function(done) { 35 | this.io 36 | .use(bundle.cookieParser()) 37 | .use(bundle.session({ secret: 'keyboard cat', cookie: { maxAge: min, httpOnly: false }})) 38 | .use(function(socket, next){ 39 | var req = socket.request; 40 | // checks that cookie options persisted 41 | expect(req.session.cookie.httpOnly).to.eql(false); 42 | 43 | req.session.count = req.session.count || 0; 44 | req.session.count++; 45 | next(); 46 | }) 47 | .on('connection', function(socket) { 48 | var req = socket.request; 49 | socket.send(req.session.count, sessionCookie(req, 'keyboard cat')); 50 | }); 51 | 52 | var socket = client(); 53 | socket.on('message', function(body, cookie) { 54 | expect(body).to.eql(1); 55 | 56 | socket = client('/', {headers: {cookie: cookie}}); 57 | socket.on('message', function(body) { 58 | expect(body).to.eql(2); 59 | done() 60 | }); 61 | }); 62 | }); 63 | 64 | describe('.regenerate()', function() { 65 | it('should destroy/replace the previous session', function(done) { 66 | this.io 67 | .use(bundle.cookieParser()) 68 | .use(bundle.session({ secret: 'keyboard cat', cookie: { maxAge: min }})) 69 | .use(function(socket, next) { 70 | var req = socket.request 71 | , id = req.session.id; 72 | req.session.regenerate(function(err) { 73 | if (err) throw err; 74 | expect(id).not.to.eql(req.session.id); 75 | next(); 76 | }); 77 | }) 78 | .on('connection', function(socket) { 79 | var req = socket.request; 80 | socket.send(sessionCookie(req, 'keyboard cat')); 81 | }); 82 | 83 | var socket = client(); 84 | socket.on('message', function(cookie) { 85 | var id = sid(cookie); 86 | 87 | var socket = client('/', {headers: {cookie: cookie}}); 88 | socket.on('message', function(cookie) { 89 | expect(sid(cookie)).not.to.eql(''); 90 | expect(sid(cookie)).not.to.eql(id); 91 | done(); 92 | }); 93 | }); 94 | }); 95 | }); 96 | 97 | it('should support req.signedCookies', function(done) { 98 | this.io 99 | .use(bundle.cookieParser('keyboard cat')) 100 | .use(bundle.session()) 101 | .use(function(socket, next) { 102 | var req = socket.request; 103 | req.session.count = req.session.count || 0; 104 | req.session.count++; 105 | next() 106 | }) 107 | .on('connection', function(socket) { 108 | var req = socket.request; 109 | socket.send(req.session.count, sessionCookie(req, 'keyboard cat')); 110 | }); 111 | 112 | var socket = client(); 113 | socket.on('message', function(body, cookie) { 114 | expect(body).to.eql(1); 115 | 116 | socket = client('/', {headers: {cookie: cookie}}); 117 | socket.on('message', function(body) { 118 | expect(body).to.eql(2); 119 | done() 120 | }); 121 | }); 122 | }); 123 | }); 124 | }); 125 | }); 126 | 127 | 128 | -------------------------------------------------------------------------------- /test/support/index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var url = require('url'); 3 | var io = require('socket.io'); 4 | var client = require('socket.io-client'); 5 | var connect = require('connect'); 6 | var signature = require('cookie-signature'); 7 | var port = 8888; 8 | 9 | 10 | exports.client = function(path, options) { 11 | path = path || '/'; 12 | options = options || {}; 13 | 14 | var uri = 'http://localhost:' + port + path; 15 | var urlObj = url.parse(uri, true); 16 | if (options.headers) { 17 | urlObj.query.headers = JSON.stringify(options.headers); 18 | delete urlObj.search; 19 | uri = url.format(urlObj); 20 | delete options.headers; 21 | } 22 | 23 | var _options = {reconnection: false}; 24 | for (var key in options) { 25 | _options[key] = options[key]; 26 | } 27 | 28 | return client.Manager(uri, _options).socket(urlObj.pathname); 29 | }; 30 | 31 | exports.startServer = function(context, done) { 32 | context.app = connect(); 33 | context.server = http.Server(context.app); 34 | context.io = io(context.server); 35 | 36 | context.io.use(exports.header); 37 | context.server.listen(port, done); 38 | 39 | context.sockets = []; 40 | context.server.on('connection', function(socket) { 41 | context.sockets.push(socket); 42 | }); 43 | }; 44 | 45 | exports.stopServer = function(context, done) { 46 | // FIXME: following doesn't work when error. 47 | // this.io.sockets.sockets.slice().forEach(function(socket) { 48 | // socket.disconnect(true); 49 | // }); 50 | 51 | context.sockets.forEach(function(socket) { 52 | socket.destroy(); 53 | }); 54 | context.server.close(done); 55 | }; 56 | 57 | exports.header = function(socket, next) { 58 | var req = socket.request; 59 | var headers = req._query.headers; 60 | 61 | if (headers) { 62 | headers = JSON.parse(headers); 63 | for (var field in headers) { 64 | req.headers[field.toLowerCase()] = headers[field]; 65 | } 66 | } 67 | next(); 68 | }; 69 | 70 | exports.sessionCookie = function(req, secret) { 71 | var cookie = req.session.cookie; 72 | var val = 's:' + signature.sign(req.sessionID, secret); 73 | 74 | return cookie.serialize('connect.sid', val); 75 | }; 76 | 77 | --------------------------------------------------------------------------------