[Pkg-javascript-commits] [node-yawl] 01/02: Imported Upstream version 1.0.2

Andrew Kelley andrewrk-guest at moszumanska.debian.org
Sat May 16 20:42:11 UTC 2015


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

andrewrk-guest pushed a commit to branch master
in repository node-yawl.

commit 8d1da3669c18575a646c6340eebee4c2721531d0
Author: Andrew Kelley <superjoe30 at gmail.com>
Date:   Sat May 16 20:35:39 2015 +0000

    Imported Upstream version 1.0.2
---
 .gitignore              |   2 +
 .travis.yml             |   7 +
 CHANGELOG.md            |  14 +
 LICENSE                 |  23 ++
 README.md               | 561 +++++++++++++++++++++++++++++
 index.js                | 935 ++++++++++++++++++++++++++++++++++++++++++++++++
 package.json            |  49 +++
 test/autobahn-client.js |  80 +++++
 test/autobahn-server.js |  30 ++
 test/perf.js            | 337 +++++++++++++++++
 test/test.js            | 647 +++++++++++++++++++++++++++++++++
 11 files changed, 2685 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7bbe1a3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/node_modules
+/coverage
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..77b7202
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+  - "0.10"
+script:
+  - "npm run test-travis"
+after_script:
+  - "npm install coveralls at 2 && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls"
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d31df08
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,14 @@
+### Version 1.0.2 (2015-02-18)
+
+ * fix compatibility with node v0.12
+
+### Version 1.0.1 (2014-12-01)
+
+ * `createClient` respects the `origin` option.
+ * add faye to other libraries in performance testing.
+ * better error handling when server does not perform websocket upgrade.
+ * avoid invalid state if `close` throws due to too long status message.
+
+### Version 1.0.0 (2014-11-25)
+
+Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0bbb53e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+The MIT License (Expat)
+
+Copyright (c) 2014 Andrew Kelley
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation files
+(the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4e86e5f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,561 @@
+# node-yawl
+
+[![Build Status](https://travis-ci.org/andrewrk/node-yawl.svg?branch=master)](https://travis-ci.org/andrewrk/node-yawl)
+[![Coverage Status](https://img.shields.io/coveralls/andrewrk/node-yawl.svg)](https://coveralls.io/r/andrewrk/node-yawl)
+
+Yet Another WebSocket Library - WebSocket server and client for Node.js
+
+# Features
+
+ * Almost [RFC 6455](https://tools.ietf.org/html/rfc6455) compliant. Exceptions:
+   - Uses Node.js's built-in UTF-8 decoding which ignores errors. The spec says
+     to close the connection when invalid UTF-8 is encountered. Instead this
+     module will silently ignore decoding errors just like the rest of your
+     Node.js code.
+   - "payload length" field is limited to `2^52` instead of `2^64`. JavaScript
+     numbers are all 64-bit double precision floating point which have a 52-bit
+     significand precision.
+ * Uses streams and handles backpressure correctly.
+ * Low level without sacrificing clean abstractions.
+ * [Secure by default](https://en.wikipedia.org/wiki/Secure_by_default),
+   [secure by design](https://en.wikipedia.org/wiki/Secure_by_design)
+ * JavaScript implementation. No compiler required.
+ * As performant as a pure JavaScript implementation is going to get. See the
+   performance section below for details.
+ * Built for Node.js only. No hacky code to make it also work in the browser.
+
+## Server Usage
+
+```js
+var yawl = require('yawl');
+var http = require('http');
+var server = http.createServer();
+var wss = yawl.createServer({
+  server: server,
+  origin: null,
+  allowTextMessages: true,
+});
+wss.on('connection', function(ws) {
+  ws.sendText('message');
+  ws.on('textMessage', function(message) {
+    console.log(message);
+  });
+});
+server.listen(port, host, function() {
+  log.info("Listening at " + protocol + "://" + host + ":" + port + "/");
+});
+```
+
+## Client Usage
+
+```js
+var yawl = require('yawl');
+var url = require('url');
+
+var options = url.parse("wss://example.com/path?query=1");
+options.extraHeaders = {
+  'User-Agent': "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:33.0) Gecko/20100101 Firefox/33.0"
+};
+options.allowTextMessages = true;
+// any options allowed in https.request also allowed here.
+
+var ws = yawl.createClient(options);
+ws.on('open', function() {
+  ws.sendText("hi");
+  fs.createReadStream("foo.txt").pipe(ws.sendStream());
+});
+ws.on('textMessage', function(message) {
+  console.log(message);
+});
+```
+
+## API Documentation
+
+### yawl.createServer(options)
+
+Creates a `WebSocketServer` instance.
+
+`options`:
+
+ * `server` - an instance of `https.Server` or `http.Server`. Required.
+ * `origin` - see `setOrigin` below
+ * `negotiate` (optional) - see `setNegotiate` below.
+ * `allowTextMessages` (optional) - see `setAllowTextMessages` below.
+ * `allowBinaryMessages` (optional) - see `setAllowBinaryMessages` below.
+ * `allowFragmentedMessages` (optional) - see `setAllowFragmentedMessages` below.
+ * `allowUnfragmentedMessages` (optional) - see `setAllowUnfragmentedMessages` below.
+ * `maxFrameSize` (optional) - See `setMaxFrameSize` below.
+
+### yawl.createClient(options)
+
+Creates a `WebSocketClient` instance.
+
+`options`:
+
+ * everything that
+   [https.request](http://nodejs.org/docs/latest/api/https.html#https_https_request_options_callback)
+   accepts. This allows you to do things such as connect to UNIX domain sockets
+   rather than ports, use SSL, etc.
+ * `extraHeaders` (optional) - `Object` of extra headers to include in the
+   upgrade request.
+ * `origin` (optional) - Sets the `Origin` header. Same as including an
+   `Origin` property in `extraHeaders`.
+ * `allowTextMessages` (optional) - See `setAllowTextMessages` below.
+ * `allowBinaryMessages` (optional) - See `setAllowBinaryMessages` below.
+ * `allowFragmentedMessages` (optional) - See `setAllowFragmentedMessages` below.
+ * `allowUnfragmentedMessages` (optional) - see `setAllowUnfragmentedMessages` below.
+ * `maxFrameSize` (optional) - See `setMaxFrameSize` below.
+
+Consider using code like this with `createClient`:
+
+```js
+var url = require('url');
+// use url.parse to create the options object
+var options = url.parse("ws://example.com/path?query=1");
+// now set more options
+options.allowTextMessages = true;
+// now create the client
+var ws = yawl.createClient(options);
+// ...
+```
+
+### yawl.parseSubProtocolList(request)
+
+Parses `request.headers['sec-websocket-protocol']` and returns an array of
+lowercase strings.
+
+Example:
+
+```
+...
+Sec-WebSocket-Protocol: chat, SuperChat
+...
+```
+
+Yields:
+
+```js
+['chat', 'superchat']
+```
+
+### yawl.parseExtensionList(request)
+
+Parses `request.headers['sec-websocket-extensions']` and returns an array of
+objects. If the header is invalid according to
+[RFC6455 9.1](https://tools.ietf.org/html/rfc6455#section-9.1) then an error
+is thrown.
+
+Example:
+
+```
+...
+Sec-WebSocket-Extensions: foo,bar; baz=2;extra, third; arg="quoted"
+...
+```
+
+Yields:
+
+```js
+[
+  {
+    name: 'foo',
+    params: [],
+  },
+  {
+    name: 'bar',
+    params: [
+      {
+        name: 'baz',
+        value: '2',
+      },
+      {
+        name: 'extra',
+        value: null,
+      },
+    ],
+  },
+  {
+    name: 'third',
+    params: [
+      {
+        name: 'arg',
+        value: 'quoted',
+      },
+    ],
+  },
+]
+```
+
+### yawl.WebSocketServer
+
+#### wss.setOrigin(value)
+
+`String` or `null`. Set to `null` to disable origin validation.
+To activate origin validation, set to a string such as:
+
+`https://example.com` or `http://example.com:1234`
+
+#### wss.setNegotiate(value)
+
+`Boolean`. Set to `true` to enable upgrade header negotiation with clients.
+Defaults to `false`. If you set this to `true`, you must listen to the
+`negotiate` event (see below).
+
+#### wss.setAllowTextMessages(value)
+
+`Boolean`. Set to `true` to allow UTF-8 encoded text messages. Defaults to
+`false`.
+
+#### wss.setAllowBinaryMessages(value)
+
+`Boolean`. Set to `true` to allow binary messages. Defaults to `false`.
+
+#### wss.setAllowFragmentedMessages(value)
+
+`Boolean`. Set to `true` to allow fragmented messages, that is, messages for
+which you do not know the total size until the message is completely sent.
+Defaults to `false`.
+
+If you set this to `true` be sure to handle the `streamMessage` event. Even
+if you are not interested in a particular message you must consume the stream.
+
+#### wss.setAllowUnfragmentedMessages(value)
+
+`Boolean`. Set to `false` to disallow unfragmented messages. Defaults to `true`.
+
+If you set this to `true` this will prevent `textMessage` and `binaryMessage`
+events from firing.
+
+You might consider instead of this, setting `maxFrameSize` to `Infinity`. This
+will have the effect of causing fragmented messages emit as `streamMessage`,
+with the `length` parameter set.
+
+#### wss.setMaxFrameSize(value)
+
+`Number`. Maximum number of bytes acceptable for non-fragmented messages.
+Defaults to 8MB.
+
+If a client attempts to transmit a larger message, the connection is closed
+according to the specification. Valid messages are buffered. Text messages
+arrive with the `textMessage` event and binary messages arrive with the
+`binaryMessage` event.
+
+If this number is set to `Infinity`, then all messages are streaming messages
+and arrive with the `streamMessage` event. If you do this, be sure to handle
+the `streamMessage` event. Even if you are not interested in a particular
+message you must consume the stream.
+
+#### Event: 'negotiate'
+
+`function (request, socket, callback) { }`
+
+ * `request` - the client request getting upgraded
+ * `socket` - `WritableStream` with which you can talk to the client
+ * `callback (extraHeaders)` - call this if you want to succeed or fail the
+   websocket connection. To fail it, pass `null` for `extraHeaders`. To
+   succeed it, pass `{}` for `extraHeaders`. You may also include extra headers
+   in this object which will be sent with the reply. If you wish, you may take
+   control of processing the request directly by writing to socket and managing
+   that connection. In this case, do not call `callback`.
+
+This event only fires if you set `negotiate` to `true` on the
+`WebSocketServer`.
+
+See also `yawl.parseSubProtocolList` and `yawl.parseExtensionList`.
+
+#### Event: 'connection'
+
+`function (ws, request) { }`
+
+ * `ws` - `WebSocketClient`.
+ * `request` - [http.IncomingMessage](http://nodejs.org/docs/latest/api/http.html#http_http_incomingmessage)
+   the HTTP upgrade request.
+
+Fires when a websocket connection is successfully negotiated. `ws` is in the
+`OPEN` state.
+
+#### Event: 'error'
+
+`function (error) { }`
+
+If an error occurs during the upgrade process before the `connection` event,
+this event will fire. For example, if writing `400 Bad Request` due to an
+invalid websocket handshake raised an error, it would be emitted here.
+
+### yawl.WebSocketClient
+
+#### ws.sendText(text)
+
+`text` is a `String`.
+
+Sends an unfragmented UTF-8 encoded text message.
+
+This method throws an error if you are in the middle of sending a stream.
+
+#### ws.sendBinary(buffer, [isUtf8])
+
+`buffer` is a `Buffer`.
+
+Sends an unfragmented binary message.
+
+If this websocket client does not represent a client connected to a server,
+`buffer` will be modified in place. Make a copy of the buffer if you do not
+want this to happen.
+
+If `isUtf8` is `true`, the message will be sent as an unfragmented text
+message.
+
+This method throws an error if you are in the middle of sending a stream.
+
+#### ws.sendStream([isUtf8], [options])
+
+Sends a fragmented message.
+
+ * `isUtf8` (optional) - `Boolean`. If `true` this message will be sent as
+   UTF-8 encoded text message. Otherwise, this message will be sent as a
+   binary message.
+ * `options` (optional):
+   - `highWaterMark` - `Number` - Buffer level when `write()` starts returning
+     `false`. Default 16KB.
+
+Returns a `Writable` stream which is sent over the websocket connection. Be
+sure to handle the `error` event of this stream.
+
+You may not send other text, binary, or stream messages while streaming.
+This method throws an error if you are in the middle of sending another stream.
+
+When you call `write()` on the `Writable` stream that is returned, if this
+websocket client does not represent a client connected to a server, the buffer
+you pass to `write()` will be modified in place. Make a copy of the buffer if
+you do not want this to happen.
+
+#### ws.sendFragment(finBit, opcode, buffer)
+
+This is a low level method that you will only need if you are writing tests or
+using yawl to test other code.
+
+ * `finBit` - Either `yawl.FIN_BIT_1` or `yawl.FIN_BIT_0`.
+ * `opcode` - One of:
+   - `yawl.OPCODE_CONTINUATION_FRAME`
+   - `yawl.OPCODE_TEXT_FRAME`
+   - `yawl.OPCODE_BINARY_FRAME`
+   - `yawl.OPCODE_CLOSE`
+   - `yawl.OPCODE_PING`
+   - `yawl.OPCODE_PONG`
+ * `buffer` - `Buffer`. If you want no fragment body, use `yawl.EMPTY_BUFFER`.
+
+### ws.close([statusCode], [message])
+
+ * `statusCode` (optional) - `Number` - See
+   [RFC6455 Section 11.7](https://tools.ietf.org/html/rfc6455#section-11.7)
+ * `message` (optional) - `String`. Must be no greater than 123 bytes when UTF-8
+   encoded.
+
+Sends a close message to the other endpoint. The state of the client becomes
+`CLOSING`.
+
+If the `WebSocketClient` represents a client connected to a server, the server
+closes the connection to the client without waiting for a corresonding close
+message from the client.
+
+Otherwise, the client waits for the server to close the connection.
+
+### ws.isOpen()
+
+Returns `true` if the state is `OPEN`. Calling any of the send functions
+while the state is not `OPEN` throws an error.
+
+### ws.sendPingBinary(buffer)
+
+Sends a ping message. `buffer.length` must be no greater than 125 bytes.
+
+If this websocket client does not represent a client connected to a server,
+`buffer` will be modified in place. Make a copy of the buffer if you do not
+want this to happen.
+
+### ws.sendPingText(string)
+
+Sends a ping message. `string` must be no greater than 125 bytes when UTF-8
+encoded.
+
+### ws.sendPongBuffer(buffer)
+
+Sends a pong message. `buffer.length` must be no greater than 125 bytes.
+
+Pong messages are automatically sent as a response to ping messages.
+
+If this websocket client does not represent a client connected to a server,
+`buffer` will be modified in place. Make a copy of the buffer if you do not
+want this to happen.
+
+### ws.sendPongText(string)
+
+Sends a pong message. `string` must be no greater than 125 bytes when UTF-8
+encoded.
+
+Pong messages are automatically sent as a response to ping messages.
+
+#### ws.socket
+
+The underlying socket for this connection.
+
+#### Event: 'open'
+
+`function (response) { }`
+
+`response` -
+[http.IncomingMessage](http://nodejs.org/docs/latest/api/http.html#http_http_incomingmessage)
+ - the HTTP response from the upgrade request.
+
+Emitted when the upgrade request succeeds and the client is in the `OPEN`
+state.
+
+This event is not fired when the `WebSocketClient` represents a client
+connected to a server. In that situation, the `WebSocketClient` parameter of
+the `connection` event is already in the `OPEN` state.
+
+#### Event: 'textMessage'
+
+`function (string) { }`
+
+This event will not fire if `maxFrameSize` is set to `Infinity`.
+
+This event will not fire unless `allowTextMessages` is set to `true`.
+
+Fragmented messages never arrive in this event.
+
+#### Event: 'binaryMessage'
+
+`function (buffer) { }`
+
+This event will not fire if `maxFrameSize` is set to `Infinity`.
+
+This event will not fire unless `allowBinaryMessages` is set to `true`.
+
+Fragmented messages never arrive in this event.
+
+#### Event: 'streamMessage'
+
+`function (stream, isUtf8, length) { }`
+
+ * `stream` - `ReadableStream`. You must consume this stream. If you are not
+   interested in this message, call `stream.resume()` to trash the data. Be
+   sure to handle the `error` event of this stream.
+ * `isUtf8` - `Boolean`. Tells whether stream was sent as a UTF-8 text message.
+ * `length` - `Number`. If `null`, this is a fragmented message. Otherwise,
+  the total size of the stream is known beforehand.
+
+If `isUtf8` is `true`, you might want to do this: `stream.setEncoding('utf8')`.
+See [readable.setEncoding(encoding)](http://nodejs.org/docs/latest/api/stream.html#stream_readable_setencoding_encoding)
+
+Unfragmented messages do not arrive in this event if `maxFrameSize` is not
+`Infinity`.
+
+Fragmented messages do not arrive in this event unless `allowFragmentedMessages`
+is set to `true`.
+
+`isUtf8` will not be `true` if `allowTextMessages` is `false`.
+
+`isUtf8` will not be `false` if `allowBinaryMessages` is `false`.
+
+#### Event: 'closeMessage'
+
+`function (statusCode, message) { }`
+
+ * `statusCode` - `Number` - See
+   [RFC6455 Section 11.7](https://tools.ietf.org/html/rfc6455#section-11.7).
+   Can be `null`.
+ * `message` - `String`. Can be `null`. Guaranteed to be no greater than 123
+   bytes when UTF-8 encoded.
+
+This event is fired when the other endpoint sends a close frame.
+
+yawl handles this message by closing the socket, so this message is shortly
+followed by the `close` event.
+
+#### Event: 'pingMessage'
+
+`function (buffer) { }`
+
+ * `buffer` - `Buffer`. Must be no greater than 125 bytes.
+
+#### Event: 'pongMessage'
+
+`function (buffer) { }`
+
+ * `buffer` - `Buffer`. Must be no greater than 125 bytes.
+
+#### Event: 'close'
+
+This event fires when the underlying socket connection is closed. It is
+guaranteed to fire even if an error occurs, unlike `closeMessage`.
+
+When this event fires the state of the websocket is now `CLOSED`.
+
+#### Event: 'error'
+
+`function (error) { }`
+
+`error` - `Error`. If this error is due to a problem with the websocket
+protocol, `error.statusCode` is set. See
+[RFC6455 Section 11.7](https://tools.ietf.org/html/rfc6455#section-11.7) for
+a list of status codes and their meanings.
+
+When an error occurs a `WebSocketClient` closes itself, so this event is
+shortly followed by the `close` event.
+
+## Performance
+
+```
+$ node -v
+v0.10.35
+$ date
+Wed Jan 21 09:24:13 MST 2015
+$ node test/perf.js
+big buffer echo (yawl): 0.52s  191MB/s
+big buffer echo (ws): 0.26s  388MB/s
+big buffer echo (faye): 0.54s  186MB/s
+many small buffers (yawl): 0.41s  12MB/s
+many small buffers (ws): 0.33s  15MB/s
+many small buffers (faye): 0.58s  8MB/s
+permessage-deflate big buffer echo (ws): 8.57s  12MB/s
+permessage-deflate many small buffers (ws): 1.76s  3MB/s
+permessage-deflate big buffer echo (faye): 4.59s  22MB/s
+permessage-deflate many small buffers (faye): 2.31s  2MB/s
+done
+```
+
+The bottleneck is in the masking code:
+
+```js
+function maskMangleBuf(buffer, mask) {
+  for (var i = 0; i < buffer.length; i += 1) {
+    buffer[i] = buffer[i] ^ mask[i % 4];
+  }
+}
+```
+
+This is as fast as it's going to get in JavaScript. Making this module faster
+requires a native add-on.
+
+## How to Run the Autobahn Tests
+
+Note that yawl has its own tests which you can run using `npm test` as usual.
+
+[Install wstest](http://autobahn.ws/testsuite/installation.html#installation)
+
+### Test the Client
+
+ 0. In one terminal, `wstest --mode=fuzzingserver --wsuri=ws://localhost:9001`
+ 0. In another terminal, `node test/autobahn-client.js`
+ 0. Open
+    [reports/clients/index.html](http://s3.amazonaws.com/superjoe/temp/yawl/clients/index.html)
+    in a web browser.
+
+### Test the Server
+
+ 0. In one terminal, `node test/autobahn-server.js`
+ 0. In another terminal, `wstest --mode=fuzzingclient --wsuri=ws://localhost:9001`
+ 0. Open
+    [reports/servers/index.html](http://s3.amazonaws.com/superjoe/temp/yawl/servers/index.html)
+    in a web browser.
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..b2252d6
--- /dev/null
+++ b/index.js
@@ -0,0 +1,935 @@
+var EventEmitter = require('events').EventEmitter;
+var stream = require('stream');
+var util = require('util');
+var crypto = require('crypto');
+var http = require('http');
+var https = require('https');
+var Pend = require('pend');
+var BufferList = require('bl');
+
+exports.createServer = createServer;
+exports.createClient = createClient;
+
+exports.WebSocketServer = WebSocketServer;
+exports.WebSocketClient = WebSocketClient;
+
+exports.parseSubProtocolList = parseSubProtocolList;
+exports.parseExtensionList = parseExtensionList;
+
+var FIN_BIT_1 = exports.FIN_BIT_1 = 0x80;
+var FIN_BIT_0 = exports.FIN_BIT_1 = 0x00;
+
+var OPCODE_CONTINUATION_FRAME = exports.OPCODE_CONTINUATION_FRAME = 0x0;
+var OPCODE_TEXT_FRAME         = exports.OPCODE_TEXT_FRAME         = 0x1;
+var OPCODE_BINARY_FRAME       = exports.OPCODE_BINARY_FRAME       = 0x2;
+var OPCODE_CLOSE              = exports.OPCODE_CLOSE              = 0x8;
+var OPCODE_PING               = exports.OPCODE_PING               = 0x9;
+var OPCODE_PONG               = exports.OPCODE_PONG               = 0xA;
+
+var EMPTY_BUFFER = exports.EMPTY_BUFFER = new Buffer(0);
+
+var HANDSHAKE_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+var STATE_COUNT          = 0;
+var STATE_START          = STATE_COUNT++;
+var STATE_HAVE_LEN       = STATE_COUNT++;
+var STATE_PAYLOAD_LEN_16 = STATE_COUNT++;
+var STATE_PAYLOAD_LEN_64 = STATE_COUNT++;
+var STATE_MASK_KEY       = STATE_COUNT++;
+var STATE_STREAM_DATA    = STATE_COUNT++;
+var STATE_BUFFER_DATA    = STATE_COUNT++;
+var STATE_CLOSE_FRAME    = STATE_COUNT++;
+var STATE_PING_FRAME     = STATE_COUNT++;
+var STATE_PONG_FRAME     = STATE_COUNT++;
+var STATE_CLOSING        = STATE_COUNT++;
+var STATE_CLOSED         = STATE_COUNT++;
+
+var DEFAULT_MAX_FRAME_SIZE = 8 * 1024 * 1024;
+
+// maximum value that the highest 32-bits of a 64-bit size can be
+// due to JavaScript not having unsigned 64-bit integer values
+var DOUBLE_MAX_HIGH_32 = Math.pow(2, 52 - 32);
+
+var KNOWN_OPCODES = [
+  true,  // continuation frame
+  true,  // text frame
+  true,  // binary frame
+  false, // reserved
+  false, // reserved
+  false, // reserved
+  false, // reserved
+  false, // reserved
+  true,  // connection close
+  true,  // ping
+  true,  // pong
+];
+var IS_CONTROL_OPCODE = [
+  false, // continuation frame
+  false, // text frame
+  false, // binary frame
+  false, // reserved
+  false, // reserved
+  false, // reserved
+  false, // reserved
+  false, // reserved
+  true,  // connection close
+  true,  // ping
+  true,  // pong
+];
+var IS_MSG_OPCODE = [
+  false, // continuation frame
+  true, // text frame
+  true, // binary frame
+];
+var CONTROL_FRAME_STATE = [
+  STATE_STREAM_DATA, // continuation frame
+  null, // text frame
+  null, // binary frame
+  null, // reserved
+  null, // reserved
+  null, // reserved
+  null, // reserved
+  null, // reserved
+  STATE_CLOSE_FRAME,  // connection close
+  STATE_PING_FRAME,  // ping
+  STATE_PONG_FRAME,  // pong
+];
+var IS_UTF8_OPCODE = [
+  OPCODE_BINARY_FRAME, // false
+  OPCODE_TEXT_FRAME,   // true
+];
+
+var BUFFER_NO_DEBUG = true;
+
+// https://tools.ietf.org/html/rfc2616#section-2.2
+var extensionsTokenizerRegex = new RegExp(
+  '([^\u0000-\u001f()<>@,;:\\\\'+'"'+'/\\[\\]?={}'+' '+'\t]+)' + '|' + // token
+  '([' +          '()<>@,;:\\\\'  +  '/\\[\\]?={}'  +  '\t])'  + '|' + // separators (without '" ')
+  '("(?:[^"\\\\]|\\\\.)*")'                                    + '|' + // quoted-string
+  '((?:\r\n)?[ \t]+)'                                          + '|' + // LWS
+  '([^])',                                                             // invalid
+  "g");
+var EXT_TOKEN_TOKEN = 1;
+var EXT_TOKEN_SEPARATOR = 2;
+var EXT_TOKEN_QUOTED_STRING = 3;
+var EXT_TOKEN_LWS = 4;
+var EXT_TOKEN_EOF = -1;
+
+var EXT_SYNTAX_ERR_MSG = "websocket-extensions syntax error";
+
+function createServer(options) {
+  return new WebSocketServer(options);
+}
+
+function createClient(options) {
+  var nonce = rando(16).toString('base64');
+  options = extend({
+    extraHeaders: {},
+  }, options);
+  options.headers = {
+    'Connection': 'keep-alive, Upgrade',
+    'Pragma': 'no-cache',
+    'Cache-Control': 'no-cache',
+    'Upgrade': 'websocket',
+    'Sec-WebSocket-Version': '13',
+    'Sec-WebSocket-Key': nonce,
+    //'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
+  };
+  if (options.origin) {
+    options.headers.Origin = options.origin;
+  }
+  extend(options.headers, options.extraHeaders);
+
+  var httpLib;
+  if (/^ws:?$/i.test(options.protocol)) {
+    httpLib = http;
+  } else if (/^wss:?$/i.test(options.protocol)) {
+    httpLib = https;
+  } else {
+    throw new Error("invalid protocol: " + options.protocol);
+  }
+  delete options.protocol;
+
+  var client = new WebSocketClient({
+    maskDirectionOut: true,
+    allowTextMessages: options.allowTextMessages,
+    allowFragmentedMessages: options.allowFragmentedMessages,
+    allowBinaryMessages: options.allowBinaryMessages,
+    allowUnfragmentedMessages: options.allowUnfragmentedMessages,
+    maxFrameSize: options.maxFrameSize,
+  });
+  var request = httpLib.request(options);
+  request.on('response', onResponse);
+  request.on('upgrade', onUpgrade);
+  request.on('error', function(err) {
+    handleError(client, err);
+  });
+  request.end();
+  return client;
+
+  function onResponse(response) {
+    var err = new Error("server returned HTTP " + response.statusCode);
+    err.response = response;
+    client.emit('error', err);
+  }
+
+  function onUpgrade(response, socket, firstBuffer) {
+    client.socket = socket;
+    socket.on('error', function(err) {
+      handleError(client, err);
+    });
+    socket.on('close', function() {
+      handleSocketClose(client);
+    });
+    if (response.statusCode !== 101) {
+      handleError(client, new Error("server sent invalid status code"));
+      return;
+    }
+    if (lowerHeader(response, 'connection') !== 'upgrade') {
+      handleError(client, new Error("server sent invalid Connection header"));
+      return;
+    }
+    if (lowerHeader(response, 'upgrade') !== 'websocket') {
+      handleError(client, new Error("server sent invalid Upgrade header"));
+      return;
+    }
+    var hash = crypto.createHash('sha1');
+    hash.update(nonce + HANDSHAKE_GUID);
+    var expectedHandshakeResponse = hash.digest().toString('base64');
+    if (response.headers['sec-websocket-accept'] !== expectedHandshakeResponse) {
+      handleError(client, new Error("server sent invalid handshake response"));
+      return;
+    }
+    client.write(firstBuffer);
+    socket.pipe(client).pipe(socket);
+    client.emit('open', response);
+  }
+}
+
+util.inherits(WebSocketServer, EventEmitter);
+function WebSocketServer(options) {
+  EventEmitter.call(this);
+  this.setNegotiate(options.negotiate);
+  this.setOrigin(options.origin);
+  this.setAllowTextMessages(options.allowTextMessages);
+  this.setAllowBinaryMessages(options.allowBinaryMessages);
+  this.setAllowFragmentedMessages(options.allowFragmentedMessages);
+  this.setAllowUnfragmentedMessages(options.allowUnfragmentedMessages);
+  this.setMaxFrameSize(options.maxFrameSize);
+
+  options.server.on('upgrade', handleUpgrade.bind(null, this));
+}
+
+WebSocketServer.prototype.setAllowFragmentedMessages = function(value) {
+  this.allowFragmentedMessages = !!value;
+};
+
+WebSocketServer.prototype.setAllowUnfragmentedMessages = function(value) {
+  this.allowUnfragmentedMessages = (value == null) ? true : !!value;
+};
+
+WebSocketServer.prototype.setMaxFrameSize = function(value) {
+  this.maxFrameSize = (value == null) ? DEFAULT_MAX_FRAME_SIZE : +value;
+};
+
+WebSocketServer.prototype.setNegotiate = function(value) {
+  this.negotiate = !!value;
+};
+
+WebSocketServer.prototype.setAllowTextMessages = function(value) {
+  this.allowTextMessages = !!value;
+};
+
+WebSocketServer.prototype.setAllowBinaryMessages = function(value) {
+  this.allowBinaryMessages = !!value;
+};
+
+WebSocketServer.prototype.setOrigin = function(origin) {
+  if (origin === undefined) {
+    throw new Error("to disable Origin validation explicitly set origin to `null`");
+  }
+  this.origin = (origin == null) ? null : origin.toLowerCase();
+};
+
+function handleUpgrade(server, request, socket, firstBuffer) {
+  if (lowerHeader(request, 'upgrade') !== 'websocket') {
+    return;
+  }
+  if (request.headers['sec-websocket-version'] !== "13") {
+    socket.on('error', onUpgradeSocketError);
+    socket.write(
+      "HTTP/1.1 426 Upgrade Required\r\n" +
+      "Sec-WebSocket-Version: 13\r\n" +
+      "Connection: close\r\n" +
+      "\r\n");
+    socket.end();
+    return;
+  }
+  if (server.origin && lowerHeader(request, 'origin') !== server.origin) {
+    socket.on('error', onUpgradeSocketError);
+    socket.write(
+      "HTTP/1.1 403 Forbidden\r\n" +
+      "Connection: close\r\n" +
+      "\r\n");
+    socket.end();
+    return;
+  }
+  var webSocketKey = request.headers['sec-websocket-key'];
+  if (!webSocketKey) {
+    socket.on('error', onUpgradeSocketError);
+    socket.write(
+      "HTTP/1.1 400 Expected WebSocket Handshake Key\r\n" +
+      "Connection: close\r\n" +
+      "\r\n");
+    socket.end();
+    return;
+  }
+  var subProtocolList = parseHeaderValueList(request.headers['sec-websocket-protocol']);
+  if (server.negotiate) {
+    server.emit('negotiate', request, socket, handleNegotiationResult);
+  } else {
+    writeResponse.call(server, {});
+  }
+
+  function onUpgradeSocketError(err) {
+    server.emit('error', err);
+  }
+
+  function handleNegotiationResult(extraHeaders) {
+    if (!extraHeaders) {
+      socket.on('error', onUpgradeSocketError);
+      socket.write(
+        "HTTP/1.1 400 Bad Request\r\n" +
+        "Connection: close\r\n" +
+        "\r\n");
+      socket.end();
+      return;
+    }
+    writeResponse(extraHeaders);
+  }
+
+  function writeResponse(extraHeaders) {
+    var hash = crypto.createHash('sha1');
+    hash.update(webSocketKey + HANDSHAKE_GUID);
+    var handshakeResponse = hash.digest().toString('base64');
+
+    var client = new WebSocketClient({
+      socket: socket,
+      maskDirectionOut: false,
+      allowTextMessages: server.allowTextMessages,
+      allowBinaryMessages: server.allowBinaryMessages,
+      allowFragmentedMessages: server.allowFragmentedMessages,
+      allowUnfragmentedMessages: server.allowUnfragmentedMessages,
+      maxFrameSize: server.maxFrameSize,
+    });
+    socket.on('error', function(err) {
+      handleError(client, err);
+    });
+    socket.on('close', function() {
+      handleSocketClose(client);
+    });
+    var responseHeaders = {
+      Upgrade: 'websocket',
+      Connection: 'Upgrade',
+      'Sec-WebSocket-Accept': handshakeResponse,
+    };
+    extend(responseHeaders, extraHeaders);
+    socket.write(
+      "HTTP/1.1 101 Switching Protocols\r\n" +
+      renderHeaders(responseHeaders) +
+      "\r\n");
+    client.write(firstBuffer);
+    socket.pipe(client).pipe(socket);
+    server.emit('connection', client, request);
+  }
+}
+
+util.inherits(WebSocketClient, stream.Transform);
+function WebSocketClient(options) {
+  stream.Transform.call(this);
+
+  this.socket = options.socket;
+  this.maskOutBit = options.maskDirectionOut ? 0x80 : 0x00;
+  this.expectedMaskInBit = +!this.maskOutBit;
+  this.maskOutSize = this.maskOutBit ? 4 : 0;
+
+  this.allowTextMessages = !!options.allowTextMessages;
+  this.allowBinaryMessages = !!options.allowBinaryMessages;
+  this.allowFragmentedMessages = !!options.allowFragmentedMessages;
+  this.allowUnfragmentedMessages = (options.allowUnfragmentedMessages == null) ?  true : !!options.allowUnfragmentedMessages;
+  this.maxFrameSize = (options.maxFrameSize == null) ? DEFAULT_MAX_FRAME_SIZE : +options.maxFrameSize;
+
+  this.error = null;
+  this.state = STATE_START;
+  this.buffer = new BufferList();
+  this.pend = new Pend();
+
+  this.fin = 0;
+  this.rsv1 = 0;
+  this.rsv2 = 0;
+  this.rsv3 = 0;
+  this.opcode = 0;
+  this.maskBit = 0;
+  this.payloadLen = 0;
+  this.mask = new Buffer(4);
+  this.msgStream = null;
+  this.msgOpcode = 0;
+  this.frameOffset = 0;
+  this.maskNextState = STATE_BUFFER_DATA;
+
+  this.sendingStream = null;
+}
+
+WebSocketClient.prototype._transform = function(buf, _encoding, callback) {
+  this.buffer.append(buf);
+
+  var b, slice;
+  var amtToRead, encoding;
+
+  outer:
+  for (;;) {
+    switch (this.state) {
+      case STATE_START:
+        if (this.buffer.length < 2) break outer;
+
+        b = this.buffer.readUInt8(0, BUFFER_NO_DEBUG);
+        this.fin    = getBits(b, 0, 1);
+        this.rsv1   = getBits(b, 1, 1);
+        this.rsv2   = getBits(b, 2, 1);
+        this.rsv3   = getBits(b, 3, 1);
+        this.opcode = getBits(b, 4, 4);
+
+        if (this.rsv1 !== 0 || this.rsv2 !== 0 || this.rsv3 !== 0) {
+          failConnection(this, 1002, "invalid reserve bits");
+          return;
+        }
+
+        if (!KNOWN_OPCODES[this.opcode]) {
+          failConnection(this, 1002, "invalid opcode");
+          return;
+        }
+
+        b = this.buffer.readUInt8(1, BUFFER_NO_DEBUG);
+        this.maskBit    = getBits(b, 0, 1);
+        this.payloadLen = getBits(b, 1, 7);
+
+        if (this.maskBit !== this.expectedMaskInBit) {
+          failConnection(this, 1002, "invalid mask bit");
+          return;
+        }
+
+        if (IS_CONTROL_OPCODE[this.opcode]) {
+          if (!this.fin) {
+            failConnection(this, 1002, "control frame must set fin");
+            return;
+          } else if (this.payloadLen > 125) {
+            failConnection(this, 1002, "control frame too big");
+            return;
+          } else if (this.opcode === OPCODE_CLOSE && this.payloadLen === 1) {
+            failConnection(this, 1002, "bad payload size for close");
+            return;
+          }
+        } else {
+          if (this.msgStream) {
+            if (this.opcode !== OPCODE_CONTINUATION_FRAME) {
+              failConnection(this, 1002, "expected continuation frame");
+              return;
+            }
+          } else if (this.opcode === OPCODE_CONTINUATION_FRAME) {
+            failConnection(this, 1002, "invalid continuation frame");
+            return;
+          } else if (this.opcode === OPCODE_TEXT_FRAME && !this.allowTextMessages) {
+            failConnection(this, 1003, "text messages not allowed");
+            return;
+          } else if (this.opcode === OPCODE_BINARY_FRAME && !this.allowBinaryMessages) {
+            failConnection(this, 1003, "binary messages not allowed");
+            return;
+          } else if (!this.fin && !this.allowFragmentedMessages) {
+            failConnection(this, 1003, "fragmented messages not allowed");
+            return;
+          } else if (this.fin && IS_MSG_OPCODE[this.opcode] && !this.allowUnfragmentedMessages) {
+            failConnection(this, 1003, "unfragmented messages not allowed");
+            return;
+          }
+        }
+        if (this.payloadLen === 126) {
+          this.state = STATE_PAYLOAD_LEN_16;
+        } else if (this.payloadLen === 127) {
+          this.state = STATE_PAYLOAD_LEN_64;
+        } else {
+          this.state = STATE_HAVE_LEN;
+        }
+        this.buffer.consume(2);
+        continue;
+      case STATE_HAVE_LEN:
+        if (this.fin && this.payloadLen > this.maxFrameSize) {
+          failConnection(this, 1009, "exceeded max frame size");
+          return;
+        }
+        this.frameOffset = 0;
+        if (IS_MSG_OPCODE[this.opcode]) {
+          if (!this.fin || this.maxFrameSize === Infinity) {
+            this.msgOpcode = this.opcode;
+            this.msgStream = new stream.PassThrough();
+            var isUtf8 = (this.opcode === OPCODE_TEXT_FRAME);
+            var streamLen = this.fin ? this.payloadLen : null;
+            this.emit('streamMessage', this.msgStream, isUtf8, streamLen);
+            this.maskNextState = STATE_STREAM_DATA;
+          } else {
+            this.maskNextState = STATE_BUFFER_DATA;
+          }
+        } else {
+          this.maskNextState = CONTROL_FRAME_STATE[this.opcode];
+        }
+        this.state = this.maskBit ? STATE_MASK_KEY : this.maskNextState;
+        continue;
+      case STATE_PAYLOAD_LEN_16:
+        if (this.buffer.length < 2) break outer;
+        this.payloadLen = this.buffer.readUInt16BE(0, BUFFER_NO_DEBUG);
+        this.buffer.consume(2);
+        this.state = STATE_HAVE_LEN;
+        continue;
+      case STATE_PAYLOAD_LEN_64:
+        if (this.buffer.length < 8) break outer;
+        var big = this.buffer.readUInt32BE(0, BUFFER_NO_DEBUG);
+        if (big > DOUBLE_MAX_HIGH_32) {
+          failConnection(this, 1009, "exceeded max frame size");
+          return;
+        }
+        var small = this.buffer.readUInt32BE(4, BUFFER_NO_DEBUG);
+        this.payloadLen = big * 0x100000000 + small;
+        this.buffer.consume(8);
+        this.state = STATE_HAVE_LEN;
+        continue;
+      case STATE_MASK_KEY:
+        if (this.buffer.length < 4) break outer;
+        this.buffer.copy(this.mask, 0, 0, 4);
+        this.buffer.consume(4);
+        this.state = this.maskNextState;
+        continue;
+      case STATE_CLOSE_FRAME:
+        if (this.buffer.length < this.payloadLen) break outer;
+        slice = this.buffer.slice(0, this.payloadLen);
+        this.buffer.consume(this.payloadLen);
+        if (this.maskBit) maskMangleBufOffset(slice, this.mask, this.frameOffset);
+        var statusCode = (slice.length >= 2) ? slice.readUInt16BE(0, BUFFER_NO_DEBUG) : 1005;
+        var message = (slice.length >= 2) ? slice.toString('utf8', 2) : "";
+        this.emit('closeMessage', statusCode, message);
+        this.close();
+        break outer;
+      case STATE_PING_FRAME:
+        if (this.buffer.length < this.payloadLen) break outer;
+        slice = this.buffer.slice(0, this.payloadLen);
+        this.buffer.consume(this.payloadLen);
+        if (this.maskBit) maskMangleBufOffset(slice, this.mask, this.frameOffset);
+        this.state = STATE_START;
+        this.emit('pingMessage', slice);
+        this.sendPongBinary(slice);
+        continue;
+      case STATE_PONG_FRAME:
+        if (this.buffer.length < this.payloadLen) break outer;
+        slice = this.buffer.slice(0, this.payloadLen);
+        this.buffer.consume(this.payloadLen);
+        if (this.maskBit) maskMangleBufOffset(slice, this.mask, this.frameOffset);
+        this.state = STATE_START;
+        this.emit('pongMessage', slice);
+        continue;
+      case STATE_BUFFER_DATA:
+        if (this.buffer.length < this.payloadLen) break outer;
+        slice = this.buffer.slice(0, this.payloadLen);
+        this.buffer.consume(this.payloadLen);
+        if (this.maskBit) maskMangleBufOffset(slice, this.mask, this.frameOffset);
+        this.state = STATE_START;
+        if (this.opcode === OPCODE_TEXT_FRAME) {
+          this.emit('textMessage', slice.toString('utf8'));
+        } else {
+          this.emit('binaryMessage', slice);
+        }
+        continue;
+      case STATE_STREAM_DATA:
+        var bytesLeftInFrame = this.payloadLen - this.frameOffset;
+        amtToRead = Math.min(this.buffer.length, bytesLeftInFrame);
+        if (amtToRead === 0) {
+          if (!this.fin || bytesLeftInFrame !== 0) break outer;
+        } else {
+          slice = this.buffer.slice(0, amtToRead)
+          if (this.maskBit) maskMangleBufOffset(slice, this.mask, this.frameOffset);
+          encoding = (this.msgOpcode === OPCODE_BINARY_FRAME) ? undefined : 'utf8';
+          this.msgStream.write(slice, encoding, this.pend.hold());
+          this.buffer.consume(amtToRead);
+          this.frameOffset += amtToRead;
+        }
+        if (bytesLeftInFrame === amtToRead) {
+          if (this.fin) {
+            this.msgStream.end();
+            this.msgStream = null;
+          }
+          this.state = STATE_START;
+        }
+        continue;
+      case STATE_CLOSING:
+      case STATE_CLOSED:
+        return;
+      default:
+        throw new Error("unknown state: " + this.state);
+    }
+  }
+
+  this.pend.wait(callback);
+};
+
+WebSocketClient.prototype.sendText = function(string) {
+  this.sendBinary(new Buffer(string, 'utf8'), true);
+};
+
+WebSocketClient.prototype.sendBinary = function(buffer, isUtf8) {
+  if (this.sendingStream) {
+    throw new Error("send stream already in progress");
+  }
+  if (this.error) {
+    throw new Error("socket in error state");
+  }
+  this.sendFragment(FIN_BIT_1, IS_UTF8_OPCODE[+!!isUtf8], buffer);
+};
+
+WebSocketClient.prototype.sendStream = function(isUtf8, options) {
+  if (this.sendingStream) {
+    throw new Error("send stream already in progress");
+  }
+  if (this.error) {
+    throw new Error("socket in error state");
+  }
+  return sendFragmentedStream(this, isUtf8, options);
+};
+
+WebSocketClient.prototype.sendFragment = function(finBit, opcode, buffer) {
+  var byte1 = finBit | opcode;
+  var header;
+  var maskOffset;
+  if (buffer.length <= 125) {
+    maskOffset = 2;
+    header = new Buffer(maskOffset + this.maskOutSize);
+    header[1] = buffer.length | this.maskOutBit;
+  } else if (buffer.length <= 65535) {
+    maskOffset = 4;
+    header = new Buffer(maskOffset + this.maskOutSize);
+    header[1] = 126 | this.maskOutBit;
+    header.writeUInt16BE(buffer.length, 2, BUFFER_NO_DEBUG);
+  } else {
+    maskOffset = 10;
+    header = new Buffer(maskOffset + this.maskOutSize);
+    header[1] = 127 | this.maskOutBit;
+    writeUInt64BE(header, buffer.length, 2);
+  }
+  header[0] = byte1;
+  if (this.maskOutBit) {
+    var mask = rando(4);
+    mask.copy(header, maskOffset);
+    maskMangleBuf(buffer, mask);
+  }
+  this.push(header);
+  this.push(buffer);
+};
+
+WebSocketClient.prototype.sendPingBinary = function(msgBuffer) {
+  if (msgBuffer.length > 125) {
+    throw new Error("ping message too long");
+  }
+  if (this.error) {
+    throw new Error("socket in error state");
+  }
+  this.sendFragment(FIN_BIT_1, OPCODE_PING, msgBuffer);
+};
+
+WebSocketClient.prototype.sendPingText = function(message) {
+  return this.sendPingBinary(new Buffer(message, 'utf8'));
+};
+
+WebSocketClient.prototype.sendPongBinary = function(msgBuffer) {
+  if (msgBuffer.length > 125) {
+    throw new Error("pong message too long");
+  }
+  if (this.error) {
+    throw new Error("socket in error state");
+  }
+  this.sendFragment(FIN_BIT_1, OPCODE_PONG, msgBuffer);
+};
+
+WebSocketClient.prototype.sendPongText = function(message) {
+  return this.sendPongBinary(new Buffer(message, 'utf8'));
+};
+
+WebSocketClient.prototype.close = function(statusCode, message) {
+  if (!this.isOpen()) return;
+  if (statusCode == null && message == null) {
+    sendCloseBare(this);
+  } else if (statusCode != null) {
+    message = message || "";
+    sendCloseWithMessage(this, statusCode, message);
+  } else {
+    sendCloseWithMessage(this, 1000, message);
+  }
+  // this is after sendClose() because those methods can throw if message
+  // is too long
+  this.state = STATE_CLOSING;
+  if (!this.maskOutBit) {
+    this.push(null);
+  }
+};
+
+WebSocketClient.prototype.isOpen = function() {
+  return this.state !== STATE_CLOSING && this.state !== STATE_CLOSED;
+};
+
+function sendCloseWithMessage(client, statusCode, message) {
+  var buffer = new Buffer(130);
+  var bytesWritten = buffer.write(message, 2, 128, 'utf8');
+  if (bytesWritten > 123) {
+    throw new Error("close message too long");
+  }
+  buffer.writeUInt16BE(statusCode, 0, BUFFER_NO_DEBUG);
+  buffer = buffer.slice(0, bytesWritten + 2);
+  client.sendFragment(FIN_BIT_1, OPCODE_CLOSE, buffer);
+}
+
+function sendFragmentedStream(client, isUtf8, options) {
+  client.sendingStream = new stream.Writable(options);
+  var first = true;
+  client.sendingStream._write = function(buffer, encoding, callback) {
+    if (client.state === STATE_CLOSING) {
+      callback(new Error("websocket is CLOSING"));
+      return;
+    } else if (client.state === STATE_CLOSED) {
+      callback(new Error("websocket is CLOSED"));
+      return;
+    }
+    var opcode;
+    if (first) {
+      first = false;
+      opcode = IS_UTF8_OPCODE[+!!isUtf8];
+    } else {
+      opcode = OPCODE_CONTINUATION_FRAME;
+    }
+    client.sendFragment(FIN_BIT_0, opcode, buffer);
+    callback();
+  };
+
+  client.sendingStream.on('finish', function() {
+    client.sendingStream = null;
+    client.sendFragment(FIN_BIT_1, OPCODE_CONTINUATION_FRAME, EMPTY_BUFFER);
+  });
+
+  return client.sendingStream;
+}
+
+function sendCloseBare(client) {
+  client.sendFragment(FIN_BIT_1, OPCODE_CLOSE, EMPTY_BUFFER);
+}
+
+function parseSubProtocolList(request) {
+  return parseHeaderValueList(request.headers['sec-websocket-protocol']);
+}
+
+function parseExtensionList(request) {
+  var headerValue = request.headers['sec-websocket-extensions'];
+  if (!headerValue) return null;
+  // https://tools.ietf.org/html/rfc6455#section-9.1
+  var tokens = [];
+  extensionsTokenizerRegex.lastIndex = 0;
+  while (true) {
+    var match = extensionsTokenizerRegex.exec(headerValue);
+    if (match == null) {
+      // this makes the code slightly easier to write below
+      tokens.push({type:EXT_TOKEN_EOF, text:EXT_TOKEN_EOF});
+      // EOF
+      break;
+    }
+    var text = match[0];
+    if (match[EXT_TOKEN_TOKEN] != null) {
+      // token
+      tokens.push({type:EXT_TOKEN_TOKEN, text:text});
+    } else if (match[EXT_TOKEN_SEPARATOR] != null) {
+      tokens.push({type:EXT_TOKEN_SEPARATOR, text:text});
+    } else if (match[EXT_TOKEN_QUOTED_STRING] != null) {
+      // strip quotes
+      text = /^"(.*)"$/.exec(text)[1];
+      // handle escapes
+      text = text.replace(/\\(.)/g, "$1");
+
+      var theSpecSays = "When using the quoted-string syntax variant, the value " +
+                        "after quoted-string unescaping MUST conform to the " +
+                        "'token' ABNF.";
+      var lastLastIndex = extensionsTokenizerRegex.lastIndex;
+      extensionsTokenizerRegex.lastIndex = 0;
+      if (extensionsTokenizerRegex.exec(text)[EXT_TOKEN_TOKEN] !== text) {
+        throw new Error(theSpecSays);
+      }
+      extensionsTokenizerRegex.lastIndex = lastLastIndex;
+
+      tokens.push({type:EXT_TOKEN_TOKEN, text:text});
+    } else if (match[EXT_TOKEN_LWS] != null) {
+      // ignore whitespace
+      continue;
+    } else {
+      // invalid
+      throw new Error(EXT_SYNTAX_ERR_MSG);
+    }
+  }
+
+  var extensions = [];
+  var tokenIndex = 0;
+  ensureNotEof();
+  while (tokens[tokenIndex].type !== EXT_TOKEN_EOF) {
+    var extensionNameToken = tokens[tokenIndex++];
+    if (extensionNameToken.type !== EXT_TOKEN_TOKEN) throw new Error(EXT_SYNTAX_ERR_MSG);
+    var extensionName = extensionNameToken.text;
+    var extensionParameters = [];
+    switch (tokens[tokenIndex].text) {
+      case ",":
+        tokenIndex++;
+        ensureNotEof();
+        break;
+      case ";":
+        tokenIndex++;
+        ensureNotEof();
+        while (tokens[tokenIndex].type !== EXT_TOKEN_EOF) {
+          var parameterNameToken = tokens[tokenIndex++];
+          if (parameterNameToken.type !== EXT_TOKEN_TOKEN) throw new Error(EXT_SYNTAX_ERR_MSG);
+          var parameterName = parameterNameToken.text;
+          var parameterValue = null;
+          if (tokens[tokenIndex].text === "=") {
+            tokenIndex++;
+            var parameterValueToken = tokens[tokenIndex++];
+            if (parameterValueToken.type !== EXT_TOKEN_TOKEN) throw new Error(EXT_SYNTAX_ERR_MSG);
+            parameterValue = parameterValueToken.text;
+          }
+          switch (tokens[tokenIndex].text) {
+            case ";":
+              tokenIndex++;
+              ensureNotEof();
+              break;
+            case ",":
+            case EXT_TOKEN_EOF:
+              break;
+            default:
+              throw new Error(EXT_SYNTAX_ERR_MSG);
+          }
+          extensionParameters.push({name:parameterName, value:parameterValue});
+          if (tokens[tokenIndex].text === ",") {
+            tokenIndex++;
+            ensureNotEof();
+            break;
+          }
+        }
+        break;
+      case EXT_TOKEN_EOF:
+        break;
+      default:
+        throw new Error(EXT_SYNTAX_ERR_MSG);
+    }
+    extensions.push({name:extensionName, params:extensionParameters});
+  }
+  return extensions;
+  function ensureNotEof() {
+    if (tokens[tokenIndex].type === EXT_TOKEN_EOF) throw new Error(EXT_SYNTAX_ERR_MSG);
+  }
+}
+
+function handleSocketClose(client) {
+  client.state = STATE_CLOSED;
+  client.emit('close');
+}
+
+function failConnection(client, statusCode, message) {
+  client.close(statusCode, message);
+  if (client.maskOutBit) {
+    client.push(null);
+  }
+  var err = new Error(message);
+  err.statusCode = statusCode;
+  handleError(client, err);
+}
+
+function handleError(client, err) {
+  if (client.error) return;
+  client.error = err;
+  client.emit('error', err);
+  if (client.msgStream) {
+    client.msgStream.emit('error', err);
+    client.msgStream = null;
+  }
+  if (client.sendingStream) {
+    client.sendingStream.emit('error', err);
+    client.sendingStream = null;
+  }
+  client.close(1011, "internal error");
+}
+
+function maskMangleBufOffset(buffer, mask, offset) {
+  for (var i = 0; i < buffer.length; i += 1) {
+    buffer[i] = buffer[i] ^ mask[(i + offset) % 4];
+  }
+}
+
+function maskMangleBuf(buffer, mask) {
+  for (var i = 0; i < buffer.length; i += 1) {
+    buffer[i] = buffer[i] ^ mask[i % 4];
+  }
+}
+
+function truthy(value) {
+  return !!value;
+}
+
+function trimAndLower(s) {
+  return s.trim().toLowerCase();
+}
+
+function parseHeaderValueList(s) {
+  return (s || "").split(/,\s*/).map(trimAndLower).filter(truthy);
+}
+
+function extend(dest, source) {
+  for (var name in source) {
+    dest[name] = source[name];
+  }
+  return dest;
+}
+
+function getBits(b, start, len) {
+  // all bitwise operations are 32-bit integers
+  // example: start=3 len=3
+  // xxxxxxxx xxxxxxxx xxxxxxxx xxxaaaxx ->
+  // aaaxx000 00000000 00000000 00000000 ->
+  // 00000000 00000000 00000000 00000aaa
+  return (b << (start + 24)) >>> (32 - len);
+}
+
+function writeUInt64BE(buffer, value, offset) {
+  var big = Math.floor(value / 0x100000000);
+  var small = value - big;
+  buffer.writeUInt32BE(big, offset, BUFFER_NO_DEBUG);
+  buffer.writeUInt32BE(small, offset + 4, BUFFER_NO_DEBUG);
+}
+
+function rando(size) {
+  try {
+    return crypto.randomBytes(size);
+  } catch (err) {
+    return crypto.pseudoRandomBytes(size);
+  }
+}
+
+function lowerHeader(request, name) {
+  var value = request.headers[name];
+  return value && value.toLowerCase();
+}
+
+function renderHeaders(headers) {
+  var s = "";
+  for (var name in headers) {
+    var value = headers[name];
+    s += name + ": " + value + "\r\n";
+  }
+  return s;
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7a0426f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,49 @@
+{
+  "name": "yawl",
+  "version": "1.0.2",
+  "description": "yet another web sockets library",
+  "scripts": {
+    "test": "mocha --reporter spec --check-leaks test/test.js",
+    "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/test.js",
+    "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/test.js"
+  },
+  "keywords": [
+    "websocket",
+    "websocketserver",
+    "ws",
+    "wss",
+    "rfc6455",
+    "rfc",
+    "6455",
+    "hixie",
+    "hybi",
+    "push",
+    "rfc-6455",
+    "server",
+    "client",
+    "websockets",
+    "autobahn"
+  ],
+  "main": "index.js",
+  "author": "Andrew Kelley <superjoe30 at gmail.com>",
+  "license": "MIT",
+  "dependencies": {
+    "bl": "~0.9.4",
+    "pend": "~1.2.0"
+  },
+  "devDependencies": {
+    "human-size": "~1.1.0",
+    "istanbul": "~0.3.5",
+    "mocha": "~2.1.0"
+  },
+  "directories": {
+    "test": "test"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/andrewrk/node-yawl.git"
+  },
+  "bugs": {
+    "url": "https://github.com/andrewrk/node-yawl/issues"
+  }
+}
diff --git a/test/autobahn-client.js b/test/autobahn-client.js
new file mode 100644
index 0000000..b8e0412
--- /dev/null
+++ b/test/autobahn-client.js
@@ -0,0 +1,80 @@
+var yawl = require('../');
+var assert = require('assert');
+var url = require('url');
+var currentTest = 1;
+var lastTest = -1;
+var testCount = null;
+
+/*
+process.on('uncaughtException', function(err) {
+  console.log("caught:", err, err.stack);
+});
+*/
+
+process.on('SIGINT', handleSigInt);
+
+var options = url.parse('ws://localhost:9001/getCaseCount');
+options.allowBinaryMessages = true;
+options.allowTextMessages = true;
+options.allowFragmentedMessages = true;
+var ws = yawl.createClient(options);
+ws.on('textMessage', function(message) {
+  testCount = parseInt(message, 10);
+});
+ws.on('error', function(err) {
+  assert.strictEqual(err.code, 'ECONNRESET');
+});
+ws.on('close', function() {
+  if (testCount > 0) {
+    nextTest();
+  }
+});
+
+function handleSigInt() {
+  process.removeListener('SIGINT', handleSigInt);
+  updateReportsAndShutDown();
+}
+
+function updateReportsAndShutDown() {
+  console.log('Updating reports and shutting down');
+  var options = url.parse('ws://localhost:9001/updateReports?agent=yawl')
+  options.allowBinaryMessages = true;
+  options.allowTextMessages = true;
+  options.allowFragmentedMessages = true;
+  var ws = yawl.createClient(options);
+  ws.on('error', function(err) {});
+  ws.on('close', function() {
+    process.exit();
+  });
+}
+
+function nextTest() {
+  if (currentTest > testCount || (lastTest !== -1 && currentTest > lastTest)) {
+    updateReportsAndShutDown();
+    return;
+  }
+  console.log('Running test case ' + currentTest + '/' + testCount);
+  var options = url.parse('ws://localhost:9001/runCase?case=' + currentTest + '&agent=yawl');
+  options.allowBinaryMessages = true;
+  options.allowTextMessages = true;
+  options.allowFragmentedMessages = true;
+  options.maxFrameSize = 32 * 1024 * 1024;
+  var ws = yawl.createClient(options);
+  ws.on('textMessage', function(message) {
+    ws.sendText(message);
+  });
+  ws.on('binaryMessage', function(message) {
+    ws.sendBinary(message);
+  });
+  ws.on('streamMessage', function(stream, isUtf8, length) {
+    stream.on('error', function(err) {});
+    var outStream = ws.sendStream(isUtf8, length);
+    stream.pipe(outStream);
+    outStream.on('error', function(err) {});
+  });
+  ws.on('close', function(data) {
+    currentTest += 1;
+    process.nextTick(nextTest);
+  });
+  ws.on('error', function(err) {});
+}
diff --git a/test/autobahn-server.js b/test/autobahn-server.js
new file mode 100644
index 0000000..ce7ef2b
--- /dev/null
+++ b/test/autobahn-server.js
@@ -0,0 +1,30 @@
+var yawl = require('../');
+var http = require('http');
+
+
+var httpServer = http.createServer();
+var wss = yawl.createServer({
+  server: httpServer,
+  allowTextMessages: true,
+  allowBinaryMessages: true,
+  allowFragmentedMessages: true,
+  origin: null,
+});
+wss.on('connection', function(ws) {
+  ws.on('error', function(err) { });
+  ws.on('textMessage', function(message) {
+    ws.sendText(message);
+  });
+  ws.on('binaryMessage', function(message) {
+    ws.sendBinary(message);
+  });
+  ws.on('streamMessage', function(stream, isUtf8, length) {
+    var outStream = ws.sendStream(isUtf8, length);
+    outStream.on('error', function(err) { });
+    stream.on('error', function(err) { });
+    stream.pipe(outStream);
+  });
+});
+httpServer.listen(9001, function() {
+  console.error("Listening: http://localhost:9001");
+});
diff --git a/test/perf.js b/test/perf.js
new file mode 100644
index 0000000..330f5bf
--- /dev/null
+++ b/test/perf.js
@@ -0,0 +1,337 @@
+var yawl = require('../');
+var http = require('http');
+var crypto = require('crypto');
+var humanSize = require('human-size');
+
+var ws;
+try {
+  ws = require('ws');
+} catch (err) {}
+
+var Faye;
+try {
+  Faye = require('faye-websocket');
+} catch (err) {}
+
+var deflate;
+try {
+  deflate = require('permessage-deflate');
+} catch (err) {}
+
+// generate a big file
+var bigFileSize = 100 * 1024 * 1024;
+var bigFileBuffer = crypto.pseudoRandomBytes(bigFileSize);
+
+var smallBufCount = 10000 //100000;
+var smallBufs = new Array(smallBufCount);
+var totalSmallBufsSize = 0;
+for (var i = 0; i < smallBufCount; i += 1) {
+  var buf = crypto.pseudoRandomBytes(Math.floor(Math.random() * 1000 + 1));
+  totalSmallBufsSize += buf.length;
+  smallBufs[i] = buf;
+}
+
+var search = process.argv[2];
+
+var tests = [
+  {
+    name: "big buffer echo (yawl)",
+    fn: bigBufferYawl,
+    req: noop,
+    size: bigFileSize,
+  },
+  {
+    name: "big buffer echo (ws)",
+    fn: makeBigBufferWs(false),
+    req: reqWs,
+    size: bigFileSize,
+  },
+  {
+    name: "big buffer echo (faye)",
+    fn: makeBigBufferFaye(false),
+    req: reqFaye,
+    size: bigFileSize,
+  },
+  {
+    name: "many small buffers (yawl)",
+    fn: smallBufferYawl,
+    req: noop,
+    size: totalSmallBufsSize,
+  },
+  {
+    name: "many small buffers (ws)",
+    fn: makeSmallBufferWs(false),
+    req: reqWs,
+    size: totalSmallBufsSize,
+  },
+  {
+    name: "many small buffers (faye)",
+    fn: makeSmallBufferFaye(false),
+    req: reqFaye,
+    size: totalSmallBufsSize,
+  },
+  {
+    name: "permessage-deflate big buffer echo (ws)",
+    fn: makeBigBufferWs(true),
+    req: reqWs,
+    size: bigFileSize,
+  },
+  {
+    name: "permessage-deflate many small buffers (ws)",
+    fn: makeSmallBufferWs(true),
+    req: reqWs,
+    size: totalSmallBufsSize,
+  },
+  {
+    name: "permessage-deflate big buffer echo (faye)",
+    fn: makeBigBufferFaye(true),
+    req: reqFayeAndDeflate,
+    size: bigFileSize,
+  },
+  {
+    name: "permessage-deflate many small buffers (faye)",
+    fn: makeSmallBufferFaye(true),
+    req: reqFayeAndDeflate,
+    size: totalSmallBufsSize,
+  },
+];
+
+var testIndex = 0;
+doOneTest();
+
+function doOneTest() {
+  var test = tests[testIndex++];
+  if (!test) {
+    console.error("done");
+    return;
+  }
+  if (search && test.name.indexOf(search) === -1) {
+    doOneTest();
+    return;
+  }
+  process.stderr.write(test.name + ": ");
+  var r = test.req();
+  if (r) {
+    process.stderr.write(r + "\n");
+    doOneTest();
+    return;
+  }
+  var start = new Date();
+  test.fn(function() {
+    var end = new Date();
+    var elapsed = (end - start) / 1000;
+    var rate = test.size / elapsed;
+    process.stderr.write(elapsed.toFixed(2) + "s  " + humanSize(rate) + "/s\n");
+    doOneTest();
+  });
+}
+
+function bigBufferYawl(cb) {
+  var httpServer = http.createServer();
+  var wss = yawl.createServer({
+    server: httpServer,
+    allowBinaryMessages: true,
+    maxFrameSize: bigFileSize,
+    origin: null,
+  });
+  wss.on('connection', function(ws) {
+    ws.on('binaryMessage', function(buffer) {
+      ws.sendBinary(buffer);
+    });
+  });
+  httpServer.listen(function() {
+    var options = {
+      host: 'localhost',
+      protocol: 'ws',
+      port: httpServer.address().port,
+      path: '/',
+      allowBinaryMessages: true,
+      maxFrameSize: bigFileSize,
+    };
+    var client = yawl.createClient(options);
+    client.on('open', function() {
+      client.sendBinary(bigFileBuffer);
+    });
+    client.on('binaryMessage', function(buffer) {
+      client.close();
+      httpServer.close(cb);
+    });
+  });
+}
+
+function makeBigBufferWs(perMessageDeflate) {
+  return function (cb) {
+    var httpServer = http.createServer();
+    var wss = new ws.Server({
+      server: httpServer,
+      perMessageDeflate: perMessageDeflate,
+    });
+    wss.on('connection', function(ws) {
+      ws.on('message', function(buffer) {
+        ws.send(buffer);
+      });
+    });
+    httpServer.listen(function() {
+      var client = new ws('ws://localhost:' + httpServer.address().port + '/');
+      client.on('open', function() {
+        client.send(bigFileBuffer);
+      });
+      client.on('message', function(buffer) {
+        client.close();
+        httpServer.close(cb);
+      });
+    });
+  };
+}
+
+function makeBigBufferFaye(perMessageDeflate) {
+  return function (cb) {
+    var httpServer = http.createServer();
+    var extensions = [];
+    if (perMessageDeflate) extensions.push(deflate);
+    httpServer.on('upgrade', function(req, socket, head) {
+      var ws = new Faye(req, socket, head, null, {
+        extensions: extensions,
+        maxLength: Infinity
+      });
+      ws.on('open', function() {
+        ws.on('message', function(buffer) {
+          ws.send(buffer);
+        });
+      });
+    });
+    httpServer.listen(function() {
+      var client = new Faye.Client(
+        'ws://localhost:' + httpServer.address().port + '/',
+        null,
+        { extensions: extensions, maxLength: Infinity }
+      );
+      client.on('open', function() {
+        client.send(bigFileBuffer);
+      });
+      client.on('message', function(buffer) {
+        client.close();
+        httpServer.close(cb);
+      });
+    });
+  };
+}
+
+function smallBufferYawl(cb) {
+  var httpServer = http.createServer();
+  var wss = yawl.createServer({
+    server: httpServer,
+    allowBinaryMessages: true,
+    origin: null,
+  });
+  wss.on('connection', function(ws) {
+    ws.on('binaryMessage', function(buffer) {
+      ws.sendBinary(buffer);
+    });
+  });
+  httpServer.listen(function() {
+    var options = {
+      host: 'localhost',
+      protocol: 'ws',
+      port: httpServer.address().port,
+      path: '/',
+      allowBinaryMessages: true,
+      maxFrameSize: bigFileSize,
+    };
+    var client = yawl.createClient(options);
+    client.on('open', function() {
+      smallBufs.forEach(function(buf) {
+        client.sendBinary(buf);
+      });
+    });
+    var count = 0;
+    client.on('binaryMessage', function(buffer) {
+      count += 1;
+      if (count === smallBufCount) {
+        client.close();
+        httpServer.close(cb);
+      }
+    });
+  });
+}
+
+function makeSmallBufferWs(perMessageDeflate) {
+  return function (cb) {
+    var httpServer = http.createServer();
+    var wss = new ws.Server({
+      server: httpServer,
+      perMessageDeflate: perMessageDeflate,
+    });
+    wss.on('connection', function(ws) {
+      ws.on('message', function(buffer) {
+        ws.send(buffer);
+      });
+    });
+    httpServer.listen(function() {
+      var client = new ws('ws://localhost:' + httpServer.address().port + '/');
+      client.on('open', function() {
+        smallBufs.forEach(function(buf) {
+          client.send(buf);
+        });
+      });
+      var count = 0;
+      client.on('message', function(buffer) {
+        count += 1;
+        if (count === smallBufCount) {
+          client.close();
+          httpServer.close(cb);
+        }
+      });
+    });
+  };
+}
+
+function makeSmallBufferFaye(perMessageDeflate) {
+  return function (cb) {
+    var httpServer = http.createServer();
+    var extensions = [];
+    if (perMessageDeflate) extensions.push(deflate);
+    httpServer.on('upgrade', function(req, socket, head) {
+      var ws = new Faye(req, socket, head, null, { extensions: extensions });
+      ws.on('open', function() {
+        ws.on('message', function(buffer) {
+          ws.send(buffer);
+        });
+      });
+    });
+    httpServer.listen(function() {
+      var client = new Faye.Client(
+        'ws://localhost:' + httpServer.address().port + '/',
+        null,
+        { extensions: extensions }
+      );
+      client.on('open', function() {
+        smallBufs.forEach(function(buf) {
+          client.send(buf);
+        });
+      });
+      var count = 0;
+      client.on('message', function(buffer) {
+        count += 1;
+        if (count === smallBufCount) {
+          client.close();
+          httpServer.close(cb);
+        }
+      });
+    });
+  };
+}
+
+function noop() { }
+
+function reqWs() {
+  return ws ? null : 'npm install ws';
+}
+
+function reqFaye() {
+  return Faye ? null : 'npm install faye-websocket';
+}
+
+function reqFayeAndDeflate() {
+  return Faye && deflate ? null : 'npm install faye-websocket permessage-deflate';
+}
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000..9c854d1
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,647 @@
+var yawl = require('../');
+var url = require('url');
+var http = require('http');
+var assert = require('assert');
+var BufferList = require('bl');
+var describe = global.describe;
+var it = global.it;
+
+describe("yawl", function() {
+  it("fragmented messages with maxFrameSize Infinity", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      allowTextMessages: true,
+      allowFragmentedMessages: true,
+      maxFrameSize: Infinity,
+      origin: null,
+    });
+    wss.on('connection', function(ws, request) {
+      assert.strictEqual(request.url, "/huzzah");
+      ws.on('streamMessage', function(msg, isUtf8, len) {
+        assert.strictEqual(isUtf8, true);
+        assert.strictEqual(len, 5);
+        var bl = new BufferList();
+        msg.pipe(bl);
+        bl.on('finish', function() {
+          assert.strictEqual(bl.toString('utf8'), "hello")
+          ws.sendBinary(new Buffer([0x1, 0x3, 0x3, 0x7]));
+        });
+      });
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/huzzah',
+        allowBinaryMessages: true,
+        maxFrameSize: Infinity,
+      };
+      var client = yawl.createClient(options);
+      client.on('open', function(response) {
+        assert.ok(response);
+        assert.ok(client.socket);
+        client.sendText("hello");
+      });
+      client.on('closeMessage', function(statusCode, reason) {
+        throw new Error("closed: " + statusCode + ": " + reason);
+      });
+      client.on('streamMessage', function(msg, isUtf8, len) {
+        assert.strictEqual(isUtf8, false);
+        assert.strictEqual(len, 4);
+        var bl = new BufferList();
+        msg.pipe(bl);
+        bl.on('finish', function() {
+          var buf = bl.slice();
+          assert.strictEqual(buf[0], 0x1);
+          assert.strictEqual(buf[1], 0x3);
+          assert.strictEqual(buf[2], 0x3);
+          assert.strictEqual(buf[3], 0x7);
+          client.close();
+          httpServer.close(cb);
+        });
+      });
+    });
+  });
+
+  it("maxFrameSize", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      maxFrameSize: 10,
+      allowTextMessages: true,
+      origin: null,
+    });
+    var gotErr = false;
+    wss.on('connection', function(ws) {
+      ws.on('error', function(err) {
+        assert.strictEqual(err.statusCode, 1009);
+        assert.strictEqual(err.message, "exceeded max frame size");
+        gotErr = true;
+      });
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      client.on('open', function() {
+        client.sendText("this is a little bit longer than 10 chars");
+      });
+      var gotCloseMessage = false;
+      client.on('closeMessage', function(statusCode, reason) {
+        assert.strictEqual(statusCode, 1009);
+        assert.strictEqual(reason, "exceeded max frame size");
+        gotCloseMessage = true;
+      });
+      client.on('close', function() {
+        assert.strictEqual(gotCloseMessage, true);
+        assert.strictEqual(gotErr, true);
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("buffered messages", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      allowTextMessages: true,
+      allowBinaryMessages: true,
+      origin: null,
+    });
+    var buf = new Buffer(65536);
+    buf.fill('a');
+    var str = buf.toString('utf8');
+    wss.on('connection', function(ws) {
+      ws.on('textMessage', function(message) {
+        assert.strictEqual(message, str);
+        var smallBuf = new Buffer(1024);
+        smallBuf[100] = 10;
+        smallBuf[200] = 20;
+        smallBuf[400] = 40;
+        smallBuf[800] = 80;
+        ws.sendBinary(smallBuf);
+      });
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+        allowBinaryMessages: true,
+      };
+      var client = yawl.createClient(options);
+      client.on('open', function() {
+        client.sendText(str);
+      });
+      var gotMessage = false;
+      client.on('binaryMessage', function(message) {
+        assert.strictEqual(message[100], 10);
+        assert.strictEqual(message[200], 20);
+        assert.strictEqual(message[400], 40);
+        assert.strictEqual(message[800], 80);
+        client.close();
+        gotMessage = true;
+      });
+      client.on('close', function() {
+        assert.strictEqual(gotMessage, true);
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("streaming messages", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      allowTextMessages: true,
+      allowBinaryMessages: true,
+      allowFragmentedMessages: true,
+      origin: null,
+    });
+    wss.on('connection', function(ws) {
+      ws.on('textMessage', function(message) {
+        ws.sendText(message);
+      });
+      ws.on('binaryMessage', function(message) {
+        ws.sendBinary(message);
+      });
+      ws.on('streamMessage', function(stream, isUtf8, length) {
+        stream.pipe(ws.sendStream(isUtf8, length));
+      });
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+        allowBinaryMessages: true,
+        allowTextMessages: true,
+        allowFragmentedMessages: true,
+      };
+      var client = yawl.createClient(options);
+      client.on('open', function() {
+        var stream = client.sendStream(true);
+        stream.write("this is the first fragment");
+        stream.write("this is the second fragment");
+        stream.end();
+      });
+      client.on('streamMessage', function(stream, isUtf8, length) {
+        assert.strictEqual(isUtf8, true);
+        assert.equal(length, null);
+        var bl = new BufferList();
+        stream.pipe(bl);
+        bl.on('finish', function() {
+          assert.strictEqual(bl.toString('utf8'),
+            "this is the first fragmentthis is the second fragment");
+          client.close();
+        });
+      });
+      client.on('close', function() {
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("client emits error when server misbehaves", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      origin: null,
+    });
+    var serverGotClose = false;
+    wss.on('connection', function(ws) {
+      ws.on('closeMessage', function(statusCode, message) {
+        assert.strictEqual(statusCode, 1002, 'invalid reserve bits');
+        serverGotClose = true;
+      });
+      ws.socket.write("trash data");
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      var errorOccurred = false;
+      var gotOpen = false;
+      client.on('open', function() {
+        gotOpen = true;
+      });
+      client.on('error', function(err) {
+        assert.strictEqual(err.statusCode, 1002);
+        errorOccurred = true;
+      });
+      client.on('closeMessage', function(statusCode, message) {
+        throw new Error("did not expect client close message");
+      });
+      client.on('close', function() {
+        assert.strictEqual(errorOccurred, true);
+        assert.strictEqual(serverGotClose, true);
+        assert.strictEqual(gotOpen, true);
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("allowUnfragmentedMessages = false", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      origin: null,
+      allowUnfragmentedMessages: false,
+      allowBinaryMessages: true,
+      allowTextMessages: true,
+    });
+    var gotServerError = false;
+    wss.on('connection', function(ws) {
+      ws.on('error', function(err) {
+        assert.strictEqual(err.statusCode, 1003);
+        assert.strictEqual(err.message, "unfragmented messages not allowed");
+        gotServerError = true;
+      });
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      var gotOpen = false;
+      client.on('open', function() {
+        client.sendText("hi");
+        gotOpen = true;
+      });
+      var gotCloseMessage = false;
+      client.on('closeMessage', function(statusCode, message) {
+        assert.strictEqual(statusCode, 1003);
+        assert.strictEqual(message, "unfragmented messages not allowed");
+        gotCloseMessage = true;
+      });
+      client.on('close', function() {
+        assert.strictEqual(gotServerError, true);
+        assert.strictEqual(gotCloseMessage, true);
+        assert.strictEqual(gotOpen, true);
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("send a ping and get a pong during a fragmented stream", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      allowBinaryMessages: true,
+      allowFragmentedMessages: true,
+      origin: null,
+    });
+    wss.on('connection', function(ws) {
+      ws.on('streamMessage', function(msg, isUtf8, length) {
+        var bl = new BufferList();
+        msg.pipe(bl);
+        bl.on('finish', function() {
+          assert.strictEqual(bl.toString('utf8'), 'msg1msg2');
+          ws.close();
+        });
+      });
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      var outStream;
+      client.on('open', function() {
+        outStream = client.sendStream();
+        outStream.write("msg1");
+        client.sendPingText("msg2");
+      });
+      client.on('pongMessage', function(buffer) {
+        outStream.write(buffer);
+        outStream.end();
+      });
+      client.on('close', function() {
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("parseExtensionList missing header", function() {
+    assert.deepEqual(yawl.parseExtensionList({headers: {}}), null);
+  });
+
+  it("parseExtensionList complicated", function() {
+    var request = {
+      headers: {
+        'sec-websocket-extensions': 'foo,bar; baz=2;extra, third; arg="quoted"',
+      },
+    };
+    var expected = [
+      {
+        name: 'foo',
+        params: [],
+      },
+      {
+        name: 'bar',
+        params: [
+          {
+            name: 'baz',
+            value: '2',
+          },
+          {
+            name: 'extra',
+            value: null,
+          },
+        ],
+      },
+      {
+        name: 'third',
+        params: [
+          {
+            name: 'arg',
+            value: 'quoted',
+          },
+        ],
+      },
+    ];
+    assert.deepEqual(yawl.parseExtensionList(request), expected);
+  });
+
+  it("parseExtensionList", function() {
+    var request = {
+      headers: {
+        'sec-websocket-extensions': 'word',
+      },
+    };
+    var expected = [
+      {
+        name: 'word',
+        params: [],
+      },
+    ];
+    assert.deepEqual(yawl.parseExtensionList(request), expected);
+  });
+
+  it("parseSubProtocolList", function() {
+    var request = {
+      headers: {
+        'sec-websocket-protocol': 'chat, SuperChat',
+      },
+    };
+    assert.deepEqual(yawl.parseSubProtocolList(request), ['chat', 'superchat']);
+  });
+
+  it("client throws error for invalid protocol", function() {
+    assert.throws(function() {
+      var ws = yawl.createClient(url.parse("http://example.com/foo"));
+    }, /invalid protocol/);
+  });
+
+  it("client emits error when server hangs up", function(cb) {
+    var httpServer = http.createServer(function(request, response) {
+      response.statusCode = 200;
+      response.write("hello");
+      response.end();
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      client.on('error', function(err) {
+        assert.strictEqual(err.code, 'ECONNRESET');
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("client emits error when server responds with HTTP", function(cb) {
+    var httpServer = http.createServer();
+    httpServer.on('upgrade', function(request, socket, firstBuffer) {
+      socket.write(
+        "HTTP/1.1 200 OK\r\n" +
+        "Connection: close\r\n" +
+        "Content-Length: 5\r\n" +
+        "\r\n" +
+        "hello");
+      socket.end();
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      client.on('error', function(err) {
+        assert.strictEqual(err.response.statusCode, 200);
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("server requires explicitly setting origin", function() {
+    var httpServer = http.createServer();
+    assert.throws(function() {
+      var wss = yawl.createServer({ server: httpServer });
+    }, /explicitly set origin/);
+  });
+
+  it("negotiating fail", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      negotiate: true,
+      origin: null,
+    });
+    wss.on('negotiate', function(request, socket, callback) {
+      callback(null);
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      client.on('error', function(err) {
+        assert.strictEqual(err.message, "server returned HTTP 400");
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("negotiating succeed", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      negotiate: true,
+      origin: null,
+    });
+    wss.on('negotiate', function(request, socket, callback) {
+      callback({});
+    });
+    wss.on('connection', function(ws) {
+      ws.on('pingMessage', function() {
+        ws.sendPingText("oh you know it");
+      });
+      ws.on('pongMessage', function() {
+        ws.close();
+        httpServer.close(cb);
+      });
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      client.on('open', function() {
+        client.sendPingText("happy yay ping time");
+      });
+    });
+  });
+
+  it("invalid origin", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      origin: "http://example.com",
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      client.on('error', function(err) {
+        assert.strictEqual(err.message, "server returned HTTP 403");
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("valid origin", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      origin: "http://example.com",
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+        origin: "http://example.com",
+      };
+      var client = yawl.createClient(options);
+      client.on('open', function() {
+        client.close();
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("invalid websocket version", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      origin: null,
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+        extraHeaders: {
+          'Sec-WebSocket-Version': "12",
+        },
+      };
+      var client = yawl.createClient(options);
+      client.on('error', function(err) {
+        assert.strictEqual(err.message, "server returned HTTP 426");
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("invalid websocket key", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      origin: null,
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+        extraHeaders: {
+          'Sec-WebSocket-Key': "",
+        },
+      };
+      var client = yawl.createClient(options);
+      client.on('error', function(err) {
+        assert.strictEqual(err.message, "server returned HTTP 400");
+        httpServer.close(cb);
+      });
+    });
+  });
+
+  it("trying to send too big payloads", function(cb) {
+    var httpServer = http.createServer();
+    var wss = yawl.createServer({
+      server: httpServer,
+      origin: null,
+    });
+    httpServer.listen(function() {
+      var options = {
+        host: 'localhost',
+        protocol: 'ws',
+        port: httpServer.address().port,
+        path: '/',
+      };
+      var client = yawl.createClient(options);
+      client.on('open', function() {
+        var tooBigForControlFrame = new Buffer(128);
+        assert.throws(function() {
+          client.sendPingBinary(tooBigForControlFrame);
+        }, /message too long/);
+        assert.throws(function() {
+          client.sendPongText(tooBigForControlFrame.toString('utf8'));
+        }, /message too long/);
+        assert.throws(function() {
+          client.close(0, tooBigForControlFrame.toString('utf8'));
+        }, /message too long/);
+        client.close();
+        httpServer.close(cb);
+      });
+    });
+  });
+});

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



More information about the Pkg-javascript-commits mailing list