/js/lib/Socket.IO-node/support/node-websocket-client/lib/websocket.js
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