[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