node-simple-router
Version:
Yet another minimalistic router for node.js
523 lines (483 loc) • 16 kB
JavaScript
// Generated by CoffeeScript 1.10.0
(function() {
var KEY_SUFFIX, URL, WebSocketClientConnection, WebSocketServer, WebSocketServerConnection, createWebSocketServer, crypto, encodeMessage, events, exports, genMask, genWebSocketKey, hashWebSocketKey, http, lowerObjKeys, opcodes, test, unmask, util, uuid;
events = require("events");
http = require("http");
crypto = require("crypto");
util = require("util");
URL = require('url');
uuid = require('./uuid');
opcodes = {
TEXT: 1,
BINARY: 2,
CLOSE: 8,
PING: 9,
PONG: 10
};
KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
lowerObjKeys = function(obj) {
var index, j, key, keys, len, lkeys, resp;
keys = (function() {
var results;
results = [];
for (key in obj) {
results.push(key);
}
return results;
})();
lkeys = keys.map(function(key) {
return key.toLowerCase();
});
resp = {};
for (index = j = 0, len = keys.length; j < len; index = ++j) {
key = keys[index];
resp[lkeys[index]] = obj[keys[index]];
}
return resp;
};
hashWebSocketKey = function(key) {
var sha1;
sha1 = crypto.createHash("sha1");
sha1.update(key + KEY_SUFFIX, "ascii");
return sha1.digest("base64");
};
genWebSocketKey = function() {
var _, index, j, key, len;
key = new Buffer(16);
for (index = j = 0, len = key.length; j < len; index = ++j) {
_ = key[index];
key.writeUInt8(Math.floor(Math.random() * 256), index);
}
return key.toString('base64');
};
genMask = function() {
var _, index, j, len, mask;
mask = new Buffer(4);
for (index = j = 0, len = mask.length; j < len; index = ++j) {
_ = mask[index];
mask.writeUInt8(Math.floor(Math.random() * 256), index);
}
return mask;
};
unmask = function(maskBytes, data) {
var i, payload;
payload = new Buffer(data.length);
i = 0;
while (i < data.length) {
payload[i] = maskBytes[i % 4] ^ data[i];
i++;
}
return payload;
};
encodeMessage = function(opcode, payload, useMask) {
var b1, b2, buf, length, mask, maskLen;
if (useMask == null) {
useMask = false;
}
buf = void 0;
mask = void 0;
maskLen = useMask ? 4 : 0;
b1 = 0x80 | opcode;
b2 = useMask ? 0x80 : 0;
length = payload.length;
if (useMask) {
mask = genMask();
payload = unmask(mask, payload);
}
if (length < 126) {
buf = new Buffer(payload.length + 2 + 0 + maskLen);
b2 |= length;
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
payload.copy(buf, 2 + maskLen);
if (useMask) {
mask.copy(buf, 2);
}
} else if (length < (1 << 16)) {
buf = new Buffer(payload.length + 2 + 2 + maskLen);
b2 |= 126;
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
buf.writeUInt16BE(length, 2);
payload.copy(buf, 4 + maskLen);
if (useMask) {
mask.copy(buf, 4);
}
} else {
buf = new Buffer(payload.length + 2 + 8 + maskLen);
b2 |= 127;
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
buf.writeUInt32BE(0, 2);
buf.writeUInt32BE(length, 6);
payload.copy(buf, 10 + maskLen);
if (useMask) {
mask.copy(buf, 10);
}
}
return buf;
};
WebSocketClientConnection = function(url, options) {
var parsed_url, ref, self;
parsed_url = URL.parse(url);
if ((ref = parsed_url.protocol) !== 'ws:' && ref !== 'wss:') {
throw new TypeError("URL scheme must be 'ws' or 'wss'");
}
self = this;
this.options = {
hostname: parsed_url.hostname,
port: parsed_url.port || (parsed_url.protocol.match(/ss/) ? 443 : 80),
path: parsed_url.path || "/",
headers: {}
};
this.options.headers.Host = this.options.hostname + ":" + this.options.port;
this.options.headers.Connection = "Upgrade";
this.options.headers.Upgrade = "websocket";
this.options.headers.Origin = (parsed_url.protocol.match(/ss/) ? 'https' : 'http') + "://" + this.options.hostname + ":" + this.options.port;
this.options.headers['Sec-WebSocket-Version'] = 13;
this.options.headers['Sec-WebSocket-Key'] = genWebSocketKey();
if ((options != null ? options['Sec-WebSocket-Protocol'] : void 0) != null) {
this.options.headers['Sec-WebSocket-Protocol'] = options['Sec-WebSocket-Protocol'];
}
if ((options != null ? options['Sec-WebSocket-Extensions'] : void 0) != null) {
this.options.headers['Sec-WebSocket-Extensions'] = options['Sec-WebSocket-Extensions'];
}
this.request = http.request(this.options);
this.request.on('upgrade', function(response, socket, upgradeHead) {
self.socket = socket;
self.socket.on('error', function(err) {
return console.log('Client Socket error:', err.message);
});
self.socket.on("data", function(buf) {
self.buffer = Buffer.concat([self.buffer, buf]);
while (self._processBuffer()) {
continue;
}
});
self.socket.on("close", function(had_error) {
if (!self.closed) {
self.emit("close", 1006);
self.closed = true;
}
});
return self.emit('open', self.id ? self.id : null);
});
this.request.end();
this.buffer = new Buffer(0);
this.closed = false;
this.currentRoundTrip = 0;
};
util.inherits(WebSocketClientConnection, events.EventEmitter);
Object.defineProperty(WebSocketClientConnection.prototype, 'readyState', {
get: function() {
var ref;
return (ref = this.socket) != null ? ref.readyState : void 0;
}
});
WebSocketClientConnection.prototype._doSend = function(opcode, payload) {
var ref;
if ((ref = this.socket) != null) {
ref.write(encodeMessage(opcode, payload, true));
}
};
WebSocketServerConnection = function(request, socket, upgradeHead) {
var key, lines, protocol, self;
self = this;
key = hashWebSocketKey(lowerObjKeys(request.headers)["sec-websocket-key"]);
protocol = (function() {
var protocols;
if ('sec-websocket-protocol' in request.headers) {
protocols = lowerObjKeys(request.headers)["sec-websocket-protocol"].split(/\s*,\s*/);
return protocols[0];
} else {
return null;
}
})();
lines = [];
lines.push("HTTP/1.1 101 Web Socket Protocol Handshake\r\n");
lines.push("Upgrade: WebSocket\r\n");
lines.push("Connection: Upgrade\r\n");
lines.push("sec-websocket-accept: " + key);
if (protocol) {
lines.push("\r\nsec-websocket-protocol: " + protocol);
}
lines.push("\r\n\r\n");
socket.write(lines.join(''));
socket.on('connect', function(evt) {
return self.emit('open', self.id ? self.id : null);
});
socket.on('error', function(err) {
return console.log('Server Socket error:', err.message);
});
socket.on("data", function(buf) {
self.buffer = Buffer.concat([self.buffer, buf]);
while (self._processBuffer()) {
continue;
}
});
socket.on("close", function(had_error) {
if (!self.closed) {
self.emit("close", 1006);
self.closed = true;
}
});
this.request = request;
this.socket = socket;
this.buffer = new Buffer(0);
this.closed = false;
this.currentRoundTrip = 0;
};
util.inherits(WebSocketServerConnection, events.EventEmitter);
Object.defineProperty(WebSocketServerConnection.prototype, 'readyState', {
get: function() {
return this.socket.readyState;
}
});
WebSocketClientConnection.prototype.ping = WebSocketServerConnection.prototype.ping = function() {
return this._doSend(opcodes.PING, new Buffer(new Date().getTime().toString()));
};
WebSocketClientConnection.prototype.send = WebSocketServerConnection.prototype.send = function(obj) {
var e, error, opcode, payload;
opcode = void 0;
payload = void 0;
if (Buffer.isBuffer(obj)) {
opcode = opcodes.BINARY;
payload = obj;
} else if (typeof obj === "string") {
opcode = opcodes.TEXT;
payload = new Buffer(obj, "utf8");
} else {
try {
obj = JSON.stringify(obj);
opcode = opcodes.TEXT;
payload = new Buffer(obj, "utf8");
} catch (error) {
e = error;
throw new Error("Cannot send object. Must be string or Buffer");
}
}
this._doSend(opcode, payload);
};
WebSocketClientConnection.prototype.close = WebSocketServerConnection.prototype.close = function(code, reason) {
var buffer, e, error, opcode;
opcode = opcodes.CLOSE;
buffer = void 0;
if (code) {
buffer = new Buffer(Buffer.byteLength(reason) + 2);
buffer.writeUInt16BE(code, 0);
buffer.write(reason, 2);
} else {
buffer = new Buffer(0);
}
this._doSend(opcode, buffer);
this.closed = true;
try {
this.socket.end();
this.socket.destroy();
} catch (error) {
e = error;
console.log("Error while destroying underlying raw socket:", e.message);
}
};
WebSocketClientConnection.prototype._processBuffer = WebSocketServerConnection.prototype._processBuffer = function() {
var b1, b2, buf, fin, highBits, idx, length, mask, maskBytes, opcode, payload;
buf = this.buffer;
if (buf.length < 2) {
return;
}
idx = 2;
b1 = buf.readUInt8(0);
fin = b1 & 0x80;
opcode = b1 & 0x0f;
b2 = buf.readUInt8(1);
mask = b2 & 0x80;
length = b2 & 0x7f;
if (length > 125) {
if (buf.length < 8) {
return;
}
if (length === 126) {
length = buf.readUInt16BE(2);
idx += 2;
} else if (length === 127) {
highBits = buf.readUInt32BE(2);
if (highBits !== 0) {
this.close(1009, "");
}
length = buf.readUInt32BE(6);
idx += 8;
}
}
if (buf.length < (idx + (mask !== 0 ? 4 : 0) + length)) {
return;
}
if (mask !== 0) {
maskBytes = buf.slice(idx, idx + 4);
idx += 4;
}
payload = buf.slice(idx, idx + length);
if (mask !== 0) {
payload = unmask(maskBytes, payload);
}
this._handleFrame(opcode, payload);
this.buffer = buf.slice(idx + length);
return true;
};
WebSocketClientConnection.prototype._handleFrame = WebSocketServerConnection.prototype._handleFrame = function(opcode, buffer) {
var code, payload, ping_millis, pong_millis, reason;
payload = void 0;
switch (opcode) {
case opcodes.TEXT:
payload = buffer.toString("utf8");
this.emit("data", opcode, payload);
break;
case opcodes.BINARY:
payload = buffer;
this.emit("data", opcode, payload);
break;
case opcodes.PING:
this._doSend(opcodes.PONG, buffer);
break;
case opcodes.PONG:
pong_millis = new Date().getTime();
ping_millis = parseInt(buffer.toString('utf8'));
this.currentRoundTrip = (pong_millis - ping_millis) / 1000;
this.emit("heartbeat", this.currentRoundTrip, pong_millis);
break;
case opcodes.CLOSE:
code = void 0;
reason = void 0;
if (buffer.length >= 2) {
code = buffer.readUInt16BE(0);
reason = buffer.toString("utf8", 2);
}
this.close(code, reason);
this.emit("close", code, reason);
break;
default:
this.close(1002, "unknown opcode");
this.emit("close", 1002, "unknown opcode");
}
};
WebSocketServerConnection.prototype._doSend = function(opcode, payload) {
var ref;
if ((ref = this.socket) != null) {
ref.write(encodeMessage(opcode, payload, false));
}
};
WebSocketServer = function(handler) {
if (handler && handler.constructor.name === "Function") {
this.connectionHandler = handler;
} else {
throw new Error("Must provide a socket handler function to instantiate a WebSocketServer");
}
};
util.inherits(WebSocketServer, events.EventEmitter);
WebSocketServer.prototype.listen = function(port, host, route) {
var self, srv;
if (route == null) {
route = "/";
}
srv = void 0;
self = this;
switch (port.constructor.name) {
case "Server":
srv = port;
break;
case "String":
srv = http.createServer(function(request, response) {
response.end("websocket server");
});
srv.listen(port);
break;
case "Number":
srv = http.createServer(function(request, response) {
response.end("websocket server");
});
srv.listen(port, (host ? host : "0.0.0.0"));
break;
default:
if (port._handle) {
srv = port;
} else {
throw new TypeError("WebSocketServer only listens on something that has a _handle.");
}
}
srv.on('listening', (function(_this) {
return function() {
return _this.emit('listening');
};
})(this));
return srv.on("upgrade", function(request, socket, upgradeHead) {
var ws;
if (URL.parse(request.url).path !== route) {
} else {
ws = new WebSocketServerConnection(request, socket, upgradeHead);
self.connectionHandler(ws);
setTimeout((function() {
return ws.periodicPing = setInterval((function() {
if (ws.readyState === 'open') {
return ws.ping();
}
}), 2000);
}), 1000);
ws.on('close', function() {
if (ws.periodicPing != null) {
return clearInterval(ws.periodicPing);
}
});
return self.emit('upgrade');
}
});
};
/*
* Didn't work because request doesn't register upgrade event. Must be done at server level.
WebSocketServer::listenOnRoute = (router, path, socket_handler_fn = null) ->
self = @
socket_handler_fn = socket_handler_fn or self.connectionHandler # use ad-hoc socket handler if provided, else use "default" socket handler
obj = router.get_route_handler(path, 'get')
if obj
http_handler_fn = obj.handler_obj.handler
else
http_handler_fn = (request, response) ->
response.end 'websocket server listening at ' + path
path = "/#{path}" unless path.charAt(0) is "/"
router.get path, (request, response) ->
if request.headers['upgrade'] or request.headers['Upgrade']
( (response, socket, upgradeHead) ->
console.log "Received upgrade request on path: #{request.url}"
ws = new WebSocketServerConnection(request, socket, upgradeHead)
socket_handler_fn ws
setTimeout (-> ws.periodicPing = setInterval (-> ws.ping() if ws.readyState is 'open'), 2000), 1000
ws.on 'close', ->
#console.log "Closing server websocket connection", ws.id
clearInterval ws.periodicPing if ws.periodicPing?
self.emit 'upgrade')(null, request.socket, '')
http_handler_fn request, response
*/
createWebSocketServer = function(socket_handler_fn) {
return new WebSocketServer(socket_handler_fn);
};
if (typeof module !== "undefined" && module !== null) {
module.exports = exports = {
createWebSocketServer: createWebSocketServer,
WebSocketServer: WebSocketServer,
WebSocketServerConnection: WebSocketServerConnection,
WebSocketClientConnection: WebSocketClientConnection,
opcodes: opcodes
};
}
test = function() {
var reverseServer;
reverseServer = createWebSocketServer(function(sock) {
sock.on("data", function(opcode, data) {
sock.send(data.split("").reverse().join(""));
});
});
reverseServer.listen(8000);
console.log("Reverse WebSocket Server listening on port 8000");
};
if (!(typeof module !== "undefined" && module !== null ? module.parent : void 0)) {
test();
}
}).call(this);