UNPKG

node-simple-router

Version:

Yet another minimalistic router for node.js

523 lines (483 loc) 16 kB
// 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);