Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for dynamic namespaces (WIP) #3195

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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)_
Expand All @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

Expand All @@ -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);
Expand Down
72 changes: 35 additions & 37 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use strict';

/**
* Module dependencies.
Expand All @@ -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');
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
};

/**
Expand Down Expand Up @@ -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];
Expand Down
39 changes: 39 additions & 0 deletions lib/parent-namespace.js
Original file line number Diff line number Diff line change
@@ -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;
98 changes: 40 additions & 58 deletions test/socket.io.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use strict';

var http = require('http').Server;
var io = require('../lib');
var fs = require('fs');
Expand Down Expand Up @@ -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();
});
});
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down