├── LICENSE ├── Makefile ├── README.md ├── examples ├── client-unix.js ├── client.js └── server-unix.js ├── lib └── websocket.js ├── package.json └── test ├── test-basic.js ├── test-client-close.js ├── test-readonly-attrs.js ├── test-ready-state.js ├── test-server-close.js ├── test-unix-send-fd.js └── test-unix-sockets.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Peter Griess 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of node-websocket-client nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This makefile exists to help run tests. 2 | # 3 | # If TEST_UNIX is a non-empty value, runs tests for UNIX sockets. This 4 | # functionality is not in node-websocket-server at the moment. 5 | 6 | .PHONY: test 7 | 8 | all: test test-unix 9 | 10 | test: 11 | for f in `ls -1 test/test-*.js | grep -v unix` ; do \ 12 | echo $$f ; \ 13 | node $$f ; \ 14 | done 15 | 16 | test-unix: 17 | if [[ -n "$$TEST_UNIX" ]] ; then \ 18 | for f in `ls -1 test/test-*.js | grep unix` ; do \ 19 | echo $$f ; \ 20 | node $$f ; \ 21 | done \ 22 | fi 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A prototype [Web Socket](http://www.whatwg.org/specs/web-socket-protocol/) 2 | client implementation for [node.js](http://nodejs.org). 3 | 4 | Tested with 5 | [miksago/node-websocket-server](http://github.com/miksago/node-websocket-server) 6 | v1.2.00. 7 | 8 | Requires [nodejs](http://nodejs.org) 0.1.98 or later. 9 | 10 | ## Installation 11 | 12 | Install this using `npm` as follows 13 | 14 | npm install websocket-client 15 | 16 | ... or just dump `lib/websocket.js` in your `$NODE_PATH`. 17 | 18 | ## Usage 19 | 20 | var sys = require('sys'); 21 | var WebSocket = require('websocket').WebSocket; 22 | 23 | var ws = new WebSocket('ws://localhost:8000/biff', 'borf'); 24 | ws.addListener('data', function(buf) { 25 | sys.debug('Got data: ' + sys.inspect(buf)); 26 | }); 27 | ws.onmessage = function(m) { 28 | sys.debug('Got message: ' + m); 29 | } 30 | 31 | ## API 32 | 33 | This supports the `send()` and `onmessage()` APIs. The `WebSocket` object will 34 | also emit `data` events that are node `Buffer` objects, in case you want to 35 | work with something lower-level than strings. 36 | 37 | ## Transports 38 | 39 | Multiple transports are supported, indicated by the scheme provided to the 40 | `WebSocket` constructor. `ws://` is a standard TCP-based Web Socket; 41 | `ws+unix://` allows connection to a UNIX socket at the given path. 42 | -------------------------------------------------------------------------------- /examples/client-unix.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var WebSocket = require('../lib/websocket').WebSocket; 3 | 4 | var ws = new WebSocket('ws+unix://' + process.argv[2], 'boffo'); 5 | 6 | ws.addListener('message', function(d) { 7 | sys.debug('Received message: ' + d.toString('utf8')); 8 | }); 9 | 10 | ws.addListener('open', function() { 11 | ws.send('This is a message', 1); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var WebSocket = require('../lib/websocket').WebSocket; 3 | 4 | var ws = new WebSocket('ws://localhost:8000/biff', 'borf'); 5 | ws.addListener('data', function(buf) { 6 | sys.debug('Got data: ' + sys.inspect(buf)); 7 | }); 8 | ws.onmessage = function(m) { 9 | sys.debug('Got message: ' + m); 10 | } 11 | -------------------------------------------------------------------------------- /examples/server-unix.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var ws = require('websocket-server/ws'); 3 | 4 | var srv = ws.createServer({ debug : true}); 5 | srv.addListener('connection', function(s) { 6 | sys.debug('Got a connection!'); 7 | 8 | s._req.socket.addListener('fd', function(fd) { 9 | sys.debug('Got an fd: ' + fd); 10 | }); 11 | }); 12 | 13 | srv.listen(process.argv[2]); 14 | -------------------------------------------------------------------------------- /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 util = require('util'); 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() { util.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(' + util.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(' + util.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(' + util.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 = 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 | util.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 600 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "websocket-client", 3 | "version" : "1.0.0", 4 | "description" : "An HTML5 Web Sockets client", 5 | "author" : "Peter Griess ", 6 | "engines" : { 7 | "node" : ">=0.1.98" 8 | }, 9 | "repositories" : [ 10 | { 11 | "type" : "git", 12 | "url" : "http://github.com/pgriess/node-websocket-client.git" 13 | } 14 | ], 15 | "licenses" : [ 16 | { 17 | "type" : "BSD", 18 | "url" : "http://github.com/pgriess/node-websocket-client/blob/master/LICENSE" 19 | } 20 | ], 21 | "main" : "./lib/websocket" 22 | } 23 | -------------------------------------------------------------------------------- /test/test-basic.js: -------------------------------------------------------------------------------- 1 | // Verify that we can connect to a WebSocket server, exchange messages, and 2 | // shut down cleanly. 3 | 4 | var assert = require('assert'); 5 | var WebSocket = require('../lib/websocket').WebSocket; 6 | var WebSocketServer = require('websocket-server/ws/server').Server; 7 | 8 | var PORT = 1024 + Math.floor(Math.random() * 4096); 9 | var C_MSG = 'Client test: ' + (Math.random() * 100); 10 | var S_MSG = 'Server test: ' + (Math.random() * 100); 11 | 12 | var serverGotConnection = false; 13 | var clientGotOpen = false; 14 | var clientGotData = false; 15 | var clientGotMessage = false; 16 | var serverGotMessage = false; 17 | var serverGotClose = false; 18 | var clientGotClose = false; 19 | 20 | var wss = new WebSocketServer(); 21 | wss.listen(PORT, 'localhost'); 22 | wss.on('connection', function(c) { 23 | serverGotConnection = true; 24 | 25 | c.on('message', function(m) { 26 | assert.equal(m, C_MSG); 27 | serverGotMessage = true; 28 | 29 | c.close(); 30 | }); 31 | 32 | c.on('close', function() { 33 | serverGotClose = true; 34 | wss.close(); 35 | }); 36 | 37 | c.write(S_MSG); 38 | }); 39 | 40 | var ws = new WebSocket('ws://localhost:' + PORT + '/', 'biff'); 41 | ws.on('open', function() { 42 | clientGotOpen = true; 43 | }); 44 | ws.on('data', function(buf) { 45 | assert.equal(typeof buf, 'object'); 46 | assert.equal(buf.toString('utf8'), S_MSG); 47 | 48 | clientGotData = true; 49 | 50 | ws.send(C_MSG); 51 | }); 52 | ws.onmessage = function(m) { 53 | assert.deepEqual(m, {data : S_MSG}); 54 | clientGotMessage = true; 55 | }; 56 | ws.onclose = function() { 57 | clientGotClose = true; 58 | }; 59 | 60 | process.on('exit', function() { 61 | assert.ok(serverGotConnection); 62 | assert.ok(clientGotOpen); 63 | assert.ok(clientGotData); 64 | assert.ok(clientGotMessage); 65 | assert.ok(serverGotMessage); 66 | assert.ok(serverGotClose); 67 | assert.ok(clientGotClose); 68 | }); 69 | -------------------------------------------------------------------------------- /test/test-client-close.js: -------------------------------------------------------------------------------- 1 | // Verify that a connection can be closed gracefully from the client. 2 | 3 | var assert = require('assert'); 4 | var WebSocket = require('../lib/websocket').WebSocket; 5 | var WebSocketServer = require('websocket-server/ws/server').Server; 6 | 7 | var PORT = 1024 + Math.floor(Math.random() * 4096); 8 | var C_MSG = 'Client test: ' + (Math.random() * 100); 9 | 10 | var serverGotClientMessage = false; 11 | var clientGotServerClose = false; 12 | var serverGotClientClose = false; 13 | 14 | var wss = new WebSocketServer(); 15 | wss.listen(PORT, 'localhost'); 16 | wss.on('connection', function(c) { 17 | c.on('message', function(m) { 18 | assert.equal(m, C_MSG); 19 | serverGotClientMessage = true; 20 | }); 21 | c.on('close', function() { 22 | serverGotClientClose = true; 23 | wss.close(); 24 | }); 25 | }); 26 | 27 | var ws = new WebSocket('ws://localhost:' + PORT); 28 | ws.onopen = function() { 29 | ws.send(C_MSG); 30 | 31 | // XXX: Add a timeout here 32 | ws.close(5); 33 | }; 34 | ws.onclose = function() { 35 | assert.equal(ws.CLOSED, ws.readyState); 36 | clientGotServerClose = true; 37 | }; 38 | 39 | process.on('exit', function() { 40 | assert.ok(serverGotClientMessage); 41 | assert.ok(clientGotServerClose); 42 | assert.ok(serverGotClientClose); 43 | }); 44 | -------------------------------------------------------------------------------- /test/test-readonly-attrs.js: -------------------------------------------------------------------------------- 1 | // Verify that some attributes of a WebSocket object are read-only. 2 | 3 | var assert = require('assert'); 4 | var sys = require('sys'); 5 | var WebSocket = require('../lib/websocket').WebSocket; 6 | var WebSocketServer = require('websocket-server/ws/server').Server; 7 | 8 | var PORT = 1024 + Math.floor(Math.random() * 4096); 9 | 10 | var wss = new WebSocketServer(); 11 | wss.listen(PORT, 'localhost'); 12 | wss.on('connection', function(c) { 13 | c.close(); 14 | wss.close(); 15 | }); 16 | var ws = new WebSocket('ws://localhost:' + PORT + '/', 'biff'); 17 | ws.on('open', function() { 18 | assert.equal(ws.CONNECTING, 0); 19 | try { 20 | ws.CONNECTING = 13; 21 | assert.equal( 22 | ws.CONNECTING, 0, 23 | 'Should not have been able to set read-only CONNECTING attribute' 24 | ); 25 | } catch (e) { 26 | assert.equal(e.type, 'no_setter_in_callback'); 27 | } 28 | 29 | assert.equal(ws.OPEN, 1); 30 | assert.equal(ws.CLOSING, 2); 31 | assert.equal(ws.CLOSED, 3); 32 | 33 | assert.equal(ws.url, 'ws://localhost:' + PORT + '/'); 34 | try { 35 | ws.url = 'foobar'; 36 | assert.equal( 37 | ws.url, 'ws://localhost:' + PORT + '/', 38 | 'Should not have been able to set read-only url attribute' 39 | ); 40 | } catch (e) { 41 | assert.equal(e.type, 'no_setter_in_callback'); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /test/test-ready-state.js: -------------------------------------------------------------------------------- 1 | // Verify that readyState transitions are implemented correctly 2 | 3 | var assert = require('assert'); 4 | var WebSocket = require('../lib/websocket').WebSocket; 5 | var WebSocketServer = require('websocket-server/ws/server').Server; 6 | 7 | var PORT = 1024 + Math.floor(Math.random() * 4096); 8 | 9 | var wss = new WebSocketServer(); 10 | wss.listen(PORT, 'localhost'); 11 | wss.on('connection', function(c) { 12 | c.close(); 13 | }); 14 | 15 | var ws = new WebSocket('ws://localhost:' + PORT); 16 | assert.equal(ws.readyState, ws.CONNECTING); 17 | ws.onopen = function() { 18 | assert.equal(ws.readyState, ws.OPEN); 19 | 20 | ws.close(); 21 | assert.ok(ws.readyState == ws.CLOSING); 22 | }; 23 | ws.onclose = function() { 24 | assert.equal(ws.readyState, ws.CLOSED); 25 | wss.close(); 26 | }; 27 | -------------------------------------------------------------------------------- /test/test-server-close.js: -------------------------------------------------------------------------------- 1 | // Verify that a connection can be closed gracefully from the server. 2 | 3 | var assert = require('assert'); 4 | var WebSocket = require('../lib/websocket').WebSocket; 5 | var WebSocketServer = require('websocket-server/ws/server').Server; 6 | 7 | var PORT = 1024 + Math.floor(Math.random() * 4096); 8 | var S_MSG = 'Server test: ' + (Math.random() * 100); 9 | 10 | var clientGotServerMessage = false; 11 | var clientGotServerClose = false; 12 | var serverGotClientClose = false; 13 | 14 | var wss = new WebSocketServer(); 15 | wss.listen(PORT, 'localhost'); 16 | wss.on('connection', function(c) { 17 | c.on('close', function() { 18 | serverGotClientClose = true; 19 | wss.close(); 20 | }); 21 | 22 | c.write(S_MSG); 23 | c.close(); 24 | }); 25 | 26 | var ws = new WebSocket('ws://localhost:' + PORT); 27 | ws.onmessage = function(m) { 28 | assert.deepEqual(m, {data: S_MSG}); 29 | 30 | clientGotServerMessage = true; 31 | }; 32 | ws.onclose = function() { 33 | assert.equal(ws.CLOSED, ws.readyState); 34 | clientGotServerClose = true; 35 | }; 36 | 37 | process.on('exit', function() { 38 | assert.ok(clientGotServerMessage); 39 | assert.ok(clientGotServerClose); 40 | assert.ok(serverGotClientClose); 41 | }); 42 | -------------------------------------------------------------------------------- /test/test-unix-send-fd.js: -------------------------------------------------------------------------------- 1 | // Verify that both sides of the WS connection can both send and receive file 2 | // descriptors. 3 | 4 | var assert = require('assert'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var sys = require('sys'); 8 | var WebSocket = require('../lib/websocket').WebSocket; 9 | var WebSocketServer = require('websocket-server/ws/server').Server; 10 | 11 | var PATH = path.join(__dirname, 'sock.' + process.pid); 12 | var C_MSG = 'Client test: ' + (Math.random() * 100); 13 | var S_MSG = 'Server test: ' + (Math.random() * 100); 14 | 15 | var clientReceivedData = false; 16 | var clientReceivedFD = false; 17 | var serverReceivedData = false; 18 | var serverReceivedFD = false; 19 | 20 | var wss = new WebSocketServer(); 21 | wss.on('listening', function() { 22 | var ws = new WebSocket('ws+unix://' + PATH); 23 | ws.on('data', function(d) { 24 | assert.equal(d.toString('utf8'), S_MSG); 25 | 26 | clientReceivedData = true; 27 | 28 | ws.send(C_MSG, 1); 29 | ws.close(); 30 | }); 31 | ws.on('fd', function(fd) { 32 | assert.ok(fd >= 0); 33 | 34 | clientReceivedFD = true; 35 | }); 36 | }); 37 | wss.on('connection', function(c) { 38 | c.write(S_MSG, 0); 39 | c._req.socket.on('fd', function(fd) { 40 | assert.ok(fd >= 0); 41 | 42 | serverReceivedFD = true; 43 | }); 44 | c.on('message', function(d) { 45 | assert.equal(d, C_MSG); 46 | 47 | serverReceivedData = true; 48 | 49 | wss.close(); 50 | }); 51 | }); 52 | wss.listen(PATH); 53 | 54 | process.on('exit', function() { 55 | assert.ok(clientReceivedFD); 56 | assert.ok(clientReceivedData); 57 | assert.ok(serverReceivedFD); 58 | assert.ok(serverReceivedData); 59 | 60 | try { 61 | fs.unlinkSync(PATH); 62 | } catch (e) { } 63 | }); 64 | -------------------------------------------------------------------------------- /test/test-unix-sockets.js: -------------------------------------------------------------------------------- 1 | // Verify that we can connect to a server over UNIX domain sockets. 2 | 3 | var assert = require('assert'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var sys = require('sys'); 7 | var WebSocket = require('../lib/websocket').WebSocket; 8 | var WebSocketServer = require('websocket-server/ws/server').Server; 9 | 10 | var PATH = path.join(__dirname, 'sock.' + process.pid); 11 | var S_MSG = 'Server test: ' + (Math.random() * 100); 12 | 13 | var serverGotConnection = false; 14 | var clientGotOpen = false; 15 | var clientGotData = false; 16 | 17 | var wss = new WebSocketServer(); 18 | wss.on('listening', function() { 19 | var ws = new WebSocket('ws+unix://' + PATH); 20 | ws.on('open', function() { 21 | clientGotOpen = true; 22 | 23 | ws.close(); 24 | }); 25 | ws.on('data', function(d) { 26 | assert.equal(d.toString('utf8'), S_MSG); 27 | clientGotData = true; 28 | }); 29 | }); 30 | wss.on('connection', function(c) { 31 | serverGotConnection = true; 32 | 33 | c.write(S_MSG); 34 | wss.close(); 35 | }); 36 | wss.listen(PATH); 37 | 38 | process.on('exit', function() { 39 | assert.ok(serverGotConnection); 40 | assert.ok(clientGotOpen); 41 | assert.ok(clientGotData); 42 | 43 | try { 44 | fs.unlinkSync(PATH); 45 | } catch(e) { } 46 | }); 47 | --------------------------------------------------------------------------------