diff --git a/docs/API.md b/docs/API.md index e59e75886c..a78cc5ed35 100644 --- a/docs/API.md +++ b/docs/API.md @@ -20,6 +20,7 @@ - [server.onconnection(socket)](#serveronconnectionsocket) - [server.of(nsp)](#serverofnsp) - [server.close([callback])](#serverclosecallback) + - [server.useNamespaceValidator(fn)](#serverusenamespacevalidatorfn) - [Class: Namespace](#namespace) - [namespace.name](#namespacename) - [namespace.connected](#namespaceconnected) @@ -321,6 +322,22 @@ server.listen(PORT); // PORT is free to use io = Server(server); ``` +#### server.useNamespaceValidator(fn) + + - `fn` _(Function)_ + +Sets up server middleware to validate whether a new namespace should be created. + +```js +io.useNamespaceValidator((nsp, next) => { + if (nsp === 'dynamic') { + next(null, true); + } else { + next(new Error('Invalid namespace')); + } +}); +``` + #### server.engine.generateId Overwrites the default method to generate your custom socket id. diff --git a/lib/client.js b/lib/client.js index 0b5f0446e9..adb5d20f73 100644 --- a/lib/client.js +++ b/lib/client.js @@ -56,17 +56,38 @@ Client.prototype.setup = function(){ * Connects a client to a namespace. * * @param {String} name namespace + * @param {String} query the query parameters * @api private */ Client.prototype.connect = function(name, query){ - debug('connecting to namespace %s', name); - var nsp = this.server.nsps[name]; - if (!nsp) { - this.packet({ type: parser.ERROR, nsp: name, data : 'Invalid namespace'}); - return; + if (this.server.nsps[name]) { + debug('connecting to namespace %s', name); + return this.doConnect(name, query); } + this.server.checkNamespace(name, (allow) => { + if (allow) { + debug('creating namespace %s', name); + this.doConnect(name, query); + } else { + debug('creation of namespace %s was denied', name); + this.packet({ type: parser.ERROR, nsp: name, data: 'Invalid namespace' }); + } + }); +}; + +/** + * Connects a client to a namespace. + * + * @param {String} name namespace + * @param {String} query the query parameters + * @api private + */ + +Client.prototype.doConnect = function(name, query){ + var nsp = this.server.of(name); + if ('/' != name && !this.nsps['/']) { this.connectBuffer.push(name); return; diff --git a/lib/index.js b/lib/index.js index 192504f77f..21cfefc9aa 100644 --- a/lib/index.js +++ b/lib/index.js @@ -46,6 +46,7 @@ function Server(srv, opts){ } opts = opts || {}; this.nsps = {}; + this.nspValidators = []; this.path(opts.path || '/socket.io'); this.serveClient(false !== opts.serveClient); this.parser = opts.parser || parser; @@ -157,6 +158,53 @@ Server.prototype.set = function(key, val){ return this; }; +/** + * Sets up server middleware to validate incoming namespaces not already created on the server. + * + * @return {Server} self + * @api public + */ + +Server.prototype.useNamespaceValidator = function(fn){ + this.nspValidators.push(fn); + return this; +}; + +/** + * Executes the middleware for an incoming namespace not already created on the server. + * + * @param name of incomming namespace + * @param {Function} last fn call in the middleware + * @api private + */ + +Server.prototype.checkNamespace = function(name, fn){ + var fns = this.nspValidators.slice(0); + if (!fns.length) return fn(false); + + var namespaceAllowed = false; // Deny unknown namespaces by default + + function run(i){ + fns[i](name, function(err, allow){ + // upon error, short-circuit + if (err) return fn(false); + + // if one piece of middleware explicitly denies namespace, short-circuit + if (allow === false) return fn(false); + + namespaceAllowed = namespaceAllowed || allow === true; + + // if no middleware left, summon callback + if (!fns[i + 1]) return fn(namespaceAllowed); + + // go on to next + run(i + 1); + }); + } + + run(0); +}; + /** * Sets the client serving path. * diff --git a/test/socket.io.js b/test/socket.io.js index f382d503d2..ae8cdcfab8 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -878,6 +878,81 @@ describe('socket.io', function(){ }); }); }); + + describe('dynamic', function () { + it('should allow connections to dynamic namespaces', function(done){ + var srv = http(); + var sio = io(srv); + srv.listen(function(){ + var namespace = '/dynamic'; + var dynamic = client(srv, namespace); + sio.useNamespaceValidator(function(nsp, next) { + expect(nsp).to.be(namespace); + next(null, true); + }); + dynamic.on('error', function(err) { + expect().fail(); + }); + dynamic.on('connect', function() { + expect(sio.nsps[namespace]).to.be.a(Namespace); + expect(Object.keys(sio.nsps[namespace].sockets).length).to.be(1); + done(); + }); + }); + }); + + it('should not allow connections to dynamic namespaces if not supported', function(done){ + var srv = http(); + var sio = io(srv); + srv.listen(function(){ + var namespace = '/dynamic'; + sio.useNamespaceValidator(function(nsp, next) { + expect(nsp).to.be(namespace); + next(null, false); + }); + sio.on('connect', function(socket) { + if (socket.nsp.name === namespace) { + expect().fail(); + } + }); + + var dynamic = client(srv,namespace); + dynamic.on('connect', function(){ + expect().fail(); + }); + dynamic.on('error', function(err) { + expect(err).to.be("Invalid namespace"); + done(); + }); + }); + }); + + it('should not allow connections to dynamic namespaces if there is an error', function(done){ + var srv = http(); + var sio = io(srv); + srv.listen(function(){ + var namespace = '/dynamic'; + sio.useNamespaceValidator(function(nsp, next) { + expect(nsp).to.be(namespace); + next(new Error(), true); + }); + sio.on('connect', function(socket) { + if (socket.nsp.name === namespace) { + expect().fail(); + } + }); + + var dynamic = client(srv,namespace); + dynamic.on('connect', function(){ + expect().fail(); + }); + dynamic.on('error', function(err) { + expect(err).to.be("Invalid namespace"); + done(); + }); + }); + }); + }); }); describe('socket', function(){