diff --git a/lib/namespace.js b/lib/namespace.js index 14fa7cb01c..758d6cd4d8 100644 --- a/lib/namespace.js +++ b/lib/namespace.js @@ -24,6 +24,7 @@ function SocketNamespace (mgr, name) { this.manager = mgr; this.name = name || ''; this.sockets = {}; + this.auth = false; this.setFlags(); }; @@ -199,6 +200,17 @@ SocketNamespace.prototype.socket = function (sid, readable) { return this.sockets[sid]; }; +/** + * Sets authorization for this namespace + * + * @api public + */ + +SocketNamespace.prototype.authorization = function (fn) { + this.auth = fn; + return this; +}; + /** * Called when a socket disconnects entirely. * @@ -211,6 +223,30 @@ SocketNamespace.prototype.handleDisconnect = function (sid, reason) { } }; +/** + * Performs authentication. + * + * @param Object client request data + * @api private + */ + +SocketNamespace.prototype.authorize = function (data, fn) { + if (this.auth) { + var self = this; + + this.auth.call(this, data, function (err, authorized) { + self.log.debug('client ' + + (authorized ? '' : 'un') + 'authorized for ' + self.name); + fn(err, authorized); + }); + } else { + this.log.debug('client authorized for ' + this.name); + fn(null, true); + } + + return this; +}; + /** * Handles a packet. * @@ -231,16 +267,42 @@ SocketNamespace.prototype.handlePacket = function (sessid, packet) { }); }; - switch (packet.type) { - case 'connect': - this.manager.onJoin(sessid, this.name); - this.store.publish('join', sessid, this.name); + function error (err) { + self.log.warn('handshake error ' + err + ' for ' + self.name); + socket.packet({ type: 'error', reason: err }); + }; - // packet echo - socket.packet({ type: 'connect' }); + function connect () { + self.manager.onJoin(sessid, self.name); + self.store.publish('join', sessid, self.name); - // emit connection event - self.emit('connection', socket); + // packet echo + socket.packet({ type: 'connect' }); + + // emit connection event + self.emit('connection', socket); + }; + + switch (packet.type) { + case 'connect': + if (packet.endpoint == '') { + connect(); + } else { + var manager = this.manager + , handshakeData = manager.handshaken[sessid]; + + this.authorize(handshakeData, function (err, authorized, newData) { + if (err) return error(err); + + if (authorized) { + manager.onHandshake(sessid, newData || handshakeData); + self.store.publish('handshake', sessid, newData || handshakeData); + connect(); + } else { + error('unauthorized'); + } + }); + } break; case 'ack': diff --git a/test/common.js b/test/common.js index 479da113d3..608019b419 100644 --- a/test/common.js +++ b/test/common.js @@ -12,7 +12,8 @@ var io = require('socket.io') , parser = io.parser , http = require('http') - , https = require('https'); + , https = require('https') + , WebSocket = require('../support/node-websocket-client/lib/websocket').WebSocket; /** * Exports. @@ -181,3 +182,61 @@ create = function (cl) { console.log(''); return io.listen(cl.port); }; + +/** + * WebSocket socket.io client. + * + * @api private + */ + +function WSClient (port, sid) { + this.sid = sid; + this.port = port; + + WebSocket.call( + this + , 'ws://localhost:' + port + '/socket.io/' + + io.protocol + '/websocket/' + sid + ); +}; + +/** + * Inherits from WebSocket. + */ + +WSClient.prototype.__proto__ = WebSocket.prototype; + +/** + * Overrides message event emission. + * + * @api private + */ + +WSClient.prototype.emit = function (name) { + var args = arguments; + + if (name == 'message' || name == 'data') { + args[1] = parser.decodePacket(args[1].toString()); + } + + return WebSocket.prototype.emit.apply(this, arguments); +}; + +/** + * Writes a packet + */ + +WSClient.prototype.packet = function (pack) { + this.write(parser.encodePacket(pack)); + return this; +}; + +/** + * Creates a websocket client. + * + * @api public + */ + +websocket = function (cl, sid) { + return new WSClient(cl.port, sid); +}; diff --git a/test/namespace.test.js b/test/namespace.test.js new file mode 100644 index 0000000000..96850236bf --- /dev/null +++ b/test/namespace.test.js @@ -0,0 +1,143 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Test dependencies. + */ + +var sio = require('socket.io') + , should = require('./common') + , ports = 15700; + +/** + * Test. + */ + +module.exports = { + 'namespace pass no authentication': function (done) { + var cl = client(++ports) + , io = create(cl) + , ws; + + io.of('/a') + .on('connection', function (socket) { + cl.end(); + ws.finishClose(); + io.server.close() + done(); + }); + + cl.handshake(function (sid) { + ws = websocket(cl, sid); + ws.on('open', function () { + ws.packet({ + type: 'connect' + , endpoint: '/a' + }); + }) + }); + }, + + 'namespace pass authentication': function (done) { + var cl = client(++ports) + , io = create(cl) + , ws; + + io.of('/a') + .authorization(function (data, fn) { + fn(null, true); + }) + .on('connection', function (socket) { + cl.end(); + ws.finishClose(); + io.server.close() + done(); + }); + + cl.handshake(function (sid) { + ws = websocket(cl, sid); + ws.on('open', function () { + ws.packet({ + type: 'connect' + , endpoint: '/a' + }); + }) + }); + }, + + 'namespace authentication handshake data': function (done) { + var cl = client(++ports) + , io = create(cl) + , ws; + + io.of('/a') + .authorization(function (data, fn) { + data.foo = 'bar'; + fn(null, true); + }) + .on('connection', function (socket) { + socket.handshake.address.address.should.equal('127.0.0.1'); + socket.handshake.address.port.should.equal(ports); + socket.handshake.headers.host.should.equal('localhost'); + socket.handshake.headers.connection.should.equal('keep-alive'); + socket.handshake.time.should.match(/GMT/); + socket.handshake.foo.should.equal('bar'); + + cl.end(); + ws.finishClose(); + io.server.close() + done(); + }); + + cl.handshake(function (sid) { + ws = websocket(cl, sid); + ws.on('open', function () { + ws.packet({ + type: 'connect' + , endpoint: '/a' + }); + }) + }); + }, + + 'namespace fail authentication': function (done) { + var cl = client(++ports) + , io = create(cl) + , calls = 0 + , ws; + + io.of('/a') + .authorization(function (data, fn) { + fn(null, false); + }) + .on('connection', function (socket) { + throw new Error('Should not be called'); + }); + + cl.handshake(function (sid) { + ws = websocket(cl, sid); + ws.on('open', function () { + ws.packet({ + type: 'connect' + , endpoint: '/a' + }); + }); + + ws.on('message', function (data) { + if (data.endpoint == '/a') { + data.type.should.eql('error'); + data.reason.should.eql('unauthorized') + + cl.end(); + ws.finishClose(); + io.server.close() + done(); + } + }) + }); + } +}; diff --git a/test/transports.websocket.test.js b/test/transports.websocket.test.js index 256efff622..149cfbae16 100644 --- a/test/transports.websocket.test.js +++ b/test/transports.websocket.test.js @@ -11,75 +11,9 @@ var sio = require('socket.io') , should = require('./common') - , HTTPClient = should.HTTPClient - , WebSocket = require('../support/node-websocket-client/lib/websocket').WebSocket , parser = sio.parser , ports = 15400; -/** - * Exports WSClient. - */ - -module.exports = exports = WSClient; - -/** - * WebSocket socket.io client. - * - * @api private - */ - -function WSClient (port, sid) { - this.sid = sid; - this.port = port; - - WebSocket.call( - this - , 'ws://localhost:' + port + '/socket.io/' - + sio.protocol + '/websocket/' + sid - ); -}; - -/** - * Inherits from WebSocket. - */ - -WSClient.prototype.__proto__ = WebSocket.prototype; - -/** - * Overrides message event emission. - * - * @api private - */ - -WSClient.prototype.emit = function (name) { - var args = arguments; - - if (name == 'message' || name == 'data') { - args[1] = parser.decodePacket(args[1].toString()); - } - - return WebSocket.prototype.emit.apply(this, arguments); -}; - -/** - * Writes a packet - */ - -WSClient.prototype.packet = function (pack) { - this.write(parser.encodePacket(pack)); - return this; -}; - -/** - * Creates a websocket client. - * - * @api public - */ - -function websocket (cl, sid) { - return new WSClient(cl.port, sid); -}; - /** * Tests. */