/js/lib/Socket.IO-node/support/node-websocket-client/lib/websocket.js
http://github.com/onedayitwillmake/RealtimeMultiplayerNodeJs · JavaScript · 562 lines · 365 code · 108 blank · 89 comment · 59 complexity · 929a4add1a4a95b25259950ac0efef86 MD5 · raw file
- 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('sys');
- 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 the connection
- var readyState = CONNECTING;
- // 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 look
- // 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 of completed frames read 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 fro 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) {
- throw new Error('High-byte frames not yet supported');
- }
- frameType = FRAME_LO;
- return 1;
- },
- // FRAME_LO
- function(buf, off) {
- assert.ok(bufs.length > 0 || bufsBytes == 0);
- 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('High-byte framing not yet supported');
- frameType = FRAME_NO;
- return buf.length - off;
- }
- ];
- // Handle data coming from our socket
- var dataListener = function(buf) {
- if (buf.length <= 0) {
- 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);
- }
- consumed = frameFuncs[frameType](buf, off);
- off += consumed;
- } while (consumed > 0 && off < buf.length);
- 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);
- }
- });
- };
- // External API
- self.close = function() {
- var f = function() {
- readyState = CLOSED;
- if (stream) {
- stream.end();
- stream.destroy();
- stream = undefined;
- }
- process.nextTick(function() {
- self.emit('close');
- if (self.onclose) {
- self.onclose();
- }
- });
- };
- switch (readyState) {
- case CLOSED:
- case CLOSING:
- break;
- case CONNECTING:
- f();
- break;
- default:
- readyState = CLOSING;
- // XXX: Run f() on the next tick so that we conform a little
- // closer to the spirit of the API in that the caller
- // never sees us transition directly to CLOSED. Instead, we
- // just seem to have an infinitely fast closing handshake.
- if (stream.write('', 'binary')) {
- process.nextTick(f);
- } else {
- stream.addListener('drain', f);
- }
- }
- };
- self.send = function(str, fd) {
- if (readyState != OPEN) {
- throw new Error('Cannot write to non-open WebSocket client');
- }
- 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;
- }
- var httpPath = '/';
- if (proto) {
- httpHeaders['Sec-WebSocket-Protocol'] = proto;
- }
- // Create the HTTP client that we'll use for handshaking. We'll cannabalize
- // its socket via the 'upgrade' event and leave it to rot.
- //
- // XXX: 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 httpClient = undefined;
- switch (getUrlScheme(url)) {
- case 'ws':
- var u = urllib.parse(url);
- httpClient = http.createClient(u.port || 80, u.hostname);
- httpPath = (u.pathname || '/') + (u.search || '');
- httpHeaders.Host = u.hostname;
- break;
- case 'ws+unix':
- var sockPath = url.substring('ws+unix://'.length, url.length);
- httpClient = http.createClient(sockPath);
- httpHeaders.Host = 'localhost';
- break;
- default:
- throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.');
- }
- httpClient.addListener('upgrade', (function() {
- var data = undefined;
- return function(req, s, head) {
- stream = s;
- stream.addListener('data', function(d) {
- if (!data) {
- data = d;
- } else {
- var data2 = new buffer.Buffer(data.length + d.length);
- if (data.length) {
- data.copy(data2, 0, 0, data.length);
- }
- if (data2.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() {
- // XXX: 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();
- }
- self.close();
- });
- }
- // 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.
- httpClient.removeAllListeners('upgrade');
- stream.removeAllListeners('data');
- stream.addListener('data', dataListener);
- // Fire the 'open' event
- process.nextTick(function() {
- self.emit('open');
- if (self.onopen) {
- self.onopen();
- }
- });
- readyState = OPEN;
- if (data.length > 16) {
- stream.emit('data', data.slice(16, data.length));
- }
- }
- });
- stream.addListener('fd', fdListener);
- stream.addListener('error', errorListener);
- stream.emit('data', head);
- };
- })());
- httpClient.addListener('error', function(e) {
- httpClient.end();
- errorListener(e);
- });
- var httpReq = httpClient.request(httpPath, httpHeaders);
- 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