From e241fd080ba57cde3185cdd599ffd8918b0a2c3c Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Mon, 5 Mar 2018 19:09:52 +0100 Subject: [PATCH 1/2] Revert "[feat] Add support for dynamic namespaces (#3187)" This reverts commit c0c79f019e7138194e438339f8192705957c8ec3. --- docs/API.md | 17 ----------- lib/client.js | 31 ++++---------------- lib/index.js | 48 ------------------------------ test/socket.io.js | 75 ----------------------------------------------- 4 files changed, 5 insertions(+), 166 deletions(-) diff --git a/docs/API.md b/docs/API.md index 3b5e1b28d7..ea7c15ed78 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) @@ -322,22 +321,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..0b5f0446e9 100644 --- a/lib/client.js +++ b/lib/client.js @@ -56,38 +56,17 @@ 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){ - if (this.server.nsps[name]) { - debug('connecting to namespace %s', name); - return this.doConnect(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; } - 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 016353966f..7bb31666b8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -46,7 +46,6 @@ 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; @@ -160,53 +159,6 @@ 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 c5f96dc63e..975d04ae0f 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -889,81 +889,6 @@ 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(){ From 9cead1ea6d00e52c947386e791b3847aa82d244b Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 29 Mar 2018 22:49:38 +0200 Subject: [PATCH 2/2] [feat] Add support for dynamic namespaces --- docs/API.md | 30 +++++++++++++++++++- lib/client.js | 31 +++++++++++++++++---- lib/index.js | 48 +++++++++++++++++++++++++++++++- lib/parent-namespace.js | 39 ++++++++++++++++++++++++++ test/socket.io.js | 61 +++++++++++++++++++++++++++++++++++++++-- 5 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 lib/parent-namespace.js diff --git a/docs/API.md b/docs/API.md index ea7c15ed78..80d184017a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -292,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. @@ -301,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)_ diff --git a/lib/client.js b/lib/client.js index 0b5f0446e9..32d179f971 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 {Object} 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, 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); + 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 7bb31666b8..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,6 +48,7 @@ function Server(srv, opts){ } opts = opts || {}; this.nsps = {}; + this.parentNsps = new Map(); this.path(opts.path || '/socket.io'); this.serveClient(false !== opts.serveClient); this.parser = opts.parser || parser; @@ -159,6 +162,37 @@ Server.prototype.set = function(key, val){ return this; }; +/** + * Executes the middleware for an incoming namespace not already created on the server. + * + * @param {String} name name of incoming namespace + * @param {Object} query the query parameters + * @param {Function} fn callback + * @api private + */ + +Server.prototype.checkNamespace = function(name, query, fn){ + if (this.parentNsps.size === 0) return fn(false); + + const keysIterator = this.parentNsps.keys(); + + 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(); +}; + /** * Sets the client serving path. * @@ -404,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 975d04ae0f..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'); @@ -889,6 +891,61 @@ describe('socket.io', function(){ }); }); }); + + 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(){ + 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(); + }); + socket.on('error', function(err) { + expect().fail(); + }); + 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 allow connections to dynamic namespaces with a function', function(done){ + const srv = http(); + const sio = io(srv); + srv.listen(function(){ + const socket = client(srv, '/dynamic-101'); + sio.of((name, query, next) => next(null, '/dynamic-101' === name)); + socket.on('connect', done); + }); + }); + + it('should disallow connections when no dynamic namespace matches', function(done){ + const srv = http(); + const sio = io(srv); + srv.listen(function(){ + 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(); + }); + }); + }); + }); }); describe('socket', function(){ @@ -1684,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(); @@ -1701,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();