UNPKG

webworker

Version:

An implementation of the HTML5 Web Worker API

409 lines (335 loc) 11.3 kB
var sys = require("sys") , events = require("events") , Buffer = require("buffer").Buffer , Crypto = require("crypto"); /*----------------------------------------------- Debugged -----------------------------------------------*/ var debug; /*----------------------------------------------- The Connection: -----------------------------------------------*/ module.exports = Connection; function Connection(server, req, socket, upgradeHead){ this.debug = server.debug; if (this.debug) { debug = function () { sys.error('\033[90mWS: ' + Array.prototype.join.call(arguments, ", ") + "\033[39m"); }; } else { debug = function () { }; } this._req = req; this._server = server; this._upgradeHead = upgradeHead; this._id = this._req.socket.remotePort; events.EventEmitter.call(this); this.version = this.getVersion(); if( !checkVersion(this)) { this.reject("Invalid version."); } else { debug(this._id, this.version+" connection"); // Set the initial connecting state. this.state(1); // Allow us to send data immediately: req.socket.setNoDelay(true); // Hopefully allow us to keep the socket open indefinitely: req.socket.setTimeout(0); req.socket.setKeepAlive(true, 0); // Handle incoming data: var parser = new Parser(this); req.socket.addListener("data", function(data){ parser.write(data); }); // Handle the end of the stream, and set the state // appropriately to notify the correct events. var connection = this; req.socket.addListener("end", function(){ connection.state(5); }); // Setup the connection manager's state change listeners: this.addListener("stateChange", function(state, oldstate){ if(state == 5){ server.manager.detach(connection._id, function(){ server.emit("close", connection); connection.emit("close"); }); } else if(state == 4){ server.manager.attach(connection._id, connection); server.emit("connection", connection); } }); // Let us see the messages when in debug mode. if(this.debug){ this.addListener("message", function(msg){ debug(connection._id, "recv: " + msg); }); } // Carry out the handshaking. // - Draft75: There's no upgradeHead, goto Then. // Draft76: If there's an upgradeHead of the right length, goto Then. // Then: carry out the handshake. // // - Currently no browsers to my knowledge split the upgradeHead off the request, // but in the case it does happen, then the state is set to waiting for // the upgradeHead. // // HANDLING FOR THIS EDGE CASE IS NOT IMPLEMENTED. // if((this.version == "draft75") || (this.version == "draft76" && this._upgradeHead && this._upgradeHead.length == 8)){ this.handshake(); } else { this.state(2); debug(this._id, "waiting."); } } }; sys.inherits(Connection, events.EventEmitter); /*----------------------------------------------- Various utility style functions: -----------------------------------------------*/ var writeSocket = function(socket, data, encoding, fd) { if(socket.writable){ socket.write(data, encoding, fd); return true; } return false; }; var closeClient = function(client){ client._req.socket.end(); client._req.socket.destroy(); client.state(5); debug(client._id, "closed"); }; function checkVersion(client){ var server_version = client._server.options.version.toLowerCase() , client_version = client.version = client.version || client.getVersion(); return (server_version == "auto" || server_version == client_version); }; function pack(num) { var result = ''; result += String.fromCharCode(num >> 24 & 0xFF); result += String.fromCharCode(num >> 16 & 0xFF); result += String.fromCharCode(num >> 8 & 0xFF); result += String.fromCharCode(num & 0xFF); return result; }; /*----------------------------------------------- Formatters for the urls -----------------------------------------------*/ function websocket_origin(){ var origin = this._server.options.origin || "*"; if(origin == "*" || typeof origin == "Array"){ origin = this._req.headers.origin; } return origin; }; function websocket_location(){ var location = "", secure = this._req.socket.secure, request_host = this._req.headers.host.split(":"), port = request_host[1]; if(secure){ location += "wss://"; } else { location += "ws://"; } location += request_host[0] if(!secure && port != 80 || secure && port != 443){ location += ":"+port; } location += this._req.url; return location; }; /*----------------------------------------------- 0. unknown 1. opening 2. waiting 3. handshaking 4, connected 5. closed -----------------------------------------------*/ Connection.prototype._state = 0; /*----------------------------------------------- Connection Public API -----------------------------------------------*/ Connection.prototype.state = function(state){ if(state !== undefined && typeof state === "number"){ var oldstate = this._state; this._state = state; this.emit("stateChange", this._state, oldstate); } }; Connection.prototype.getVersion = function(){ if(this._req.headers["sec-websocket-key1"] && this._req.headers["sec-websocket-key2"]){ return "draft76"; } else { return "draft75"; } }; Connection.prototype.write = function(data, fd){ var socket = this._req.socket; if(this._state == 4){ debug(this._id, "write: "+data); if( writeSocket(socket, "\x00", "binary") && writeSocket(socket, data, "utf8", fd) && writeSocket(socket, "\xff", "binary") ){ return true; } else { debug(this._id, "\033[31mERROR: write: "+data); } } else { debug(this._id, "\033[31mCouldn't send."); } return false; }; Connection.prototype.close = function(){ var socket = this._req.socket; if(this._state == 4 && socket.writable){ writeSocket(socket, "\x00", "binary"); writeSocket(socket, "\xff", "binary"); } closeClient(this); }; Connection.prototype.reject = function(reason){ debug(this._id, "rejected. Reason: "+reason); this.emit("rejected"); closeClient(this); }; Connection.prototype.handshake = function(){ if(this._state < 3){ debug(this._id, this.version+" handshake"); this.state(3); doHandshake[this.version].call(this); } else { debug(this._id, "Already handshaked."); } }; /*----------------------------------------------- Do the handshake. -----------------------------------------------*/ var doHandshake = { /* Using draft75, work out and send the handshake. */ draft75: function(){ var res = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" + "WebSocket-Origin: "+websocket_origin.call(this)+"\r\n" + "WebSocket-Location: "+websocket_location.call(this); if(this._server.options.subprotocol && typeof this._server.options.subprotocol == "string") { res += "\r\nWebSocket-Protocol: "+this._server.options.subprotocol; } writeSocket(this._req.socket, res+"\r\n\r\n", "ascii"); this.state(4); }, /* Using draft76 (security model), work out and send the handshake. */ draft76: function(){ var data = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Origin: "+websocket_origin.call(this)+"\r\n" + "Sec-WebSocket-Location: "+websocket_location.call(this); if(this._server.options.subprotocol && typeof this._server.options.subprotocol == "string") { res += "\r\nSec-WebSocket-Protocol: "+this._server.options.subprotocol; } var strkey1 = this._req.headers['sec-websocket-key1'] , strkey2 = this._req.headers['sec-websocket-key2'] , numkey1 = parseInt(strkey1.replace(/[^\d]/g, ""), 10) , numkey2 = parseInt(strkey2.replace(/[^\d]/g, ""), 10) , spaces1 = strkey1.replace(/[^\ ]/g, "").length , spaces2 = strkey2.replace(/[^\ ]/g, "").length; if (spaces1 == 0 || spaces2 == 0 || numkey1 % spaces1 != 0 || numkey2 % spaces2 != 0) { this.reject("WebSocket contained an invalid key -- closing connection."); } else { var hash = Crypto.createHash("md5") , key1 = pack(parseInt(numkey1/spaces1)) , key2 = pack(parseInt(numkey2/spaces2)); hash.update(key1); hash.update(key2); hash.update(this._upgradeHead.toString("binary")); data += "\r\n\r\n"; data += hash.digest("binary"); writeSocket(this._req.socket, data, "binary"); this.state(4); } } }; /*----------------------------------------------- The new onData callback for http.Server IncomingMessage -----------------------------------------------*/ var Parser = function(client){ this.frameData = []; this.order = 0; this.client = client; }; Parser.prototype.write = function(data){ var pkt, msg; for(var i = 0, len = data.length; i<len; i++){ if(this.order == 0){ if(data[i] & 0x80 == 0x80){ this.order = 1; } else { this.order = -1; } } else if(this.order == -1){ if(data[i] === 0xFF){ pkt = new Buffer(this.frameData); this.order = 0; this.frameData = []; this.client.emit("message", pkt.toString("utf8", 0, pkt.length)); } else { this.frameData.push(data[i]); } } else if(this.order == 1){ debug(this.client._id, "High Order packet handling is not yet implemented."); this.order = 0; } } }; /* function ondata(data, start, end){ if(this.state == 2 && this.version == "draft76"){ // TODO: I need to figure out an alternative here. // data.copy(this._req.upgradeHead, 0, start, end); debug.call(this, "Using draft76 & upgrade body not sent with request."); this.reject("missing upgrade body"); // Assume the data is now a message: } else if(this.state == 4){ data = data.slice(start, end); var frame_type = null, length, b; var parser_offset = -1; var raw_data = []; while(parser_offset < data.length-2){ frame_type = data[parser_offset++]; if(frame_type & 0x80 == 0x80){ debug.call(this, "high"); b = null; length = 1; while(length--){ b = data[parser_offset++]; length = length * 128 + (b & 0x7F); if(b & 0x80 == 0){ break; } } parser_offset += length; if(frame_type == 0xFF && length == 0){ this.close(); } } else { raw_data = []; while(parser_offset <= data.length){ b = data[parser_offset++]; if(b == 0xFF){ var buf = new Buffer(raw_data); this.emit("message", buf.toString("utf8", 0, buf.length)); break; } raw_data.push(b); } } } } }; */