diff options
Diffstat (limited to 'node_modules/socket.io/support/node-websocket-client/lib')
| -rw-r--r-- | node_modules/socket.io/support/node-websocket-client/lib/websocket.js | 617 |
1 files changed, 617 insertions, 0 deletions
diff --git a/node_modules/socket.io/support/node-websocket-client/lib/websocket.js b/node_modules/socket.io/support/node-websocket-client/lib/websocket.js new file mode 100644 index 0000000..4f7f734 --- /dev/null +++ b/node_modules/socket.io/support/node-websocket-client/lib/websocket.js @@ -0,0 +1,617 @@ +var assert = require('assert'); +var buffer = require('buffer'); +var crypto = require('crypto'); +var events = require('events'); +var http = require('http'); +var net = require('net'); +var urllib = require('url'); +var sys = require('util'); + +var FRAME_NO = 0; +var FRAME_LO = 1; +var FRAME_HI = 2; + +// Values for readyState as per the W3C spec +var CONNECTING = 0; +var OPEN = 1; +var CLOSING = 2; +var CLOSED = 3; + +var debugLevel = parseInt(process.env.NODE_DEBUG, 16); +var debug = (debugLevel & 0x4) ? + function() { sys.error.apply(this, arguments); } : + function() { }; + +// Generate a Sec-WebSocket-* value +var createSecretKey = function() { + // How many spaces will we be inserting? + var numSpaces = 1 + Math.floor(Math.random() * 12); + assert.ok(1 <= numSpaces && numSpaces <= 12); + + // What is the numerical value of our key? + var keyVal = (Math.floor( + Math.random() * (4294967295 / numSpaces) + ) * numSpaces); + + // Our string starts with a string representation of our key + var s = keyVal.toString(); + + // Insert 'numChars' worth of noise in the character ranges + // [0x21, 0x2f] (14 characters) and [0x3a, 0x7e] (68 characters) + var numChars = 1 + Math.floor(Math.random() * 12); + assert.ok(1 <= numChars && numChars <= 12); + + for (var i = 0; i < numChars; i++) { + var pos = Math.floor(Math.random() * s.length + 1); + + var c = Math.floor(Math.random() * (14 + 68)); + c = (c <= 14) ? + String.fromCharCode(c + 0x21) : + String.fromCharCode((c - 14) + 0x3a); + + s = s.substring(0, pos) + c + s.substring(pos, s.length); + } + + // We shoudln't have any spaces in our value until we insert them + assert.equal(s.indexOf(' '), -1); + + // Insert 'numSpaces' worth of spaces + for (var i = 0; i < numSpaces; i++) { + var pos = Math.floor(Math.random() * (s.length - 1)) + 1; + s = s.substring(0, pos) + ' ' + s.substring(pos, s.length); + } + + assert.notEqual(s.charAt(0), ' '); + assert.notEqual(s.charAt(s.length), ' '); + + return s; +}; + +// Generate a challenge sequence +var createChallenge = function() { + var c = ''; + for (var i = 0; i < 8; i++) { + c += String.fromCharCode(Math.floor(Math.random() * 255)); + } + + return c; +}; + +// Get the value of a secret key string +// +// This strips non-digit values and divides the result by the number of +// spaces found. +var secretKeyValue = function(sk) { + var ns = 0; + var v = 0; + + for (var i = 0; i < sk.length; i++) { + var cc = sk.charCodeAt(i); + + if (cc == 0x20) { + ns++; + } else if (0x30 <= cc && cc <= 0x39) { + v = v * 10 + cc - 0x30; + } + } + + return Math.floor(v / ns); +} + +// Get the to-be-hashed value of a secret key string +// +// This takes the result of secretKeyValue() and encodes it in a big-endian +// byte string +var secretKeyHashValue = function(sk) { + var skv = secretKeyValue(sk); + + var hv = ''; + hv += String.fromCharCode((skv >> 24) & 0xff); + hv += String.fromCharCode((skv >> 16) & 0xff); + hv += String.fromCharCode((skv >> 8) & 0xff); + hv += String.fromCharCode((skv >> 0) & 0xff); + + return hv; +}; + +// Compute the secret key signature based on two secret key strings and some +// handshaking data. +var computeSecretKeySignature = function(s1, s2, hs) { + assert.equal(hs.length, 8); + + var hash = crypto.createHash('md5'); + + hash.update(secretKeyHashValue(s1)); + hash.update(secretKeyHashValue(s2)); + hash.update(hs); + + return hash.digest('binary'); +}; + +// Return a hex representation of the given binary string; used for debugging +var str2hex = function(str) { + var hexChars = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f' + ]; + + var out = ''; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + out += hexChars[(c & 0xf0) >>> 4]; + out += hexChars[c & 0x0f]; + out += ' '; + } + + return out.trim(); +}; + +// Get the scheme for a URL, undefined if none is found +var getUrlScheme = function(url) { + var i = url.indexOf(':'); + if (i == -1) { + return undefined; + } + + return url.substring(0, i); +}; + +// Set a constant on the given object +var setConstant = function(obj, name, value) { + Object.defineProperty(obj, name, { + get : function() { + return value; + } + }); +}; + +// WebSocket object +// +// This is intended to conform (mostly) to http://dev.w3.org/html5/websockets/ +// +// N.B. Arguments are parsed in the anonymous function at the bottom of the +// constructor. +var WebSocket = function(url, proto, opts) { + events.EventEmitter.call(this); + + // Retain a reference to our object + var self = this; + + // State of our end of the connection + var readyState = CONNECTING; + + // Whether or not the server has sent a close handshake + var serverClosed = false; + + // Our underlying net.Stream instance + var stream = undefined; + + opts = opts || { + origin : 'http://www.example.com' + }; + + // Frame parsing functions + // + // These read data from the given buffer starting at the given offset, + // looking for the end of the current frame. If found, the current frame is + // emitted and the function returns. Only a single frame is processed at a + // time. + // + // The number of bytes read to complete a frame is returned, which the + // caller is to use to advance along its buffer. If 0 is returned, no + // completed frame bytes were found, and the caller should probably enqueue + // the buffer as a continuation of the current message. If a complete frame + // is read, the function is responsible for resting 'frameType'. + + // Framing data + var frameType = FRAME_NO; + var bufs = []; + var bufsBytes = 0; + + // Frame-parsing functions + var frameFuncs = [ + // FRAME_NO + function(buf, off) { + if (buf[off] & 0x80) { + frameType = FRAME_HI; + } else { + frameType = FRAME_LO; + } + + return 1; + }, + + // FRAME_LO + function(buf, off) { + debug('frame_lo(' + sys.inspect(buf) + ', ' + off + ')'); + + // Find the first instance of 0xff, our terminating byte + for (var i = off; i < buf.length && buf[i] != 0xff; i++) + ; + + // We didn't find a terminating byte + if (i >= buf.length) { + return 0; + } + + // We found a terminating byte; collect all bytes into a single buffer + // and emit it + var mb = null; + if (bufs.length == 0) { + mb = buf.slice(off, i); + } else { + mb = new buffer.Buffer(bufsBytes + i); + + var mbOff = 0; + bufs.forEach(function(b) { + b.copy(mb, mbOff, 0, b.length); + mbOff += b.length; + }); + + assert.equal(mbOff, bufsBytes); + + // Don't call Buffer.copy() if we're coping 0 bytes. Rather + // than being a no-op, this will trigger a range violation on + // the destination. + if (i > 0) { + buf.copy(mb, mbOff, off, i); + } + + // We consumed all of the buffers that we'd been saving; clear + // things out + bufs = []; + bufsBytes = 0; + } + + process.nextTick(function() { + var b = mb; + return function() { + var m = b.toString('utf8'); + + self.emit('data', b); + self.emit('message', m); // wss compat + + if (self.onmessage) { + self.onmessage({data: m}); + } + }; + }()); + + frameType = FRAME_NO; + return i - off + 1; + }, + + // FRAME_HI + function(buf, off) { + debug('frame_hi(' + sys.inspect(buf) + ', ' + off + ')'); + + if (buf[off] !== 0) { + throw new Error('High-byte framing not supported.'); + } + + serverClosed = true; + return 1; + } + ]; + + // Handle data coming from our socket + var dataListener = function(buf) { + if (buf.length <= 0 || serverClosed) { + return; + } + + debug('dataListener(' + sys.inspect(buf) + ')'); + + var off = 0; + var consumed = 0; + + do { + if (frameType < 0 || frameFuncs.length <= frameType) { + throw new Error('Unexpected frame type: ' + frameType); + } + + assert.equal(bufs.length === 0, bufsBytes === 0); + assert.ok(off < buf.length); + + consumed = frameFuncs[frameType](buf, off); + off += consumed; + } while (!serverClosed && consumed > 0 && off < buf.length); + + if (serverClosed) { + serverCloseHandler(); + } + + if (consumed == 0) { + bufs.push(buf.slice(off, buf.length)); + bufsBytes += buf.length - off; + } + }; + + // Handle incoming file descriptors + var fdListener = function(fd) { + self.emit('fd', fd); + }; + + // Handle errors from any source (HTTP client, stream, etc) + var errorListener = function(e) { + process.nextTick(function() { + self.emit('wserror', e); + + if (self.onerror) { + self.onerror(e); + } + }); + }; + + // Finish the closing process; destroy the socket and tell the application + // that we've closed. + var finishClose = self.finishClose = function() { + readyState = CLOSED; + if (stream) { + stream.end(); + stream.destroy(); + stream = undefined; + } + + process.nextTick(function() { + self.emit('close'); + if (self.onclose) { + self.onclose(); + } + }); + }; + + // Send a close frame to the server + var sendClose = function() { + assert.equal(OPEN, readyState); + + readyState = CLOSING; + stream.write('\xff\x00', 'binary'); + }; + + // Handle a close packet sent from the server + var serverCloseHandler = function() { + assert.ok(serverClosed); + assert.ok(readyState === OPEN || readyState === CLOSING); + + bufs = []; + bufsBytes = 0; + + // Handle state transitions asynchronously so that we don't change + // readyState before the application has had a chance to process data + // events which are already in the delivery pipeline. For example, a + // 'data' event could be delivered with a readyState of CLOSING if we + // received both frames in the same packet. + process.nextTick(function() { + if (readyState === OPEN) { + sendClose(); + } + + finishClose(); + }); + }; + + // External API + self.close = function(timeout) { + if (readyState === CONNECTING) { + // If we're still in the process of connecting, the server is not + // in a position to understand our close frame. Just nuke the + // connection and call it a day. + finishClose(); + } else if (readyState === OPEN) { + sendClose(); + + if (timeout) { + setTimeout(finishClose, timeout * 1000); + } + } + }; + + self.send = function(str, fd) { + if (readyState != OPEN) { + return; + } + + stream.write('\x00', 'binary'); + stream.write(str, 'utf8', fd); + stream.write('\xff', 'binary'); + }; + + // wss compat + self.write = self.send; + + setConstant(self, 'url', url); + + Object.defineProperty(self, 'readyState', { + get : function() { + return readyState; + } + }); + + // Connect and perform handshaking with the server + (function() { + // Parse constructor arguments + if (!url) { + throw new Error('Url and must be specified.'); + } + + // Secrets used for handshaking + var key1 = createSecretKey(); + var key2 = createSecretKey(); + var challenge = createChallenge(); + + debug( + 'key1=\'' + str2hex(key1) + '\'; ' + + 'key2=\'' + str2hex(key2) + '\'; ' + + 'challenge=\'' + str2hex(challenge) + '\'' + ); + + var httpHeaders = { + 'Connection' : 'Upgrade', + 'Upgrade' : 'WebSocket', + 'Sec-WebSocket-Key1' : key1, + 'Sec-WebSocket-Key2' : key2 + }; + if (opts.origin) { + httpHeaders['Origin'] = opts.origin; + } + if (proto) { + httpHeaders['Sec-WebSocket-Protocol'] = proto; + } + + var httpPath = '/'; + + // Create the HTTP client that we'll use for handshaking. We'll cannabalize + // its socket via the 'upgrade' event and leave it to rot. + // + // N.B. The ws+unix:// scheme makes use of the implementation detail + // that http.Client passes its constructor arguments through, + // un-inspected to net.Stream.connect(). The latter accepts a + // string as its first argument to connect to a UNIX socket. + var opt = {}; + var agent = null; + switch (getUrlScheme(url)) { + case 'ws': + var u = urllib.parse(url); + agent = new http.Agent({ + host: u.hostname, + port: u.port || 80 + }); + opt.agent = agent; + opt.host = u.hostname; + opt.port = u.port || 80; + opt.path = (u.pathname || '/') + (u.search || ''); + opt.headers = httpHeaders; + break; + + case 'ws+unix': + var sockPath = url.substring('ws+unix://'.length, url.length); + var u = urllib.parse(url); + agent = new http.Agent({ + host: 'localhost', + port: sockPath + }); + opt.agent = agent; + opt.host = 'localhost'; + opt.path = sockPath; + opt.headers = httpHeaders; + break; + + default: + throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.'); + } + + var httpReq = http.request(opt, function() { }); + var upgradeHandler = (function() { + var data = undefined; + + return function(req, s, head) { + req.socket.setNoDelay(true); + stream = s; + + if (readyState == CLOSED) { + stream.end(); + stream.destroy(); + stream = undefined; + return; + } + + stream.on('data', function(d) { + if (d.length <= 0) { + return; + } + + if (!data) { + data = d; + } else { + var data2 = new buffer.Buffer(data.length + d.length); + + data.copy(data2, 0, 0, data.length); + d.copy(data2, data.length, 0, d.length); + + data = data2; + } + + if (data.length >= 16) { + var expected = computeSecretKeySignature(key1, key2, challenge); + var actual = data.slice(0, 16).toString('binary'); + + // Handshaking fails; we're donezo + if (actual != expected) { + debug( + 'expected=\'' + str2hex(expected) + '\'; ' + + 'actual=\'' + str2hex(actual) + '\'' + ); + + process.nextTick(function() { + // N.B. Emit 'wserror' here, as 'error' is a reserved word in the + // EventEmitter world, and gets thrown. + self.emit( + 'wserror', + new Error('Invalid handshake from server:' + + 'expected \'' + str2hex(expected) + '\', ' + + 'actual \'' + str2hex(actual) + '\'' + ) + ); + + if (self.onerror) { + self.onerror(); + } + + finishClose(); + }); + } + + // Un-register our data handler and add the one to be used + // for the normal, non-handshaking case. If we have extra + // data left over, manually fire off the handler on + // whatever remains. + // + // XXX: This is lame. We should only remove the listeners + // that we added. + httpReq.removeAllListeners('upgrade'); + stream.removeAllListeners('data'); + stream.on('data', dataListener); + + readyState = OPEN; + + process.nextTick(function() { + self.emit('open'); + + if (self.onopen) { + self.onopen(); + } + }); + + // Consume any leftover data + if (data.length > 16) { + stream.emit('data', data.slice(16, data.length)); + } + } + }); + stream.on('fd', fdListener); + stream.on('error', errorListener); + stream.on('close', function() { + errorListener(new Error('Stream closed unexpectedly.')); + }); + + stream.emit('data', head); + }; + })(); + agent.on('upgrade', upgradeHandler); // node v0.4 + httpReq.on('upgrade', upgradeHandler); // node v0.5+ + + httpReq.write(challenge, 'binary'); + httpReq.end(); + })(); +}; +sys.inherits(WebSocket, events.EventEmitter); +exports.WebSocket = WebSocket; + +// Add some constants to the WebSocket object +setConstant(WebSocket.prototype, 'CONNECTING', CONNECTING); +setConstant(WebSocket.prototype, 'OPEN', OPEN); +setConstant(WebSocket.prototype, 'CLOSING', CLOSING); +setConstant(WebSocket.prototype, 'CLOSED', CLOSED); + +// vim:ts=4 sw=4 et |
