Inital Commit
[oweals/finalsclub.git] / node_modules / socket.io / support / node-websocket-client / lib / websocket.js
1 var assert = require('assert');
2 var buffer = require('buffer');
3 var crypto = require('crypto');
4 var events = require('events');
5 var http = require('http');
6 var net = require('net');
7 var urllib = require('url');
8 var sys = require('sys');
9
10 var FRAME_NO = 0;
11 var FRAME_LO = 1;
12 var FRAME_HI = 2;
13
14 // Values for readyState as per the W3C spec
15 var CONNECTING = 0;
16 var OPEN = 1;
17 var CLOSING = 2;
18 var CLOSED = 3;
19
20 var debugLevel = parseInt(process.env.NODE_DEBUG, 16);
21 var debug = (debugLevel & 0x4) ?
22     function() { sys.error.apply(this, arguments); } :
23     function() { };
24
25 // Generate a Sec-WebSocket-* value
26 var 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
71 var 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.
84 var 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
105 var 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.
119 var 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
132 var 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
150 var 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
160 var 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.
174 var 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 our end of the connection
181     var readyState = CONNECTING;
182
183     // Whether or not the server has sent a close handshake
184     var serverClosed = false;
185
186     // Our underlying net.Stream instance
187     var stream = undefined;
188
189     opts = opts || {
190         origin : 'http://www.example.com'
191     };
192
193     // Frame parsing functions
194     //
195     // These read data from the given buffer starting at the given offset,
196     // looking for the end of the current frame. If found, the current frame is
197     // emitted and the function returns. Only a single frame is processed at a
198     // time.
199     //
200     // The number of bytes read to complete a frame is returned, which the
201     // caller is to use to advance along its buffer. If 0 is returned, no
202     // completed frame bytes were found, and the caller should probably enqueue
203     // the buffer as a continuation of the current message. If a complete frame
204     // is read, the function is responsible for resting 'frameType'.
205
206     // Framing data
207     var frameType = FRAME_NO;
208     var bufs = [];
209     var bufsBytes = 0;
210
211     // Frame-parsing functions
212     var frameFuncs = [
213         // FRAME_NO
214         function(buf, off) {
215             if (buf[off] & 0x80) {
216                 frameType = FRAME_HI;
217             } else {
218                 frameType = FRAME_LO;
219             }
220
221             return 1;
222         },
223
224         // FRAME_LO
225         function(buf, off) {
226             debug('frame_lo(' + sys.inspect(buf) + ', ' + off + ')');
227
228             // Find the first instance of 0xff, our terminating byte
229             for (var i = off; i < buf.length && buf[i] != 0xff; i++)
230                 ;
231
232             // We didn't find a terminating byte
233             if (i >= buf.length) {
234                 return 0;
235             }
236
237             // We found a terminating byte; collect all bytes into a single buffer
238             // and emit it
239             var mb = null;
240             if (bufs.length == 0) {
241                 mb = buf.slice(off, i);
242             } else {
243                 mb = new buffer.Buffer(bufsBytes + i);
244
245                 var mbOff = 0;
246                 bufs.forEach(function(b) {
247                     b.copy(mb, mbOff, 0, b.length);
248                     mbOff += b.length;
249                 });
250
251                 assert.equal(mbOff, bufsBytes);
252
253                 // Don't call Buffer.copy() if we're coping 0 bytes. Rather
254                 // than being a no-op, this will trigger a range violation on
255                 // the destination.
256                 if (i > 0) {
257                     buf.copy(mb, mbOff, off, i);
258                 }
259
260                 // We consumed all of the buffers that we'd been saving; clear
261                 // things out
262                 bufs = [];
263                 bufsBytes = 0;
264             }
265
266             process.nextTick(function() {
267                 var b = mb;
268                 return function() {
269                     var m = b.toString('utf8');
270
271                     self.emit('data', b);
272                     self.emit('message', m);        // wss compat
273
274                     if (self.onmessage) {
275                         self.onmessage({data: m});
276                     }
277                 };
278             }());
279
280             frameType = FRAME_NO;
281             return i - off + 1;
282         },
283
284         // FRAME_HI
285         function(buf, off) {
286             debug('frame_hi(' + sys.inspect(buf) + ', ' + off + ')');
287
288             if (buf[off] !== 0) {
289                 throw new Error('High-byte framing not supported.');
290             }
291
292             serverClosed = true;
293             return 1;
294         }
295     ];
296
297     // Handle data coming from our socket
298     var dataListener = function(buf) {
299         if (buf.length <= 0 || serverClosed) {
300             return;
301         }
302
303         debug('dataListener(' + sys.inspect(buf) + ')');
304
305         var off = 0;
306         var consumed = 0;
307
308         do {
309             if (frameType < 0 || frameFuncs.length <= frameType) {
310                 throw new Error('Unexpected frame type: ' + frameType);
311             }
312
313             assert.equal(bufs.length === 0, bufsBytes === 0);
314             assert.ok(off < buf.length);
315
316             consumed = frameFuncs[frameType](buf, off);
317             off += consumed;
318         } while (!serverClosed && consumed > 0 && off < buf.length);
319
320         if (serverClosed) {
321             serverCloseHandler();
322         }
323         
324         if (consumed == 0) {
325             bufs.push(buf.slice(off, buf.length));
326             bufsBytes += buf.length - off;
327         }
328     };
329
330     // Handle incoming file descriptors
331     var fdListener = function(fd) {
332         self.emit('fd', fd);
333     };
334
335     // Handle errors from any source (HTTP client, stream, etc)
336     var errorListener = function(e) {
337         process.nextTick(function() {
338             self.emit('wserror', e);
339
340             if (self.onerror) {
341                 self.onerror(e);
342             }
343         });
344     };
345
346     // Finish the closing process; destroy the socket and tell the application
347     // that we've closed.
348     var finishClose = self.finishClose = function() {
349         readyState = CLOSED;
350
351         if (stream) {
352             stream.end();
353             stream.destroy();
354             stream = undefined;
355         }
356
357         process.nextTick(function() {
358             self.emit('close');
359             if (self.onclose) {
360                 self.onclose();
361             }
362         });
363     };
364
365     // Send a close frame to the server
366     var sendClose = function() {
367         assert.equal(OPEN, readyState);
368
369         readyState = CLOSING;
370         stream.write('\xff\x00', 'binary');
371     };
372
373     // Handle a close packet sent from the server
374     var serverCloseHandler = function() {
375         assert.ok(serverClosed);
376         assert.ok(readyState === OPEN || readyState === CLOSING);
377
378         bufs = [];
379         bufsBytes = 0;
380
381         // Handle state transitions asynchronously so that we don't change
382         // readyState before the application has had a chance to process data
383         // events which are already in the delivery pipeline. For example, a
384         // 'data' event could be delivered with a readyState of CLOSING if we
385         // received both frames in the same packet.
386         process.nextTick(function() {
387             if (readyState === OPEN) {
388                 sendClose();
389             }
390
391             finishClose();
392         });
393     };
394
395     // External API
396     self.close = function(timeout) {
397         if (readyState === CONNECTING) {
398             // If we're still in the process of connecting, the server is not
399             // in a position to understand our close frame. Just nuke the
400             // connection and call it a day.
401             finishClose();
402         } else if (readyState === OPEN) {
403             sendClose();
404
405             if (timeout) {
406                 setTimeout(finishClose, timeout * 1000);
407             }
408         }
409     };
410
411     self.send = function(str, fd) {
412         if (readyState != OPEN) {
413             return;
414         }
415
416         stream.write('\x00', 'binary');
417         stream.write(str, 'utf8', fd);
418         stream.write('\xff', 'binary');
419     };
420
421     // wss compat
422     self.write = self.send;
423
424     setConstant(self, 'url', url);
425
426     Object.defineProperty(self, 'readyState',  {
427         get : function() {
428             return readyState;
429         }
430     });
431
432     // Connect and perform handshaking with the server
433     (function() {
434         // Parse constructor arguments 
435         if (!url) {
436             throw new Error('Url and must be specified.');
437         }
438
439         // Secrets used for handshaking
440         var key1 = createSecretKey();
441         var key2 = createSecretKey();
442         var challenge = createChallenge();
443
444         debug(
445             'key1=\'' + str2hex(key1) + '\'; ' +
446             'key2=\'' + str2hex(key2) + '\'; ' +
447             'challenge=\'' + str2hex(challenge) + '\''
448         );
449
450         var httpHeaders = {
451             'Connection' : 'Upgrade',
452             'Upgrade' : 'WebSocket',
453             'Sec-WebSocket-Key1' : key1,
454             'Sec-WebSocket-Key2' : key2
455         };
456         if (opts.origin) {
457             httpHeaders['Origin'] = opts.origin;
458         }
459         if (proto) {
460             httpHeaders['Sec-WebSocket-Protocol'] = proto;
461         }
462
463         var httpPath = '/';
464
465         // Create the HTTP client that we'll use for handshaking. We'll cannabalize
466         // its socket via the 'upgrade' event and leave it to rot.
467         //
468         // N.B. The ws+unix:// scheme makes use of the implementation detail
469         //      that http.Client passes its constructor arguments through,
470         //      un-inspected to net.Stream.connect(). The latter accepts a
471         //      string as its first argument to connect to a UNIX socket.
472         var httpClient = undefined;
473         switch (getUrlScheme(url)) {
474         case 'ws':
475             var u = urllib.parse(url);
476             httpClient = http.createClient(u.port || 80, u.hostname);
477             httpPath = (u.pathname || '/') + (u.search || '');
478             httpHeaders.Host = u.hostname + (u.port ? (":" + u.port) : "");
479             break;
480
481         case 'ws+unix':
482             var sockPath = url.substring('ws+unix://'.length, url.length);
483             httpClient = http.createClient(sockPath);
484             httpHeaders.Host = 'localhost';
485             break;
486
487         default:
488             throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.');
489         }
490
491         httpClient.on('upgrade', (function() {
492             var data = undefined;
493
494             return function(req, s, head) {
495                 stream = s;
496
497                 stream.on('data', function(d) {
498                     if (d.length <= 0) {
499                         return;
500                     }
501
502                     if (!data) {
503                         data = d;
504                     } else {
505                         var data2 = new buffer.Buffer(data.length + d.length);
506
507                         data.copy(data2, 0, 0, data.length);
508                         d.copy(data2, data.length, 0, d.length);
509
510                         data = data2;
511                     }
512
513                     if (data.length >= 16) {
514                         var expected = computeSecretKeySignature(key1, key2, challenge);
515                         var actual = data.slice(0, 16).toString('binary');
516
517                         // Handshaking fails; we're donezo
518                         if (actual != expected) {
519                             debug(
520                                 'expected=\'' + str2hex(expected) + '\'; ' +
521                                 'actual=\'' + str2hex(actual) + '\''
522                             );
523
524                             process.nextTick(function() {
525                                 // N.B. Emit 'wserror' here, as 'error' is a reserved word in the
526                                 //      EventEmitter world, and gets thrown.
527                                 self.emit(
528                                     'wserror',
529                                     new Error('Invalid handshake from server:' +
530                                         'expected \'' + str2hex(expected) + '\', ' +
531                                         'actual \'' + str2hex(actual) + '\''
532                                     )
533                                 );
534
535                                 if (self.onerror) {
536                                     self.onerror();
537                                 }
538
539                                 finishClose();
540                             });
541                         }
542
543                         // Un-register our data handler and add the one to be used
544                         // for the normal, non-handshaking case. If we have extra
545                         // data left over, manually fire off the handler on
546                         // whatever remains.
547                         //
548                         // XXX: This is lame. We should only remove the listeners
549                         //      that we added.
550                         httpClient.removeAllListeners('upgrade');
551                         stream.removeAllListeners('data');
552                         stream.on('data', dataListener);
553
554                         readyState = OPEN;
555
556                         process.nextTick(function() {
557                             self.emit('open');
558
559                             if (self.onopen) {
560                                 self.onopen();
561                             }
562                         });
563
564                         // Consume any leftover data
565                         if (data.length > 16) {
566                             stream.emit('data', data.slice(16, data.length));
567                         }
568                     }
569                 });
570                 stream.on('fd', fdListener);
571                 stream.on('error', errorListener);
572                 stream.on('close', function() {
573                     errorListener(new Error('Stream closed unexpectedly.'));
574                 });
575
576                 stream.emit('data', head);
577             };
578         })());
579         httpClient.on('error', function(e) {
580             httpClient.end();
581             errorListener(e);
582         });
583
584         var httpReq = httpClient.request(httpPath, httpHeaders);
585
586         httpReq.write(challenge, 'binary');
587         httpReq.end();
588     })();
589 };
590 sys.inherits(WebSocket, events.EventEmitter);
591 exports.WebSocket = WebSocket;
592
593 // Add some constants to the WebSocket object
594 setConstant(WebSocket.prototype, 'CONNECTING', CONNECTING);
595 setConstant(WebSocket.prototype, 'OPEN', OPEN);
596 setConstant(WebSocket.prototype, 'CLOSING', CLOSING);
597 setConstant(WebSocket.prototype, 'CLOSED', CLOSED);
598
599 // vim:ts=4 sw=4 et