// Github: http://github.com/ncr/node.ws.js // Compatible with node v0.1.91 // Author: Jacek Becela // Contributors: // Michael Stillwell http://github.com/ithinkihaveacat // Nick Chapman http://github.com/nchapman // Dmitriy Shalashov http://github.com/skaurus // Johan Dahlberg // Andreas Kompanez // License: MIT // Based on: http://github.com/Guille/node.websocket.js function nano(template, data) { return template.replace(/\{([\w\.]*)}/g, function (str, key) { var keys = key.split("."), value = data[keys.shift()]; keys.forEach(function (key) { value = value[key] }); return value; }); } 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; } var sys = require("sys"), net = require("net"), crypto = require("crypto"), requiredHeaders = { 'get': /^GET (\/[^\s]*)/, 'upgrade': /^WebSocket$/, 'connection': /^Upgrade$/, 'host': /^(.+)$/, 'origin': /^(.+)$/, }, handshakeTemplate75 = [ 'HTTP/1.1 101 Web Socket Protocol Handshake', 'Upgrade: WebSocket', 'Connection: Upgrade', 'WebSocket-Origin: {origin}', 'WebSocket-Location: ws://{host}{resource}', '', '' ].join("\r\n"), handshakeTemplate76 = [ 'HTTP/1.1 101 WebSocket Protocol Handshake', // note a diff here 'Upgrade: WebSocket', 'Connection: Upgrade', 'Sec-WebSocket-Origin: {origin}', 'Sec-WebSocket-Location: ws://{host}{resource}', '', '{data}' ].join("\r\n"), flashPolicy = ''; exports.createServer = function (websocketListener, options) { if (!options) options = {}; if (!options.flashPolicy) options.flashPolicy = flashPolicy; return net.createServer(function (socket) { socket.setTimeout(0); socket.setNoDelay(true); socket.setKeepAlive(true, 0); var emitter = new process.EventEmitter(), handshaked = false, buffer = ""; function handle(data) { buffer += data; var chunks = buffer.split("\ufffd"), count = chunks.length - 1; // last is "" or a partial packet for(var i = 0; i < count; i++) { var chunk = chunks[i]; if(chunk[0] == "\u0000") { emitter.emit("data", chunk.slice(1)); } else { socket.end(); return; } } buffer = chunks[count]; } function handshake(data) { var _headers = data.split("\r\n"); if ( //.exec(_headers[0]) ) { socket.write( options.flashPolicy ); socket.end(); return; } // go to more convenient hash form var headers = {}, upgradeHead, len = _headers.length; if ( _headers[0].match(/^GET /) ) { headers["get"] = _headers[0]; } else { socket.end(); return; } if ( _headers[ _headers.length - 1 ] ) { upgradeHead = _headers[ _headers.length - 1 ]; len--; } while (--len) { // _headers[0] will be skipped var header = _headers[len]; if (!header) continue; var split = header.split(": ", 2); // second parameter actually seems to not work in node headers[ split[0].toLowerCase() ] = split[1]; } // check if we have all needed headers and fetch data from them var data = {}, match; for (var header in requiredHeaders) { // regexp actual header value if ( match = requiredHeaders[ header ].exec( headers[header] ) ) { data[header] = match; } else { socket.end(); return; } } // draft auto-sensing if ( headers["sec-websocket-key1"] && headers["sec-websocket-key2"] && upgradeHead ) { // 76 var strkey1 = headers["sec-websocket-key1"] , strkey2 = 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) { socket.end(); return; } var hash = crypto.createHash("md5") , key1 = pack(parseInt(numkey1/spaces1)) , key2 = pack(parseInt(numkey2/spaces2)); hash.update(key1); hash.update(key2); hash.update(upgradeHead); socket.write(nano(handshakeTemplate76, { resource: data.get[1], host: data.host[1], origin: data.origin[1], data: hash.digest("binary"), }), "binary"); } else { // 75 socket.write(nano(handshakeTemplate75, { resource: data.get[1], host: data.host[1], origin: data.origin[1], })); } handshaked = true; emitter.emit("connect", data.get[1]); } socket.addListener("data", function (data) { if(handshaked) { handle(data.toString("utf8")); } else { handshake(data.toString("binary")); // because of draft76 handshakes } }).addListener("end", function () { socket.end(); }).addListener("close", function () { if (handshaked) { // don't emit close from policy-requests emitter.emit("close"); } }).addListener("error", function (exception) { if (emitter.listeners("error").length > 0) { emitter.emit("error", exception); } else { throw exception; } }); emitter.remoteAddress = socket.remoteAddress; emitter.write = function (data) { try { socket.write('\u0000', 'binary'); socket.write(data, 'utf8'); socket.write('\uffff', 'binary'); } catch(e) { // Socket not open for writing, // should get "close" event just before. socket.end(); } } emitter.end = function () { socket.end(); } websocketListener(emitter); // emits: "connect", "data", "close", provides: write(data), end() }); }