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');
14 // Values for readyState as per the W3C spec
20 var debugLevel = parseInt(process.env.NODE_DEBUG, 16);
21 var debug = (debugLevel & 0x4) ?
22 function() { sys.error.apply(this, arguments); } :
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);
31 // What is the numerical value of our key?
32 var keyVal = (Math.floor(
33 Math.random() * (4294967295 / numSpaces)
36 // Our string starts with a string representation of our key
37 var s = keyVal.toString();
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);
44 for (var i = 0; i < numChars; i++) {
45 var pos = Math.floor(Math.random() * s.length + 1);
47 var c = Math.floor(Math.random() * (14 + 68));
49 String.fromCharCode(c + 0x21) :
50 String.fromCharCode((c - 14) + 0x3a);
52 s = s.substring(0, pos) + c + s.substring(pos, s.length);
55 // We shoudln't have any spaces in our value until we insert them
56 assert.equal(s.indexOf(' '), -1);
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);
64 assert.notEqual(s.charAt(0), ' ');
65 assert.notEqual(s.charAt(s.length), ' ');
70 // Generate a challenge sequence
71 var createChallenge = function() {
73 for (var i = 0; i < 8; i++) {
74 c += String.fromCharCode(Math.floor(Math.random() * 255));
80 // Get the value of a secret key string
82 // This strips non-digit values and divides the result by the number of
84 var secretKeyValue = function(sk) {
88 for (var i = 0; i < sk.length; i++) {
89 var cc = sk.charCodeAt(i);
93 } else if (0x30 <= cc && cc <= 0x39) {
94 v = v * 10 + cc - 0x30;
98 return Math.floor(v / ns);
101 // Get the to-be-hashed value of a secret key string
103 // This takes the result of secretKeyValue() and encodes it in a big-endian
105 var secretKeyHashValue = function(sk) {
106 var skv = secretKeyValue(sk);
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);
117 // Compute the secret key signature based on two secret key strings and some
119 var computeSecretKeySignature = function(s1, s2, hs) {
120 assert.equal(hs.length, 8);
122 var hash = crypto.createHash('md5');
124 hash.update(secretKeyHashValue(s1));
125 hash.update(secretKeyHashValue(s2));
128 return hash.digest('binary');
131 // Return a hex representation of the given binary string; used for debugging
132 var str2hex = function(str) {
134 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
135 'a', 'b', 'c', 'd', 'e', 'f'
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];
149 // Get the scheme for a URL, undefined if none is found
150 var getUrlScheme = function(url) {
151 var i = url.indexOf(':');
156 return url.substring(0, i);
159 // Set a constant on the given object
160 var setConstant = function(obj, name, value) {
161 Object.defineProperty(obj, name, {
170 // This is intended to conform (mostly) to http://dev.w3.org/html5/websockets/
172 // N.B. Arguments are parsed in the anonymous function at the bottom of the
174 var WebSocket = function(url, proto, opts) {
175 events.EventEmitter.call(this);
177 // Retain a reference to our object
180 // State of our end of the connection
181 var readyState = CONNECTING;
183 // Whether or not the server has sent a close handshake
184 var serverClosed = false;
186 // Our underlying net.Stream instance
187 var stream = undefined;
190 origin : 'http://www.example.com'
193 // Frame parsing functions
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
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'.
207 var frameType = FRAME_NO;
211 // Frame-parsing functions
215 if (buf[off] & 0x80) {
216 frameType = FRAME_HI;
218 frameType = FRAME_LO;
226 debug('frame_lo(' + sys.inspect(buf) + ', ' + off + ')');
228 // Find the first instance of 0xff, our terminating byte
229 for (var i = off; i < buf.length && buf[i] != 0xff; i++)
232 // We didn't find a terminating byte
233 if (i >= buf.length) {
237 // We found a terminating byte; collect all bytes into a single buffer
240 if (bufs.length == 0) {
241 mb = buf.slice(off, i);
243 mb = new buffer.Buffer(bufsBytes + i);
246 bufs.forEach(function(b) {
247 b.copy(mb, mbOff, 0, b.length);
251 assert.equal(mbOff, bufsBytes);
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
257 buf.copy(mb, mbOff, off, i);
260 // We consumed all of the buffers that we'd been saving; clear
266 process.nextTick(function() {
269 var m = b.toString('utf8');
271 self.emit('data', b);
272 self.emit('message', m); // wss compat
274 if (self.onmessage) {
275 self.onmessage({data: m});
280 frameType = FRAME_NO;
286 debug('frame_hi(' + sys.inspect(buf) + ', ' + off + ')');
288 if (buf[off] !== 0) {
289 throw new Error('High-byte framing not supported.');
297 // Handle data coming from our socket
298 var dataListener = function(buf) {
299 if (buf.length <= 0 || serverClosed) {
303 debug('dataListener(' + sys.inspect(buf) + ')');
309 if (frameType < 0 || frameFuncs.length <= frameType) {
310 throw new Error('Unexpected frame type: ' + frameType);
313 assert.equal(bufs.length === 0, bufsBytes === 0);
314 assert.ok(off < buf.length);
316 consumed = frameFuncs[frameType](buf, off);
318 } while (!serverClosed && consumed > 0 && off < buf.length);
321 serverCloseHandler();
325 bufs.push(buf.slice(off, buf.length));
326 bufsBytes += buf.length - off;
330 // Handle incoming file descriptors
331 var fdListener = function(fd) {
335 // Handle errors from any source (HTTP client, stream, etc)
336 var errorListener = function(e) {
337 process.nextTick(function() {
338 self.emit('wserror', e);
346 // Finish the closing process; destroy the socket and tell the application
347 // that we've closed.
348 var finishClose = self.finishClose = function() {
357 process.nextTick(function() {
365 // Send a close frame to the server
366 var sendClose = function() {
367 assert.equal(OPEN, readyState);
369 readyState = CLOSING;
370 stream.write('\xff\x00', 'binary');
373 // Handle a close packet sent from the server
374 var serverCloseHandler = function() {
375 assert.ok(serverClosed);
376 assert.ok(readyState === OPEN || readyState === CLOSING);
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) {
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.
402 } else if (readyState === OPEN) {
406 setTimeout(finishClose, timeout * 1000);
411 self.send = function(str, fd) {
412 if (readyState != OPEN) {
416 stream.write('\x00', 'binary');
417 stream.write(str, 'utf8', fd);
418 stream.write('\xff', 'binary');
422 self.write = self.send;
424 setConstant(self, 'url', url);
426 Object.defineProperty(self, 'readyState', {
432 // Connect and perform handshaking with the server
434 // Parse constructor arguments
436 throw new Error('Url and must be specified.');
439 // Secrets used for handshaking
440 var key1 = createSecretKey();
441 var key2 = createSecretKey();
442 var challenge = createChallenge();
445 'key1=\'' + str2hex(key1) + '\'; ' +
446 'key2=\'' + str2hex(key2) + '\'; ' +
447 'challenge=\'' + str2hex(challenge) + '\''
451 'Connection' : 'Upgrade',
452 'Upgrade' : 'WebSocket',
453 'Sec-WebSocket-Key1' : key1,
454 'Sec-WebSocket-Key2' : key2
457 httpHeaders['Origin'] = opts.origin;
460 httpHeaders['Sec-WebSocket-Protocol'] = proto;
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.
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)) {
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) : "");
482 var sockPath = url.substring('ws+unix://'.length, url.length);
483 httpClient = http.createClient(sockPath);
484 httpHeaders.Host = 'localhost';
488 throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.');
491 httpClient.on('upgrade', (function() {
492 var data = undefined;
494 return function(req, s, head) {
497 stream.on('data', function(d) {
505 var data2 = new buffer.Buffer(data.length + d.length);
507 data.copy(data2, 0, 0, data.length);
508 d.copy(data2, data.length, 0, d.length);
513 if (data.length >= 16) {
514 var expected = computeSecretKeySignature(key1, key2, challenge);
515 var actual = data.slice(0, 16).toString('binary');
517 // Handshaking fails; we're donezo
518 if (actual != expected) {
520 'expected=\'' + str2hex(expected) + '\'; ' +
521 'actual=\'' + str2hex(actual) + '\''
524 process.nextTick(function() {
525 // N.B. Emit 'wserror' here, as 'error' is a reserved word in the
526 // EventEmitter world, and gets thrown.
529 new Error('Invalid handshake from server:' +
530 'expected \'' + str2hex(expected) + '\', ' +
531 'actual \'' + str2hex(actual) + '\''
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
548 // XXX: This is lame. We should only remove the listeners
550 httpClient.removeAllListeners('upgrade');
551 stream.removeAllListeners('data');
552 stream.on('data', dataListener);
556 process.nextTick(function() {
564 // Consume any leftover data
565 if (data.length > 16) {
566 stream.emit('data', data.slice(16, data.length));
570 stream.on('fd', fdListener);
571 stream.on('error', errorListener);
572 stream.on('close', function() {
573 errorListener(new Error('Stream closed unexpectedly.'));
576 stream.emit('data', head);
579 httpClient.on('error', function(e) {
584 var httpReq = httpClient.request(httpPath, httpHeaders);
586 httpReq.write(challenge, 'binary');
590 sys.inherits(WebSocket, events.EventEmitter);
591 exports.WebSocket = WebSocket;
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);