diff --git a/docs/API.md b/docs/API.md index 3b5e1b28d7..80d184017a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -20,7 +20,6 @@ - [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) @@ -293,7 +292,7 @@ Advanced use only. Creates a new `socket.io` client from the incoming engine.io #### server.of(nsp) - - `nsp` _(String)_ + - `nsp` _(String|RegExp|Function)_ - **Returns** `Namespace` Initializes and retrieves the given `Namespace` by its pathname identifier `nsp`. If the namespace was already initialized it returns it immediately. @@ -302,6 +301,34 @@ Initializes and retrieves the given `Namespace` by its pathname identifier `nsp` const adminNamespace = io.of('/admin'); ``` +A regex or a function can also be provided, in order to create namespace in a dynamic way: + +```js +const dynamicNsp = io.of(/^\/dynamic-\d+$/).on('connect', (socket) => { + const newNamespace = socket.nsp; // newNamespace.name === '/dynamic-101' + + // broadcast to all clients in the given sub-namespace + newNamespace.emit('hello'); +}); + +// client-side +const socket = io('/dynamic-101'); + +// broadcast to all clients in each sub-namespace +dynamicNsp.emit('hello'); + +// use a middleware for each sub-namespace +dynamicNsp.use((socket, next) => { /* ... */ }); +``` + +With a function: + +```js +io.of((name, query, next) => { + next(null, checkToken(query.token)); +}).on('connect', (socket) => { /* ... */ }); +``` + #### server.close([callback]) - `callback` _(Function)_ @@ -322,22 +349,6 @@ 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 adb5d20f73..32d179f971 100644 --- a/lib/client.js +++ b/lib/client.js @@ -56,7 +56,7 @@ Client.prototype.setup = function(){ * Connects a client to a namespace. * * @param {String} name namespace - * @param {String} query the query parameters + * @param {Object} query the query parameters * @api private */ @@ -66,9 +66,9 @@ Client.prototype.connect = function(name, query){ return this.doConnect(name, query); } - this.server.checkNamespace(name, (allow) => { - if (allow) { - debug('creating namespace %s', name); + this.server.checkNamespace(name, query, (dynamicNsp) => { + if (dynamicNsp) { + debug('dynamic namespace %s was created', dynamicNsp.name); this.doConnect(name, query); } else { debug('creation of namespace %s was denied', name); diff --git a/lib/index.js b/lib/index.js index 016353966f..388fecd4c0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1,4 @@ +'use strict'; /** * Module dependencies. @@ -12,6 +13,7 @@ var clientVersion = require('socket.io-client/package.json').version; var Client = require('./client'); var Emitter = require('events').EventEmitter; var Namespace = require('./namespace'); +var ParentNamespace = require('./parent-namespace'); var Adapter = require('socket.io-adapter'); var parser = require('socket.io-parser'); var debug = require('debug')('socket.io:server'); @@ -46,7 +48,7 @@ function Server(srv, opts){ } opts = opts || {}; this.nsps = {}; - this.nspValidators = []; + this.parentNsps = new Map(); this.path(opts.path || '/socket.io'); this.serveClient(false !== opts.serveClient); this.parser = opts.parser || parser; @@ -160,51 +162,35 @@ 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 + * @param {String} name name of incoming namespace + * @param {Object} query the query parameters + * @param {Function} fn callback * @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); +Server.prototype.checkNamespace = function(name, query, fn){ + if (this.parentNsps.size === 0) return fn(false); - // if one piece of middleware explicitly denies namespace, short-circuit - if (allow === false) return fn(false); + const keysIterator = this.parentNsps.keys(); - namespaceAllowed = namespaceAllowed || allow === true; - - // if no middleware left, summon callback - if (!fns[i + 1]) return fn(namespaceAllowed); - - // go on to next - run(i + 1); + const run = () => { + let nextFn = keysIterator.next(); + if (nextFn.done) { + return fn(false); + } + nextFn.value(name, query, (err, allow) => { + if (err || !allow) { + run(); + } else { + fn(this.parentNsps.get(nextFn.value).createChild(name)); + } }); - } + }; - run(0); + run(); }; /** @@ -452,12 +438,24 @@ Server.prototype.onconnection = function(conn){ /** * Looks up a namespace. * - * @param {String} name nsp name + * @param {String|RegExp|Function} name nsp name * @param {Function} [fn] optional, nsp `connection` ev handler * @api public */ Server.prototype.of = function(name, fn){ + if (typeof name === 'function' || name instanceof RegExp) { + const parentNsp = new ParentNamespace(this); + debug('initializing parent namespace %s', parentNsp.name); + if (typeof name === 'function') { + this.parentNsps.set(name, parentNsp); + } else { + this.parentNsps.set((nsp, conn, next) => next(null, name.test(nsp)), parentNsp); + } + if (fn) parentNsp.on('connect', fn); + return parentNsp; + } + if (String(name)[0] !== '/') name = '/' + name; var nsp = this.nsps[name]; diff --git a/lib/parent-namespace.js b/lib/parent-namespace.js new file mode 100644 index 0000000000..5a2b4fa8e1 --- /dev/null +++ b/lib/parent-namespace.js @@ -0,0 +1,39 @@ +'use strict'; + +const Namespace = require('./namespace'); + +let count = 0; + +class ParentNamespace extends Namespace { + + constructor(server) { + super(server, '/_' + (count++)); + this.children = new Set(); + } + + initAdapter() {} + + emit() { + const args = Array.prototype.slice.call(arguments); + + this.children.forEach(nsp => { + nsp.rooms = this.rooms; + nsp.flags = this.flags; + nsp.emit.apply(nsp, args); + }); + this.rooms = []; + this.flags = {}; + } + + createChild(name) { + const namespace = new Namespace(this.server, name); + namespace.fns = this.fns.slice(0); + this.listeners('connect').forEach(listener => namespace.on('connect', listener)); + this.listeners('connection').forEach(listener => namespace.on('connection', listener)); + this.children.add(namespace); + this.server.nsps[name] = namespace; + return namespace; + } +} + +module.exports = ParentNamespace; diff --git a/test/socket.io.js b/test/socket.io.js index c5f96dc63e..aec781455e 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -1,3 +1,5 @@ +'use strict'; + var http = require('http').Server; var io = require('../lib'); var fs = require('fs'); @@ -890,75 +892,55 @@ describe('socket.io', function(){ }); }); - describe('dynamic', function () { - it('should allow connections to dynamic namespaces', function(done){ - var srv = http(); - var sio = io(srv); + describe('dynamic namespaces', function () { + it('should allow connections to dynamic namespaces with a regex', function(done){ + const srv = http(); + const sio = io(srv); + let count = 0; srv.listen(function(){ - var namespace = '/dynamic'; - var dynamic = client(srv, namespace); - sio.useNamespaceValidator(function(nsp, next) { - expect(nsp).to.be(namespace); - next(null, true); + const socket = client(srv, '/dynamic-101'); + let dynamicNsp = sio.of(/^\/dynamic-\d+$/).on('connect', (socket) => { + expect(socket.nsp.name).to.be('/dynamic-101'); + dynamicNsp.emit('hello', 1, '2', { 3: '4'}); + if (++count === 4) done(); + }).use((socket, next) => { + next(); + if (++count === 4) done(); }); - dynamic.on('error', function(err) { + socket.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(); + socket.on('connect', () => { + if (++count === 4) done(); + }); + socket.on('hello', (a, b, c) => { + expect(a).to.eql(1); + expect(b).to.eql('2'); + expect(c).to.eql({ 3: '4' }); + if (++count === 4) done(); }); }); }); - it('should not allow connections to dynamic namespaces if not supported', function(done){ - var srv = http(); - var sio = io(srv); + it('should allow connections to dynamic namespaces with a function', function(done){ + const srv = http(); + const 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(); - }); + const socket = client(srv, '/dynamic-101'); + sio.of((name, query, next) => next(null, '/dynamic-101' === name)); + socket.on('connect', done); }); }); - it('should not allow connections to dynamic namespaces if there is an error', function(done){ - var srv = http(); - var sio = io(srv); + it('should disallow connections when no dynamic namespace matches', function(done){ + const srv = http(); + const 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"); + const socket = client(srv, '/abc'); + sio.of(/^\/dynamic-\d+$/); + sio.of((name, query, next) => next(null, '/dynamic-101' === name)); + socket.on('error', (err) => { + expect(err).to.be('Invalid namespace'); done(); }); }); @@ -1759,7 +1741,7 @@ describe('socket.io', function(){ var socket = client(srv, { reconnection: false }); sio.on('connection', function(s){ s.conn.on('upgrade', function(){ - console.log('\033[96mNote: warning expected and normal in test.\033[39m'); + console.log('\u001b[96mNote: warning expected and normal in test.\u001b[39m'); socket.io.engine.write('5woooot'); setTimeout(function(){ done(); @@ -1776,7 +1758,7 @@ describe('socket.io', function(){ var socket = client(srv, { reconnection: false }); sio.on('connection', function(s){ s.conn.on('upgrade', function(){ - console.log('\033[96mNote: warning expected and normal in test.\033[39m'); + console.log('\u001b[96mNote: warning expected and normal in test.\u001b[39m'); socket.io.engine.write('44["handle me please"]'); setTimeout(function(){ done();