Skip to content

Commit

Permalink
Make a copy of listeners for hijacking events.
Browse files Browse the repository at this point in the history
Node.js v0.7+ `event.listeners()` returns a reference
that continues to be used internally after calling
`removeAllListeners()`. Make a copy before calling
the original functions to prevent recursion on v0.7+.

Since this pattern is common in Yeti, move this
event hijacking into its own module.
  • Loading branch information
reid committed Jun 12, 2012
1 parent ab10c71 commit 51aa577
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 63 deletions.
50 changes: 6 additions & 44 deletions lib/blizzard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ var http = require("http");
var util = require("util");
var urlParser = require("url");

var hijack = require("../hijack");

var BlizzardSession = require("./session");

var EventEmitter2 = require("eventemitter2").EventEmitter2;
Expand Down Expand Up @@ -177,50 +179,10 @@ Blizzard.prototype.connect = function (url, cb) {
* @param {HTTPServer} httpServer
*/
Blizzard.prototype.listen = function (httpServer) {
var self = this,
listeners = httpServer.listeners("upgrade");

httpServer.removeAllListeners("upgrade");

httpServer.on("upgrade", function (req, socket, head) {

// First, attempt to upgrade to Blizzard.
var success = self.serverUpgrade(req, socket, head),
written = false,
originalWrite;

if (!success) {
// Blizzard was not the protocol asked for.

// If data is written to the socket,
// the connection was upgraded.
originalWrite = socket.write;
socket.write = function (string, encoding, fd) {
written = true;
originalWrite.call(this, string, encoding, fd);
};

// Try other upgrade listeners, e.g. Socket.io.
listeners.forEach(function (fn) {
fn(req, socket, head);
});

// Restore original write.
socket.write = originalWrite;

// No listener wrote to the socket.
// Destroy the connection.
if (!written && socket.writable) {
socket.write([
"HTTP/1.1 400 Bad Request",
"X-Reason: Protocol not supported",
"Connection: close",
"Content-Length: 0",
"", ""
].join("\r\n"));
socket.end();
}
}
var self = this;

hijack(httpServer, "upgrade", function (req, socket, head) {
return self.serverUpgrade(req, socket, head);
});

return this;
Expand Down
58 changes: 58 additions & 0 deletions lib/hijack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use strict";

/**
* @module hijack
*/

/**
* Attach the provided function as the first
* listener of the given EventEmitter event.
*
* All listeners of the event will be removed
* and replaced with a listener that will run
* the provided function first, followed by
* the original listeners if the function
* returned false.
*
* Works with Node.js v0.7+ where the array
* returned by `ee.listeners()` is a reference.
*
* @method hijack
* @param {EventEmitter} ee Emitter.
* @param {String} event Event name.
* @param {Function} firstFn Function to run first.
*/
module.exports = function hijack(ee, event, firstFn) {
var listeners = ee.listeners(event),
i = 0,
length = listeners.length,
originalListeners = [];

// Note: listeners is a reference in Node v0.7.
// Calling `removeAllListeners` no longer destroys
// the listener array, which causes it survive
// as a reference. See joyent/node commits:
// - 78dc13fbf97e2e3003e6f3baacdd5ff60e8de3f7
// - 928ea564d16da47e615ddac627e0b4d4a40d8196
//
// Make a copy first.
for (; i < length; i += 1) {
originalListeners[i] = listeners[i];
}

ee.removeAllListeners(event);

ee.on(event, function () {
var args = Array.prototype.slice.call(arguments),
stack = [firstFn].concat(originalListeners),
handled;

handled = firstFn.apply(ee, args);

if (!handled) {
originalListeners.forEach(function (fn) {
fn.apply(ee, args);
});
}
});
};
26 changes: 7 additions & 19 deletions lib/hub/listener.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*/

var util = require("util"),
url = require("url");
url = require("url"),
hijack = require("../hijack");

/**
* A **HubListener** augments an existing `http.Server`,
Expand Down Expand Up @@ -39,14 +40,9 @@ var proto = HubListener.prototype;
* @private
*/
proto._setupServer = function () {
var self = this,
upgradeListeners = this.server.listeners("upgrade"),
requestListeners = this.server.listeners("request");
var self = this;

this.server.removeAllListeners("upgrade");
this.server.removeAllListeners("request");

this.server.on("upgrade", function (req, socket, head) {
hijack(this.server, "upgrade", function (req, socket, head) {
var server = this,
parsedUrl = url.parse(req.url),
resolvedUrl,
Expand All @@ -70,22 +66,14 @@ proto._setupServer = function () {

if (yetiUpgrade) {
self.hub.server.emit("upgrade", req, socket, head);
return;
return true;
}

upgradeListeners.forEach(function (listener) {
listener.call(server, req, socket, head);
});
});

this.server.on("request", function (req, res) {
hijack(this.server, "request", function faxd (req, res) {
if (self.match(req, res)) {
return;
return true;
}
var server = this;
requestListeners.forEach(function (listener) {
listener.call(server, req, res);
});
});
};

Expand Down

0 comments on commit 51aa577

Please sign in to comment.