PageRenderTime 47ms CodeModel.GetById 19ms app.highlight 23ms RepoModel.GetById 1ms app.codeStats 0ms

/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
  1var assert = require('assert');
  2var buffer = require('buffer');
  3var crypto = require('crypto');
  4var events = require('events');
  5var http = require('http');
  6var net = require('net');
  7var urllib = require('url');
  8var sys = require('sys');
  9
 10var FRAME_NO = 0;
 11var FRAME_LO = 1;
 12var FRAME_HI = 2;
 13
 14// Values for readyState as per the W3C spec
 15var CONNECTING = 0;
 16var OPEN = 1;
 17var CLOSING = 2;
 18var CLOSED = 3;
 19
 20var debugLevel = parseInt(process.env.NODE_DEBUG, 16);
 21var debug = (debugLevel & 0x4) ?
 22    function() { sys.error.apply(this, arguments); } :
 23    function() { };
 24
 25// Generate a Sec-WebSocket-* value
 26var createSecretKey = function() {
 27    // How many spaces will we be inserting?
 28    var numSpaces = 1 + Math.floor(Math.random() * 12);
 29    assert.ok(1 <= numSpaces && numSpaces <= 12);
 30
 31    // What is the numerical value of our key?
 32    var keyVal = (Math.floor(
 33        Math.random() * (4294967295 / numSpaces)
 34    ) * numSpaces);
 35
 36    // Our string starts with a string representation of our key
 37    var s = keyVal.toString();
 38
 39    // Insert 'numChars' worth of noise in the character ranges
 40    // [0x21, 0x2f] (14 characters) and [0x3a, 0x7e] (68 characters)
 41    var numChars = 1 + Math.floor(Math.random() * 12);
 42    assert.ok(1 <= numChars && numChars <= 12);
 43    
 44    for (var i = 0; i < numChars; i++) {
 45        var pos = Math.floor(Math.random() * s.length + 1);
 46
 47        var c = Math.floor(Math.random() * (14 + 68));
 48        c = (c <= 14) ?
 49            String.fromCharCode(c + 0x21) :
 50            String.fromCharCode((c - 14) + 0x3a);
 51
 52        s = s.substring(0, pos) + c + s.substring(pos, s.length);
 53    }
 54
 55    // We shoudln't have any spaces in our value until we insert them
 56    assert.equal(s.indexOf(' '), -1);
 57
 58    // Insert 'numSpaces' worth of spaces
 59    for (var i = 0; i < numSpaces; i++) {
 60        var pos = Math.floor(Math.random() * (s.length - 1)) + 1;
 61        s = s.substring(0, pos) + ' ' + s.substring(pos, s.length);
 62    }
 63
 64    assert.notEqual(s.charAt(0), ' ');
 65    assert.notEqual(s.charAt(s.length), ' ');
 66
 67    return s;
 68};
 69
 70// Generate a challenge sequence
 71var createChallenge = function() {
 72    var c = ''; 
 73    for (var i = 0; i < 8; i++) {
 74        c += String.fromCharCode(Math.floor(Math.random() * 255));
 75    }
 76
 77    return c;
 78};
 79
 80// Get the value of a secret key string
 81//
 82// This strips non-digit values and divides the result by the number of
 83// spaces found.
 84var secretKeyValue = function(sk) {
 85    var ns = 0;
 86    var v = 0;
 87
 88    for (var i = 0; i < sk.length; i++) {
 89        var cc = sk.charCodeAt(i);
 90        
 91        if (cc == 0x20) {
 92            ns++;
 93        } else if (0x30 <= cc && cc <= 0x39) {
 94            v = v * 10 + cc - 0x30;
 95        }
 96    }
 97
 98    return Math.floor(v / ns);
 99}
100
101// Get the to-be-hashed value of a secret key string
102//
103// This takes the result of secretKeyValue() and encodes it in a big-endian
104// byte string
105var secretKeyHashValue = function(sk) {
106    var skv = secretKeyValue(sk);
107   
108    var hv = '';
109    hv += String.fromCharCode((skv >> 24) & 0xff);
110    hv += String.fromCharCode((skv >> 16) & 0xff);
111    hv += String.fromCharCode((skv >> 8) & 0xff);
112    hv += String.fromCharCode((skv >> 0) & 0xff);
113
114    return hv;
115};
116
117// Compute the secret key signature based on two secret key strings and some
118// handshaking data.
119var computeSecretKeySignature = function(s1, s2, hs) { 
120    assert.equal(hs.length, 8);
121
122    var hash = crypto.createHash('md5');
123
124    hash.update(secretKeyHashValue(s1));
125    hash.update(secretKeyHashValue(s2));
126    hash.update(hs);
127
128    return hash.digest('binary');
129};
130
131// Return a hex representation of the given binary string; used for debugging
132var str2hex = function(str) {
133    var hexChars = [
134        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 
135        'a', 'b', 'c', 'd', 'e', 'f'
136    ];
137
138    var out = '';
139    for (var i = 0; i < str.length; i++) {
140        var c = str.charCodeAt(i);
141        out += hexChars[(c & 0xf0) >>> 4];
142        out += hexChars[c & 0x0f];
143        out += ' ';
144    }
145
146    return out.trim();
147};
148
149// Get the scheme for a URL, undefined if none is found
150var getUrlScheme = function(url) {
151    var i = url.indexOf(':');
152    if (i == -1) {
153        return undefined;
154    }
155
156    return url.substring(0, i);
157};
158
159// Set a constant on the given object
160var setConstant = function(obj, name, value) {
161    Object.defineProperty(obj, name, {
162        get : function() {
163            return value;
164        }
165    });
166};
167
168// WebSocket object
169//
170// This is intended to conform (mostly) to http://dev.w3.org/html5/websockets/
171//
172// N.B. Arguments are parsed in the anonymous function at the bottom of the
173//      constructor.
174var WebSocket = function(url, proto, opts) {
175    events.EventEmitter.call(this);
176
177    // Retain a reference to our object
178    var self = this;
179
180    // State of the connection
181    var readyState = CONNECTING;
182
183    // Our underlying net.Stream instance
184    var stream = undefined;
185
186    opts = opts || {
187        origin : 'http://www.example.com'
188    };
189
190    // Frame parsing functions
191    //
192    // These read data from the given buffer starting at the given offset look
193    // for the end of the current frame. If found, the current frame is emitted
194    // and the function returns. Only a single frame is processed at a time.
195    //
196    // The number of bytes of completed frames read is returned, which the
197    // caller is to use to advance along its buffer. If 0 is returned, no
198    // completed frame bytes were found, and the caller should probably enqueue
199    // the buffer as a continuation of the current message. If a complete frame
200    // is read, the function is responsible fro resting 'frameType'.
201
202    // Framing data
203    var frameType = FRAME_NO;
204    var bufs = [];
205    var bufsBytes = 0;
206
207    // Frame-parsing functions
208    var frameFuncs = [
209        // FRAME_NO
210        function(buf, off) {
211            if (buf[off] & 0x80) {
212                throw new Error('High-byte frames not yet supported');
213            }
214
215            frameType = FRAME_LO;
216            return 1;
217        },
218
219        // FRAME_LO
220        function(buf, off) {
221            assert.ok(bufs.length > 0 || bufsBytes == 0);
222
223            debug('frame_lo(' + sys.inspect(buf) + ', ' + off + ')');
224
225            // Find the first instance of 0xff, our terminating byte
226            for (var i = off; i < buf.length && buf[i] != 0xff; i++)
227                ;
228
229            // We didn't find a terminating byte
230            if (i >= buf.length) {
231                return 0;
232            }
233
234            // We found a terminating byte; collect all bytes into a single buffer
235            // and emit it
236            var mb = null;
237            if (bufs.length == 0) {
238                mb = buf.slice(off, i);
239            } else {
240                mb = new buffer.Buffer(bufsBytes + i);
241
242                var mbOff = 0;
243                bufs.forEach(function(b) {
244                    b.copy(mb, mbOff, 0, b.length);
245                    mbOff += b.length;
246                });
247
248                assert.equal(mbOff, bufsBytes);
249
250                // Don't call Buffer.copy() if we're coping 0 bytes. Rather
251                // than being a no-op, this will trigger a range violation on
252                // the destination.
253                if (i > 0) {
254                    buf.copy(mb, mbOff, off, i);
255                }
256
257                // We consumed all of the buffers that we'd been saving; clear
258                // things out
259                bufs = [];
260                bufsBytes = 0;
261            }
262
263            process.nextTick(function() {
264                var b = mb;
265                return function() {
266                    var m = b.toString('utf8');
267
268                    self.emit('data', b);
269                    self.emit('message', m);        // wss compat
270
271                    if (self.onmessage) {
272                        self.onmessage({data: m});
273                    }
274                };
275            }());
276
277            frameType = FRAME_NO;
278            return i - off + 1;
279        },
280
281        // FRAME_HI
282        function(buf, off) {
283            debug('High-byte framing not yet supported');
284
285            frameType = FRAME_NO;
286            return buf.length - off;
287        }
288    ];
289
290    // Handle data coming from our socket
291    var dataListener = function(buf) {
292        if (buf.length <= 0) {
293            return;
294        }
295
296        debug('dataListener(' + sys.inspect(buf) + ')');
297
298        var off = 0;
299        var consumed = 0;
300
301        do {
302            if (frameType < 0 || frameFuncs.length <= frameType) {
303                throw new Error('Unexpected frame type: ' + frameType);
304            }
305
306            consumed = frameFuncs[frameType](buf, off);
307            off += consumed;
308        } while (consumed > 0 && off < buf.length);
309
310        if (consumed == 0) {
311            bufs.push(buf.slice(off, buf.length));
312            bufsBytes += buf.length - off;
313        }
314    };
315
316    // Handle incoming file descriptors
317    var fdListener = function(fd) {
318        self.emit('fd', fd);
319    };
320
321    // Handle errors from any source (HTTP client, stream, etc)
322    var errorListener = function(e) {
323        process.nextTick(function() {
324            self.emit('wserror', e);
325
326            if (self.onerror) {
327                self.onerror(e);
328            }
329        });
330    };
331
332    // External API
333    self.close = function() {
334        var f = function() {
335            readyState = CLOSED;
336
337            if (stream) {
338                stream.end();
339                stream.destroy();
340                stream = undefined;
341            }
342
343            process.nextTick(function() {
344                self.emit('close');
345
346                if (self.onclose) {
347                    self.onclose();
348                }
349            });
350        };
351
352        switch (readyState) {
353        case CLOSED:
354        case CLOSING:
355            break;
356
357        case CONNECTING:
358            f();
359            break;
360
361        default:
362            readyState = CLOSING;
363
364            // XXX: Run f() on the next tick so that we conform a little
365            //      closer to the spirit of the API in that the caller
366            //      never sees us transition directly to CLOSED. Instead, we
367            //      just seem to have an infinitely fast closing handshake.
368            if (stream.write('', 'binary')) {
369                process.nextTick(f);
370            } else {
371                stream.addListener('drain', f);
372            }
373        }
374    };
375
376    self.send = function(str, fd) {
377        if (readyState != OPEN) {
378            throw new Error('Cannot write to non-open WebSocket client');
379        }
380
381        stream.write('\x00', 'binary');
382        stream.write(str, 'utf8', fd);
383        stream.write('\xff', 'binary');
384    };
385
386    // wss compat
387    self.write = self.send;
388
389    setConstant(self, 'url', url);
390
391    Object.defineProperty(self, 'readyState',  {
392        get : function() {
393            return readyState;
394        }
395    });
396
397    // Connect and perform handshaking with the server
398    (function() {
399        // Parse constructor arguments 
400        if (!url) {
401            throw new Error('Url and must be specified.');
402        }
403
404        // Secrets used for handshaking
405        var key1 = createSecretKey();
406        var key2 = createSecretKey();
407        var challenge = createChallenge();
408
409        debug(
410            'key1=\'' + str2hex(key1) + '\'; ' +
411            'key2=\'' + str2hex(key2) + '\'; ' +
412            'challenge=\'' + str2hex(challenge) + '\''
413        );
414
415        var httpHeaders = {
416            'Connection' : 'Upgrade',
417            'Upgrade' : 'WebSocket',
418            'Sec-WebSocket-Key1' : key1,
419            'Sec-WebSocket-Key2' : key2
420        };
421        if (opts.origin) {
422            httpHeaders['Origin'] = opts.origin;
423        }
424
425        var httpPath = '/';
426
427        if (proto) {
428            httpHeaders['Sec-WebSocket-Protocol'] = proto;
429        }
430
431        // Create the HTTP client that we'll use for handshaking. We'll cannabalize
432        // its socket via the 'upgrade' event and leave it to rot.
433        //
434        // XXX: The ws+unix:// scheme makes use of the implementation detail
435        //      that http.Client passes its constructor arguments through,
436        //      un-inspected to net.Stream.connect(). The latter accepts a
437        //      string as its first argument to connect to a UNIX socket.
438        var httpClient = undefined;
439        switch (getUrlScheme(url)) {
440        case 'ws':
441            var u = urllib.parse(url);
442            httpClient = http.createClient(u.port || 80, u.hostname);
443            httpPath = (u.pathname || '/') + (u.search || '');
444            httpHeaders.Host = u.hostname;
445            break;
446
447        case 'ws+unix':
448            var sockPath = url.substring('ws+unix://'.length, url.length);
449            httpClient = http.createClient(sockPath);
450            httpHeaders.Host = 'localhost';
451            break;
452
453        default:
454            throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.');
455        }
456
457        httpClient.addListener('upgrade', (function() {
458            var data = undefined;
459
460            return function(req, s, head) {
461                stream = s;
462
463                stream.addListener('data', function(d) {
464                    if (!data) {
465                        data = d;
466                    } else {
467                        var data2 = new buffer.Buffer(data.length + d.length);
468
469                        if (data.length) {
470                            data.copy(data2, 0, 0, data.length);
471                        }
472                        if (data2.length) {
473                            d.copy(data2, data.length, 0, d.length);
474                        }
475
476                        data = data2;
477                    }
478
479                    if (data.length >= 16) {
480                        var expected = computeSecretKeySignature(key1, key2, challenge);
481                        var actual = data.slice(0, 16).toString('binary');
482
483                        // Handshaking fails; we're donezo
484                        if (actual != expected) {
485                            debug(
486                                'expected=\'' + str2hex(expected) + '\'; ' +
487                                'actual=\'' + str2hex(actual) + '\''
488                            );
489
490                            process.nextTick(function() {
491                                // XXX: Emit 'wserror' here, as 'error' is a reserved word in the
492                                //      EventEmitter world, and gets thrown.
493                                self.emit(
494                                    'wserror',
495                                    new Error('Invalid handshake from server:' +
496                                        'expected \'' + str2hex(expected) + '\', ' +
497                                        'actual \'' + str2hex(actual) + '\''
498                                    )
499                                );
500
501                                if (self.onerror) {
502                                    self.onerror();
503                                }
504
505                                self.close();
506                            });
507                        }
508
509                        // Un-register our data handler and add the one to be used
510                        // for the normal, non-handshaking case. If we have extra
511                        // data left over, manually fire off the handler on
512                        // whatever remains.
513                        //
514                        // XXX: This is lame. We should only remove the listeners
515                        //      that we added.
516                        httpClient.removeAllListeners('upgrade');
517                        stream.removeAllListeners('data');
518                        stream.addListener('data', dataListener);
519
520                        // Fire the 'open' event
521                        process.nextTick(function() {
522                            self.emit('open');
523
524                            if (self.onopen) {
525                                self.onopen();
526                            }
527                        });
528
529                        readyState = OPEN;
530
531                        if (data.length > 16) {
532                            stream.emit('data', data.slice(16, data.length));
533                        }
534                    }
535                });
536                stream.addListener('fd', fdListener);
537                stream.addListener('error', errorListener);
538
539                stream.emit('data', head);
540            };
541        })());
542        httpClient.addListener('error', function(e) {
543            httpClient.end();
544            errorListener(e);
545        });
546
547        var httpReq = httpClient.request(httpPath, httpHeaders);
548
549        httpReq.write(challenge, 'binary');
550        httpReq.end();
551    })();
552};
553sys.inherits(WebSocket, events.EventEmitter);
554exports.WebSocket = WebSocket;
555
556// Add some constants to the WebSocket object
557setConstant(WebSocket.prototype, 'CONNECTING', CONNECTING);
558setConstant(WebSocket.prototype, 'OPEN', OPEN);
559setConstant(WebSocket.prototype, 'CLOSING', CLOSING);
560setConstant(WebSocket.prototype, 'CLOSED', CLOSED);
561
562// vim:ts=4 sw=4 et