[Pkg-javascript-commits] [node-ws] 01/10: Imported Upstream version 0.7.1

Ximin Luo infinity0 at pwned.gg
Mon Mar 30 02:37:19 UTC 2015


This is an automated email from the git hooks/post-receive script.

infinity0-guest pushed a commit to branch master
in repository node-ws.

commit 18e194e751081455a6493771b014fdbcaa656514
Author: Ximin Luo <infinity0 at pwned.gg>
Date:   Mon Mar 30 02:28:37 2015 +0200

    Imported Upstream version 0.7.1
---
 History.md                     | 312 ---------------------------
 Makefile                       |   2 +-
 README.md                      | 182 ++++++++--------
 bin/wscat                      | 222 -------------------
 binding.gyp                    |  16 --
 doc/ws.md                      |  39 +++-
 examples/serverstats/server.js |   2 +-
 index.js                       |  47 ++--
 lib/BufferUtil.js              |  13 +-
 lib/Extensions.js              |  70 ++++++
 lib/PerMessageDeflate.js       | 289 +++++++++++++++++++++++++
 lib/Receiver.js                | 199 +++++++++++++----
 lib/Sender.hixie.js            |   2 +
 lib/Sender.js                  | 102 ++++++++-
 lib/Validation.js              |  13 +-
 lib/WebSocket.js               | 471 +++++++++++++++++++++++++++--------------
 lib/WebSocketServer.js         |  42 +++-
 package.json                   |  33 ++-
 src/bufferutil.cc              | 117 ----------
 src/validation.cc              | 145 -------------
 test/Extensions.test.js        |  53 +++++
 test/PerMessageDeflate.test.js | 267 +++++++++++++++++++++++
 test/Receiver.test.js          |  62 +++++-
 test/Sender.test.js            |  53 ++++-
 test/WebSocket.test.js         | 219 ++++++++++++++++++-
 test/WebSocketServer.test.js   | 178 ++++++++++++++--
 test/testserver.js             |   5 +-
 27 files changed, 1965 insertions(+), 1190 deletions(-)

diff --git a/History.md b/History.md
deleted file mode 100644
index 63cf0ea..0000000
--- a/History.md
+++ /dev/null
@@ -1,312 +0,0 @@
-v0.4.31 - September 23th, 2013
-=====================
-
-* Component support
-
-v0.4.30 - August 30th, 2013
-=====================
-
-* BufferedAmount could be undefined, default to 0 [TooTallNate]
-* Support protocols as second argument and options as third [TooTallNate]
-* Proper browserify shim [mcollina]
-* Broadcasting example in README [stefanocudini]
-
-v0.4.29 - August 23th, 2013
-=====================
-* Small clean up of the Node 0.11 support by using NAN from the NPM registry [kkoopa]
-* Support for custom `Agent`'s through the options. [gramakri] & [TooTallNate]
-* Support for custom headers through the options [3rd-Eden]
-* Added a `gypfile` flag to the package.json for compiled module discovery [wolfeidau]
-
-v0.4.28 - August 16th, 2013
-=====================
-* Node 0.11 support. [kkoopa]
-* Authorization headers are sent when basic auth is used in the url [jcrugzz]
-* Origin header will now include the port number [Jason Plum]
-* Race condition fixed where data was received before the readyState was updated. [saschagehlich]
-
-v0.4.27 - June 27th, 2013
-=====================
-* Frames are no longer masked in `wscat`. [slaskis]
-* Don't retrain reference to large slab buffers. [jmatthewsr-msi]
-* Don't use Buffer.byteLength for ArrayBuffer's. [Anthony Pesch]
-* Fix browser field in package.json. [shtylman]
-* Client-side certificate support & documentation improvements. [Lukas Berns]
-* WebSocket readyState's is added to the prototype for spec compatiblity. [BallBearing]
-* Use Object.defineProperty. [arlolra]
-* Autodetect ArrayBuffers as binary when sending. [BallBearing]
-* Check instanceof Buffer for binary data. [arlolra]
-* Emit the close event before destroying the internal socket. [3rd-Eden]
-* Don't setup multiply timeouts for one connection. [AndreasMadsen]
-* Allow support for binding to ethereal port. [wpreul]
-* Fix broken terminate reference. [3rd-Eden]
-* Misc node 0.10 test fixes and documentation improvements. [3rd-Eden]
-* Ensure ssl options are propagated to request. [einaros]
-* Add 'Host' and 'Origin' to request header. [Lars-Magnus Skog]
-* Subprotocol support. [kanaka]
-* Honor ArrayBufferView's byteOffset when sending. [Anthony Pesch]
-* Added target attribute for events. [arlolra]
-
-v0.4.26 - Skipped
-=====================
-
-v0.4.25 - December 17th, 2012
-=====================
-* Removed install.js. [shtylman]
-* Added browser field to package.json. [shtylman]
-* Support overwriting host header. [Raynos]
-* Emit 'listening' also with custom http server. [sebiq]
-
-v0.4.24 - December 6th, 2012
-=====================
-* Yet another intermediate release, to  not delay minor features any longer.
-* Native support installation issues further circumvented. [einaros]
-
-v0.4.23 - November 19th, 2012
-=====================
-* Service release - last before major upgrade.
-* Changes default host from 127.0.0.1 to 0.0.0.0. [einaros]
-
-v0.4.22 - October 3rd, 2012
-=====================
-* clear failsafe cleanup timeout once cleanup is called [AndreasMadsen]
-* added w3c compatible CloseEvent for onclose / addEventListener("close", ...). [einaros]
-* fix the sub protocol header handler [sonnyp]
-* fix unhandled exception if socket closes and 'error' is emitted [jmatthewsr-ms]
-
-v0.4.21 - July 14th, 2012
-=====================
-* Emit error if server reponds with anything other than status code 101. [einaros]
-* Added 'headers' event to server. [rauchg]
-* path.exists moved to fs.exists. [blakmatrix]
-
-v0.4.20 - June 26th, 2012
-=====================
-* node v0.8.0 compatibility release.
-
-v0.4.19 - June 19th, 2012
-=====================
-* Change sender to merge buffers for relatively small payloads, may improve perf in some cases [einaros]
-* Avoid EventEmitter for Receiver classes. As above this may improve perf. [einaros]
-* Renamed fallback files from the somewhat misleading '*Windows'. [einaros]
-
-v0.4.18 - June 14th 2012
-=====================
-* Fixed incorrect md5 digest encoding in Hixie handshake [nicokaiser]
-* Added example of use with Express 3 [einaros]
-* Change installation procedure to not require --ws:native to build native extensions. They will now build if a compiler is available. [einaros]
-
-v0.4.17 - June 13th 2012
-=====================
-* Improve error handling during connection handshaking [einaros]
-* Ensure that errors are caught also after connection teardown [nicokaiser]
-* Update 'mocha' version to 1.1.0. [einaros]
-* Stop showing 'undefined' for some error logs. [tricknotes]
-* Update 'should' version to 0.6.3 [tricknotes]
-
-v0.4.16 - June 1st 2012
-=====================
-* Build fix for Windows. [einaros]
-
-v0.4.15 - May 20th 2012
-=====================
-* Enable fauxe streaming for hixie tansport. [einaros]
-* Allow hixie sender to deal with buffers. [einaros/pigne]
-* Allow error code 1011. [einaros]
-* Fix framing for empty packets (empty pings and pongs might break). [einaros]
-* Improve error and close handling, to avoid connections lingering in CLOSING state. [einaros]
-
-v0.4.14 - Apr 30th 2012
-=====================
-* use node-gyp instead of node-waf [TooTallNate]
-* remove old windows compatibility makefile, and silently fall back to native modules [einaros]
-* ensure connection status [nicokaiser]
-* websocket client updated to use port 443 by default for wss:// connections [einaros]
-* support unix sockets [kschzt]
-
-v0.4.13 - Apr 12th 2012
-=====================
-
-* circumvent node 0.6+ related memory leak caused by Object.defineProperty [nicokaiser]
-* improved error handling, improving stability in massive load use cases [nicokaiser]
-
-v0.4.12 - Mar 30th 2012
-=====================
-
-* various memory leak / possible memory leak cleanups [einaros]
-* api documentation [nicokaiser]
-* add option to disable client tracking [nicokaiser]
-
-v0.4.11 - Mar 24th 2012
-=====================
-
-* node v0.7 compatibillity release
-* gyp support [TooTallNate]
-* commander dependency update [jwueller]
-* loadbalancer support [nicokaiser]
-
-v0.4.10 - Mar 22th 2012
-=====================
-
-* Final hixie close frame fixes. [nicokaiser]
-
-v0.4.9 - Mar 21st 2012
-=====================
-
-* Various hixie bugfixes (such as proper close frame handling). [einaros]
-
-v0.4.8 - Feb 29th 2012
-=====================
-
-* Allow verifyClient to run asynchronously [karlsequin]
-* Various bugfixes and cleanups. [einaros]
-
-v0.4.7 - Feb 21st 2012
-=====================
-
-* Exposed bytesReceived from websocket client object, which makes it possible to implement bandwidth sampling. [einaros]
-* Updated browser based file upload example to include and output per websocket channel bandwidth sampling. [einaros]
-* Changed build scripts to check which architecture is currently in use. Required after the node.js changes to have prebuilt packages target ia32 by default. [einaros]
-
-v0.4.6 - Feb 9th 2012
-=====================
-
-* Added browser based file upload example. [einaros]
-* Added server-to-browser status push example. [einaros]
-* Exposed pause() and resume() on WebSocket object, to enable client stream shaping. [einaros]
-
-v0.4.5 - Feb 7th 2012
-=====================
-
-* Corrected regression bug in handling of connections with the initial frame delivered across both http upgrade head and a standalone packet. This would lead to a race condition, which in some cases could cause message corruption. [einaros]
-
-v0.4.4 - Feb 6th 2012
-=====================
-
-* Pass original request object to verifyClient, for cookie or authentication verifications. [einaros]
-* Implemented addEventListener and slightly improved the emulation API by adding a MessageEvent with a readonly data attribute. [aslakhellesoy]
-* Rewrite parts of hybi receiver to avoid stack overflows for large amounts of packets bundled in the same buffer / packet. [einaros]
-
-v0.4.3 - Feb 4th 2012
-=====================
-
-* Prioritized update: Corrected issue which would cause sockets to stay open longer than necessary, and resource leakage because of this. [einaros]
-
-v0.4.2 - Feb 4th 2012
-=====================
-
-* Breaking change: WebSocketServer's verifyOrigin option has been renamed to verifyClient. [einaros]
-* verifyClient now receives { origin: 'origin header', secure: true/false }, where 'secure' will be true for ssl connections. [einaros]
-* Split benchmark, in preparation for more thorough case. [einaros]
-* Introduced hixie-76 draft support for server, since Safari (iPhone / iPad / OS X) and Opera still aren't updated to use Hybi. [einaros]
-* Expose 'supports' object from WebSocket, to indicate e.g. the underlying transport's support for binary data. [einaros]
-* Test and code cleanups. [einaros]
-
-v0.4.1 - Jan 25th 2012
-=====================
-
-* Use readline in wscat [tricknotes]
-* Refactor _state away, in favor of the new _readyState [tricknotes]
-* travis-ci integration [einaros]
-* Fixed race condition in testsuite, causing a few tests to fail (without actually indicating errors) on travis [einaros]
-* Expose pong event [paddybyers]
-* Enabled running of WebSocketServer in noServer-mode, meaning that upgrades are passed in manually. [einaros]
-* Reworked connection procedure for WebSocketServer, and cleaned up tests. [einaros]
-
-v0.4.0 - Jan 2nd 2012
-=====================
-
-* Windows compatibility [einaros]
-* Windows compatible test script [einaros]
-
-v0.3.9 - Jan 1st 2012
-======================
-
-* Improved protocol framing performance [einaros]
-* WSS support [kazuyukitanimura]
-* WSS tests [einaros]
-* readyState exposed [justinlatimer, tricknotes]
-* url property exposed [justinlatimer]
-* Removed old 'state' property [einaros]
-* Test cleanups [einaros]
-
-v0.3.8 - Dec 27th 2011
-======================
-
-* Made it possible to listen on specific paths, which is especially good to have for precreated http servers [einaros]
-* Extensive WebSocket / WebSocketServer cleanup, including changing all internal properties to unconfigurable, unenumerable properties [einaros]
-* Receiver modifications to ensure even better performance with fragmented sends [einaros]
-* Fixed issue in sender.js, which would cause SlowBuffer instances (such as returned from the crypto library's randomBytes) to be copied (and thus be dead slow) [einaros]
-* Removed redundant buffer copy in sender.js, which should improve server performance [einaros]
-
-v0.3.7 - Dec 25nd 2011
-======================
-
-* Added a browser based API which uses EventEmitters internally [3rd-Eden]
-* Expose request information from upgrade event for websocket server clients [mmalecki]
-
-v0.3.6 - Dec 19th 2011
-======================
-
-* Added option to let WebSocket.Server use an already existing http server [mmalecki]
-* Migrating various option structures to use options.js module [einaros]
-* Added a few more tests, options and handshake verifications to ensure that faulty connections are dealt with [einaros]
-* Code cleanups in Sender and Receiver, to ensure even faster parsing [einaros]
-
-v0.3.5 - Dec 13th 2011
-======================
-
-* Optimized Sender.js, Receiver.js and bufferutil.cc:
- * Apply loop-unrolling-like small block copies rather than use node.js Buffer#copy() (which is slow).
- * Mask blocks of data using combination of 32bit xor and loop-unrolling, instead of single bytes.
- * Keep pre-made send buffer for small transfers.
-* Leak fixes and code cleanups.
-
-v0.3.3 - Dec 12th 2011
-======================
-
-* Compile fix for Linux.
-* Rewrote parts of WebSocket.js, to avoid try/catch and thus avoid optimizer bailouts.
-
-v0.3.2 - Dec 11th 2011
-======================
-
-* Further performance updates, including the additions of a native BufferUtil module, which deals with several of the cpu intensive WebSocket operations.
-
-v0.3.1 - Dec 8th 2011
-======================
-
-* Service release, fixing broken tests.
-
-v0.3.0 - Dec 8th 2011
-======================
-
-* Node.js v0.4.x compatibility.
-* Code cleanups and efficiency improvements.
-* WebSocket server added, although this will still mainly be a client library.
-* WebSocket server certified to pass the Autobahn test suite.
-* Protocol improvements and corrections - such as handling (redundant) masks for empty fragments.
-* 'wscat' command line utility added, which can act as either client or server.
-
-v0.2.6 - Dec 3rd 2011
-======================
-
-* Renamed to 'ws'. Big woop, right -- but easy-websocket really just doesn't cut it anymore!
-
-v0.2.5 - Dec 3rd 2011
-======================
-
-  * Rewrote much of the WebSocket parser, to ensure high speed for highly fragmented messages.
-  * Added a BufferPool, as a start to more efficiently deal with allocations for WebSocket connections. More work to come, in that area.
-  * Updated the Autobahn report, at http://einaros.github.com/easy-websocket, with comparisons against WebSocket-Node 1.0.2 and Chrome 16.
-
-v0.2.0 - Nov 25th 2011
-======================
-
-  * Major rework to make sure all the Autobahn test cases pass. Also updated the internal tests to cover more corner cases.
-
-v0.1.2 - Nov 14th 2011
-======================
-
-  * Back and forth, back and forth: now settled on keeping the api (event names, methods) closer to the websocket browser api. This will stick now.
-  * Started keeping this history record. Better late than never, right?
diff --git a/Makefile b/Makefile
index 151aa2b..00f19fa 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ clean:
 
 run-tests:
 	@./node_modules/.bin/mocha \
-		-t 2000 \
+		-t 5000 \
 		-s 2400 \
 		$(TESTFLAGS) \
 		$(TESTS)
diff --git a/README.md b/README.md
index cf1f1fb..fef2fe6 100644
--- a/README.md
+++ b/README.md
@@ -1,151 +1,165 @@
-[![Build Status](https://secure.travis-ci.org/einaros/ws.png)](http://travis-ci.org/einaros/ws)
+# ws: a node.js websocket library
 
-# ws: a node.js websocket library #
+[![Build Status](https://travis-ci.org/einaros/ws.svg?branch=master)](https://travis-ci.org/einaros/ws)
 
-`ws` is a simple to use websocket implementation, up-to-date against RFC-6455, and [probably the fastest WebSocket library for node.js](http://web.archive.org/web/20130314230536/http://hobbycoding.posterous.com/the-fastest-websocket-module-for-nodejs).
+`ws` is a simple to use WebSocket implementation, up-to-date against RFC-6455,
+and [probably the fastest WebSocket library for node.js][archive].
 
-Passes the quite extensive Autobahn test suite. See http://einaros.github.com/ws for the full reports.
+Passes the quite extensive Autobahn test suite. See http://einaros.github.com/ws
+for the full reports.
 
-Comes with a command line utility, `wscat`, which can either act as a server (--listen), or client (--connect); Use it to debug simple websocket services.
+## Protocol support
 
-## Protocol support ##
+* **Hixie draft 76** (Old and deprecated, but still in use by Safari and Opera.
+  Added to ws version 0.4.2, but server only. Can be disabled by setting the
+  `disableHixie` option to true.)
+* **HyBi drafts 07-12** (Use the option `protocolVersion: 8`)
+* **HyBi drafts 13-17** (Current default, alternatively option `protocolVersion: 13`)
 
-* **Hixie draft 76** (Old and deprecated, but still in use by Safari and Opera. Added to ws version 0.4.2, but server only. Can be disabled by setting the `disableHixie` option to true.)
-* **HyBi drafts 07-12** (Use the option `protocolVersion: 8`, or argument `-p 8` for wscat)
-* **HyBi drafts 13-17** (Current default, alternatively option `protocolVersion: 13`, or argument `-p 13` for wscat)
+### Installing
 
-_See the echo.websocket.org example below for how to use the `protocolVersion` option._
-
-## Usage ##
-
-### Installing ###
-
-`npm install ws`
+```
+npm install --save ws
+```
 
-### Sending and receiving text data ###
+### Sending and receiving text data
 
 ```js
 var WebSocket = require('ws');
 var ws = new WebSocket('ws://www.host.com/path');
-ws.on('open', function() {
-    ws.send('something');
+
+ws.on('open', function open() {
+  ws.send('something');
 });
+
 ws.on('message', function(data, flags) {
-    // flags.binary will be set if a binary data is received
-    // flags.masked will be set if the data was masked
+  // flags.binary will be set if a binary data is received.
+  // flags.masked will be set if the data was masked.
 });
 ```
 
-### Sending binary data ###
+### Sending binary data
 
 ```js
 var WebSocket = require('ws');
 var ws = new WebSocket('ws://www.host.com/path');
-ws.on('open', function() {
-    var array = new Float32Array(5);
-    for (var i = 0; i < array.length; ++i) array[i] = i / 2;
-    ws.send(array, {binary: true, mask: true});
+
+ws.on('open', function open() {
+  var array = new Float32Array(5);
+
+  for (var i = 0; i < array.length; ++i) {
+    array[i] = i / 2;
+  }
+
+  ws.send(array, { binary: true, mask: true });
 });
 ```
 
-Setting `mask`, as done for the send options above, will cause the data to be masked according to the websocket protocol. The same option applies for text data.
+Setting `mask`, as done for the send options above, will cause the data to be
+masked according to the WebSocket protocol. The same option applies for text
+data.
 
-### Server example ###
+### Server example
 
 ```js
 var WebSocketServer = require('ws').Server
-  , wss = new WebSocketServer({port: 8080});
-wss.on('connection', function(ws) {
-    ws.on('message', function(message) {
-        console.log('received: %s', message);
-    });
-    ws.send('something');
+  , wss = new WebSocketServer({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+  ws.on('message', function incoming(message) {
+    console.log('received: %s', message);
+  });
+
+  ws.send('something');
 });
 ```
 
-### Server sending broadcast data ###
+### Server sending broadcast data
 
 ```js
 var WebSocketServer = require('ws').Server
-  , wss = new WebSocketServer({port: 8080});
-  
-wss.broadcast = function(data) {
-	for(var i in this.clients)
-		this.clients[i].send(data);
+  , wss = new WebSocketServer({ port: 8080 });
+
+wss.broadcast = function broadcast(data) {
+  wss.clients.forEach(function each(client) {
+    client.send(data);
+  });
 };
 ```
 
-### Error handling best practices ###
+### Error handling best practices
 
 ```js
 // If the WebSocket is closed before the following send is attempted
 ws.send('something');
 
-// Errors (both immediate and async write errors) can be detected in an optional callback.
-// The callback is also the only way of being notified that data has actually been sent.
-ws.send('something', function(error) {
-    // if error is null, the send has been completed,
-    // otherwise the error object will indicate what failed.
+// Errors (both immediate and async write errors) can be detected in an optional
+// callback. The callback is also the only way of being notified that data has
+// actually been sent.
+ws.send('something', function ack(error) {
+  // if error is not defined, the send has been completed,
+  // otherwise the error object will indicate what failed.
 });
 
-// Immediate errors can also be handled with try/catch-blocks, but **note**
-// that since sends are inherently asynchronous, socket write failures will *not*
-// be captured when this technique is used.
-try {
-    ws.send('something');
-}
-catch (e) {
-    // handle error
-}
+// Immediate errors can also be handled with try/catch-blocks, but **note** that
+// since sends are inherently asynchronous, socket write failures will *not* be
+// captured when this technique is used.
+try { ws.send('something'); }
+catch (e) { /* handle error */ }
 ```
 
-### echo.websocket.org demo ###
+### echo.websocket.org demo
 
 ```js
 var WebSocket = require('ws');
-var ws = new WebSocket('ws://echo.websocket.org/', {protocolVersion: 8, origin: 'http://websocket.org'});
-ws.on('open', function() {
-    console.log('connected');
-    ws.send(Date.now().toString(), {mask: true});
+var ws = new WebSocket('ws://echo.websocket.org/', {
+  protocolVersion: 8, 
+  origin: 'http://websocket.org'
 });
-ws.on('close', function() {
-    console.log('disconnected');
+
+ws.on('open', function open() {
+  console.log('connected');
+  ws.send(Date.now().toString(), {mask: true});
 });
-ws.on('message', function(data, flags) {
-    console.log('Roundtrip time: ' + (Date.now() - parseInt(data)) + 'ms', flags);
-    setTimeout(function() {
-        ws.send(Date.now().toString(), {mask: true});
-    }, 500);
+
+ws.on('close', function close() {
+  console.log('disconnected');
 });
-```
 
-### wscat against echo.websocket.org ###
+ws.on('message', function message(data, flags) {
+  console.log('Roundtrip time: ' + (Date.now() - parseInt(data)) + 'ms', flags);
 
-    $ npm install -g ws
-    $ wscat -c ws://echo.websocket.org 
-    connected (press CTRL+C to quit)
-    > hi there
-    < hi there
-    > are you a happy parrot?
-    < are you a happy parrot?
+  setTimeout(function timeout() {
+    ws.send(Date.now().toString(), {mask: true});
+  }, 500);
+});
+```
 
-### Other examples ###
+### Other examples
 
-For a full example with a browser client communicating with a ws server, see the examples folder.
+For a full example with a browser client communicating with a ws server, see the
+examples folder.
 
-Note that the usage together with Express 3.0 is quite different from Express 2.x. The difference is expressed in the two different serverstats-examples.
+Note that the usage together with Express 3.0 is quite different from Express
+2.x. The difference is expressed in the two different serverstats-examples.
 
 Otherwise, see the test cases.
 
-### Running the tests ###
+### Running the tests
 
-`make test`
+```
+make test
+```
 
-## API Docs ##
+## API Docs
 
 See the doc/ directory for Node.js-like docs for the ws classes.
 
-## License ##
+## Changelog
+
+We're using the GitHub `releases` for changelog entries.
+
+## License
 
 (The MIT License)
 
@@ -169,3 +183,5 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+[archive]: http://web.archive.org/web/20130314230536/http://hobbycoding.posterous.com/the-fastest-websocket-module-for-nodejs
diff --git a/bin/wscat b/bin/wscat
deleted file mode 100755
index 7c66600..0000000
--- a/bin/wscat
+++ /dev/null
@@ -1,222 +0,0 @@
-#!/usr/bin/env node
-
-/*!
- * ws: a node.js websocket client
- * Copyright(c) 2011 Einar Otto Stangvik <einaros at gmail.com>
- * MIT Licensed
- */
-
-/**
- * Module dependencies.
- */
-
-var WebSocket = require('../')
-  , fs = require('fs')
-  , program = require('commander')
-  , util = require('util')
-  , events = require('events')
-  , readline = require('readline');
-
-/**
- * InputReader - processes console input
- */
-
-function Console() {
-  this.stdin = process.stdin;
-  this.stdout = process.stdout;
-
-  this.readlineInterface = readline.createInterface(this.stdin, this.stdout);
-
-  var self = this;
-  this.readlineInterface.on('line', function(data) {
-    self.emit('line', data);
-  });
-  this.readlineInterface.on('close', function() {
-    self.emit('close');
-  });
-
-  this._resetInput = function() {
-    self.clear();
-  }
-}
-util.inherits(Console, events.EventEmitter);
-
-Console.Colors = {
-  Red: '\033[31m',
-  Green: '\033[32m',
-  Yellow: '\033[33m',
-  Blue: '\033[34m',
-  Default: '\033[39m'
-};
-
-Console.prototype.prompt = function() {
-  this.readlineInterface.prompt();
-}
-
-Console.prototype.print = function(msg, color) {
-  this.clear();
-  color = color || Console.Colors.Default;
-  this.stdout.write(color + msg + Console.Colors.Default + '\n');
-  this.prompt();
-}
-
-Console.prototype.clear = function() {
-  this.stdout.write('\033[2K\033[E');
-}
-
-Console.prototype.pause = function() {
-  this.stdin.on('keypress', this._resetInput);
-}
-
-Console.prototype.resume = function() {
-  this.stdin.removeListener('keypress', this._resetInput);
-}
-
-function appender(xs) {
-  xs = xs || [];
-  return function (x) {
-    xs.push(x);
-    return xs;
-  }
-}
-
-function into(obj, kvals) {
-  kvals.forEach(function (kv) {
-    obj[kv[0]] = kv[1];
-  });
-  return obj;
-}
-
-function splitOnce(sep, str) { // sep can be either String or RegExp
-  var tokens = str.split(sep);
-  return [tokens[0], str.replace(sep, '').substr(tokens[0].length)];
-}
-
-/**
- * The actual application
- */
-
-var version = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8')).version;
-program
-  .version(version)
-  .usage('[options] <url>')
-  .option('-l, --listen <port>', 'listen on port')
-  .option('-c, --connect <url>', 'connect to a websocket server')
-  .option('-p, --protocol <version>', 'optional protocol version')
-  .option('-o, --origin <origin>', 'optional origin')
-  .option('--host <host>', 'optional host')
-  .option('-s, --subprotocol <protocol>', 'optional subprotocol')
-  .option('-n, --no-check', 'Do not check for unauthorized certificates')
-  .option('-H, --header <header:value>', 'Set an HTTP header. Repeat to set multiple. (--connect only)', appender(), [])
-  .option('--auth <username:password>', 'Add basic HTTP authentication header. (--connect only)')
-  .parse(process.argv);
-
-if (program.listen && program.connect) {
-  console.error('\033[33merror: use either --listen or --connect\033[39m');
-  process.exit(-1);
-}
-else if (program.listen) {
-  var wsConsole = new Console();
-  wsConsole.pause();
-  var options = {};
-  if (program.protocol) options.protocolVersion = program.protocol;
-  if (program.origin) options.origin = program.origin;
-  if (program.subprotocol) options.protocol = program.subprotocol;
-  if (!program.check) options.rejectUnauthorized = program.check;
-  var ws = null;
-  var wss = new WebSocket.Server({port: program.listen}, function() {
-    wsConsole.print('listening on port ' + program.listen + ' (press CTRL+C to quit)', Console.Colors.Green);
-    wsConsole.clear();
-  });
-  wsConsole.on('close', function() {
-    if (ws) {
-      try {
-        ws.close();
-      }
-      catch (e) {}
-    }
-    process.exit(0);
-  });
-  wsConsole.on('line', function(data) {
-    if (ws) {
-      ws.send(data, {mask: false});
-      wsConsole.prompt();
-    }
-  });
-  wss.on('connection', function(newClient) {
-    if (ws) {
-      // limit to one client
-      newClient.terminate();
-      return;
-    };
-    ws = newClient;
-    wsConsole.resume();
-    wsConsole.prompt();
-    wsConsole.print('client connected', Console.Colors.Green);
-    ws.on('close', function() {
-      wsConsole.print('disconnected', Console.Colors.Green);
-      wsConsole.clear();
-      wsConsole.pause();
-      ws = null;
-    });
-    ws.on('error', function(code, description) {
-      wsConsole.print('error: ' + code + (description ? ' ' + description : ''), Console.Colors.Yellow);
-    });
-    ws.on('message', function(data, flags) {
-      wsConsole.print('< ' + data, Console.Colors.Blue);
-    });
-  });
-  wss.on('error', function(error) {
-    wsConsole.print('error: ' + error.toString(), Console.Colors.Yellow);
-    process.exit(-1);
-  });
-}
-else if (program.connect) {
-  var wsConsole = new Console();
-  var options = {};
-  if (program.protocol) options.protocolVersion = program.protocol;
-  if (program.origin) options.origin = program.origin;
-  if (program.subprotocol) options.protocol = program.subprotocol;
-  if (program.host) options.host = program.host;
-  if (!program.check) options.rejectUnauthorized = program.check;
-  var headers = into({}, (program.header || []).map(function (s) {
-                                                      return splitOnce(':', s)
-                                                    }));
-  if (program.auth) {
-    headers['Authorization'] = 'Basic ' + new Buffer(program.auth).toString('base64');
-  }
-  options.headers = headers;
-  var ws = new WebSocket(program.connect, options);
-  ws.on('open', function() {
-    wsConsole.print('connected (press CTRL+C to quit)', Console.Colors.Green);
-    wsConsole.on('line', function(data) {
-      ws.send(data, {mask: true});
-      wsConsole.prompt();
-    });
-  });
-  ws.on('close', function() {
-    wsConsole.print('disconnected', Console.Colors.Green);
-    wsConsole.clear();
-    process.exit();
-  });
-  ws.on('error', function(code, description) {
-    wsConsole.print('error: ' + code + (description ? ' ' + description : ''), Console.Colors.Yellow);
-    process.exit(-1);
-  });
-  ws.on('message', function(data, flags) {
-    wsConsole.print('< ' + data, Console.Colors.Blue);
-  });
-  wsConsole.on('close', function() {
-    if (ws) {
-      try {
-        ws.close();
-      }
-      catch(e) {}
-      process.exit();
-    }
-  });
-}
-else {
-  console.error('\033[33merror: use either --listen or --connect\033[39m');
-  process.exit(-1);
-}
diff --git a/binding.gyp b/binding.gyp
deleted file mode 100644
index 600f9d1..0000000
--- a/binding.gyp
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-  'targets': [
-    {
-      'target_name': 'validation',
-      'include_dirs': ["<!(node -e \"require('nan')\")"],
-      'cflags': [ '-O3' ],
-      'sources': [ 'src/validation.cc' ]
-    },
-    {
-      'target_name': 'bufferutil',
-      'include_dirs': ["<!(node -e \"require('nan')\")"],
-      'cflags': [ '-O3' ],
-      'sources': [ 'src/bufferutil.cc' ]
-    }
-  ]
-}
diff --git a/doc/ws.md b/doc/ws.md
index d84fd62..0668994 100644
--- a/doc/ws.md
+++ b/doc/ws.md
@@ -11,6 +11,7 @@ This class is a WebSocket server. It is an `EventEmitter`.
   * `port` Number
   * `server` http.Server
   * `verifyClient` Function
+  * `handleProtocols` Function
   * `path` String
   * `noServer` Boolean
   * `disableHixie` Boolean
@@ -23,7 +24,36 @@ Either `port` or `server` must be provided, otherwise you might enable
 `noServer` if you want to pass the requests directly. Please note that the
 `callback` is only used when you supply the a `port` number in the options.
 
-### server.close([code], [data])
+### options.verifyClient
+
+`verifyClient` can be used in two different ways. If it is provided with two arguments then those are:
+* `info` Object:
+  * `origin` String: The value in the Origin header indicated by the client.
+  * `req` http.ClientRequest: The client HTTP GET request.
+  * `secure` Boolean: `true` if `req.connection.authorized` or `req.connection.encypted` is set.
+* `cb` Function: A callback that must be called by the user upon inspection of the `info` fields. Arguments in this callback are:
+  * `result` Boolean: Whether the user accepts or not the handshake.
+  * `code` Number: If `result` is `false` this field determines the HTTP error status code to be sent to the client.
+  * `name` String: If `result` is `false` this field determines the HTTP reason phrase.
+
+If `verifyClient` is provided with a single argument then that is:
+* `info` Object: Same as above.
+
+In this case the return code (Boolean) of the function determines whether the handshake is accepted or not.
+
+If `verifyClient` is not set then the handshake is automatically accepted.
+
+### options.handleProtocols
+
+`handleProtocols` receives two arguments:
+* `protocols` Array: The list of WebSocket sub-protocols indicated by the client in the Sec-WebSocket-Protocol header.
+* `cb` Function: A callback that must be called by the user upon inspection of the protocols. Arguments in this callback are:
+  * `result` Boolean: Whether the user accepts or not the handshake.
+  * `protocol` String: If `result` is `true` then this field sets the value of the Sec-WebSocket-Protocol header in the HTTP 101 response.
+
+If `handleProtocols` is not set then the handshake is accepted regardless the value of Sec-WebSocket-Protocol header. If it is set but the user does not invoke the `cb` callback then the handshake is rejected with error HTTP 501.
+
+### server.close()
 
 Close the server and terminate all clients
 
@@ -97,6 +127,10 @@ The URL of the WebSocket server (only for clients)
 
 Describes the feature of the used protocol version. E.g. `supports.binary` is a boolean that describes if the connection supports binary messages.
 
+### websocket.upgradeReq
+
+The http request that initiated the upgrade. Useful for parsing authorty headers, cookie headers and other information to associate a specific Websocket to a specific Client. This is only available for WebSockets constructed by a Server.
+
 ### websocket.close([code], [data])
 
 Gracefully closes the connection, after sending a description message
@@ -120,7 +154,7 @@ Resume the client stream
 
 ### websocket.send(data, [options], [callback])
 
-Sends `data` through the connection. `options` can be an object with members `mask` and `binary`. The optional `callback` is executed after the send completes.
+Sends `data` through the connection. `options` can be an object with members `mask`, `binary` and `compress`. The optional `callback` is executed after the send completes.
 
 ### websocket.stream([options], callback)
 
@@ -178,4 +212,3 @@ Is emitted when a pong is received. `flags` is an object with member `binary`.
 `function () { }`
 
 Emitted when the connection is established.
-
diff --git a/examples/serverstats/server.js b/examples/serverstats/server.js
index 0bbce36..d7845e0 100644
--- a/examples/serverstats/server.js
+++ b/examples/serverstats/server.js
@@ -15,5 +15,5 @@ wss.on('connection', function(ws) {
   ws.on('close', function() {
     console.log('stopping client interval');
     clearInterval(id);
-  })
+  });
 });
diff --git a/index.js b/index.js
index 3423ff2..a7e8644 100644
--- a/index.js
+++ b/index.js
@@ -1,26 +1,49 @@
+'use strict';
+
 /*!
  * ws: a node.js websocket client
  * Copyright(c) 2011 Einar Otto Stangvik <einaros at gmail.com>
  * MIT Licensed
  */
 
-module.exports = require('./lib/WebSocket');
-module.exports.Server = require('./lib/WebSocketServer');
-module.exports.Sender = require('./lib/Sender');
-module.exports.Receiver = require('./lib/Receiver');
+var WS = module.exports = require('./lib/WebSocket');
+
+WS.Server = require('./lib/WebSocketServer');
+WS.Sender = require('./lib/Sender');
+WS.Receiver = require('./lib/Receiver');
+
+/**
+ * Create a new WebSocket server.
+ *
+ * @param {Object} options Server options
+ * @param {Function} fn Optional connection listener.
+ * @returns {WS.Server}
+ * @api public
+ */
+WS.createServer = function createServer(options, fn) {
+  var server = new WS.Server(options);
 
-module.exports.createServer = function (options, connectionListener) {
-  var server = new module.exports.Server(options);
-  if (typeof connectionListener === 'function') {
-    server.on('connection', connectionListener);
+  if (typeof fn === 'function') {
+    server.on('connection', fn);
   }
+
   return server;
 };
 
-module.exports.connect = module.exports.createConnection = function (address, openListener) {
-  var client = new module.exports(address);
-  if (typeof openListener === 'function') {
-    client.on('open', openListener);
+/**
+ * Create a new WebSocket connection.
+ *
+ * @param {String} address The URL/address we need to connect to.
+ * @param {Function} fn Open listener.
+ * @returns {WS}
+ * @api public
+ */
+WS.connect = WS.createConnection = function connect(address, fn) {
+  var client = new WS(address);
+
+  if (typeof fn === 'function') {
+    client.on('open', fn);
   }
+
   return client;
 };
diff --git a/lib/BufferUtil.js b/lib/BufferUtil.js
index 15d35b9..18c6998 100644
--- a/lib/BufferUtil.js
+++ b/lib/BufferUtil.js
@@ -1,3 +1,5 @@
+'use strict';
+
 /*!
  * ws: a node.js websocket client
  * Copyright(c) 2011 Einar Otto Stangvik <einaros at gmail.com>
@@ -5,12 +7,7 @@
  */
 
 try {
-  module.exports = require('../build/Release/bufferutil');
-} catch (e) { try {
-  module.exports = require('../build/default/bufferutil');
-} catch (e) { try {
-  module.exports = require('./BufferUtil.fallback');
+  module.exports = require('bufferutil');
 } catch (e) {
-  console.error('bufferutil.node seems to not have been built. Run npm install.');
-  throw e;
-}}}
+  module.exports = require('./BufferUtil.fallback');
+}
diff --git a/lib/Extensions.js b/lib/Extensions.js
new file mode 100644
index 0000000..a465ace
--- /dev/null
+++ b/lib/Extensions.js
@@ -0,0 +1,70 @@
+
+var util = require('util');
+
+/**
+ * Module exports.
+ */
+
+exports.parse = parse;
+exports.format = format;
+
+/**
+ * Parse extensions header value
+ */
+
+function parse(value) {
+  value = value || '';
+
+  var extensions = {};
+
+  value.split(',').forEach(function(v) {
+    var params = v.split(';');
+    var token = params.shift().trim();
+    var paramsList = extensions[token] = extensions[token] || [];
+    var parsedParams = {};
+
+    params.forEach(function(param) {
+      var parts = param.trim().split('=');
+      var key = parts[0];
+      var value = parts[1];
+      if (typeof value === 'undefined') {
+        value = true;
+      } else {
+        // unquote value
+        if (value[0] === '"') {
+          value = value.slice(1);
+        }
+        if (value[value.length - 1] === '"') {
+          value = value.slice(0, value.length - 1);
+        }
+      }
+      (parsedParams[key] = parsedParams[key] || []).push(value);
+    });
+
+    paramsList.push(parsedParams);
+  });
+
+  return extensions;
+}
+
+/**
+ * Format extensions header value
+ */
+
+function format(value) {
+  return Object.keys(value).map(function(token) {
+    var paramsList = value[token];
+    if (!util.isArray(paramsList)) {
+      paramsList = [paramsList];
+    }
+    return paramsList.map(function(params) {
+      return [token].concat(Object.keys(params).map(function(k) {
+        var p = params[k];
+        if (!util.isArray(p)) p = [p];
+        return p.map(function(v) {
+          return v === true ? k : k + '=' + v;
+        }).join('; ');
+      })).join('; ');
+    }).join(', ');
+  }).join(', ');
+}
diff --git a/lib/PerMessageDeflate.js b/lib/PerMessageDeflate.js
new file mode 100644
index 0000000..f735977
--- /dev/null
+++ b/lib/PerMessageDeflate.js
@@ -0,0 +1,289 @@
+
+var zlib = require('zlib');
+
+var AVAILABLE_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15];
+var DEFAULT_WINDOW_BITS = 15;
+var DEFAULT_MEM_LEVEL = 8;
+
+PerMessageDeflate.extensionName = 'permessage-deflate';
+
+/**
+ * Per-message Compression Extensions implementation
+ */
+
+function PerMessageDeflate(options, isServer) {
+  this._options = options || {};
+  this._isServer = !!isServer;
+  this._inflate = null;
+  this._deflate = null;
+  this.params = null;
+}
+
+/**
+ * Create extension parameters offer
+ *
+ * @api public
+ */
+
+PerMessageDeflate.prototype.offer = function() {
+  var params = {};
+  if (this._options.serverNoContextTakeover) {
+    params.server_no_context_takeover = true;
+  }
+  if (this._options.clientNoContextTakeover) {
+    params.client_no_context_takeover = true;
+  }
+  if (this._options.serverMaxWindowBits) {
+    params.server_max_window_bits = this._options.serverMaxWindowBits;
+  }
+  if (this._options.clientMaxWindowBits) {
+    params.client_max_window_bits = this._options.clientMaxWindowBits;
+  } else if (this._options.clientMaxWindowBits == null) {
+    params.client_max_window_bits = true;
+  }
+  return params;
+};
+
+/**
+ * Accept extension offer
+ *
+ * @api public
+ */
+
+PerMessageDeflate.prototype.accept = function(paramsList) {
+  paramsList = this.normalizeParams(paramsList);
+
+  var params;
+  if (this._isServer) {
+    params = this.acceptAsServer(paramsList);
+  } else {
+    params = this.acceptAsClient(paramsList);
+  }
+
+  this.params = params;
+  return params;
+};
+
+/**
+ * Accept extension offer from client
+ *
+ * @api private
+ */
+
+PerMessageDeflate.prototype.acceptAsServer = function(paramsList) {
+  var accepted = {};
+  var result = paramsList.some(function(params) {
+    accepted = {};
+    if (this._options.serverNoContextTakeover === false && params.server_no_context_takeover) {
+      return;
+    }
+    if (this._options.serverMaxWindowBits === false && params.server_max_window_bits) {
+      return;
+    }
+    if (typeof this._options.serverMaxWindowBits === 'number' &&
+        typeof params.server_max_window_bits === 'number' &&
+        this._options.serverMaxWindowBits > params.server_max_window_bits) {
+      return;
+    }
+    if (typeof this._options.clientMaxWindowBits === 'number' && !params.client_max_window_bits) {
+      return;
+    }
+
+    if (this._options.serverNoContextTakeover || params.server_no_context_takeover) {
+      accepted.server_no_context_takeover = true;
+    }
+    if (this._options.clientNoContextTakeover) {
+      accepted.client_no_context_takeover = true;
+    }
+    if (this._options.clientNoContextTakeover !== false && params.client_no_context_takeover) {
+      accepted.client_no_context_takeover = true;
+    }
+    if (typeof this._options.serverMaxWindowBits === 'number') {
+      accepted.server_max_window_bits = this._options.serverMaxWindowBits;
+    } else if (typeof params.server_max_window_bits === 'number') {
+      accepted.server_max_window_bits = params.server_max_window_bits;
+    }
+    if (typeof this._options.clientMaxWindowBits === 'number') {
+      accepted.client_max_window_bits = this._options.clientMaxWindowBits;
+    } else if (this._options.clientMaxWindowBits !== false && typeof params.client_max_window_bits === 'number') {
+      accepted.client_max_window_bits = params.client_max_window_bits;
+    }
+    return true;
+  }, this);
+
+  if (!result) {
+    throw new Error('Doesn\'t support the offered configuration');
+  }
+
+  return accepted;
+};
+
+/**
+ * Accept extension response from server
+ *
+ * @api privaye
+ */
+
+PerMessageDeflate.prototype.acceptAsClient = function(paramsList) {
+  var params = paramsList[0];
+  if (this._options.clientNoContextTakeover != null) {
+    if (this._options.clientNoContextTakeover === false && params.client_no_context_takeover) {
+      throw new Error('Invalid value for "client_no_context_takeover"');
+    }
+  }
+  if (this._options.clientMaxWindowBits != null) {
+    if (this._options.clientMaxWindowBits === false && params.client_max_window_bits) {
+      throw new Error('Invalid value for "client_max_window_bits"');
+    }
+    if (typeof this._options.clientMaxWindowBits === 'number' &&
+        (!params.client_max_window_bits || params.client_max_window_bits > this._options.clientMaxWindowBits)) {
+      throw new Error('Invalid value for "client_max_window_bits"');
+    }
+  }
+  return params;
+};
+
+/**
+ * Normalize extensions parameters
+ *
+ * @api private
+ */
+
+PerMessageDeflate.prototype.normalizeParams = function(paramsList) {
+  return paramsList.map(function(params) {
+    Object.keys(params).forEach(function(key) {
+      var value = params[key];
+      if (value.length > 1) {
+        throw new Error('Multiple extension parameters for ' + key);
+      }
+
+      value = value[0];
+
+      switch (key) {
+      case 'server_no_context_takeover':
+      case 'client_no_context_takeover':
+        if (value !== true) {
+          throw new Error('invalid extension parameter value for ' + key + ' (' + value + ')');
+        }
+        params[key] = true;
+        break;
+      case 'server_max_window_bits':
+      case 'client_max_window_bits':
+        if (typeof value === 'string') {
+          value = parseInt(value, 10);
+          if (!~AVAILABLE_WINDOW_BITS.indexOf(value)) {
+            throw new Error('invalid extension parameter value for ' + key + ' (' + value + ')');
+          }
+        }
+        if (!this._isServer && value === true) {
+          throw new Error('Missing extension parameter value for ' + key);
+        }
+        params[key] = value;
+        break;
+      default:
+        throw new Error('Not defined extension parameter (' + key + ')');
+      }
+    }, this);
+    return params;
+  }, this);
+};
+
+/**
+ * Decompress message
+ *
+ * @api public
+ */
+
+PerMessageDeflate.prototype.decompress = function (data, fin, callback) {
+  var endpoint = this._isServer ? 'client' : 'server';
+
+  if (!this._inflate) {
+    var maxWindowBits = this.params[endpoint + '_max_window_bits'];
+    this._inflate = zlib.createInflateRaw({
+      windowBits: 'number' === typeof maxWindowBits ? maxWindowBits : DEFAULT_WINDOW_BITS
+    });
+  }
+
+  var self = this;
+  var buffers = [];
+
+  this._inflate.on('error', onError).on('data', onData);
+  this._inflate.write(data);
+  if (fin) {
+    this._inflate.write(new Buffer([0x00, 0x00, 0xff, 0xff]));
+  }
+  this._inflate.flush(function() {
+    cleanup();
+    callback(null, Buffer.concat(buffers));
+  });
+
+  function onError(err) {
+    cleanup();
+    callback(err);
+  }
+
+  function onData(data) {
+    buffers.push(data);
+  }
+
+  function cleanup() {
+    self._inflate.removeListener('error', onError);
+    self._inflate.removeListener('data', onData);
+    if (fin && self.params[endpoint + '_no_context_takeover']) {
+      self._inflate = null;
+    }
+  }
+};
+
+/**
+ * Compress message
+ *
+ * @api public
+ */
+
+PerMessageDeflate.prototype.compress = function (data, fin, callback) {
+  var endpoint = this._isServer ? 'server' : 'client';
+
+  if (!this._deflate) {
+    var maxWindowBits = this.params[endpoint + '_max_window_bits'];
+    this._deflate = zlib.createDeflateRaw({
+      flush: zlib.Z_SYNC_FLUSH,
+      windowBits: 'number' === typeof maxWindowBits ? maxWindowBits : DEFAULT_WINDOW_BITS,
+      memLevel: this._options.memLevel || DEFAULT_MEM_LEVEL
+    });
+  }
+
+  var self = this;
+  var buffers = [];
+
+  this._deflate.on('error', onError).on('data', onData);
+  this._deflate.write(data);
+  this._deflate.flush(function() {
+    cleanup();
+    var data = Buffer.concat(buffers);
+    if (fin) {
+      data = data.slice(0, data.length - 4);
+    }
+    callback(null, data);
+  });
+
+  function onError(err) {
+    cleanup();
+    callback(err);
+  }
+
+  function onData(data) {
+    buffers.push(data);
+  }
+
+  function cleanup() {
+    self._deflate.removeListener('error', onError);
+    self._deflate.removeListener('data', onData);
+    if (fin && self.params[endpoint + '_no_context_takeover']) {
+      self._deflate = null;
+    }
+  }
+};
+
+module.exports = PerMessageDeflate;
+
diff --git a/lib/Receiver.js b/lib/Receiver.js
index 004cd32..1ae1c4e 100644
--- a/lib/Receiver.js
+++ b/lib/Receiver.js
@@ -8,13 +8,14 @@ var util = require('util')
   , Validation = require('./Validation').Validation
   , ErrorCodes = require('./ErrorCodes')
   , BufferPool = require('./BufferPool')
-  , bufferUtil = require('./BufferUtil').BufferUtil;
+  , bufferUtil = require('./BufferUtil').BufferUtil
+  , PerMessageDeflate = require('./PerMessageDeflate');
 
 /**
  * HyBi Receiver implementation
  */
 
-function Receiver () {
+function Receiver (extensions) {
   // memory pool for fragmented messages
   var fragmentedPoolPrevUsed = -1;
   this.fragmentedBufferPool = new BufferPool(1024, function(db, length) {
@@ -35,6 +36,7 @@ function Receiver () {
       db.used;
   });
 
+  this.extensions = extensions || {};
   this.state = {
     activeFragmentedOperation: null,
     lastFragment: false,
@@ -48,8 +50,10 @@ function Receiver () {
   this.expectBuffer = null;
   this.expectHandler = null;
   this.currentMessage = [];
+  this.messageHandlers = [];
   this.expectHeader(2, this.processPacket);
   this.dead = false;
+  this.processing = false;
 
   this.onerror = function() {};
   this.ontext = function() {};
@@ -177,14 +181,26 @@ Receiver.prototype.allocateFromPool = function(length, isFragmented) {
  */
 
 Receiver.prototype.processPacket = function (data) {
-  if ((data[0] & 0x70) != 0) {
-    this.error('reserved fields must be empty', 1002);
-    return;
+  if (this.extensions[PerMessageDeflate.extensionName]) {
+    if ((data[0] & 0x30) != 0) {
+      this.error('reserved fields (2, 3) must be empty', 1002);
+      return;
+    }
+  } else {
+    if ((data[0] & 0x70) != 0) {
+      this.error('reserved fields must be empty', 1002);
+      return;
+    }
   }
   this.state.lastFragment = (data[0] & 0x80) == 0x80;
   this.state.masked = (data[1] & 0x80) == 0x80;
+  var compressed = (data[0] & 0x40) == 0x40;
   var opcode = data[0] & 0xf;
   if (opcode === 0) {
+    if (compressed) {
+      this.error('continuation frame cannot have the Per-message Compressed bits', 1002);
+      return;
+    }
     // continuation frame
     this.state.fragmentedOperation = true;
     this.state.opcode = this.state.activeFragmentedOperation;
@@ -198,6 +214,11 @@ Receiver.prototype.processPacket = function (data) {
       this.error('data frames after the initial data frame must have opcode 0', 1002);
       return;
     }
+    if (opcode >= 8 && compressed) {
+      this.error('control frames cannot have the Per-message Compressed bits', 1002);
+      return;
+    }
+    this.state.compressed = compressed;
     this.state.opcode = opcode;
     if (this.state.lastFragment === false) {
       this.state.fragmentedOperation = true;
@@ -256,6 +277,7 @@ Receiver.prototype.reset = function() {
   this.expectHandler = null;
   this.overflow = [];
   this.currentMessage = [];
+  this.messageHandlers = [];
 };
 
 /**
@@ -297,6 +319,49 @@ Receiver.prototype.error = function (reason, protocolErrorCode) {
 };
 
 /**
+ * Execute message handler buffers
+ *
+ * @api private
+ */
+
+Receiver.prototype.flush = function() {
+  if (this.processing || this.dead) return;
+
+  var handler = this.messageHandlers.shift();
+  if (!handler) return;
+
+  this.processing = true;
+  var self = this;
+
+  handler(function() {
+    self.processing = false;
+    self.flush();
+  });
+};
+
+/**
+ * Apply extensions to message
+ *
+ * @api private
+ */
+
+Receiver.prototype.applyExtensions = function(messageBuffer, fin, compressed, callback) {
+  var self = this;
+  if (compressed) {
+    this.extensions[PerMessageDeflate.extensionName].decompress(messageBuffer, fin, function(err, buffer) {
+      if (self.dead) return;
+      if (err) {
+        callback(new Error('invalid compressed data'));
+        return;
+      }
+      callback(null, buffer);
+    });
+  } else {
+    callback(null, messageBuffer);
+  }
+};
+
+/**
  * Buffer utilities
  */
 
@@ -334,6 +399,16 @@ function fastCopy(length, srcBuffer, dstBuffer, dstOffset) {
   }
 }
 
+function clone(obj) {
+  var cloned = {};
+  for (var k in obj) {
+    if (obj.hasOwnProperty(k)) {
+      cloned[k] = obj[k];
+    }
+  }
+  return cloned;
+}
+
 /**
  * Opcode handlers
  */
@@ -380,17 +455,27 @@ var opcodes = {
       }
     },
     finish: function(mask, data) {
-      var packet = this.unmask(mask, data, true);
-      if (packet != null) this.currentMessage.push(packet);
-      if (this.state.lastFragment) {
-        var messageBuffer = this.concatBuffers(this.currentMessage);
-        if (!Validation.isValidUTF8(messageBuffer)) {
-          this.error('invalid utf8 sequence', 1007);
-          return;
-        }
-        this.ontext(messageBuffer.toString('utf8'), {masked: this.state.masked, buffer: messageBuffer});
-        this.currentMessage = [];
-      }
+      var self = this;
+      var packet = this.unmask(mask, data, true) || new Buffer(0);
+      var state = clone(this.state);
+      this.messageHandlers.push(function(callback) {
+        self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) {
+          if (err) return self.error(err.message, 1007);
+          if (buffer != null) self.currentMessage.push(buffer);
+
+          if (state.lastFragment) {
+            var messageBuffer = self.concatBuffers(self.currentMessage);
+            self.currentMessage = [];
+            if (!Validation.isValidUTF8(messageBuffer)) {
+              self.error('invalid utf8 sequence', 1007);
+              return;
+            }
+            self.ontext(messageBuffer.toString('utf8'), {masked: state.masked, buffer: messageBuffer});
+          }
+          callback();
+        });
+      });
+      this.flush();
       this.endPacket();
     }
   },
@@ -435,13 +520,22 @@ var opcodes = {
       }
     },
     finish: function(mask, data) {
-      var packet = this.unmask(mask, data, true);
-      if (packet != null) this.currentMessage.push(packet);
-      if (this.state.lastFragment) {
-        var messageBuffer = this.concatBuffers(this.currentMessage);
-        this.onbinary(messageBuffer, {masked: this.state.masked, buffer: messageBuffer});
-        this.currentMessage = [];
-      }
+      var self = this;
+      var packet = this.unmask(mask, data, true) || new Buffer(0);
+      var state = clone(this.state);
+      this.messageHandlers.push(function(callback) {
+        self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) {
+          if (err) return self.error(err.message, 1007);
+          if (buffer != null) self.currentMessage.push(buffer);
+          if (state.lastFragment) {
+            var messageBuffer = self.concatBuffers(self.currentMessage);
+            self.currentMessage = [];
+            self.onbinary(messageBuffer, {masked: state.masked, buffer: messageBuffer});
+          }
+          callback();
+        });
+      });
+      this.flush();
       this.endPacket();
     }
   },
@@ -482,26 +576,31 @@ var opcodes = {
     finish: function(mask, data) {
       var self = this;
       data = self.unmask(mask, data, true);
-      if (data && data.length == 1) {
-        self.error('close packets with data must be at least two bytes long', 1002);
-        return;
-      }
-      var code = data && data.length > 1 ? readUInt16BE.call(data, 0) : 1000;
-      if (!ErrorCodes.isValidErrorCode(code)) {
-        self.error('invalid error code', 1002);
-        return;
-      }
-      var message = '';
-      if (data && data.length > 2) {
-        var messageBuffer = data.slice(2);
-        if (!Validation.isValidUTF8(messageBuffer)) {
-          self.error('invalid utf8 sequence', 1007);
+
+      var state = clone(this.state);
+      this.messageHandlers.push(function() {
+        if (data && data.length == 1) {
+          self.error('close packets with data must be at least two bytes long', 1002);
           return;
         }
-        message = messageBuffer.toString('utf8');
-      }
-      this.onclose(code, message, {masked: self.state.masked});
-      this.reset();
+        var code = data && data.length > 1 ? readUInt16BE.call(data, 0) : 1000;
+        if (!ErrorCodes.isValidErrorCode(code)) {
+          self.error('invalid error code', 1002);
+          return;
+        }
+        var message = '';
+        if (data && data.length > 2) {
+          var messageBuffer = data.slice(2);
+          if (!Validation.isValidUTF8(messageBuffer)) {
+            self.error('invalid utf8 sequence', 1007);
+            return;
+          }
+          message = messageBuffer.toString('utf8');
+        }
+        self.onclose(code, message, {masked: state.masked});
+        self.reset();
+      });
+      this.flush();
     },
   },
   // ping
@@ -539,7 +638,14 @@ var opcodes = {
       }
     },
     finish: function(mask, data) {
-      this.onping(this.unmask(mask, data, true), {masked: this.state.masked, binary: true});
+      var self = this;
+      data = this.unmask(mask, data, true);
+      var state = clone(this.state);
+      this.messageHandlers.push(function(callback) {
+        self.onping(data, {masked: state.masked, binary: true});
+        callback();
+      });
+      this.flush();
       this.endPacket();
     }
   },
@@ -578,7 +684,14 @@ var opcodes = {
       }
     },
     finish: function(mask, data) {
-      this.onpong(this.unmask(mask, data, true), {masked: this.state.masked, binary: true});
+      var self = this;
+      data = self.unmask(mask, data, true);
+      var state = clone(this.state);
+      this.messageHandlers.push(function(callback) {
+        self.onpong(data, {masked: state.masked, binary: true});
+        callback();
+      });
+      this.flush();
       this.endPacket();
     }
   }
diff --git a/lib/Sender.hixie.js b/lib/Sender.hixie.js
index c715dbd..fd2fd25 100644
--- a/lib/Sender.hixie.js
+++ b/lib/Sender.hixie.js
@@ -13,6 +13,8 @@ var events = require('events')
  */
 
 function Sender(socket) {
+  events.EventEmitter.call(this);
+
   this.socket = socket;
   this.continuationFrame = false;
   this.isClosed = false;
diff --git a/lib/Sender.js b/lib/Sender.js
index bc6ea73..f346748 100644
--- a/lib/Sender.js
+++ b/lib/Sender.js
@@ -8,15 +8,22 @@ var events = require('events')
   , util = require('util')
   , EventEmitter = events.EventEmitter
   , ErrorCodes = require('./ErrorCodes')
-  , bufferUtil = require('./BufferUtil').BufferUtil;
+  , bufferUtil = require('./BufferUtil').BufferUtil
+  , PerMessageDeflate = require('./PerMessageDeflate');
 
 /**
  * HyBi Sender implementation
  */
 
-function Sender(socket) {
+function Sender(socket, extensions) {
+  events.EventEmitter.call(this);
+
   this._socket = socket;
+  this.extensions = extensions || {};
   this.firstFragment = true;
+  this.compress = false;
+  this.messageHandlers = [];
+  this.processing = false;
 }
 
 /**
@@ -31,7 +38,7 @@ util.inherits(Sender, events.EventEmitter);
  * @api public
  */
 
-Sender.prototype.close = function(code, data, mask) {
+Sender.prototype.close = function(code, data, mask, cb) {
   if (typeof code !== 'undefined') {
     if (typeof code !== 'number' ||
       !ErrorCodes.isValidErrorCode(code)) throw new Error('first argument must be a valid error code number');
@@ -40,7 +47,14 @@ Sender.prototype.close = function(code, data, mask) {
   var dataBuffer = new Buffer(2 + (data ? Buffer.byteLength(data) : 0));
   writeUInt16BE.call(dataBuffer, code, 0);
   if (dataBuffer.length > 2) dataBuffer.write(data, 2);
-  this.frameAndSend(0x8, dataBuffer, true, mask);
+
+  var self = this;
+  this.messageHandlers.push(function(callback) {
+    self.frameAndSend(0x8, dataBuffer, true, mask);
+    callback();
+    if (typeof cb == 'function') cb();
+  });
+  this.flush();
 };
 
 /**
@@ -51,7 +65,12 @@ Sender.prototype.close = function(code, data, mask) {
 
 Sender.prototype.ping = function(data, options) {
   var mask = options && options.mask;
-  this.frameAndSend(0x9, data || '', true, mask);
+  var self = this;
+  this.messageHandlers.push(function(callback) {
+    self.frameAndSend(0x9, data || '', true, mask);
+    callback();
+  });
+  this.flush();
 };
 
 /**
@@ -62,7 +81,12 @@ Sender.prototype.ping = function(data, options) {
 
 Sender.prototype.pong = function(data, options) {
   var mask = options && options.mask;
-  this.frameAndSend(0xa, data || '', true, mask);
+  var self = this;
+  this.messageHandlers.push(function(callback) {
+    self.frameAndSend(0xa, data || '', true, mask);
+    callback();
+  });
+  this.flush();
 };
 
 /**
@@ -74,11 +98,32 @@ Sender.prototype.pong = function(data, options) {
 Sender.prototype.send = function(data, options, cb) {
   var finalFragment = options && options.fin === false ? false : true;
   var mask = options && options.mask;
+  var compress = options && options.compress;
   var opcode = options && options.binary ? 2 : 1;
-  if (this.firstFragment === false) opcode = 0;
-  else this.firstFragment = false;
+  if (this.firstFragment === false) {
+    opcode = 0;
+    compress = false;
+  } else {
+    this.firstFragment = false;
+    this.compress = compress;
+  }
   if (finalFragment) this.firstFragment = true
-  this.frameAndSend(opcode, data, finalFragment, mask, cb);
+
+  var compressFragment = this.compress;
+
+  var self = this;
+  this.messageHandlers.push(function(callback) {
+    self.applyExtensions(data, finalFragment, compressFragment, function(err, data) {
+      if (err) {
+        if (typeof cb == 'function') cb(err);
+        else self.emit('error', err);
+        return;
+      }
+      self.frameAndSend(opcode, data, finalFragment, mask, compress, cb);
+      callback();
+    });
+  });
+  this.flush();
 };
 
 /**
@@ -87,7 +132,7 @@ Sender.prototype.send = function(data, options, cb) {
  * @api private
  */
 
-Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData, cb) {
+Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData, compressed, cb) {
   var canModifyData = false;
 
   if (!data) {
@@ -127,6 +172,7 @@ Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData,
   var totalLength = mergeBuffers ? dataLength + dataOffset : dataOffset;
   var outputBuffer = new Buffer(totalLength);
   outputBuffer[0] = finalFragment ? opcode | 0x80 : opcode;
+  if (compressed) outputBuffer[0] |= 0x40;
 
   switch (secondByte) {
     case 126:
@@ -191,6 +237,42 @@ Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData,
   }
 };
 
+/**
+ * Execute message handler buffers
+ *
+ * @api private
+ */
+
+Sender.prototype.flush = function() {
+  if (this.processing) return;
+
+  var handler = this.messageHandlers.shift();
+  if (!handler) return;
+
+  this.processing = true;
+
+  var self = this;
+
+  handler(function() {
+    self.processing = false;
+    self.flush();
+  });
+};
+
+/**
+ * Apply extensions to message
+ *
+ * @api private
+ */
+
+Sender.prototype.applyExtensions = function(data, fin, compress, callback) {
+  if (compress && data) {
+    this.extensions[PerMessageDeflate.extensionName].compress(data, fin, callback);
+  } else {
+    callback(null, data);
+  }
+};
+
 module.exports = Sender;
 
 function writeUInt16BE(value, offset) {
diff --git a/lib/Validation.js b/lib/Validation.js
index 0f3109a..0795fb7 100644
--- a/lib/Validation.js
+++ b/lib/Validation.js
@@ -1,3 +1,5 @@
+'use strict';
+
 /*!
  * ws: a node.js websocket client
  * Copyright(c) 2011 Einar Otto Stangvik <einaros at gmail.com>
@@ -5,12 +7,7 @@
  */
 
 try {
-  module.exports = require('../build/Release/validation');
-} catch (e) { try {
-  module.exports = require('../build/default/validation');
-} catch (e) { try {
-  module.exports = require('./Validation.fallback');
+  module.exports = require('utf-8-validate');
 } catch (e) {
-  console.error('validation.node seems to not have been built. Run npm install.');
-  throw e;
-}}}
+  module.exports = require('./Validation.fallback');
+}
diff --git a/lib/WebSocket.js b/lib/WebSocket.js
index 8c304eb..bc3f84a 100644
--- a/lib/WebSocket.js
+++ b/lib/WebSocket.js
@@ -1,21 +1,26 @@
+'use strict';
+
 /*!
  * ws: a node.js websocket client
  * Copyright(c) 2011 Einar Otto Stangvik <einaros at gmail.com>
  * MIT Licensed
  */
 
-var util = require('util')
-  , events = require('events')
+var url = require('url')
+  , util = require('util')
   , http = require('http')
   , https = require('https')
   , crypto = require('crypto')
-  , url = require('url')
   , stream = require('stream')
+  , Ultron = require('ultron')
   , Options = require('options')
   , Sender = require('./Sender')
   , Receiver = require('./Receiver')
   , SenderHixie = require('./Sender.hixie')
-  , ReceiverHixie = require('./Receiver.hixie');
+  , ReceiverHixie = require('./Receiver.hixie')
+  , Extensions = require('./Extensions')
+  , PerMessageDeflate = require('./PerMessageDeflate')
+  , EventEmitter = require('events').EventEmitter;
 
 /**
  * Constants
@@ -27,31 +32,41 @@ var protocolVersion = 13;
 
 // Close timeout
 
-var closeTimeout = 30000; // Allow 5 seconds to terminate the connection cleanly
+var closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly
 
 /**
  * WebSocket implementation
+ *
+ * @constructor
+ * @param {String} address Connection address.
+ * @param {String|Array} protocols WebSocket protocols.
+ * @param {Object} options Additional connection options.
+ * @api public
  */
-
 function WebSocket(address, protocols, options) {
+  EventEmitter.call(this);
 
-  if (protocols && !Array.isArray(protocols) && 'object' == typeof protocols) {
+  if (protocols && !Array.isArray(protocols) && 'object' === typeof protocols) {
     // accept the "options" Object as the 2nd argument
     options = protocols;
     protocols = null;
   }
-  if ('string' == typeof protocols) {
+
+  if ('string' === typeof protocols) {
     protocols = [ protocols ];
   }
+
   if (!Array.isArray(protocols)) {
     protocols = [];
   }
-  // TODO: actually handle the `Sub-Protocols` part of the WebSocket client
 
   this._socket = null;
+  this._ultron = null;
+  this._closeReceived = false;
   this.bytesReceived = 0;
   this.readyState = null;
   this.supports = {};
+  this.extensions = {};
 
   if (Array.isArray(address)) {
     initAsServerClient.apply(this, address.concat(options));
@@ -63,14 +78,12 @@ function WebSocket(address, protocols, options) {
 /**
  * Inherits from EventEmitter.
  */
-
-util.inherits(WebSocket, events.EventEmitter);
+util.inherits(WebSocket, EventEmitter);
 
 /**
  * Ready States
  */
-
-["CONNECTING", "OPEN", "CLOSING", "CLOSED"].forEach(function (state, index) {
+["CONNECTING", "OPEN", "CLOSING", "CLOSED"].forEach(function each(state, index) {
     WebSocket.prototype[state] = WebSocket[state] = index;
 });
 
@@ -80,38 +93,53 @@ util.inherits(WebSocket, events.EventEmitter);
  * @param {Object} data to be sent to the server
  * @api public
  */
+WebSocket.prototype.close = function close(code, data) {
+  if (this.readyState === WebSocket.CLOSED) return;
 
-WebSocket.prototype.close = function(code, data) {
-  if (this.readyState == WebSocket.CLOSING || this.readyState == WebSocket.CLOSED) return;
-  if (this.readyState == WebSocket.CONNECTING) {
+  if (this.readyState === WebSocket.CONNECTING) {
     this.readyState = WebSocket.CLOSED;
     return;
   }
+
+  if (this.readyState === WebSocket.CLOSING) {
+    if (this._closeReceived && this._isServer) {
+      this.terminate();
+    }
+    return;
+  }
+
+  var self = this;
   try {
     this.readyState = WebSocket.CLOSING;
     this._closeCode = code;
     this._closeMessage = data;
     var mask = !this._isServer;
-    this._sender.close(code, data, mask);
-  }
-  catch (e) {
+    this._sender.close(code, data, mask, function(err) {
+      if (err) self.emit('error', err);
+
+      if (self._closeReceived && self._isServer) {
+        self.terminate();
+      } else {
+        // ensure that the connection is cleaned up even when no response of closing handshake.
+        clearTimeout(self._closeTimer);
+        self._closeTimer = setTimeout(cleanupWebsocketResources.bind(self, true), closeTimeout);
+      }
+    });
+  } catch (e) {
     this.emit('error', e);
   }
-  finally {
-    this.terminate();
-  }
-}
+};
 
 /**
  * Pause the client stream
  *
  * @api public
  */
+WebSocket.prototype.pause = function pauser() {
+  if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
 
-WebSocket.prototype.pause = function() {
-  if (this.readyState != WebSocket.OPEN) throw new Error('not opened');
   return this._socket.pause();
-}
+};
 
 /**
  * Sends a ping
@@ -121,16 +149,18 @@ WebSocket.prototype.pause = function() {
  * @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
  * @api public
  */
-
-WebSocket.prototype.ping = function(data, options, dontFailWhenClosed) {
-  if (this.readyState != WebSocket.OPEN) {
+WebSocket.prototype.ping = function ping(data, options, dontFailWhenClosed) {
+  if (this.readyState !== WebSocket.OPEN) {
     if (dontFailWhenClosed === true) return;
     throw new Error('not opened');
   }
+
   options = options || {};
-  if (typeof options.mask == 'undefined') options.mask = !this._isServer;
+
+  if (typeof options.mask === 'undefined') options.mask = !this._isServer;
+
   this._sender.ping(data, options);
-}
+};
 
 /**
  * Sends a pong
@@ -140,56 +170,62 @@ WebSocket.prototype.ping = function(data, options, dontFailWhenClosed) {
  * @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
  * @api public
  */
-
 WebSocket.prototype.pong = function(data, options, dontFailWhenClosed) {
-  if (this.readyState != WebSocket.OPEN) {
+  if (this.readyState !== WebSocket.OPEN) {
     if (dontFailWhenClosed === true) return;
     throw new Error('not opened');
   }
+
   options = options || {};
-  if (typeof options.mask == 'undefined') options.mask = !this._isServer;
+
+  if (typeof options.mask === 'undefined') options.mask = !this._isServer;
+
   this._sender.pong(data, options);
-}
+};
 
 /**
  * Resume the client stream
  *
  * @api public
  */
+WebSocket.prototype.resume = function resume() {
+  if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
 
-WebSocket.prototype.resume = function() {
-  if (this.readyState != WebSocket.OPEN) throw new Error('not opened');
   return this._socket.resume();
-}
+};
 
 /**
  * Sends a piece of data
  *
  * @param {Object} data to be sent to the server
- * @param {Object} Members - mask: boolean, binary: boolean
+ * @param {Object} Members - mask: boolean, binary: boolean, compress: boolean
  * @param {function} Optional callback which is executed after the send completes
  * @api public
  */
 
-WebSocket.prototype.send = function(data, options, cb) {
-  if (typeof options == 'function') {
+WebSocket.prototype.send = function send(data, options, cb) {
+  if (typeof options === 'function') {
     cb = options;
     options = {};
   }
-  if (this.readyState != WebSocket.OPEN) {
-    if (typeof cb == 'function') cb(new Error('not opened'));
+
+  if (this.readyState !== WebSocket.OPEN) {
+    if (typeof cb === 'function') cb(new Error('not opened'));
     else throw new Error('not opened');
     return;
   }
+
   if (!data) data = '';
   if (this._queue) {
     var self = this;
     this._queue.push(function() { self.send(data, options, cb); });
     return;
   }
+
   options = options || {};
   options.fin = true;
-  if (typeof options.binary == 'undefined') {
+
+  if (typeof options.binary === 'undefined') {
     options.binary = (data instanceof ArrayBuffer || data instanceof Buffer ||
       data instanceof Uint8Array ||
       data instanceof Uint16Array ||
@@ -200,78 +236,103 @@ WebSocket.prototype.send = function(data, options, cb) {
       data instanceof Float32Array ||
       data instanceof Float64Array);
   }
-  if (typeof options.mask == 'undefined') options.mask = !this._isServer;
-  var readable = typeof stream.Readable == 'function' ? stream.Readable : stream.Stream;
+
+  if (typeof options.mask === 'undefined') options.mask = !this._isServer;
+  if (typeof options.compress === 'undefined') options.compress = true;
+  if (!this.extensions[PerMessageDeflate.extensionName]) {
+    options.compress = false;
+  }
+
+  var readable = typeof stream.Readable === 'function'
+    ? stream.Readable
+    : stream.Stream;
+
   if (data instanceof readable) {
     startQueue(this);
     var self = this;
-    sendStream(this, data, options, function(error) {
-      process.nextTick(function() { executeQueueSends(self); });
-      if (typeof cb == 'function') cb(error);
+
+    sendStream(this, data, options, function send(error) {
+      process.nextTick(function tock() {
+        executeQueueSends(self);
+      });
+
+      if (typeof cb === 'function') cb(error);
     });
+  } else {
+    this._sender.send(data, options, cb);
   }
-  else this._sender.send(data, options, cb);
-}
+};
 
 /**
  * Streams data through calls to a user supplied function
  *
- * @param {Object} Members - mask: boolean, binary: boolean
+ * @param {Object} Members - mask: boolean, binary: boolean, compress: boolean
  * @param {function} 'function (error, send)' which is executed on successive ticks of which send is 'function (data, final)'.
  * @api public
  */
-
-WebSocket.prototype.stream = function(options, cb) {
-  if (typeof options == 'function') {
+WebSocket.prototype.stream = function stream(options, cb) {
+  if (typeof options === 'function') {
     cb = options;
     options = {};
   }
+
   var self = this;
-  if (typeof cb != 'function') throw new Error('callback must be provided');
-  if (this.readyState != WebSocket.OPEN) {
-    if (typeof cb == 'function') cb(new Error('not opened'));
+
+  if (typeof cb !== 'function') throw new Error('callback must be provided');
+
+  if (this.readyState !== WebSocket.OPEN) {
+    if (typeof cb === 'function') cb(new Error('not opened'));
     else throw new Error('not opened');
     return;
   }
+
   if (this._queue) {
-    this._queue.push(function() { self.stream(options, cb); });
+    this._queue.push(function () { self.stream(options, cb); });
     return;
   }
+
   options = options || {};
-  if (typeof options.mask == 'undefined') options.mask = !this._isServer;
+
+  if (typeof options.mask === 'undefined') options.mask = !this._isServer;
+  if (typeof options.compress === 'undefined') options.compress = true;
+  if (!this.extensions[PerMessageDeflate.extensionName]) {
+    options.compress = false;
+  }
+
   startQueue(this);
-  var send = function(data, final) {
+
+  function send(data, final) {
     try {
-      if (self.readyState != WebSocket.OPEN) throw new Error('not opened');
+      if (self.readyState !== WebSocket.OPEN) throw new Error('not opened');
       options.fin = final === true;
       self._sender.send(data, options);
       if (!final) process.nextTick(cb.bind(null, null, send));
       else executeQueueSends(self);
-    }
-    catch (e) {
-      if (typeof cb == 'function') cb(e);
+    } catch (e) {
+      if (typeof cb === 'function') cb(e);
       else {
         delete self._queue;
         self.emit('error', e);
       }
     }
   }
+
   process.nextTick(cb.bind(null, null, send));
-}
+};
 
 /**
  * Immediately shuts down the connection
  *
  * @api public
  */
+WebSocket.prototype.terminate = function terminate() {
+  if (this.readyState === WebSocket.CLOSED) return;
 
-WebSocket.prototype.terminate = function() {
-  if (this.readyState == WebSocket.CLOSED) return;
   if (this._socket) {
-    try {
-      // End the connection
-      this._socket.end();
-    }
+    this.readyState = WebSocket.CLOSING;
+
+    // End the connection
+    try { this._socket.end(); }
     catch (e) {
       // Socket error during end() call, so just destroy it right now
       cleanupWebsocketResources.call(this, true);
@@ -281,9 +342,12 @@ WebSocket.prototype.terminate = function() {
     // Add a timeout to ensure that the connection is completely
     // cleaned up within 30 seconds, even if the clean close procedure
     // fails for whatever reason
+    // First cleanup any pre-existing timeout from an earlier "terminate" call,
+    // if one exists.  Otherwise terminate calls in quick succession will leak timeouts
+    // and hold the program open for `closeTimout` time.
+    if (this._closeTimer) { clearTimeout(this._closeTimer); }
     this._closeTimer = setTimeout(cleanupWebsocketResources.bind(this, true), closeTimeout);
-  }
-  else if (this.readyState == WebSocket.CONNECTING) {
+  } else if (this.readyState === WebSocket.CONNECTING) {
     cleanupWebsocketResources.call(this, true);
   }
 };
@@ -293,7 +357,6 @@ WebSocket.prototype.terminate = function() {
  *
  * @api public
  */
-
 Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
   get: function get() {
     var amount = 0;
@@ -310,7 +373,6 @@ Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
  * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
  * @api public
  */
-
 ['open', 'error', 'close', 'message'].forEach(function(method) {
   Object.defineProperty(WebSocket.prototype, 'on' + method, {
     /**
@@ -319,7 +381,6 @@ Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
      * @returns {Mixed} the set function or undefined
      * @api public
      */
-
     get: function get() {
       var listener = this.listeners(method)[0];
       return listener ? (listener._listener ? listener._listener : listener) : undefined;
@@ -332,7 +393,6 @@ Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
      * @returns {Mixed} the set function or undefined
      * @api public
      */
-
     set: function set(listener) {
       this.removeAllListeners(method);
       this.addEventListener(method, listener);
@@ -349,41 +409,50 @@ Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
  */
 WebSocket.prototype.addEventListener = function(method, listener) {
   var target = this;
+
+  function onMessage (data, flags) {
+    listener.call(target, new MessageEvent(data, flags.binary ? 'Binary' : 'Text', target));
+  }
+
+  function onClose (code, message) {
+    listener.call(target, new CloseEvent(code, message, target));
+  }
+
+  function onError (event) {
+    event.target = target;
+    listener.call(target, event);
+  }
+
+  function onOpen () {
+    listener.call(target, new OpenEvent(target));
+  }
+
   if (typeof listener === 'function') {
     if (method === 'message') {
-      function onMessage (data, flags) {
-        listener.call(this, new MessageEvent(data, flags.binary ? 'Binary' : 'Text', target));
-      }
-      // store a reference so we can return the original function from the addEventListener hook
+      // store a reference so we can return the original function from the
+      // addEventListener hook
       onMessage._listener = listener;
       this.on(method, onMessage);
     } else if (method === 'close') {
-      function onClose (code, message) {
-        listener.call(this, new CloseEvent(code, message, target));
-      }
-      // store a reference so we can return the original function from the addEventListener hook
+      // store a reference so we can return the original function from the
+      // addEventListener hook
       onClose._listener = listener;
       this.on(method, onClose);
     } else if (method === 'error') {
-      function onError (event) {
-        event.target = target;
-        listener.call(this, event);
-      }
-      // store a reference so we can return the original function from the addEventListener hook
+      // store a reference so we can return the original function from the
+      // addEventListener hook
       onError._listener = listener;
       this.on(method, onError);
     } else if (method === 'open') {
-      function onOpen () {
-        listener.call(this, new OpenEvent(target));
-      }
-      // store a reference so we can return the original function from the addEventListener hook
+      // store a reference so we can return the original function from the
+      // addEventListener hook
       onOpen._listener = listener;
       this.on(method, onOpen);
     } else {
       this.on(method, listener);
     }
   }
-}
+};
 
 module.exports = WebSocket;
 
@@ -391,9 +460,9 @@ module.exports = WebSocket;
  * W3C MessageEvent
  *
  * @see http://www.w3.org/TR/html5/comms.html
+ * @constructor
  * @api private
  */
-
 function MessageEvent(dataArg, typeArg, target) {
   this.data = dataArg;
   this.type = typeArg;
@@ -404,11 +473,11 @@ function MessageEvent(dataArg, typeArg, target) {
  * W3C CloseEvent
  *
  * @see http://www.w3.org/TR/html5/comms.html
+ * @constructor
  * @api private
  */
-
 function CloseEvent(code, reason, target) {
-  this.wasClean = (typeof code == 'undefined' || code == 1000);
+  this.wasClean = (typeof code === 'undefined' || code === 1000);
   this.code = code;
   this.reason = reason;
   this.target = target;
@@ -418,9 +487,9 @@ function CloseEvent(code, reason, target) {
  * W3C OpenEvent
  *
  * @see http://www.w3.org/TR/html5/comms.html
+ * @constructor
  * @api private
  */
-
 function OpenEvent(target) {
   this.target = target;
 }
@@ -429,24 +498,28 @@ function OpenEvent(target) {
  * Entirely private apis,
  * which may or may not be bound to a sepcific WebSocket instance.
  */
-
 function initAsServerClient(req, socket, upgradeHead, options) {
   options = new Options({
     protocolVersion: protocolVersion,
-    protocol: null
+    protocol: null,
+    extensions: {}
   }).merge(options);
 
   // expose state properties
   this.protocol = options.value.protocol;
   this.protocolVersion = options.value.protocolVersion;
-  this.supports.binary = (this.protocolVersion != 'hixie-76');
+  this.extensions = options.value.extensions;
+  this.supports.binary = (this.protocolVersion !== 'hixie-76');
   this.upgradeReq = req;
   this.readyState = WebSocket.CONNECTING;
   this._isServer = true;
 
   // establish connection
-  if (options.value.protocolVersion == 'hixie-76') establishConnection.call(this, ReceiverHixie, SenderHixie, socket, upgradeHead);
-  else establishConnection.call(this, Receiver, Sender, socket, upgradeHead);
+  if (options.value.protocolVersion === 'hixie-76') {
+    establishConnection.call(this, ReceiverHixie, SenderHixie, socket, upgradeHead);
+  } else {
+    establishConnection.call(this, Receiver, Sender, socket, upgradeHead);
+  }
 }
 
 function initAsClient(address, protocols, options) {
@@ -455,7 +528,7 @@ function initAsClient(address, protocols, options) {
     protocolVersion: protocolVersion,
     host: null,
     headers: null,
-    protocol: null,
+    protocol: protocols.join(','),
     agent: null,
 
     // ssl-related options
@@ -465,13 +538,15 @@ function initAsClient(address, protocols, options) {
     cert: null,
     ca: null,
     ciphers: null,
-    rejectUnauthorized: null
+    rejectUnauthorized: null,
+    perMessageDeflate: true
   }).merge(options);
-  if (options.value.protocolVersion != 8 && options.value.protocolVersion != 13) {
+
+  if (options.value.protocolVersion !== 8 && options.value.protocolVersion !== 13) {
     throw new Error('unsupported protocol version');
   }
 
-  // verify url and establish http class
+  // verify URL and establish http class
   var serverUrl = url.parse(address);
   var isUnixSocket = serverUrl.protocol === 'ws+unix:';
   if (!serverUrl.host && !isUnixSocket) throw new Error('invalid url');
@@ -480,11 +555,19 @@ function initAsClient(address, protocols, options) {
   var port = serverUrl.port || (isSecure ? 443 : 80);
   var auth = serverUrl.auth;
 
+  // prepare extensions
+  var extensionsOffer = {};
+  var perMessageDeflate;
+  if (options.value.perMessageDeflate) {
+    perMessageDeflate = new PerMessageDeflate(typeof options.value.perMessageDeflate !== true ? options.value.perMessageDeflate : {}, false);
+    extensionsOffer[PerMessageDeflate.extensionName] = perMessageDeflate.offer();
+  }
+
   // expose state properties
   this._isServer = false;
   this.url = address;
   this.protocolVersion = options.value.protocolVersion;
-  this.supports.binary = (this.protocolVersion != 'hixie-76');
+  this.supports.binary = (this.protocolVersion !== 'hixie-76');
 
   // begin handshake
   var key = new Buffer(options.value.protocolVersion + '-' + Date.now()).toString('base64');
@@ -495,9 +578,10 @@ function initAsClient(address, protocols, options) {
   var agent = options.value.agent;
 
   var headerHost = serverUrl.hostname;
-  // Append port number to Host and Origin header, only if specified in the url and non-default
-  if(serverUrl.port) {
-    if((isSecure && (port != 443)) || (!isSecure && (port != 80))){
+  // Append port number to Host and Origin header, only if specified in the url
+  // and non-default
+  if (serverUrl.port) {
+    if ((isSecure && (port !== 443)) || (!isSecure && (port !== 80))){
       headerHost = headerHost + ':' + port;
     }
   }
@@ -517,7 +601,7 @@ function initAsClient(address, protocols, options) {
 
   // If we have basic auth.
   if (auth) {
-    requestOptions.headers['Authorization'] = 'Basic ' + new Buffer(auth).toString('base64');
+    requestOptions.headers.Authorization = 'Basic ' + new Buffer(auth).toString('base64');
   }
 
   if (options.value.protocol) {
@@ -525,7 +609,7 @@ function initAsClient(address, protocols, options) {
   }
 
   if (options.value.host) {
-    requestOptions.headers['Host'] = options.value.host;
+    requestOptions.headers.Host = options.value.host;
   }
 
   if (options.value.headers) {
@@ -536,6 +620,10 @@ function initAsClient(address, protocols, options) {
     }
   }
 
+  if (Object.keys(extensionsOffer).length) {
+    requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format(extensionsOffer);
+  }
+
   if (options.isDefinedAndNonNull('pfx')
    || options.isDefinedAndNonNull('key')
    || options.isDefinedAndNonNull('passphrase')
@@ -569,36 +657,40 @@ function initAsClient(address, protocols, options) {
   }
   if (options.value.origin) {
     if (options.value.protocolVersion < 13) requestOptions.headers['Sec-WebSocket-Origin'] = options.value.origin;
-    else requestOptions.headers['Origin'] = options.value.origin;
+    else requestOptions.headers.Origin = options.value.origin;
   }
 
   var self = this;
   var req = httpObj.request(requestOptions);
 
-  req.on('error', function(error) {
+  req.on('error', function onerror(error) {
     self.emit('error', error);
     cleanupWebsocketResources.call(this, error);
   });
 
-  req.once('response', function(res) {
+  req.once('response', function response(res) {
+    var error;
+
     if (!self.emit('unexpected-response', req, res)) {
-      var error = new Error('unexpected server response (' + res.statusCode + ')');
+      error = new Error('unexpected server response (' + res.statusCode + ')');
       req.abort();
       self.emit('error', error);
     }
+
     cleanupWebsocketResources.call(this, error);
   });
 
-  req.once('upgrade', function(res, socket, upgradeHead) {
-    if (self.readyState == WebSocket.CLOSED) {
+  req.once('upgrade', function upgrade(res, socket, upgradeHead) {
+    if (self.readyState === WebSocket.CLOSED) {
       // client closed before server accepted connection
       self.emit('close');
       self.removeAllListeners();
       socket.end();
       return;
     }
+
     var serverKey = res.headers['sec-websocket-accept'];
-    if (typeof serverKey == 'undefined' || serverKey !== expectedServerKey) {
+    if (typeof serverKey === 'undefined' || serverKey !== expectedServerKey) {
       self.emit('error', 'invalid server key');
       self.removeAllListeners();
       socket.end();
@@ -608,20 +700,35 @@ function initAsClient(address, protocols, options) {
     var serverProt = res.headers['sec-websocket-protocol'];
     var protList = (options.value.protocol || "").split(/, */);
     var protError = null;
+
     if (!options.value.protocol && serverProt) {
-        protError = 'server sent a subprotocol even though none requested';
+      protError = 'server sent a subprotocol even though none requested';
     } else if (options.value.protocol && !serverProt) {
-        protError = 'server sent no subprotocol even though requested';
+      protError = 'server sent no subprotocol even though requested';
     } else if (serverProt && protList.indexOf(serverProt) === -1) {
-        protError = 'server responded with an invalid protocol';
+      protError = 'server responded with an invalid protocol';
     }
+
     if (protError) {
-        self.emit('error', protError);
+      self.emit('error', protError);
+      self.removeAllListeners();
+      socket.end();
+      return;
+    } else if (serverProt) {
+      self.protocol = serverProt;
+    }
+
+    var serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']);
+    if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) {
+      try {
+        perMessageDeflate.accept(serverExtensions[PerMessageDeflate.extensionName]);
+      } catch (err) {
+        self.emit('error', 'invalid extension parameter');
         self.removeAllListeners();
         socket.end();
         return;
-    } else if (serverProt) {
-        self.protocol = serverProt;
+      }
+      self.extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
     }
 
     establishConnection.call(self, Receiver, Sender, socket, upgradeHead);
@@ -637,38 +744,46 @@ function initAsClient(address, protocols, options) {
 }
 
 function establishConnection(ReceiverClass, SenderClass, socket, upgradeHead) {
+  var ultron = this._ultron = new Ultron(socket);
   this._socket = socket;
+
   socket.setTimeout(0);
   socket.setNoDelay(true);
   var self = this;
-  this._receiver = new ReceiverClass();
+  this._receiver = new ReceiverClass(this.extensions);
 
   // socket cleanup handlers
-  socket.on('end', cleanupWebsocketResources.bind(this));
-  socket.on('close', cleanupWebsocketResources.bind(this));
-  socket.on('error', cleanupWebsocketResources.bind(this));
+  ultron.on('end', cleanupWebsocketResources.bind(this));
+  ultron.on('close', cleanupWebsocketResources.bind(this));
+  ultron.on('error', cleanupWebsocketResources.bind(this));
 
   // ensure that the upgradeHead is added to the receiver
   function firstHandler(data) {
-    if (self.readyState != WebSocket.OPEN) return;
+    if (self.readyState !== WebSocket.OPEN && self.readyState !== WebSocket.CLOSING) return;
+
     if (upgradeHead && upgradeHead.length > 0) {
       self.bytesReceived += upgradeHead.length;
       var head = upgradeHead;
       upgradeHead = null;
       self._receiver.add(head);
     }
+
     dataHandler = realHandler;
+
     if (data) {
       self.bytesReceived += data.length;
       self._receiver.add(data);
     }
   }
+
   // subsequent packets are pushed straight to the receiver
   function realHandler(data) {
     if (data) self.bytesReceived += data.length;
     self._receiver.add(data);
   }
+
   var dataHandler = firstHandler;
+
   // if data was passed along with the http upgrade,
   // this will schedule a push of that on to the receiver.
   // this has to be done on next tick, since the caller
@@ -677,43 +792,58 @@ function establishConnection(ReceiverClass, SenderClass, socket, upgradeHead) {
   process.nextTick(firstHandler);
 
   // receiver event handlers
-  self._receiver.ontext = function (data, flags) {
+  self._receiver.ontext = function ontext(data, flags) {
     flags = flags || {};
+
     self.emit('message', data, flags);
   };
-  self._receiver.onbinary = function (data, flags) {
+
+  self._receiver.onbinary = function onbinary(data, flags) {
     flags = flags || {};
+
     flags.binary = true;
     self.emit('message', data, flags);
   };
-  self._receiver.onping = function(data, flags) {
+
+  self._receiver.onping = function onping(data, flags) {
     flags = flags || {};
-    self.pong(data, {mask: !self._isServer, binary: flags.binary === true}, true);
+
+    self.pong(data, {
+      mask: !self._isServer,
+      binary: flags.binary === true
+    }, true);
+
     self.emit('ping', data, flags);
   };
-  self._receiver.onpong = function(data, flags) {
-    self.emit('pong', data, flags);
+
+  self._receiver.onpong = function onpong(data, flags) {
+    self.emit('pong', data, flags || {});
   };
-  self._receiver.onclose = function(code, data, flags) {
+
+  self._receiver.onclose = function onclose(code, data, flags) {
     flags = flags || {};
+
+    self._closeReceived = true;
     self.close(code, data);
   };
-  self._receiver.onerror = function(reason, errorCode) {
+
+  self._receiver.onerror = function onerror(reason, errorCode) {
     // close the connection when the receiver reports a HyBi error code
-    self.close(typeof errorCode != 'undefined' ? errorCode : 1002, '');
+    self.close(typeof errorCode !== 'undefined' ? errorCode : 1002, '');
     self.emit('error', reason, errorCode);
   };
 
   // finalize the client
-  this._sender = new SenderClass(socket);
-  this._sender.on('error', function(error) {
+  this._sender = new SenderClass(socket, this.extensions);
+  this._sender.on('error', function onerror(error) {
     self.close(1002, '');
     self.emit('error', error);
   });
+
   this.readyState = WebSocket.OPEN;
   this.emit('open');
 
-  socket.on('data', dataHandler);
+  ultron.on('data', dataHandler);
 }
 
 function startQueue(instance) {
@@ -722,7 +852,8 @@ function startQueue(instance) {
 
 function executeQueueSends(instance) {
   var queue = instance._queue;
-  if (typeof queue == 'undefined') return;
+  if (typeof queue === 'undefined') return;
+
   delete instance._queue;
   for (var i = 0, l = queue.length; i < l; ++i) {
     queue[i]();
@@ -730,65 +861,77 @@ function executeQueueSends(instance) {
 }
 
 function sendStream(instance, stream, options, cb) {
-  stream.on('data', function(data) {
-    if (instance.readyState != WebSocket.OPEN) {
-      if (typeof cb == 'function') cb(new Error('not opened'));
+  stream.on('data', function incoming(data) {
+    if (instance.readyState !== WebSocket.OPEN) {
+      if (typeof cb === 'function') cb(new Error('not opened'));
       else {
         delete instance._queue;
         instance.emit('error', new Error('not opened'));
       }
       return;
     }
+
     options.fin = false;
     instance._sender.send(data, options);
   });
-  stream.on('end', function() {
-    if (instance.readyState != WebSocket.OPEN) {
-      if (typeof cb == 'function') cb(new Error('not opened'));
+
+  stream.on('end', function end() {
+    if (instance.readyState !== WebSocket.OPEN) {
+      if (typeof cb === 'function') cb(new Error('not opened'));
       else {
         delete instance._queue;
         instance.emit('error', new Error('not opened'));
       }
       return;
     }
+
     options.fin = true;
     instance._sender.send(null, options);
-    if (typeof cb == 'function') cb(null);
+
+    if (typeof cb === 'function') cb(null);
   });
 }
 
 function cleanupWebsocketResources(error) {
-  if (this.readyState == WebSocket.CLOSED) return;
-  var emitClose = this.readyState != WebSocket.CONNECTING;
+  if (this.readyState === WebSocket.CLOSED) return;
+
+  var emitClose = this.readyState !== WebSocket.CONNECTING;
   this.readyState = WebSocket.CLOSED;
 
   clearTimeout(this._closeTimer);
   this._closeTimer = null;
-  if (emitClose) this.emit('close', this._closeCode || 1000, this._closeMessage || '');
+
+  if (emitClose) {
+    this.emit('close', this._closeCode || 1000, this._closeMessage || '');
+  }
 
   if (this._socket) {
-    this._socket.removeAllListeners();
-    // catch all socket error after removing all standard handlers
-    var socket = this._socket;
-    this._socket.on('error', function() {
-      try { socket.destroy(); } catch (e) {}
+    if (this._ultron) this._ultron.destroy();
+    this._socket.on('error', function onerror() {
+      try { this.destroy(); }
+      catch (e) {}
     });
+
     try {
       if (!error) this._socket.end();
       else this._socket.destroy();
-    }
-    catch (e) { /* Ignore termination errors */ }
+    } catch (e) { /* Ignore termination errors */ }
+
     this._socket = null;
+    this._ultron = null;
   }
+
   if (this._sender) {
     this._sender.removeAllListeners();
     this._sender = null;
   }
+
   if (this._receiver) {
     this._receiver.cleanup();
     this._receiver = null;
   }
+
   this.removeAllListeners();
-  this.on('error', function() {}); // catch all errors after this
+  this.on('error', function onerror() {}); // catch all errors after this
   delete this._queue;
 }
diff --git a/lib/WebSocketServer.js b/lib/WebSocketServer.js
index 5cbd195..a2f3a61 100644
--- a/lib/WebSocketServer.js
+++ b/lib/WebSocketServer.js
@@ -10,6 +10,8 @@ var util = require('util')
   , crypto = require('crypto')
   , Options = require('options')
   , WebSocket = require('./WebSocket')
+  , Extensions = require('./Extensions')
+  , PerMessageDeflate = require('./PerMessageDeflate')
   , tls = require('tls')
   , url = require('url');
 
@@ -18,6 +20,8 @@ var util = require('util')
  */
 
 function WebSocketServer(options, callback) {
+  events.EventEmitter.call(this);
+
   options = new Options({
     host: '0.0.0.0',
     port: null,
@@ -27,7 +31,8 @@ function WebSocketServer(options, callback) {
     path: null,
     noServer: false,
     disableHixie: false,
-    clientTracking: true
+    clientTracking: true,
+    perMessageDeflate: true
   }).merge(options);
 
   if (!options.isDefinedAndNonNull('port') && !options.isDefinedAndNonNull('server') && !options.value.noServer) {
@@ -182,6 +187,9 @@ function handleHybiUpgrade(req, socket, upgradeHead, cb) {
     req.headers['sec-websocket-origin'] :
     req.headers['origin'];
 
+  // handle extensions offer
+  var extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']);
+
   // handler to call when the connection sequence completes
   var self = this;
   var completeHybiUpgrade2 = function(protocol) {
@@ -203,6 +211,22 @@ function handleHybiUpgrade(req, socket, upgradeHead, cb) {
       headers.push('Sec-WebSocket-Protocol: ' + protocol);
     }
 
+    var extensions = {};
+    try {
+      extensions = acceptExtensions.call(self, extensionsOffer);
+    } catch (err) {
+      abortConnection(socket, 400, 'Bad Request');
+      return;
+    }
+
+    if (Object.keys(extensions).length) {
+      var serverExtensions = {};
+      Object.keys(extensions).forEach(function(token) {
+        serverExtensions[token] = [extensions[token].params]
+      });
+      headers.push('Sec-WebSocket-Extensions: ' + Extensions.format(serverExtensions));
+    }
+
     // allows external modification/inspection of handshake headers
     self.emit('headers', headers);
 
@@ -219,7 +243,8 @@ function handleHybiUpgrade(req, socket, upgradeHead, cb) {
 
     var client = new WebSocket([req, socket, upgradeHead], {
       protocolVersion: version,
-      protocol: protocol
+      protocol: protocol,
+      extensions: extensions
     });
 
     if (self.options.clientTracking) {
@@ -246,7 +271,7 @@ function handleHybiUpgrade(req, socket, upgradeHead, cb) {
         var callbackCalled = false;
         var res = self.options.handleProtocols(protList, function(result, protocol) {
           callbackCalled = true;
-          if (!result) abortConnection(socket, 404, 'Unauthorized')
+          if (!result) abortConnection(socket, 401, 'Unauthorized');
           else completeHybiUpgrade2(protocol);
         });
         if (!callbackCalled) {
@@ -449,6 +474,17 @@ function handleHixieUpgrade(req, socket, upgradeHead, cb) {
   onClientVerified();
 }
 
+function acceptExtensions(offer) {
+  var extensions = {};
+  var options = this.options.perMessageDeflate;
+  if (options && offer[PerMessageDeflate.extensionName]) {
+    var perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true);
+    perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]);
+    extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
+  }
+  return extensions;
+}
+
 function abortConnection(socket, code, name) {
   try {
     var response = [
diff --git a/package.json b/package.json
index 4d4ea31..aa99418 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,8 @@
   "author": "Einar Otto Stangvik <einaros at gmail.com> (http://2x.io)",
   "name": "ws",
   "description": "simple to use, blazing fast and thoroughly tested websocket client, server and console for node.js, up-to-date against RFC-6455",
-  "version": "0.4.32",
+  "version": "0.7.1",
+  "license": "MIT",
   "keywords": [
     "Hixie",
     "HyBi",
@@ -14,30 +15,26 @@
   ],
   "repository": {
     "type": "git",
-    "url": "git://github.com/einaros/ws.git"
-  },
-  "bin": {
-    "wscat": "./bin/wscat"
+    "url": "git://github.com/websockets/ws.git"
   },
   "scripts": {
-    "test": "make test",
-    "install": "(node-gyp rebuild 2> builderror.log) || (exit 0)"
-  },
-  "engines": {
-    "node": ">=0.4.0"
+    "test": "make test"
   },
   "dependencies": {
-    "commander": "~2.1.0",
-    "nan": "~1.0.0",
-    "tinycolor": "0.x",
-    "options": ">=0.0.5"
+    "options": ">=0.0.5",
+    "ultron": "1.0.x"
+  },
+  "optionalDependencies": {
+    "bufferutil": "1.0.x",
+    "utf-8-validate": "1.0.x"
   },
   "devDependencies": {
-    "mocha": "1.12.0",
-    "should": "1.2.x",
-    "expect.js": "0.2.x",
+    "ansi": "0.3.x",
     "benchmark": "0.3.x",
-    "ansi": "latest"
+    "expect.js": "0.3.x",
+    "mocha": "2.0.x",
+    "should": "4.3.x",
+    "tinycolor": "0.0.x"
   },
   "browser": "./lib/browser.js",
   "component": {
diff --git a/src/bufferutil.cc b/src/bufferutil.cc
deleted file mode 100644
index 7f99bd6..0000000
--- a/src/bufferutil.cc
+++ /dev/null
@@ -1,117 +0,0 @@
-/*!
- * ws: a node.js websocket client
- * Copyright(c) 2011 Einar Otto Stangvik <einaros at gmail.com>
- * MIT Licensed
- */
-
-#include <v8.h>
-#include <node.h>
-#include <node_buffer.h>
-#include <node_object_wrap.h>
-#include <stdlib.h>
-#include <string.h>
-#include <wchar.h>
-#include <stdio.h>
-#include "nan.h"
-
-using namespace v8;
-using namespace node;
-
-class BufferUtil : public ObjectWrap
-{
-public:
-
-  static void Initialize(v8::Handle<v8::Object> target)
-  {
-    NanScope();
-    Local<FunctionTemplate> t = NanNew<FunctionTemplate>(New);
-    t->InstanceTemplate()->SetInternalFieldCount(1);
-    NODE_SET_METHOD(t, "unmask", BufferUtil::Unmask);
-    NODE_SET_METHOD(t, "mask", BufferUtil::Mask);
-    NODE_SET_METHOD(t, "merge", BufferUtil::Merge);
-    target->Set(NanSymbol("BufferUtil"), t->GetFunction());
-  }
-
-protected:
-
-  static NAN_METHOD(New)
-  {
-    NanScope();
-    BufferUtil* bufferUtil = new BufferUtil();
-    bufferUtil->Wrap(args.This());
-    NanReturnValue(args.This());
-  }
-
-  static NAN_METHOD(Merge)
-  {
-    NanScope();
-    Local<Object> bufferObj = args[0]->ToObject();
-    char* buffer = Buffer::Data(bufferObj);
-    Local<Array> array = Local<Array>::Cast(args[1]);
-    unsigned int arrayLength = array->Length();
-    size_t offset = 0;
-    unsigned int i;
-    for (i = 0; i < arrayLength; ++i) {
-      Local<Object> src = array->Get(i)->ToObject();
-      size_t length = Buffer::Length(src);
-      memcpy(buffer + offset, Buffer::Data(src), length);
-      offset += length;
-    }
-    NanReturnValue(NanTrue());
-  }
-
-  static NAN_METHOD(Unmask)
-  {
-    NanScope();
-    Local<Object> buffer_obj = args[0]->ToObject();
-    size_t length = Buffer::Length(buffer_obj);
-    Local<Object> mask_obj = args[1]->ToObject();
-    unsigned int *mask = (unsigned int*)Buffer::Data(mask_obj);
-    unsigned int* from = (unsigned int*)Buffer::Data(buffer_obj);
-    size_t len32 = length / 4;
-    unsigned int i;
-    for (i = 0; i < len32; ++i) *(from + i) ^= *mask;
-    from += i;
-    switch (length % 4) {
-      case 3: *((unsigned char*)from+2) = *((unsigned char*)from+2) ^ ((unsigned char*)mask)[2];
-      case 2: *((unsigned char*)from+1) = *((unsigned char*)from+1) ^ ((unsigned char*)mask)[1];
-      case 1: *((unsigned char*)from  ) = *((unsigned char*)from  ) ^ ((unsigned char*)mask)[0];
-      case 0:;
-    }
-    NanReturnValue(NanTrue());
-  }
-
-  static NAN_METHOD(Mask)
-  {
-    NanScope();
-    Local<Object> buffer_obj = args[0]->ToObject();
-    Local<Object> mask_obj = args[1]->ToObject();
-    unsigned int *mask = (unsigned int*)Buffer::Data(mask_obj);
-    Local<Object> output_obj = args[2]->ToObject();
-    unsigned int dataOffset = args[3]->Int32Value();
-    unsigned int length = args[4]->Int32Value();
-    unsigned int* to = (unsigned int*)(Buffer::Data(output_obj) + dataOffset);
-    unsigned int* from = (unsigned int*)Buffer::Data(buffer_obj);
-    unsigned int len32 = length / 4;
-    unsigned int i;
-    for (i = 0; i < len32; ++i) *(to + i) = *(from + i) ^ *mask;
-    to += i;
-    from += i;
-    switch (length % 4) {
-      case 3: *((unsigned char*)to+2) = *((unsigned char*)from+2) ^ *((unsigned char*)mask+2);
-      case 2: *((unsigned char*)to+1) = *((unsigned char*)from+1) ^ *((unsigned char*)mask+1);
-      case 1: *((unsigned char*)to  ) = *((unsigned char*)from  ) ^ *((unsigned char*)mask);
-      case 0:;
-    }
-    NanReturnValue(NanTrue());
-  }
-};
-
-extern "C" void init (Handle<Object> target)
-{
-  NanScope();
-  BufferUtil::Initialize(target);
-}
-
-NODE_MODULE(bufferutil, init)
-
diff --git a/src/validation.cc b/src/validation.cc
deleted file mode 100644
index 0d9e242..0000000
--- a/src/validation.cc
+++ /dev/null
@@ -1,145 +0,0 @@
-/*!
- * ws: a node.js websocket client
- * Copyright(c) 2011 Einar Otto Stangvik <einaros at gmail.com>
- * MIT Licensed
- */
-
-#include <v8.h>
-#include <node.h>
-#include <node_buffer.h>
-#include <node_object_wrap.h>
-#include <stdlib.h>
-#include <wchar.h>
-#include <stdio.h>
-#include "nan.h"
-
-using namespace v8;
-using namespace node;
-
-#define UNI_SUR_HIGH_START   (uint32_t) 0xD800
-#define UNI_SUR_LOW_END    (uint32_t) 0xDFFF
-#define UNI_REPLACEMENT_CHAR (uint32_t) 0x0000FFFD
-#define UNI_MAX_LEGAL_UTF32  (uint32_t) 0x0010FFFF
-
-static const uint8_t trailingBytesForUTF8[256] = {
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
-  2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5
-};
-
-static const uint32_t offsetsFromUTF8[6] = {
-  0x00000000, 0x00003080, 0x000E2080,
-  0x03C82080, 0xFA082080, 0x82082080
-};
-
-static int isLegalUTF8(const uint8_t *source, const int length)
-{
-  uint8_t a;
-  const uint8_t *srcptr = source+length;
-  switch (length) {
-  default: return 0;
-  /* Everything else falls through when "true"... */
-  /* RFC3629 makes 5 & 6 bytes UTF-8 illegal
-  case 6: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0;
-  case 5: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0; */
-  case 4: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0;
-  case 3: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0;
-  case 2: if ((a = (*--srcptr)) > 0xBF) return 0;
-    switch (*source) {
-      /* no fall-through in this inner switch */
-      case 0xE0: if (a < 0xA0) return 0; break;
-      case 0xED: if (a > 0x9F) return 0; break;
-      case 0xF0: if (a < 0x90) return 0; break;
-      case 0xF4: if (a > 0x8F) return 0; break;
-      default:   if (a < 0x80) return 0;
-    }
-
-  case 1: if (*source >= 0x80 && *source < 0xC2) return 0;
-  }
-  if (*source > 0xF4) return 0;
-  return 1;
-}
-
-int is_valid_utf8 (size_t len, char *value)
-{
-  /* is the string valid UTF-8? */
-  for (unsigned int i = 0; i < len; i++) {
-    uint32_t ch = 0;
-    uint8_t  extrabytes = trailingBytesForUTF8[(uint8_t) value[i]];
-
-    if (extrabytes + i >= len)
-      return 0;
-
-    if (isLegalUTF8 ((uint8_t *) (value + i), extrabytes + 1) == 0) return 0;
-
-    switch (extrabytes) {
-      case 5 : ch += (uint8_t) value[i++]; ch <<= 6;
-      case 4 : ch += (uint8_t) value[i++]; ch <<= 6;
-      case 3 : ch += (uint8_t) value[i++]; ch <<= 6;
-      case 2 : ch += (uint8_t) value[i++]; ch <<= 6;
-      case 1 : ch += (uint8_t) value[i++]; ch <<= 6;
-      case 0 : ch += (uint8_t) value[i];
-    }
-
-    ch -= offsetsFromUTF8[extrabytes];
-
-    if (ch <= UNI_MAX_LEGAL_UTF32) {
-      if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END)
-        return 0;
-    } else {
-      return 0;
-    }
-  }
-
-  return 1;
-}
-
-class Validation : public ObjectWrap
-{
-public:
-
-  static void Initialize(v8::Handle<v8::Object> target)
-  {
-    NanScope();
-    Local<FunctionTemplate> t = NanNew<FunctionTemplate>(New);
-    t->InstanceTemplate()->SetInternalFieldCount(1);
-    NODE_SET_METHOD(t, "isValidUTF8", Validation::IsValidUTF8);
-    target->Set(NanSymbol("Validation"), t->GetFunction());
-  }
-
-protected:
-
-  static NAN_METHOD(New)
-  {
-    NanScope();
-    Validation* validation = new Validation();
-    validation->Wrap(args.This());
-    NanReturnValue(args.This());
-  }
-
-  static NAN_METHOD(IsValidUTF8)
-  {
-    NanScope();
-    if (!Buffer::HasInstance(args[0])) {
-      return NanThrowTypeError("First argument needs to be a buffer");
-    }
-    Local<Object> buffer_obj = args[0]->ToObject();
-    char *buffer_data = Buffer::Data(buffer_obj);
-    size_t buffer_length = Buffer::Length(buffer_obj);
-    NanReturnValue(is_valid_utf8(buffer_length, buffer_data) == 1 ? NanTrue() : NanFalse());
-  }
-};
-
-extern "C" void init (Handle<Object> target)
-{
-  NanScope();
-  Validation::Initialize(target);
-}
-
-NODE_MODULE(validation, init)
-
diff --git a/test/Extensions.test.js b/test/Extensions.test.js
new file mode 100644
index 0000000..84ec5ed
--- /dev/null
+++ b/test/Extensions.test.js
@@ -0,0 +1,53 @@
+var Extensions = require('../lib/Extensions');
+require('should');
+
+describe('Extensions', function() {
+  describe('parse', function() {
+    it('should parse', function() {
+      var extensions = Extensions.parse('foo');
+      extensions.should.eql({ foo: [{}] });
+    });
+
+    it('should parse params', function() {
+      var extensions = Extensions.parse('foo; bar; baz=1; bar=2');
+      extensions.should.eql({
+        foo: [{ bar: [true, '2'], baz: ['1'] }]
+      });
+    });
+
+    it('should parse multiple extensions', function() {
+      var extensions = Extensions.parse('foo, bar; baz, foo; baz');
+      extensions.should.eql({
+        foo: [{}, { baz: [true] }],
+        bar: [{ baz: [true] }]
+      });
+    });
+
+    it('should parse quoted params', function() {
+      var extensions = Extensions.parse('foo; bar="hi"');
+      extensions.should.eql({
+        foo: [{ bar: ['hi'] }]
+      });
+    });
+  });
+
+  describe('format', function() {
+    it('should format', function() {
+      var extensions = Extensions.format({ foo: {} });
+      extensions.should.eql('foo');
+    });
+
+    it('should format params', function() {
+      var extensions = Extensions.format({ foo: { bar: [true, 2], baz: 1 } });
+      extensions.should.eql('foo; bar; bar=2; baz=1');
+    });
+
+    it('should format multiple extensions', function() {
+      var extensions = Extensions.format({
+        foo: [{}, { baz: true }],
+        bar: { baz: true }
+      });
+      extensions.should.eql('foo, foo; baz, bar; baz');
+    });
+  });
+});
diff --git a/test/PerMessageDeflate.test.js b/test/PerMessageDeflate.test.js
new file mode 100644
index 0000000..81eceec
--- /dev/null
+++ b/test/PerMessageDeflate.test.js
@@ -0,0 +1,267 @@
+var PerMessageDeflate = require('../lib/PerMessageDeflate');
+var Extensions = require('../lib/Extensions');
+require('should');
+
+describe('PerMessageDeflate', function() {
+  describe('#offer', function() {
+    it('should create default params', function() {
+      var perMessageDeflate = new PerMessageDeflate();
+      perMessageDeflate.offer().should.eql({ client_max_window_bits: true });
+    });
+
+    it('should create params from options', function() {
+      var perMessageDeflate = new PerMessageDeflate({
+        serverNoContextTakeover: true,
+        clientNoContextTakeover: true,
+        serverMaxWindowBits: 10,
+        clientMaxWindowBits: 11
+      });
+      perMessageDeflate.offer().should.eql({
+        server_no_context_takeover: true,
+        client_no_context_takeover: true,
+        server_max_window_bits: 10,
+        client_max_window_bits: 11
+      });
+    });
+  });
+
+  describe('#accept', function() {
+    describe('as server', function() {
+      it('should accept empty offer', function() {
+        var perMessageDeflate = new PerMessageDeflate({}, true);
+        perMessageDeflate.accept([{}]).should.eql({});
+      });
+
+      it('should accept offer', function() {
+        var perMessageDeflate = new PerMessageDeflate({}, true);
+        var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11');
+        perMessageDeflate.accept(extensions['permessage-deflate']).should.eql({
+          server_no_context_takeover: true,
+          client_no_context_takeover: true,
+          server_max_window_bits: 10,
+          client_max_window_bits: 11
+        });
+      });
+
+      it('should prefer configuration than offer', function() {
+        var perMessageDeflate = new PerMessageDeflate({
+          serverNoContextTakeover: true,
+          clientNoContextTakeover: true,
+          serverMaxWindowBits: 12,
+          clientMaxWindowBits: 11
+        }, true);
+        var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=14; client_max_window_bits=13');
+        perMessageDeflate.accept(extensions['permessage-deflate']).should.eql({
+          server_no_context_takeover: true,
+          client_no_context_takeover: true,
+          server_max_window_bits: 12,
+          client_max_window_bits: 11
+        });
+      });
+
+      it('should fallback', function() {
+        var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true);
+        var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10, permessage-deflate');
+        perMessageDeflate.accept(extensions['permessage-deflate']).should.eql({
+          server_max_window_bits: 11
+        });
+      });
+
+      it('should throw an error if server_no_context_takeover is unsupported', function() {
+        var perMessageDeflate = new PerMessageDeflate({ serverNoContextTakeover: false }, true);
+        var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if server_max_window_bits is unsupported', function() {
+        var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: false }, true);
+        var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if server_max_window_bits is less than configuration', function() {
+        var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true);
+        var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if client_max_window_bits is unsupported on client', function() {
+        var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }, true);
+        var extensions = Extensions.parse('permessage-deflate');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+    });
+
+    describe('as client', function() {
+      it('should accept empty response', function() {
+        var perMessageDeflate = new PerMessageDeflate({});
+        perMessageDeflate.accept([{}]).should.eql({});
+      });
+
+      it('should accept response parameter', function() {
+        var perMessageDeflate = new PerMessageDeflate({});
+        var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11');
+        perMessageDeflate.accept(extensions['permessage-deflate']).should.eql({
+          server_no_context_takeover: true,
+          client_no_context_takeover: true,
+          server_max_window_bits: 10,
+          client_max_window_bits: 11
+        });
+      });
+
+      it('should throw an error if client_no_context_takeover is unsupported', function() {
+        var perMessageDeflate = new PerMessageDeflate({ clientNoContextTakeover: false });
+        var extensions = Extensions.parse('permessage-deflate; client_no_context_takeover');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if client_max_window_bits is unsupported', function() {
+        var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: false });
+        var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=10');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if client_max_window_bits is greater than configuration', function() {
+        var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 });
+        var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=11');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+    });
+
+    describe('validate parameters', function() {
+      it('should throw an error if a parameter has multiple values', function() {
+        var perMessageDeflate = new PerMessageDeflate();
+        var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; server_no_context_takeover');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if a parameter is undefined', function() {
+        var perMessageDeflate = new PerMessageDeflate();
+        var extensions = Extensions.parse('permessage-deflate; foo;');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if server_no_context_takeover has a value', function() {
+        var perMessageDeflate = new PerMessageDeflate();
+        var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover=10');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if client_no_context_takeover has a value', function() {
+        var perMessageDeflate = new PerMessageDeflate();
+        var extensions = Extensions.parse('permessage-deflate; client_no_context_takeover=10');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if server_max_window_bits has an invalid value', function() {
+        var perMessageDeflate = new PerMessageDeflate();
+        var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=7');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+
+      it('should throw an error if client_max_window_bits has an invalid value', function() {
+        var perMessageDeflate = new PerMessageDeflate();
+        var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=16');
+        (function() {
+          perMessageDeflate.accept(extensions['permessage-deflate']);
+        }).should.throw();
+      });
+    });
+  });
+
+  describe('#compress/#decompress', function() {
+    it('should compress/decompress data', function(done) {
+      var perMessageDeflate = new PerMessageDeflate();
+      perMessageDeflate.accept([{}]);
+      perMessageDeflate.compress(new Buffer([1, 2, 3]), true, function(err, compressed) {
+        if (err) return done(err);
+        perMessageDeflate.decompress(compressed, true, function(err, data) {
+          if (err) return done(err);
+          data.should.eql(new Buffer([1, 2, 3]));
+          done();
+        });
+      });
+    });
+
+    it('should compress/decompress fragments', function(done) {
+      var perMessageDeflate = new PerMessageDeflate();
+      perMessageDeflate.accept([{}]);
+
+      var buf = new Buffer([1, 2, 3, 4]);
+      perMessageDeflate.compress(buf.slice(0, 2), false, function(err, compressed1) {
+        if (err) return done(err);
+        perMessageDeflate.compress(buf.slice(2), true, function(err, compressed2) {
+          if (err) return done(err);
+          perMessageDeflate.decompress(compressed1, false, function(err, data1) {
+            if (err) return done(err);
+            perMessageDeflate.decompress(compressed2, true, function(err, data2) {
+              if (err) return done(err);
+              new Buffer.concat([data1, data2]).should.eql(new Buffer([1, 2, 3, 4]));
+              done();
+            });
+          });
+        });
+      });
+    });
+
+    it('should compress/decompress data with parameters', function(done) {
+      var perMessageDeflate = new PerMessageDeflate({ memLevel: 5 });
+      var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11');
+      perMessageDeflate.accept(extensions['permessage-deflate']);
+      perMessageDeflate.compress(new Buffer([1, 2, 3]), true, function(err, compressed) {
+        if (err) return done(err);
+        perMessageDeflate.decompress(compressed, true, function(err, data) {
+          if (err) return done(err);
+          data.should.eql(new Buffer([1, 2, 3]));
+          done();
+        });
+      });
+    });
+
+    it('should compress/decompress data with no context takeover', function(done) {
+      var perMessageDeflate = new PerMessageDeflate();
+      var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover');
+      perMessageDeflate.accept(extensions['permessage-deflate']);
+      var buf = new Buffer('foofoo');
+      perMessageDeflate.compress(buf, true, function(err, compressed1) {
+        if (err) return done(err);
+        perMessageDeflate.decompress(compressed1, true, function(err, data) {
+          if (err) return done(err);
+          perMessageDeflate.compress(data, true, function(err, compressed2) {
+            if (err) return done(err);
+            perMessageDeflate.decompress(compressed2, true, function(err, data) {
+              if (err) return done(err);
+              compressed2.length.should.equal(compressed1.length);
+              data.should.eql(buf);
+              done();
+            });
+          });
+        });
+      });
+    });
+  });
+});
diff --git a/test/Receiver.test.js b/test/Receiver.test.js
index b0b5c0a..2c34d1e 100644
--- a/test/Receiver.test.js
+++ b/test/Receiver.test.js
@@ -1,5 +1,6 @@
 var assert = require('assert')
-  , Receiver = require('../lib/Receiver');
+  , Receiver = require('../lib/Receiver')
+  , PerMessageDeflate = require('../lib/PerMessageDeflate');
 require('should');
 require('./hybi-common');
 
@@ -251,5 +252,64 @@ describe('Receiver', function() {
     p.add(getBufferFromHexString(packet));
     gotData.should.be.ok;
   });
+  it('can parse compressed message', function(done) {
+    var perMessageDeflate = new PerMessageDeflate();
+    perMessageDeflate.accept([{}]);
+
+    var p = new Receiver({ 'permessage-deflate': perMessageDeflate });
+    var buf = new Buffer('Hello');
+
+    p.ontext = function(data) {
+      assert.equal('Hello', data);
+      done();
+    };
+
+    perMessageDeflate.compress(buf, true, function(err, compressed) {
+      if (err) return done(err);
+      p.add(new Buffer([0xc1, compressed.length]));
+      p.add(compressed);
+    });
+  });
+  it('can parse compressed fragments', function(done) {
+    var perMessageDeflate = new PerMessageDeflate();
+    perMessageDeflate.accept([{}]);
+
+    var p = new Receiver({ 'permessage-deflate': perMessageDeflate });
+    var buf1 = new Buffer('foo');
+    var buf2 = new Buffer('bar');
+
+    p.ontext = function(data) {
+      assert.equal('foobar', data);
+      done();
+    };
+
+    perMessageDeflate.compress(buf1, false, function(err, compressed1) {
+      if (err) return done(err);
+      p.add(new Buffer([0x41, compressed1.length]));
+      p.add(compressed1);
+
+      perMessageDeflate.compress(buf2, true, function(err, compressed2) {
+        p.add(new Buffer([0x80, compressed2.length]));
+        p.add(compressed2);
+      });
+    });
+  });
+  it('can cleanup during consuming data', function(done) {
+    var perMessageDeflate = new PerMessageDeflate();
+    perMessageDeflate.accept([{}]);
+
+    var p = new Receiver({ 'permessage-deflate': perMessageDeflate });
+    var buf = new Buffer('Hello');
+
+    perMessageDeflate.compress(buf, true, function(err, compressed) {
+      if (err) return done(err);
+      var data = Buffer.concat([new Buffer([0xc1, compressed.length]), compressed]);
+      p.add(data);
+      p.add(data);
+      p.add(data);
+      p.cleanup();
+      setTimeout(done, 1000);
+    });
+  });
 });
 
diff --git a/test/Sender.test.js b/test/Sender.test.js
index 43b4864..d70a111 100644
--- a/test/Sender.test.js
+++ b/test/Sender.test.js
@@ -1,4 +1,5 @@
-var Sender = require('../lib/Sender');
+var Sender = require('../lib/Sender')
+  , PerMessageDeflate = require('../lib/PerMessageDeflate');
 require('should');
 
 describe('Sender', function() {
@@ -20,5 +21,55 @@ describe('Sender', function() {
       sender.frameAndSend(1, text, true, true);
       text.should.eql('hi there');
     });
+
+    it('sets rsv1 flag if compressed', function(done) {
+      var sender = new Sender({
+        write: function(data) {
+          (data[0] & 0x40).should.equal(0x40);
+          done();
+        }
+      });
+      sender.frameAndSend(1, 'hi', true, false, true);
+    });
+  });
+
+  describe('#send', function() {
+    it('compresses data if compress option is enabled', function(done) {
+      var perMessageDeflate = new PerMessageDeflate();
+      perMessageDeflate.accept([{}]);
+
+      var sender = new Sender({
+        write: function(data) {
+          (data[0] & 0x40).should.equal(0x40);
+          done();
+        }
+      }, {
+        'permessage-deflate': perMessageDeflate
+      });
+      sender.send('hi', { compress: true });
+    });
+  });
+
+  describe('#close', function() {
+    it('should consume all data before closing', function(done) {
+      var perMessageDeflate = new PerMessageDeflate();
+      perMessageDeflate.accept([{}]);
+
+      var count = 0;
+      var sender = new Sender({
+        write: function(data) {
+          count++;
+        }
+      }, {
+        'permessage-deflate': perMessageDeflate
+      });
+      sender.send('foo', {compress: true});
+      sender.send('bar', {compress: true});
+      sender.send('baz', {compress: true});
+      sender.close(1000, null, false, function(err) {
+        count.should.be.equal(4);
+        done(err);
+      });
+    });
   });
 });
diff --git a/test/WebSocket.test.js b/test/WebSocket.test.js
index f5598cb..c3ffbb0 100644
--- a/test/WebSocket.test.js
+++ b/test/WebSocket.test.js
@@ -70,7 +70,7 @@ describe('WebSocket', function() {
   describe('properties', function() {
     it('#bytesReceived exposes number of bytes received', function(done) {
       var wss = new WebSocketServer({port: ++port}, function() {
-        var ws = new WebSocket('ws://localhost:' + port);
+        var ws = new WebSocket('ws://localhost:' + port, { perMessageDeflate: false });
         ws.on('message', function() {
           ws.bytesReceived.should.eql(8);
           wss.close();
@@ -139,7 +139,7 @@ describe('WebSocket', function() {
 
       it('stress kernel write buffer', function(done) {
         var wss = new WebSocketServer({port: ++port}, function() {
-          var ws = new WebSocket('ws://localhost:' + port);
+          var ws = new WebSocket('ws://localhost:' + port, { perMessageDeflate: false });
         });
         wss.on('connection', function(ws) {
           while (true) {
@@ -1425,6 +1425,28 @@ describe('WebSocket', function() {
         });
       });
     });
+
+    it('consumes all data when the server socket closed', function(done) {
+      var wss = new WebSocketServer({port: ++port}, function() {
+        wss.on('connection', function(conn) {
+          conn.send('foo');
+          conn.send('bar');
+          conn.send('baz');
+          conn.close();
+        });
+        var ws = new WebSocket('ws://localhost:' + port);
+        var messages = [];
+        ws.on('message', function (message) {
+          messages.push(message);
+          if (messages.length === 3) {
+            assert.deepEqual(messages, ['foo', 'bar', 'baz']);
+            wss.close();
+            ws.terminate();
+            done();
+          }
+        });
+      });
+    });
   });
 
   describe('W3C API emulation', function() {
@@ -1770,4 +1792,197 @@ describe('WebSocket', function() {
     });
   });
 
+  describe('permessage-deflate', function() {
+    it('is enabled by default', function(done) {
+      var srv = http.createServer(function (req, res) {});
+      var wss = new WebSocketServer({server: srv, perMessageDeflate: true});
+      srv.listen(++port, function() {
+        var ws = new WebSocket('ws://localhost:' + port);
+        srv.on('upgrade', function(req, socket, head) {
+          assert.ok(~req.headers['sec-websocket-extensions'].indexOf('permessage-deflate'));
+        });
+        ws.on('open', function() {
+          assert.ok(ws.extensions['permessage-deflate']);
+          ws.terminate();
+          wss.close();
+          done();
+        });
+      });
+    });
+
+    it('can be disabled', function(done) {
+      var srv = http.createServer(function (req, res) {});
+      var wss = new WebSocketServer({server: srv, perMessageDeflate: true});
+      srv.listen(++port, function() {
+        var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: false});
+        srv.on('upgrade', function(req, socket, head) {
+          assert.ok(!req.headers['sec-websocket-extensions']);
+          ws.terminate();
+          wss.close();
+          done();
+        });
+      });
+    });
+
+    it('can send extension parameters', function(done) {
+      var srv = http.createServer(function (req, res) {});
+      var wss = new WebSocketServer({server: srv, perMessageDeflate: true});
+      srv.listen(++port, function() {
+        var ws = new WebSocket('ws://localhost:' + port, {
+          perMessageDeflate: {
+            serverNoContextTakeover: true,
+            clientNoContextTakeover: true,
+            serverMaxWindowBits: 10,
+            clientMaxWindowBits: true
+          }
+        });
+        srv.on('upgrade', function(req, socket, head) {
+          var extensions = req.headers['sec-websocket-extensions'];
+          assert.ok(~extensions.indexOf('permessage-deflate'));
+          assert.ok(~extensions.indexOf('server_no_context_takeover'));
+          assert.ok(~extensions.indexOf('client_no_context_takeover'));
+          assert.ok(~extensions.indexOf('server_max_window_bits=10'));
+          assert.ok(~extensions.indexOf('client_max_window_bits'));
+          ws.terminate();
+          wss.close();
+          done();
+        });
+      });
+    });
+
+    it('can send and receive text data', function(done) {
+      var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() {
+        var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true});
+        ws.on('open', function() {
+          ws.send('hi', {compress: true});
+        });
+        ws.on('message', function(message, flags) {
+          assert.equal('hi', message);
+          ws.terminate();
+          wss.close();
+          done();
+        });
+      });
+      wss.on('connection', function(ws) {
+        ws.on('message', function(message, flags) {
+          ws.send(message, {compress: true});
+        });
+      });
+    });
+
+    it('with binary stream will send fragmented data', function(done) {
+      var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() {
+        var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true});
+        var callbackFired = false;
+        ws.on('open', function() {
+          var fileStream = fs.createReadStream('test/fixtures/textfile');
+          fileStream.bufferSize = 100;
+          ws.send(fileStream, {binary: true, compress: true}, function(error) {
+            assert.equal(null, error);
+            callbackFired = true;
+          });
+        });
+        ws.on('close', function() {
+          assert.ok(callbackFired);
+          wss.close();
+          done();
+        });
+      });
+      wss.on('connection', function(ws) {
+        ws.on('message', function(data, flags) {
+          assert.ok(flags.binary);
+          assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile'), data));
+          ws.terminate();
+        });
+      });
+    });
+
+    describe('#send', function() {
+      it('can set the compress option true when perMessageDeflate is disabled', function(done) {
+        var wss = new WebSocketServer({port: ++port}, function() {
+          var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: false});
+          ws.on('open', function() {
+            ws.send('hi', {compress: true});
+          });
+          ws.on('message', function(message, flags) {
+            assert.equal('hi', message);
+            ws.terminate();
+            wss.close();
+            done();
+          });
+        });
+        wss.on('connection', function(ws) {
+          ws.on('message', function(message, flags) {
+            ws.send(message, {compress: true});
+          });
+        });
+      });
+    });
+
+    describe('#close', function() {
+      it('should not raise error callback, if any, if called during send data', function(done) {
+        var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() {
+          var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true});
+          var errorGiven = false;
+          ws.on('open', function() {
+            ws.send('hi', function(error) {
+              errorGiven = error != null;
+            });
+            ws.close();
+          });
+          ws.on('close', function() {
+            setTimeout(function() {
+              assert.ok(!errorGiven);
+              wss.close();
+              ws.terminate();
+              done();
+            }, 1000);
+          });
+        });
+      });
+    });
+
+    describe('#terminate', function() {
+      it('will raise error callback, if any, if called during send data', function(done) {
+        var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() {
+          var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true});
+          var errorGiven = false;
+          ws.on('open', function() {
+            ws.send('hi', function(error) {
+              errorGiven = error != null;
+            });
+            ws.terminate();
+          });
+          ws.on('close', function() {
+            setTimeout(function() {
+              assert.ok(errorGiven);
+              wss.close();
+              ws.terminate();
+              done();
+            }, 1000);
+          });
+        });
+      });
+
+      it('can call during receiving data', function(done) {
+        var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() {
+          var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true});
+          wss.on('connection', function(client) {
+            for (var i = 0; i < 10; i++) {
+              client.send('hi');
+            }
+            client.send('hi', function() {
+              ws.terminate();
+            });
+          });
+          ws.on('close', function() {
+            setTimeout(function() {
+              wss.close();
+              done();
+            }, 1000);
+          });
+        });
+      });
+    });
+  });
 });
diff --git a/test/WebSocketServer.test.js b/test/WebSocketServer.test.js
index 0395286..7227a18 100644
--- a/test/WebSocketServer.test.js
+++ b/test/WebSocketServer.test.js
@@ -60,8 +60,12 @@ describe('WebSocketServer', function() {
     });
 
     it('emits an error if http server bind fails', function(done) {
-      var wss = new WebSocketServer({port: 1});
-      wss.on('error', function() { done(); });
+      var wss1 = new WebSocketServer({port: 50003});
+      var wss2 = new WebSocketServer({port: 50003});
+      wss2.on('error', function() {
+        wss1.close();
+        done();
+      });
     });
 
     it('starts a server on a given port', function(done) {
@@ -88,20 +92,23 @@ describe('WebSocketServer', function() {
       });
     });
 
-    it('uses a precreated http server listening on unix socket', function (done) {
-      var srv = http.createServer();
-      var sockPath = '/tmp/ws_socket_'+new Date().getTime()+'.'+Math.floor(Math.random() * 1000);
-      srv.listen(sockPath, function () {
-        var wss = new WebSocketServer({server: srv});
-        var ws = new WebSocket('ws+unix://'+sockPath);
+    // Don't test this on Windows. It throws errors for obvious reasons.
+    if(!/^win/i.test(process.platform)) {
+      it('uses a precreated http server listening on unix socket', function (done) {
+        var srv = http.createServer();
+        var sockPath = '/tmp/ws_socket_'+new Date().getTime()+'.'+Math.floor(Math.random() * 1000);
+        srv.listen(sockPath, function () {
+          var wss = new WebSocketServer({server: srv});
+          var ws = new WebSocket('ws+unix://'+sockPath);
 
-        wss.on('connection', function(client) {
-          wss.close();
-          srv.close();
-          done();
+          wss.on('connection', function(client) {
+            wss.close();
+            srv.close();
+            done();
+          });
         });
       });
-    });
+    }
 
     it('emits path specific connection event', function (done) {
       var srv = http.createServer();
@@ -679,7 +686,7 @@ describe('WebSocketServer', function() {
 
       it('selects the first protocol by default', function(done) {
         var wss = new WebSocketServer({port: ++port}, function() {
-          var ws = new WebSocket('ws://localhost:' + port, {protocol: 'prot1, prot2'});
+          var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']);
           ws.on('open', function(client) {
               ws.protocol.should.eql('prot1');
               wss.close();
@@ -691,7 +698,7 @@ describe('WebSocketServer', function() {
       it('selects the last protocol via protocol handler', function(done) {
         var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) {
             cb(true, ps[ps.length-1]); }}, function() {
-          var ws = new WebSocket('ws://localhost:' + port, {protocol: 'prot1, prot2'});
+          var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']);
           ws.on('open', function(client) {
               ws.protocol.should.eql('prot2');
               wss.close();
@@ -703,7 +710,7 @@ describe('WebSocketServer', function() {
       it('client detects invalid server protocol', function(done) {
         var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) {
             cb(true, 'prot3'); }}, function() {
-          var ws = new WebSocket('ws://localhost:' + port, {protocol: 'prot1, prot2'});
+          var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']);
           ws.on('open', function(client) {
               done(new Error('connection must not be established'));
           });
@@ -716,7 +723,7 @@ describe('WebSocketServer', function() {
       it('client detects no server protocol', function(done) {
         var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) {
             cb(true); }}, function() {
-          var ws = new WebSocket('ws://localhost:' + port, {protocol: 'prot1, prot2'});
+          var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']);
           ws.on('open', function(client) {
               done(new Error('connection must not be established'));
           });
@@ -729,7 +736,7 @@ describe('WebSocketServer', function() {
       it('client refuses server protocols', function(done) {
         var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) {
             cb(false); }}, function() {
-          var ws = new WebSocket('ws://localhost:' + port, {protocol: 'prot1, prot2'});
+          var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']);
           ws.on('open', function(client) {
               done(new Error('connection must not be established'));
           });
@@ -739,6 +746,36 @@ describe('WebSocketServer', function() {
         });
       });
 
+      it('server detects unauthorized protocol handler', function(done) {
+        var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) {
+          cb(false);
+        }}, function() {
+          var options = {
+            port: port,
+            host: '127.0.0.1',
+            headers: {
+              'Connection': 'Upgrade',
+              'Upgrade': 'websocket',
+              'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+              'Sec-WebSocket-Version': 13,
+              'Sec-WebSocket-Origin': 'http://foobar.com'
+            }
+          };
+          options.port = port;
+          var req = http.request(options);
+          req.end();
+          req.on('response', function(res) {
+            res.statusCode.should.eql(401);
+            wss.close();
+            done();
+          });
+        });
+        wss.on('connection', function(ws) {
+          done(new Error('connection must not be established'));
+        });
+        wss.on('error', function() {});
+      });
+
       it('server detects invalid protocol handler', function(done) {
         var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) {
             // not calling callback is an error and shouldn't timeout
@@ -768,6 +805,30 @@ describe('WebSocketServer', function() {
         });
         wss.on('error', function() {});
       });
+
+      it('accept connections with sec-websocket-extensions', function(done) {
+        var wss = new WebSocketServer({port: ++port}, function() {
+          var options = {
+            port: port,
+            host: '127.0.0.1',
+            headers: {
+              'Connection': 'Upgrade',
+              'Upgrade': 'websocket',
+              'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+              'Sec-WebSocket-Version': 13,
+              'Sec-WebSocket-Extensions': 'permessage-foo; x=10'
+            }
+          };
+          var req = http.request(options);
+          req.end();
+        });
+        wss.on('connection', function(ws) {
+          ws.terminate();
+          wss.close();
+          done();
+        });
+        wss.on('error', function() {});
+      });
     });
 
     describe('messaging', function() {
@@ -1143,7 +1204,7 @@ describe('WebSocketServer', function() {
   describe('client properties', function() {
     it('protocol is exposed', function(done) {
       var wss = new WebSocketServer({port: ++port}, function() {
-        var ws = new WebSocket('ws://localhost:' + port, {protocol: 'hi'});
+        var ws = new WebSocket('ws://localhost:' + port, 'hi');
       });
       wss.on('connection', function(client) {
         client.protocol.should.eql('hi');
@@ -1175,4 +1236,83 @@ describe('WebSocketServer', function() {
     });
   });
 
+  describe('permessage-deflate', function() {
+    it('accept connections with permessage-deflate extension', function(done) {
+      var wss = new WebSocketServer({port: ++port}, function() {
+        var options = {
+          port: port,
+          host: '127.0.0.1',
+          headers: {
+            'Connection': 'Upgrade',
+            'Upgrade': 'websocket',
+            'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+            'Sec-WebSocket-Version': 13,
+            'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits=8; server_max_window_bits=8; client_no_context_takeover; server_no_context_takeover'
+          }
+        };
+        var req = http.request(options);
+        req.end();
+      });
+      wss.on('connection', function(ws) {
+        ws.terminate();
+        wss.close();
+        done();
+      });
+      wss.on('error', function() {});
+    });
+
+    it('does not accept connections with not defined extension parameter', function(done) {
+      var wss = new WebSocketServer({port: ++port}, function() {
+        var options = {
+          port: port,
+          host: '127.0.0.1',
+          headers: {
+            'Connection': 'Upgrade',
+            'Upgrade': 'websocket',
+            'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+            'Sec-WebSocket-Version': 13,
+            'Sec-WebSocket-Extensions': 'permessage-deflate; foo=15'
+          }
+        };
+        var req = http.request(options);
+        req.end();
+        req.on('response', function(res) {
+          res.statusCode.should.eql(400);
+          wss.close();
+          done();
+        });
+      });
+      wss.on('connection', function(ws) {
+        done(new Error('connection must not be established'));
+      });
+      wss.on('error', function() {});
+    });
+
+    it('does not accept connections with invalid extension parameter', function(done) {
+      var wss = new WebSocketServer({port: ++port}, function() {
+        var options = {
+          port: port,
+          host: '127.0.0.1',
+          headers: {
+            'Connection': 'Upgrade',
+            'Upgrade': 'websocket',
+            'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+            'Sec-WebSocket-Version': 13,
+            'Sec-WebSocket-Extensions': 'permessage-deflate; server_max_window_bits=foo'
+          }
+        };
+        var req = http.request(options);
+        req.end();
+        req.on('response', function(res) {
+          res.statusCode.should.eql(400);
+          wss.close();
+          done();
+        });
+      });
+      wss.on('connection', function(ws) {
+        done(new Error('connection must not be established'));
+      });
+      wss.on('error', function() {});
+    });
+  });
 });
diff --git a/test/testserver.js b/test/testserver.js
index b83f61f..e17cbb8 100644
--- a/test/testserver.js
+++ b/test/testserver.js
@@ -85,7 +85,10 @@ function validServer(server, req, socket) {
   };
   receiver.onclose = function (code, message, flags) {
     flags = flags || {};
-    server.emit('close', code, message, flags);
+    sender.close(code, message, false, function(err) {
+      server.emit('close', code, message, flags);
+      socket.end();
+    });
   };
   socket.on('data', function (data) {
     receiver.add(data);

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-ws.git



More information about the Pkg-javascript-commits mailing list