Skip to content
Closed
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
Add setupNamespace dynamic namespace support.
  • Loading branch information
davidbau committed Nov 15, 2014
commit 66a725abe05d71e24774d296ae47c9badfbb1ef1
7 changes: 4 additions & 3 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ module.exports = Client;
* @api private
*/

function Client(server, conn){
function Client(server, conn, host){
this.server = server;
this.conn = conn;
this.host = host;
this.encoder = new parser.Encoder();
this.decoder = new parser.Decoder();
this.id = conn.id;
Expand Down Expand Up @@ -57,11 +58,11 @@ Client.prototype.setup = function(){

Client.prototype.connect = function(name){
debug('connecting to namespace %s', name);
if (!this.server.nsps[name]) {
var nsp = this.server.of(name, this.host, null, true);
if (nsp == null) {
this.packet({ type: parser.ERROR, nsp: name, data : 'Invalid namespace'});
return;
}
var nsp = this.server.of(name);
if ('/' != name && !this.nsps['/']) {
this.connectBuffer.push(name);
return;
Expand Down
258 changes: 246 additions & 12 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ function Server(srv, opts){
this.serveClient(false !== opts.serveClient);
this.adapter(opts.adapter || Adapter);
this.origins(opts.origins || '*:*');
this._cleanupTimer = null;
this._cleanupTime = null;
this._setupNames = {};
this._setupPatterns = [];
this._mainHost = makePattern(opts.host || '*');
this._defaultRetirement = opts.retirement || 10000;
this._serveStatus = opts.serveStatus || false;

this.sockets = this.of('/');
if (srv) this.attach(srv, opts);
}
Expand Down Expand Up @@ -226,7 +234,7 @@ Server.prototype.attach = function(srv, opts){
this.eio = engine.attach(srv, opts);

// attach static file serving
if (this._serveClient) this.attachServe(srv);
if (this._serveClient || this._serveStatus) this.attachServe(srv);

// Export http server
this.httpServer = srv;
Expand All @@ -246,13 +254,16 @@ Server.prototype.attach = function(srv, opts){

Server.prototype.attachServe = function(srv){
debug('attaching client serving req handler');
var url = this._path + '/socket.io.js';
var clienturl = this._path + '/socket.io.js';
var statusurl = this._path + '/status';
var evs = srv.listeners('request').slice(0);
var self = this;
srv.removeAllListeners('request');
srv.on('request', function(req, res) {
if (0 == req.url.indexOf(url)) {
if (self._serveClient && 0 == req.url.indexOf(clienturl)) {
self.serve(req, res);
} else if (self._serveState && 0 == req.url.indexOf(statusurl)) {
self.status(req, res);
} else {
for (var i = 0; i < evs.length; i++) {
evs[i].call(srv, req, res);
Expand Down Expand Up @@ -287,6 +298,81 @@ Server.prototype.serve = function(req, res){
res.end(clientSource);
};

/**
* Handles a request serving `/status`
*
* @param {http.Request} req
* @param {http.Response} res
* @api private
*/

Server.prototype.status = function(req, res){
var match = '*';
if (req && !matchPattern(this._mainHost, req.headers.host)) {
match = req.headers.host;
}
var html = !res ? [] : ['<!doctype html>', '<html>', '<body>', '<pre>',
'<a href="status">Refresh</a> active namespaces on ' + match, ''];
function addText(str) {
html.push(str.replace(/&/g, '&amp;').replace(/</g, '&lt;'));
}
var sorted = [];
for (var j in this.nsps) {
if (this.nsps.hasOwnProperty(j)) {
var nsp = this.nsps[j];
if (match != '*' && nsp.host != match) continue;
sorted.push(j);
}
}
sorted.sort(function(a, b) {
// Sort slashes last.
if (a == b) return 0;
a = a.replace(/\//g, '\uffff');
b = b.replace(/\//g, '\uffff');
if (a < b) return -1;
else return 1;
});
var now = +(new Date);
for (j = 0; j < sorted.length; ++j) {
var nsp = this.nsps[sorted[j]];
addText(match == '*' ? nsp.fullname() : nsp.name);
if (nsp.rooms && nsp.rooms.length > 1) {
addText(' rooms: ' + nsp.rooms.join(' '));
}
if (nsp.sockets.length == 0) {
var remaining = nsp._expiration() - now;
var expinfo = '';
if (remaining < Infinity) {
expinfo = '; expires ' + remaining / 1000 + 's';
}
addText(' (no sockets' + expinfo + ')');
} else for (var k = 0; k < nsp.sockets.length; ++k) {
var socket = nsp.sockets[k];
var clientdesc = '';
if (socket.request.connection.remoteAddress) {
clientdesc += ' from ' + socket.request.connection.remoteAddress;
}
var roomdesc = '';
if (socket.rooms.length > 1) {
for (var m = 0; m < socket.rooms.length; ++m) {
if (socket.rooms[m] != socket.client.id) {
roomdesc += ' ' + socket.rooms[m];
}
}
}
addText(' socket ' + socket.id + clientdesc + roomdesc);
}
html.push('');
}
if (!res) {
return html.join('\n');
}
html.push('</pre>', '</body>', '</html>');
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(html.join('\n'));
};

/**
* Binds socket.io to an engine.io instance.
*
Expand All @@ -311,29 +397,135 @@ Server.prototype.bind = function(engine){

Server.prototype.onconnection = function(conn){
debug('incoming connection with id %s', conn.id);
var client = new Client(this, conn);
var host = this.getHost(conn);
if (!host || matchPattern(this._mainHost, host)) {
// The main host gets nulled out.
host = null;
}
var client = new Client(this, conn, host);
client.connect('/');
return this;
};

/**
* Extracts the host name from a connection. May be overridden.
*
* @param {Connection} connection
* @return {String} host name
* @api public
*/

Server.prototype.getHost = function(conn){
return conn.request.headers.host;
};

/**
* For initialization, allow paterns to be regexps, '*', true, or a string.
* Patterns containing special regexp characters are parsed as RegExps.
*
* @param {String|RegExp} given pattern
* @return {String|RegExp} created pattern
* @api private
*/
function makePattern(pattern) {
if (pattern === true) return new RegExp('.^'); // matches nothing.
if (pattern === '*') return new RegExp('.*'); // matches everything.
if (/[*?+\[\](){}]/.test(pattern)) return new RegExp(pattern);
if (pattern instanceof RegExp) return pattern;
return pattern;
}

/**
* Returns a match-like object, matching either a string or a RegExp.
*
* @param {String|RegExp} pattern is a string or RegExp
* @param {String} str to match
* @return {Object} regexp-match-like object
* @api private
*/
function matchPattern(pattern, str) {
if (pattern instanceof RegExp) {
return pattern.exec(str);
} else {
return pattern == str ? {'0': str, index: 0, input: str} : null;
}
}

/**
* Set up intiialization for a namespace pattern.
*
* @param {String|RegExp} nsp name
* @param {Function} nsp initiialization calback
* @api public
*/

Server.prototype.setupNamespace = function(name, fn) {
var pattern = makePattern(name);
if (pattern instanceof RegExp) {
this._setupPatterns.push({pattern: pattern, setup: fn});
} else {
this._setupNames[name] = fn;
}
for (var j in this.nsps) {
if (this.nsps.hasOwnProperty(j)) {
var nsp = this.nsps[j];
if (!nsp.setupDone && null != (match = matchPattern(pattern, j))) {
nsp.setupDone = -1;
if (false === fn.apply(this, [nsp, match])) {
nsp.setupDone = 0;
} else {
nsp.setupDone = 1;
}
}
}
}
};

/**
* Looks up a namespace.
*
* @param {String} nsp name
* @param {String} optional hostname
* @param {Function} optional, nsp `connection` ev handler
* @param {Boolean} auto (internal) true to request a dynamic namespace
* @api public
*/

Server.prototype.of = function(name, fn){
Server.prototype.of = function(name, host, fn, auto){
if (fn == null && 'function' == typeof host) {
fn = host;
host = null;
}
if (String(name)[0] !== '/') name = '/' + name;

if (!this.nsps[name]) {
debug('initializing namespace %s', name);
var nsp = new Namespace(this, name);
this.nsps[name] = nsp;
var fullname = Namespace.qualify(name, host);
var setup = null, match, j;
if (!this.nsps[fullname]) {
debug('initializing namespace %s', fullname);
if (this._setupNames.hasOwnProperty(fullname)) {
setup = this._setupNames[fullname];
} else for (j = this._setupPatterns.length - 1; j >= 0; --j) {
if (!!(match = matchPattern(this._setupPatterns[j].pattern, fullname))) {
setup = this._setupPatterns[j].setup;
break;
}
}
if (auto && !setup) return null;
var nsp = new Namespace(this, name, host);
if (auto) nsp.retirement = this._defaultRetirement;
this.nsps[fullname] = nsp;
if (setup) {
nsp.setupDone = -1;
if (false === setup.apply(this, [nsp, match])) {
debug('namespace %s rejected', fullname);
delete this.nsps[fullname];
return null;
} else {
nsp.setupDone = 1;
}
}
}
if (fn) this.nsps[name].on('connect', fn);
return this.nsps[name];
if (fn) this.nsps[fullname].on('connect', fn);
return this.nsps[fullname];
};

/**
Expand All @@ -354,6 +546,48 @@ Server.prototype.close = function(){
}
};

/**
* Schedules a cleanup timer for deleting unused namespaces.
*
* @param {Number} millisecond delay
* @api private
*/
Server.prototype.requestCleanupAfter = function(delay) {
delay = Math.max(0, delay || 0);
if (!(delay < Infinity)) return;
var cleanupTime = delay + +(new Date);
if (this._cleanupTimer && cleanupTime < this._cleanupTime) {
clearTimeout(this._cleanupTimer);
this._cleanupTimer = null;
}
// Do cleanup in 5-second batches.
delay += Math.max(1, Math.min(delay, 5000));
var server = this;
if (!this._cleanupTimer) {
this._cleanupTime = cleanupTime;
this._cleanupTimer = setTimeout(doCleanup, delay);
}
function doCleanup() {
server._cleanupTimer = null;
server._cleanupTime = null;
var earliestUnexpired = Infinity;
var now = +(new Date);
for (var j in server.nsps) {
if (server.nsps.hasOwnProperty(j)) {
var nsp = server.nsps[j];
var expiration = nsp._expiration();
if (expiration <= now) {
nsp.expire(true);
delete server.nsps[j];
} else {
earliestUnexpired = Math.min(earliestUnexpired, expiration);
}
}
}
server.requestCleanupAfter(earliestUnexpired - now);
}
};

/**
* Expose main namespace (/).
*/
Expand Down
Loading