pax_global_header00006660000000000000000000000064125257250330014516gustar00rootroot0000000000000052 comment=8d1da3669c18575a646c6340eebee4c2721531d0 node-yawl-1.0.2/000077500000000000000000000000001252572503300134155ustar00rootroot00000000000000node-yawl-1.0.2/.gitignore000066400000000000000000000000301252572503300153760ustar00rootroot00000000000000/node_modules /coverage node-yawl-1.0.2/.travis.yml000066400000000000000000000002601252572503300155240ustar00rootroot00000000000000language: node_js node_js: - "0.10" script: - "npm run test-travis" after_script: - "npm install coveralls@2 && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" node-yawl-1.0.2/CHANGELOG.md000066400000000000000000000006211252572503300152250ustar00rootroot00000000000000### 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. node-yawl-1.0.2/LICENSE000066400000000000000000000020721252572503300144230ustar00rootroot00000000000000The 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. node-yawl-1.0.2/README.md000066400000000000000000000416651252572503300147100ustar00rootroot00000000000000# 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. node-yawl-1.0.2/index.js000066400000000000000000000725171252572503300150760ustar00rootroot00000000000000var 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; } node-yawl-1.0.2/package.json000066400000000000000000000021531252572503300157040ustar00rootroot00000000000000{ "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 ", "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" } } node-yawl-1.0.2/test/000077500000000000000000000000001252572503300143745ustar00rootroot00000000000000node-yawl-1.0.2/test/autobahn-client.js000066400000000000000000000043561252572503300200170ustar00rootroot00000000000000var 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) {}); } node-yawl-1.0.2/test/autobahn-server.js000066400000000000000000000014571252572503300200460ustar00rootroot00000000000000var 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"); }); node-yawl-1.0.2/test/perf.js000066400000000000000000000177061252572503300157010ustar00rootroot00000000000000var 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'; } node-yawl-1.0.2/test/test.js000066400000000000000000000442121252572503300157140ustar00rootroot00000000000000var 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); }); }); }); });