diff --git a/benchmark/websockets/parser.benchmark.js b/benchmark/websockets/parser.benchmark.js new file mode 100644 index 00000000000000..ff5f737c0febc7 --- /dev/null +++ b/benchmark/websockets/parser.benchmark.js @@ -0,0 +1,115 @@ +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +/** + * Benchmark dependencies. + */ + +var benchmark = require('benchmark') + , Receiver = require('../').Receiver + , suite = new benchmark.Suite('Receiver'); +require('tinycolor'); +require('./util'); + +/** + * Setup receiver. + */ + +suite.on('start', function () { + receiver = new Receiver(); +}); + +suite.on('cycle', function () { + receiver = new Receiver(); +}); + +/** + * Benchmarks. + */ + +var pingMessage = 'Hello' + , pingPacket1 = getBufferFromHexString('89 ' + (pack(2, 0x80 | pingMessage.length)) + + ' 34 83 a8 68 '+ getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'))); +suite.add('ping message', function () { + receiver.add(pingPacket1); +}); + +var pingPacket2 = getBufferFromHexString('89 00') +suite.add('ping with no data', function () { + receiver.add(pingPacket2); +}); + +var closePacket = getBufferFromHexString('88 00'); +suite.add('close message', function () { + receiver.add(closePacket); + receiver.endPacket(); +}); + +var maskedTextPacket = getBufferFromHexString('81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5'); +suite.add('masked text message', function () { + receiver.add(maskedTextPacket); +}); + +binaryDataPacket = (function() { + var length = 125 + , message = new Buffer(length) + for (var i = 0; i < length; ++i) message[i] = i % 10; + return getBufferFromHexString('82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + + getHexStringFromBuffer(mask(message), '34 83 a8 68')); +})(); +suite.add('binary data (125 bytes)', function () { + try { + receiver.add(binaryDataPacket); + + } + catch(e) {console.log(e)} +}); + +binaryDataPacket2 = (function() { + var length = 65535 + , message = new Buffer(length) + for (var i = 0; i < length; ++i) message[i] = i % 10; + return getBufferFromHexString('82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + + getHexStringFromBuffer(mask(message), '34 83 a8 68')); +})(); +suite.add('binary data (65535 bytes)', function () { + receiver.add(binaryDataPacket2); +}); + +binaryDataPacket3 = (function() { + var length = 200*1024 + , message = new Buffer(length) + for (var i = 0; i < length; ++i) message[i] = i % 10; + return getBufferFromHexString('82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + + getHexStringFromBuffer(mask(message), '34 83 a8 68')); +})(); +suite.add('binary data (200 kB)', function () { + receiver.add(binaryDataPacket3); +}); + +/** + * Output progress. + */ + +suite.on('cycle', function (bench, details) { + console.log('\n ' + suite.name.grey, details.name.white.bold); + console.log(' ' + [ + details.hz.toFixed(2).cyan + ' ops/sec'.grey + , details.count.toString().white + ' times executed'.grey + , 'benchmark took '.grey + details.times.elapsed.toString().white + ' sec.'.grey + , + ].join(', '.grey)); +}); + +/** + * Run/export benchmarks. + */ + +if (!module.parent) { + suite.run(); +} else { + module.exports = suite; +} diff --git a/benchmark/websockets/sender.benchmark.js b/benchmark/websockets/sender.benchmark.js new file mode 100644 index 00000000000000..20c171a509a0e2 --- /dev/null +++ b/benchmark/websockets/sender.benchmark.js @@ -0,0 +1,66 @@ +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +/** + * Benchmark dependencies. + */ + +var benchmark = require('benchmark') + , Sender = require('../').Sender + , suite = new benchmark.Suite('Sender'); +require('tinycolor'); +require('./util'); + +/** + * Setup sender. + */ + +suite.on('start', function () { + sender = new Sender(); + sender._socket = { write: function() {} }; +}); + +suite.on('cycle', function () { + sender = new Sender(); + sender._socket = { write: function() {} }; +}); + +/** + * Benchmarks + */ + +framePacket = new Buffer(200*1024); +framePacket.fill(99); +suite.add('frameAndSend, unmasked (200 kB)', function () { + sender.frameAndSend(0x2, framePacket, true, false); +}); +suite.add('frameAndSend, masked (200 kB)', function () { + sender.frameAndSend(0x2, framePacket, true, true); +}); + +/** + * Output progress. + */ + +suite.on('cycle', function (bench, details) { + console.log('\n ' + suite.name.grey, details.name.white.bold); + console.log(' ' + [ + details.hz.toFixed(2).cyan + ' ops/sec'.grey + , details.count.toString().white + ' times executed'.grey + , 'benchmark took '.grey + details.times.elapsed.toString().white + ' sec.'.grey + , + ].join(', '.grey)); +}); + +/** + * Run/export benchmarks. + */ + +if (!module.parent) { + suite.run(); +} else { + module.exports = suite; +} diff --git a/benchmark/websockets/speed.js b/benchmark/websockets/speed.js new file mode 100644 index 00000000000000..3ce6414610a785 --- /dev/null +++ b/benchmark/websockets/speed.js @@ -0,0 +1,105 @@ +var cluster = require('cluster') + , WebSocket = require('../') + , WebSocketServer = WebSocket.Server + , crypto = require('crypto') + , util = require('util') + , ansi = require('ansi'); +require('tinycolor'); + +function roundPrec(num, prec) { + var mul = Math.pow(10, prec); + return Math.round(num * mul) / mul; +} + +function humanSize(bytes) { + if (bytes >= 1048576) return roundPrec(bytes / 1048576, 2) + ' MB'; + if (bytes >= 1024) return roundPrec(bytes / 1024, 2) + ' kB'; + return roundPrec(bytes, 2) + ' B'; +} + +function generateRandomData(size) { + var buffer = new Buffer(size); + for (var i = 0; i < size; ++i) { + buffer[i] = ~~(Math.random() * 127); + } + return buffer; +} + +if (cluster.isMaster) { + var wss = new WebSocketServer({port: 8181}, function() { + cluster.fork(); + }); + wss.on('connection', function(ws) { + ws.on('message', function(data, flags) { + ws.send(data, {binary: flags&&flags.binary}); + }); + ws.on('close', function() {}); + }); + cluster.on('death', function(worker) { + wss.close(); + }); +} +else { + var cursor = ansi(process.stdout); + + var configs = [ + [true, 10000, 64], + [true, 5000, 16*1024], + [true, 1000, 128*1024], + [true, 100, 1024*1024], + [true, 1, 500*1024*1024], + [false, 10000, 64], + [false, 5000, 16*1024], + [false, 1000, 128*1024], + [false, 100, 1024*1024], + ]; + + var largest = configs[0][1]; + for (var i = 0, l = configs.length; i < l; ++i) { + if (configs[i][2] > largest) largest = configs[i][2]; + } + + console.log('Generating %s of test data ...', humanSize(largest)); + var randomBytes = generateRandomData(largest); + + function roundtrip(useBinary, roundtrips, size, cb) { + var data = randomBytes.slice(0, size); + var prefix = util.format('Running %d roundtrips of %s %s data', roundtrips, humanSize(size), useBinary ? 'binary' : 'text'); + console.log(prefix); + var client = new WebSocket('ws://localhost:' + '8181'); + var dt; + var roundtrip = 0; + function send() { + client.send(data, {binary: useBinary}); + } + client.on('error', function(e) { + console.error(e); + process.exit(); + }); + client.on('open', function() { + dt = Date.now(); + send(); + }); + client.on('message', function(data, flags) { + if (++roundtrip == roundtrips) { + var elapsed = Date.now() - dt; + cursor.up(); + console.log('%s:\t%ss\t%s' + , useBinary ? prefix.green : prefix.cyan + , roundPrec(elapsed / 1000, 1).toString().green.bold + , (humanSize((size * roundtrips) / elapsed * 1000) + '/s').blue.bold); + client.close(); + cb(); + return; + } + process.nextTick(send); + }); + } + + (function run() { + if (configs.length == 0) process.exit(); + var config = configs.shift(); + config.push(run); + roundtrip.apply(null, config); + })(); +} \ No newline at end of file diff --git a/benchmark/websockets/util.js b/benchmark/websockets/util.js new file mode 100644 index 00000000000000..5f0128190841e3 --- /dev/null +++ b/benchmark/websockets/util.js @@ -0,0 +1,105 @@ +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +/** + * Returns a Buffer from a "ff 00 ff"-type hex string. + */ + +getBufferFromHexString = function(byteStr) { + var bytes = byteStr.split(' '); + var buf = new Buffer(bytes.length); + for (var i = 0; i < bytes.length; ++i) { + buf[i] = parseInt(bytes[i], 16); + } + return buf; +} + +/** + * Returns a hex string from a Buffer. + */ + +getHexStringFromBuffer = function(data) { + var s = ''; + for (var i = 0; i < data.length; ++i) { + s += padl(data[i].toString(16), 2, '0') + ' '; + } + return s.trim(); +} + +/** + * Splits a buffer in two parts. + */ + +splitBuffer = function(buffer) { + var b1 = new Buffer(Math.ceil(buffer.length / 2)); + buffer.copy(b1, 0, 0, b1.length); + var b2 = new Buffer(Math.floor(buffer.length / 2)); + buffer.copy(b2, 0, b1.length, b1.length + b2.length); + return [b1, b2]; +} + +/** + * Performs hybi07+ type masking on a hex string or buffer. + */ + +mask = function(buf, maskString) { + if (typeof buf == 'string') buf = new Buffer(buf); + var mask = getBufferFromHexString(maskString || '34 83 a8 68'); + for (var i = 0; i < buf.length; ++i) { + buf[i] ^= mask[i % 4]; + } + return buf; +} + +/** + * Returns a hex string representing the length of a message + */ + +getHybiLengthAsHexString = function(len, masked) { + if (len < 126) { + var buf = new Buffer(1); + buf[0] = (masked ? 0x80 : 0) | len; + } + else if (len < 65536) { + var buf = new Buffer(3); + buf[0] = (masked ? 0x80 : 0) | 126; + getBufferFromHexString(pack(4, len)).copy(buf, 1); + } + else { + var buf = new Buffer(9); + buf[0] = (masked ? 0x80 : 0) | 127; + getBufferFromHexString(pack(16, len)).copy(buf, 1); + } + return getHexStringFromBuffer(buf); +} + +/** + * Unpacks a Buffer into a number. + */ + +unpack = function(buffer) { + var n = 0; + for (var i = 0; i < buffer.length; ++i) { + n = (i == 0) ? buffer[i] : (n * 256) + buffer[i]; + } + return n; +} + +/** + * Returns a hex string, representing a specific byte count 'length', from a number. + */ + +pack = function(length, number) { + return padl(number.toString(16), length, '0').replace(/([0-9a-f][0-9a-f])/gi, '$1 ').trim(); +} + +/** + * Left pads the string 's' to a total length of 'n' with char 'c'. + */ + +padl = function(s, n, c) { + return new Array(1 + n - s.length).join(c) + s; +} diff --git a/doc/api/websockets.md b/doc/api/websockets.md new file mode 100644 index 00000000000000..8f23d51c74e0ca --- /dev/null +++ b/doc/api/websockets.md @@ -0,0 +1,234 @@ +# ws + +## Class: ws.Server + +This class is a WebSocket server. It is an `EventEmitter`. + +### new ws.Server([options], [callback]) + +* `options` Object + * `host` String + * `port` Number + * `server` http.Server + * `verifyClient` Function + * `handleProtocols` Function + * `path` String + * `noServer` Boolean + * `disableHixie` Boolean + * `clientTracking` Boolean + * `perMessageDeflate` Boolean|Object +* `callback` Function + +Construct a new server object. + +Either `port` or `server` must be provided, otherwise you might enable +`noServer` if you want to pass the requests directly. Please note that the +`callback` is only used when you supply the a `port` number in the options. + +### options.verifyClient + +`verifyClient` can be used in two different ways. If it is provided with two arguments then those are: +* `info` Object: + * `origin` String: The value in the Origin header indicated by the client. + * `req` http.ClientRequest: The client HTTP GET request. + * `secure` Boolean: `true` if `req.connection.authorized` or `req.connection.encrypted` is set. +* `cb` Function: A callback that must be called by the user upon inspection of the `info` fields. Arguments in this callback are: + * `result` Boolean: Whether the user accepts or not the handshake. + * `code` Number: If `result` is `false` this field determines the HTTP error status code to be sent to the client. + * `name` String: If `result` is `false` this field determines the HTTP reason phrase. + +If `verifyClient` is provided with a single argument then that is: +* `info` Object: Same as above. + +In this case the return code (Boolean) of the function determines whether the handshake is accepted or not. + +If `verifyClient` is not set then the handshake is automatically accepted. + +### options.handleProtocols + +`handleProtocols` receives two arguments: +* `protocols` Array: The list of WebSocket sub-protocols indicated by the client in the Sec-WebSocket-Protocol header. +* `cb` Function: A callback that must be called by the user upon inspection of the protocols. Arguments in this callback are: + * `result` Boolean: Whether the user accepts or not the handshake. + * `protocol` String: If `result` is `true` then this field sets the value of the Sec-WebSocket-Protocol header in the HTTP 101 response. + +If `handleProtocols` is not set then the handshake is accepted regardless the value of Sec-WebSocket-Protocol header. If it is set but the user does not invoke the `cb` callback then the handshake is rejected with error HTTP 501. + +### options.perMessageDeflate + +`perMessageDeflate` can be used to control the behavior of [permessage-deflate extension](https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19). The extension is disabled when `false`. Defaults to `true`. If an object is provided then that is extension parameters: + +* `serverNoContextTakeover` Boolean: Whether to use context take over or not. +* `clientNoContextTakeover` Boolean: The value to be requested to clients whether to use context take over or not. +* `serverMaxWindowBits` Number: The value of windowBits. +* `clientMaxWindowBits` Number: The value of max windowBits to be requested to clients. +* `memLevel` Number: The value of memLevel. + +If a property is empty then either an offered configuration or a default value is used. + +### server.close() + +Close the server and terminate all clients + +### server.handleUpgrade(request, socket, upgradeHead, callback) + +Handles a HTTP Upgrade request. `request` is an instance of `http.ServerRequest`, `socket` is an instance of `net.Socket`. + +When the Upgrade was successfully, the `callback` will be called with a `ws.WebSocket` object as parameter. + +### Event: 'error' + +`function (error) { }` + +If the underlying server emits an error, it will be forwarded here. + +### Event: 'headers' + +`function (headers) { }` + +Emitted with the object of HTTP headers that are going to be written to the `Stream` as part of the handshake. + +### Event: 'connection' + +`function (socket) { }` + +When a new WebSocket connection is established. `socket` is an object of type `ws.WebSocket`. + + +## Class: ws.WebSocket + +This class represents a WebSocket connection. It is an `EventEmitter`. + +### new ws.WebSocket(address, [protocols], [options]) + +* `address` String +* `protocols` String|Array +* `options` Object + * `protocol` String + * `agent` Agent + * `headers` Object + * `protocolVersion` Number|String + -- the following only apply if `address` is a String + * `host` String + * `origin` String + * `pfx` String|Buffer + * `key` String|Buffer + * `passphrase` String + * `cert` String|Buffer + * `ca` Array + * `ciphers` String + * `rejectUnauthorized` Boolean + * `perMessageDeflate` Boolean|Object + * `localAddress` String + +Instantiating with an `address` creates a new WebSocket client object. If `address` is an Array (request, socket, rest), it is instantiated as a Server client (e.g. called from the `ws.Server`). + +### options.perMessageDeflate + +Parameters of permessage-deflate extension which have the same form with the one for `ws.Server` except the direction of requests. (e.g. `serverNoContextTakeover` is the value to be requested to the server) + +### websocket.bytesReceived + +Received bytes count. + +### websocket.readyState + +Possible states are `WebSocket.CONNECTING`, `WebSocket.OPEN`, `WebSocket.CLOSING`, `WebSocket.CLOSED`. + +### websocket.protocolVersion + +The WebSocket protocol version used for this connection, `8`, `13` or `hixie-76` (the latter only for server clients). + +### websocket.url + +The URL of the WebSocket server (only for clients) + +### websocket.supports + +Describes the feature of the used protocol version. E.g. `supports.binary` is a boolean that describes if the connection supports binary messages. + +### websocket.upgradeReq + +The http request that initiated the upgrade. Useful for parsing authorty headers, cookie headers and other information to associate a specific Websocket to a specific Client. This is only available for WebSockets constructed by a Server. + +### websocket.close([code], [data]) + +Gracefully closes the connection, after sending a description message + +### websocket.pause() + +Pause the client stream + +### websocket.ping([data], [options], [dontFailWhenClosed]) + +Sends a ping. `data` is sent, `options` is an object with members `mask` and `binary`. `dontFailWhenClosed` indicates whether or not to throw if the connection isnt open. + +### websocket.pong([data], [options], [dontFailWhenClosed]) + +Sends a pong. `data` is sent, `options` is an object with members `mask` and `binary`. `dontFailWhenClosed` indicates whether or not to throw if the connection isnt open. + + +### websocket.resume() + +Resume the client stream + +### websocket.send(data, [options], [callback]) + +Sends `data` through the connection. `options` can be an object with members `mask`, `binary` and `compress`. The optional `callback` is executed after the send completes. + +### websocket.stream([options], callback) + +Streams data through calls to a user supplied function. `options` can be an object with members `mask` and `binary`. `callback` is executed on successive ticks of which send is `function (data, final)`. + +### websocket.terminate() + +Immediately shuts down the connection + +### websocket.onopen +### websocket.onerror +### websocket.onclose +### websocket.onmessage + +Emulates the W3C Browser based WebSocket interface using function members. + +### websocket.addEventListener(method, listener) + +Emulates the W3C Browser based WebSocket interface using addEventListener. + +### Event: 'error' + +`function (error) { }` + +If the client emits an error, this event is emitted (errors from the underlying `net.Socket` are forwarded here). + +### Event: 'close' + +`function (code, message) { }` + +Is emitted when the connection is closed. `code` is defined in the WebSocket specification. + +The `close` event is also emitted when then underlying `net.Socket` closes the connection (`end` or `close`). + +### Event: 'message' + +`function (data, flags) { }` + +Is emitted when data is received. `flags` is an object with member `binary`. + +### Event: 'ping' + +`function (data, flags) { }` + +Is emitted when a ping is received. `flags` is an object with member `binary`. + +### Event: 'pong' + +`function (data, flags) { }` + +Is emitted when a pong is received. `flags` is an object with member `binary`. + +### Event: 'open' + +`function () { }` + +Emitted when the connection is established. diff --git a/lib/internal/websockets/Extensions.js b/lib/internal/websockets/Extensions.js new file mode 100644 index 00000000000000..cc25f65c71dc2c --- /dev/null +++ b/lib/internal/websockets/Extensions.js @@ -0,0 +1,61 @@ + +const util = require('util'); + +/** + * Parse extensions header value + */ +function parse(value) { + value = value || ''; + + var extensions = {}; + + value.split(',').forEach(function(v) { + var params = v.split(';'); + var token = params.shift().trim(); + var paramsList = extensions[token] = extensions[token] || []; + var parsedParams = {}; + + params.forEach(function(param) { + var parts = param.trim().split('='); + var key = parts[0]; + var value = parts[1]; + if (typeof value === 'undefined') { + value = true; + } else { + // unquote value + if (value[0] === '"') { + value = value.slice(1); + } + if (value[value.length - 1] === '"') { + value = value.slice(0, value.length - 1); + } + } + (parsedParams[key] = parsedParams[key] || []).push(value); + }); + + paramsList.push(parsedParams); + }); + + return extensions; +} + +function format(value) { + return Object.keys(value).map(function(token) { + var paramsList = value[token]; + if (!util.isArray(paramsList)) { + paramsList = [paramsList]; + } + return paramsList.map(function(params) { + return [token].concat(Object.keys(params).map(function(k) { + var p = params[k]; + if (!util.isArray(p)) p = [p]; + return p.map(function(v) { + return v === true ? k : k + '=' + v; + }).join('; '); + })).join('; '); + }).join(', '); + }).join(', '); +} + +exports.parse = parse; +exports.format = format; diff --git a/lib/internal/websockets/Opcodes.js b/lib/internal/websockets/Opcodes.js new file mode 100644 index 00000000000000..bbee300ece97d4 --- /dev/null +++ b/lib/internal/websockets/Opcodes.js @@ -0,0 +1,319 @@ +'use strict' +const WSErrorCodes = require('websockets').WSErrorCodes; + +// Legacy from cpp dep TODO(eljefedelrodeodeljefe): cleanup +const Validation = { + isValidUTF8: function(buffer) { + return true; + } + }; + +function readUInt16BE(start) { + return (this[start]<<8) + this[start+1]; +} + +function readUInt32BE(start) { + return (this[start]<<24) + (this[start+1]<<16) + (this[start+2]<<8) + this[start+3]; +} + +function clone(obj) { + var cloned = {}; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + cloned[k] = obj[k]; + } + } + return cloned; +} + +/** +* Opcode handlers +*/ +var opcodes = {} + +opcodes['text'] = { + start: function(data) { + var self = this; + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + opcodes['text'].getData.call(self, firstLength); + } + else if (firstLength == 126) { + self.expectHeader(2, function(data) { + opcodes['text'].getData.call(self, readUInt16BE.call(data, 0)); + }); + } + else if (firstLength == 127) { + self.expectHeader(8, function(data) { + if (readUInt32BE.call(data, 0) != 0) { + self.error('packets with length spanning more than 32 bit is currently not supported', 1008); + return; + } + opcodes['text'].getData.call(self, readUInt32BE.call(data, 4)); + }); + } + }, + getData: function(length) { + var self = this; + if (self.state.masked) { + self.expectHeader(4, function(data) { + var mask = data; + self.expectData(length, function(data) { + opcodes['text'].finish.call(self, mask, data); + }); + }); + } + else { + self.expectData(length, function(data) { + opcodes['text'].finish.call(self, null, data); + }); + } + }, + finish: function(mask, data) { + var self = this; + var packet = this.unmask(mask, data, true) || new Buffer(0); + var state = clone(this.state); + this.messageHandlers.push(function(cb) { + self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) { + if (err) return self.error(err.message, 1007); + if (buffer != null) self.currentMessage.push(buffer); + + if (state.lastFragment) { + var messageBuffer = self.concatBuffers(self.currentMessage); + self.currentMessage = []; + if (!Validation.isValidUTF8(messageBuffer)) { + self.error('invalid utf8 sequence', 1007); + return; + } + self.ontext(messageBuffer.toString('utf8'), {masked: state.masked, buffer: messageBuffer}); + } + cb(); + }); + }); + this.flush(); + this.endPacket(); + } +} + + +opcodes['binary'] = { + start: function(data) { + var self = this; + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + opcodes['binary'].getData.call(self, firstLength); + } + else if (firstLength == 126) { + self.expectHeader(2, function(data) { + opcodes['binary'].getData.call(self, readUInt16BE.call(data, 0)); + }); + } + else if (firstLength == 127) { + self.expectHeader(8, function(data) { + if (readUInt32BE.call(data, 0) != 0) { + self.error('packets with length spanning more than 32 bit is currently not supported', 1008); + return; + } + opcodes['binary'].getData.call(self, readUInt32BE.call(data, 4, true)); + }); + } + }, + getData: function(length) { + var self = this; + if (self.state.masked) { + self.expectHeader(4, function(data) { + var mask = data; + self.expectData(length, function(data) { + opcodes['binary'].finish.call(self, mask, data); + }); + }); + } + else { + self.expectData(length, function(data) { + opcodes['binary'].finish.call(self, null, data); + }); + } + }, + finish: function(mask, data) { + var self = this; + var packet = this.unmask(mask, data, true) || new Buffer(0); + var state = clone(this.state); + this.messageHandlers.push(function(cb) { + self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) { + if (err) return self.error(err.message, 1007); + if (buffer != null) self.currentMessage.push(buffer); + if (state.lastFragment) { + var messageBuffer = self.concatBuffers(self.currentMessage); + self.currentMessage = []; + self.onbinary(messageBuffer, {masked: state.masked, buffer: messageBuffer}); + } + cb(); + }); + }); + this.flush(); + this.endPacket(); + } +}, + + +opcodes['close'] = { + start: function(data) { + var self = this; + if (self.state.lastFragment == false) { + self.error('fragmented close is not supported', 1002); + return; + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + opcodes['close'].getData.call(self, firstLength); + } + else { + self.error('control frames cannot have more than 125 bytes of data', 1002); + } + }, + getData: function(length) { + var self = this; + if (self.state.masked) { + self.expectHeader(4, function(data) { + var mask = data; + self.expectData(length, function(data) { + opcodes['close'].finish.call(self, mask, data); + }); + }); + } + else { + self.expectData(length, function(data) { + opcodes['close'].finish.call(self, null, data); + }); + } + }, + finish: function(mask, data) { + var self = this; + data = self.unmask(mask, data, true); + + var state = clone(this.state); + this.messageHandlers.push(function() { + if (data && data.length == 1) { + self.error('close packets with data must be at least two bytes long', 1002); + return; + } + var code = data && data.length > 1 ? readUInt16BE.call(data, 0) : 1000; + if (!WSErrorCodes.isValidErrorCode(code)) { + self.error('invalid error code', 1002); + return; + } + var message = ''; + if (data && data.length > 2) { + var messageBuffer = data.slice(2); + if (!Validation.isValidUTF8(messageBuffer)) { + self.error('invalid utf8 sequence', 1007); + return; + } + message = messageBuffer.toString('utf8'); + } + self.onclose(code, message, {masked: state.masked}); + self.reset(); + }); + this.flush(); + }, +} + + +opcodes['ping'] = { + start: function(data) { + var self = this; + if (self.state.lastFragment == false) { + self.error('fragmented ping is not supported', 1002); + return; + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + opcodes['ping'].getData.call(self, firstLength); + } + else { + self.error('control frames cannot have more than 125 bytes of data', 1002); + } + }, + getData: function(length) { + var self = this; + if (self.state.masked) { + self.expectHeader(4, function(data) { + var mask = data; + self.expectData(length, function(data) { + opcodes['ping'].finish.call(self, mask, data); + }); + }); + } + else { + self.expectData(length, function(data) { + opcodes['ping'].finish.call(self, null, data); + }); + } + }, + finish: function(mask, data) { + var self = this; + data = this.unmask(mask, data, true); + var state = clone(this.state); + this.messageHandlers.push(function(cb) { + self.onping(data, {masked: state.masked, binary: true}); + cb(); + }); + this.flush(); + this.endPacket(); + } +} + + +opcodes['pong'] = { + start: function(data) { + var self = this; + if (self.state.lastFragment == false) { + self.error('fragmented pong is not supported', 1002); + return; + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + opcodes['pong'].getData.call(self, firstLength); + } + else { + self.error('control frames cannot have more than 125 bytes of data', 1002); + } + }, + getData: function(length) { + var self = this; + if (this.state.masked) { + this.expectHeader(4, function(data) { + var mask = data; + self.expectData(length, function(data) { + opcodes['pong'].finish.call(self, mask, data); + }); + }); + } + else { + this.expectData(length, function(data) { + opcodes['pong'].finish.call(self, null, data); + }); + } + }, + finish: function(mask, data) { + var self = this; + data = self.unmask(mask, data, true); + var state = clone(this.state); + this.messageHandlers.push(function(cb) { + self.onpong(data, {masked: state.masked, binary: true}); + cb(); + }); + this.flush(); + this.endPacket(); + } +} + +module.exports = opcodes diff --git a/lib/internal/websockets/PerMessageDeflate.js b/lib/internal/websockets/PerMessageDeflate.js new file mode 100644 index 00000000000000..4485d11fe26872 --- /dev/null +++ b/lib/internal/websockets/PerMessageDeflate.js @@ -0,0 +1,276 @@ + +const zlib = require('zlib'); + +const AVAILABLE_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15]; +const DEFAULT_WINDOW_BITS = 15; +const DEFAULT_MEM_LEVEL = 8; + +PerMessageDeflate.extensionName = 'permessage-deflate'; + +/** + * Per-message Compression Extensions implementation + */ +function PerMessageDeflate(options, isServer) { + if (this instanceof PerMessageDeflate === false) { + throw new TypeError("Classes can't be function-called"); + } + + this._options = options || {}; + this._isServer = !!isServer; + this._inflate = null; + this._deflate = null; + this.params = null; +} + +PerMessageDeflate.prototype.offer = function() { + var params = {}; + if (this._options.serverNoContextTakeover) { + params.server_no_context_takeover = true; + } + if (this._options.clientNoContextTakeover) { + params.client_no_context_takeover = true; + } + if (this._options.serverMaxWindowBits) { + params.server_max_window_bits = this._options.serverMaxWindowBits; + } + if (this._options.clientMaxWindowBits) { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } else if (this._options.clientMaxWindowBits == null) { + params.client_max_window_bits = true; + } + return params; +}; + +PerMessageDeflate.prototype.accept = function(paramsList) { + paramsList = this.normalizeParams(paramsList); + + var params; + if (this._isServer) { + params = this.acceptAsServer(paramsList); + } else { + params = this.acceptAsClient(paramsList); + } + + this.params = params; + return params; +}; + +PerMessageDeflate.prototype.cleanup = function() { + if (this._inflate) { + if (this._inflate.writeInProgress) { + this._inflate.pendingClose = true; + } else { + if (this._inflate.close) this._inflate.close(); + this._inflate = null; + } + } + if (this._deflate) { + if (this._deflate.writeInProgress) { + this._deflate.pendingClose = true; + } else { + if (this._deflate.close) this._deflate.close(); + this._deflate = null; + } + } +}; + +PerMessageDeflate.prototype.acceptAsServer = function(paramsList) { + var accepted = {}; + var result = paramsList.some(function(params) { + accepted = {}; + if (this._options.serverNoContextTakeover === false && params.server_no_context_takeover) { + return; + } + if (this._options.serverMaxWindowBits === false && params.server_max_window_bits) { + return; + } + if (typeof this._options.serverMaxWindowBits === 'number' && + typeof params.server_max_window_bits === 'number' && + this._options.serverMaxWindowBits > params.server_max_window_bits) { + return; + } + if (typeof this._options.clientMaxWindowBits === 'number' && !params.client_max_window_bits) { + return; + } + + if (this._options.serverNoContextTakeover || params.server_no_context_takeover) { + accepted.server_no_context_takeover = true; + } + if (this._options.clientNoContextTakeover) { + accepted.client_no_context_takeover = true; + } + if (this._options.clientNoContextTakeover !== false && params.client_no_context_takeover) { + accepted.client_no_context_takeover = true; + } + if (typeof this._options.serverMaxWindowBits === 'number') { + accepted.server_max_window_bits = this._options.serverMaxWindowBits; + } else if (typeof params.server_max_window_bits === 'number') { + accepted.server_max_window_bits = params.server_max_window_bits; + } + if (typeof this._options.clientMaxWindowBits === 'number') { + accepted.client_max_window_bits = this._options.clientMaxWindowBits; + } else if (this._options.clientMaxWindowBits !== false && typeof params.client_max_window_bits === 'number') { + accepted.client_max_window_bits = params.client_max_window_bits; + } + return true; + }, this); + + if (!result) { + throw new Error('Doesn\'t support the offered configuration'); + } + + return accepted; +}; + +PerMessageDeflate.prototype.acceptAsClient = function(paramsList) { + var params = paramsList[0]; + if (this._options.clientNoContextTakeover != null) { + if (this._options.clientNoContextTakeover === false && params.client_no_context_takeover) { + throw new Error('Invalid value for "client_no_context_takeover"'); + } + } + if (this._options.clientMaxWindowBits != null) { + if (this._options.clientMaxWindowBits === false && params.client_max_window_bits) { + throw new Error('Invalid value for "client_max_window_bits"'); + } + if (typeof this._options.clientMaxWindowBits === 'number' && + (!params.client_max_window_bits || params.client_max_window_bits > this._options.clientMaxWindowBits)) { + throw new Error('Invalid value for "client_max_window_bits"'); + } + } + return params; +}; + +PerMessageDeflate.prototype.normalizeParams = function(paramsList) { + return paramsList.map(function(params) { + Object.keys(params).forEach(function(key) { + var value = params[key]; + if (value.length > 1) { + throw new Error('Multiple extension parameters for ' + key); + } + + value = value[0]; + + switch (key) { + case 'server_no_context_takeover': + case 'client_no_context_takeover': + if (value !== true) { + throw new Error('invalid extension parameter value for ' + key + ' (' + value + ')'); + } + params[key] = true; + break; + case 'server_max_window_bits': + case 'client_max_window_bits': + if (typeof value === 'string') { + value = parseInt(value, 10); + if (!~AVAILABLE_WINDOW_BITS.indexOf(value)) { + throw new Error('invalid extension parameter value for ' + key + ' (' + value + ')'); + } + } + if (!this._isServer && value === true) { + throw new Error('Missing extension parameter value for ' + key); + } + params[key] = value; + break; + default: + throw new Error('Not defined extension parameter (' + key + ')'); + } + }, this); + return params; + }, this); +}; + +PerMessageDeflate.prototype.decompress = function (data, fin, cb) { + var endpoint = this._isServer ? 'client' : 'server'; + + if (!this._inflate) { + var maxWindowBits = this.params[endpoint + '_max_window_bits']; + this._inflate = zlib.createInflateRaw({ + windowBits: 'number' === typeof maxWindowBits ? maxWindowBits : DEFAULT_WINDOW_BITS + }); + } + this._inflate.writeInProgress = true; + + var self = this; + var buffers = []; + + this._inflate.on('error', onError).on('data', onData); + this._inflate.write(data); + if (fin) { + this._inflate.write(new Buffer([0x00, 0x00, 0xff, 0xff])); + } + this._inflate.flush(function() { + cleanup(); + cb(null, Buffer.concat(buffers)); + }); + + function onError(err) { + cleanup(); + cb(err); + } + + function onData(data) { + buffers.push(data); + } + + function cleanup() { + if (!self._inflate) return; + self._inflate.removeListener('error', onError); + self._inflate.removeListener('data', onData); + self._inflate.writeInProgress = false; + if ((fin && self.params[endpoint + '_no_context_takeover']) || self._inflate.pendingClose) { + if (self._inflate.close) self._inflate.close(); + self._inflate = null; + } + } +}; + +PerMessageDeflate.prototype.compress = function (data, fin, cb) { + var endpoint = this._isServer ? 'server' : 'client'; + + if (!this._deflate) { + var maxWindowBits = this.params[endpoint + '_max_window_bits']; + this._deflate = zlib.createDeflateRaw({ + flush: zlib.Z_SYNC_FLUSH, + windowBits: 'number' === typeof maxWindowBits ? maxWindowBits : DEFAULT_WINDOW_BITS, + memLevel: this._options.memLevel || DEFAULT_MEM_LEVEL + }); + } + this._deflate.writeInProgress = true; + + var self = this; + var buffers = []; + + this._deflate.on('error', onError).on('data', onData); + this._deflate.write(data); + this._deflate.flush(function() { + cleanup(); + var data = Buffer.concat(buffers); + if (fin) { + data = data.slice(0, data.length - 4); + } + cb(null, data); + }); + + function onError(err) { + cleanup(); + cb(err); + } + + function onData(data) { + buffers.push(data); + } + + function cleanup() { + if (!self._deflate) return; + self._deflate.removeListener('error', onError); + self._deflate.removeListener('data', onData); + self._deflate.writeInProgress = false; + if ((fin && self.params[endpoint + '_no_context_takeover']) || self._deflate.pendingClose) { + if (self._deflate.close) self._deflate.close(); + self._deflate = null; + } + } +}; + +module.exports = PerMessageDeflate; diff --git a/lib/internal/websockets/Receiver.js b/lib/internal/websockets/Receiver.js new file mode 100644 index 00000000000000..1fc8c86f9fa456 --- /dev/null +++ b/lib/internal/websockets/Receiver.js @@ -0,0 +1,448 @@ +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +const util = require('util'); +const PerMessageDeflate = require('internal/websockets/PerMessageDeflate'); +const opcodes = require('internal/websockets/Opcodes') + +function fastCopy(length, srcBuffer, dstBuffer, dstOffset) { + switch (length) { + default: srcBuffer.copy(dstBuffer, dstOffset, 0, length); break; + case 16: dstBuffer[dstOffset+15] = srcBuffer[15]; + case 15: dstBuffer[dstOffset+14] = srcBuffer[14]; + case 14: dstBuffer[dstOffset+13] = srcBuffer[13]; + case 13: dstBuffer[dstOffset+12] = srcBuffer[12]; + case 12: dstBuffer[dstOffset+11] = srcBuffer[11]; + case 11: dstBuffer[dstOffset+10] = srcBuffer[10]; + case 10: dstBuffer[dstOffset+9] = srcBuffer[9]; + case 9: dstBuffer[dstOffset+8] = srcBuffer[8]; + case 8: dstBuffer[dstOffset+7] = srcBuffer[7]; + case 7: dstBuffer[dstOffset+6] = srcBuffer[6]; + case 6: dstBuffer[dstOffset+5] = srcBuffer[5]; + case 5: dstBuffer[dstOffset+4] = srcBuffer[4]; + case 4: dstBuffer[dstOffset+3] = srcBuffer[3]; + case 3: dstBuffer[dstOffset+2] = srcBuffer[2]; + case 2: dstBuffer[dstOffset+1] = srcBuffer[1]; + case 1: dstBuffer[dstOffset] = srcBuffer[0]; + } +} + +function bufferUtilUnmask(data, mask) { + var maskNum = mask.readUInt32LE(0, true); + var length = data.length; + var i = 0; + for (; i < length - 3; i += 4) { + var num = maskNum ^ data.readUInt32LE(i, true); + if (num < 0) num = 4294967296 + num; + data.writeUInt32LE(num, i, true); + } + switch (length % 4) { + case 3: data[i + 2] = data[i + 2] ^ mask[2]; + case 2: data[i + 1] = data[i + 1] ^ mask[1]; + case 1: data[i] = data[i] ^ mask[0]; + case 0:; + } +} + +bufferUtilMerge(mergedBuffer, buffers) { + var offset = 0; + for (var i = 0, l = buffers.length; i < l; ++i) { + var buf = buffers[i]; + buf.copy(mergedBuffer, offset); + offset += buf.length; + } +} + +function BufferPool(initialSize, growStrategy, shrinkStrategy) { + if (this instanceof BufferPool === false) { + throw new TypeError("Classes can't be function-called"); + } + + if (typeof initialSize === 'function') { + shrinkStrategy = growStrategy; + growStrategy = initialSize; + initialSize = 0; + } + else if (typeof initialSize === 'undefined') { + initialSize = 0; + } + this._growStrategy = (growStrategy || function(db, size) { + return db.used + size; + }).bind(null, this); + this._shrinkStrategy = (shrinkStrategy || function(db) { + return initialSize; + }).bind(null, this); + this._buffer = initialSize ? new Buffer(initialSize) : null; + this._offset = 0; + this._used = 0; + this._changeFactor = 0; + this.__defineGetter__('size', function(){ + return this._buffer == null ? 0 : this._buffer.length; + }); + this.__defineGetter__('used', function(){ + return this._used; + }); +} + +BufferPool.prototype.get = function(length) { + if (this._buffer == null || this._offset + length > this._buffer.length) { + var newBuf = new Buffer(this._growStrategy(length)); + this._buffer = newBuf; + this._offset = 0; + } + this._used += length; + var buf = this._buffer.slice(this._offset, this._offset + length); + this._offset += length; + return buf; +} + +BufferPool.prototype.reset = function(forceNewBuffer) { + var len = this._shrinkStrategy(); + if (len < this.size) this._changeFactor -= 1; + if (forceNewBuffer || this._changeFactor < -2) { + this._changeFactor = 0; + this._buffer = len ? new Buffer(len) : null; + } + this._offset = 0; + this._used = 0; +} + +/** + * HyBi Receiver implementation + */ + +function Receiver (extensions) { + if (this instanceof Receiver === false) { + throw new TypeError("Classes can't be function-called"); + } + + // memory pool for fragmented messages + var fragmentedPoolPrevUsed = -1; + this.fragmentedBufferPool = new BufferPool(1024, function(db, length) { + return db.used + length; + }, function(db) { + return fragmentedPoolPrevUsed = fragmentedPoolPrevUsed >= 0 ? + Math.ceil((fragmentedPoolPrevUsed + db.used) / 2) : + db.used; + }); + + // memory pool for unfragmented messages + var unfragmentedPoolPrevUsed = -1; + this.unfragmentedBufferPool = new BufferPool(1024, function(db, length) { + return db.used + length; + }, function(db) { + return unfragmentedPoolPrevUsed = unfragmentedPoolPrevUsed >= 0 ? + Math.ceil((unfragmentedPoolPrevUsed + db.used) / 2) : + db.used; + }); + + this.extensions = extensions || {}; + this.state = { + activeFragmentedOperation: null, + lastFragment: false, + masked: false, + opcode: 0, + fragmentedOperation: false + }; + this.overflow = []; + this.headerBuffer = new Buffer(10); + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + this.currentMessage = []; + this.messageHandlers = []; + this.expectHeader(2, this.processPacket); + this.dead = false; + this.processing = false; + + this.onerror = function() {}; + this.ontext = function() {}; + this.onbinary = function() {}; + this.onclose = function() {}; + this.onping = function() {}; + this.onpong = function() {}; +} + +/** + * Add new data to the parser. + * + */ +Receiver.prototype.add = function(data) { + var dataLength = data.length; + if (dataLength == 0) return; + if (this.expectBuffer == null) { + this.overflow.push(data); + return; + } + var toRead = Math.min(dataLength, this.expectBuffer.length - this.expectOffset); + fastCopy(toRead, data, this.expectBuffer, this.expectOffset); + this.expectOffset += toRead; + if (toRead < dataLength) { + this.overflow.push(data.slice(toRead)); + } + while (this.expectBuffer && this.expectOffset == this.expectBuffer.length) { + var bufferForHandler = this.expectBuffer; + this.expectBuffer = null; + this.expectOffset = 0; + this.expectHandler.call(this, bufferForHandler); + } +}; + +/** + * Releases all resources used by the receiver. + * + */ +Receiver.prototype.cleanup = function() { + this.dead = true; + this.overflow = null; + this.headerBuffer = null; + this.expectBuffer = null; + this.expectHandler = null; + this.unfragmentedBufferPool = null; + this.fragmentedBufferPool = null; + this.state = null; + this.currentMessage = null; + this.onerror = null; + this.ontext = null; + this.onbinary = null; + this.onclose = null; + this.onping = null; + this.onpong = null; +}; + +/** + * Waits for a certain amount of header bytes to be available, then fires a callback. + * + */ +Receiver.prototype.expectHeader = function(length, handler) { + if (length == 0) { + handler(null); + return; + } + this.expectBuffer = this.headerBuffer.slice(this.expectOffset, this.expectOffset + length); + this.expectHandler = handler; + var toRead = length; + while (toRead > 0 && this.overflow.length > 0) { + var fromOverflow = this.overflow.pop(); + if (toRead < fromOverflow.length) this.overflow.push(fromOverflow.slice(toRead)); + var read = Math.min(fromOverflow.length, toRead); + fastCopy(read, fromOverflow, this.expectBuffer, this.expectOffset); + this.expectOffset += read; + toRead -= read; + } +}; + +/** + * Waits for a certain amount of data bytes to be available, then fires a callback. + * + */ +Receiver.prototype.expectData = function(length, handler) { + if (length == 0) { + handler(null); + return; + } + this.expectBuffer = this.allocateFromPool(length, this.state.fragmentedOperation); + this.expectHandler = handler; + var toRead = length; + while (toRead > 0 && this.overflow.length > 0) { + var fromOverflow = this.overflow.pop(); + if (toRead < fromOverflow.length) this.overflow.push(fromOverflow.slice(toRead)); + var read = Math.min(fromOverflow.length, toRead); + fastCopy(read, fromOverflow, this.expectBuffer, this.expectOffset); + this.expectOffset += read; + toRead -= read; + } +}; + +/** + * Allocates memory from the buffer pool. + * + */ +Receiver.prototype.allocateFromPool = function(length, isFragmented) { + return (isFragmented ? this.fragmentedBufferPool : this.unfragmentedBufferPool).get(length); +}; + +/** + * Start processing a new packet. + * + */ +Receiver.prototype.processPacket = function (data) { + if (this.extensions[PerMessageDeflate.extensionName]) { + if ((data[0] & 0x30) != 0) { + this.error('reserved fields (2, 3) must be empty', 1002); + return; + } + } else { + if ((data[0] & 0x70) != 0) { + this.error('reserved fields must be empty', 1002); + return; + } + } + this.state.lastFragment = (data[0] & 0x80) == 0x80; + this.state.masked = (data[1] & 0x80) == 0x80; + var compressed = (data[0] & 0x40) == 0x40; + var opcode = data[0] & 0xf; + if (opcode === 0) { + if (compressed) { + this.error('continuation frame cannot have the Per-message Compressed bits', 1002); + return; + } + // continuation frame + this.state.fragmentedOperation = true; + this.state.opcode = this.state.activeFragmentedOperation; + if (!(this.state.opcode == 1 || this.state.opcode == 2)) { + this.error('continuation frame cannot follow current opcode', 1002); + return; + } + } + else { + if (opcode < 3 && this.state.activeFragmentedOperation != null) { + this.error('data frames after the initial data frame must have opcode 0', 1002); + return; + } + if (opcode >= 8 && compressed) { + this.error('control frames cannot have the Per-message Compressed bits', 1002); + return; + } + this.state.compressed = compressed; + this.state.opcode = opcode; + if (this.state.lastFragment === false) { + this.state.fragmentedOperation = true; + this.state.activeFragmentedOperation = opcode; + } + else this.state.fragmentedOperation = false; + } + var handler; + switch (this.state.opcode) { + case 1: + handler = opcodes['text'] + break; + case 2: + handler = opcodes['binary'] + break; + case 8: + handler = opcodes['close'] + break; + case 9: + handler = opcodes['ping'] + break; + case 10: + handler = opcodes['pong'] + break; + } + if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode, 1002); + else { + handler.start.call(this, data); + } +}; + +/** + * Endprocessing a packet. + * + */ +Receiver.prototype.endPacket = function() { + if (!this.state.fragmentedOperation) this.unfragmentedBufferPool.reset(true); + else if (this.state.lastFragment) this.fragmentedBufferPool.reset(true); + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + if (this.state.lastFragment && this.state.opcode === this.state.activeFragmentedOperation) { + // end current fragmented operation + this.state.activeFragmentedOperation = null; + } + this.state.lastFragment = false; + this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0; + this.state.masked = false; + this.expectHeader(2, this.processPacket); +}; + +/** + * Reset the parser state. + * + */ +Receiver.prototype.reset = function() { + if (this.dead) return; + this.state = { + activeFragmentedOperation: null, + lastFragment: false, + masked: false, + opcode: 0, + fragmentedOperation: false + }; + this.fragmentedBufferPool.reset(true); + this.unfragmentedBufferPool.reset(true); + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + this.overflow = []; + this.currentMessage = []; + this.messageHandlers = []; +}; + +/** + * Unmask received data. + * + */ +Receiver.prototype.unmask = function (mask, buf, binary) { + if (mask != null && buf != null) bufferUtilUnmask(buf, mask); + if (binary) return buf; + return buf != null ? buf.toString('utf8') : ''; +}; + +/** + * Concatenates a list of buffers. + * + */ +Receiver.prototype.concatBuffers = function(buffers) { + var length = 0; + for (var i = 0, l = buffers.length; i < l; ++i) length += buffers[i].length; + var mergedBuffer = new Buffer(length); + bufferUtilMerge(mergedBuffer, buffers); + return mergedBuffer; +}; + +Receiver.prototype.error = function (reason, protocolErrorCode) { + this.reset(); + this.onerror(reason, protocolErrorCode); + return this; +}; + +Receiver.prototype.flush = function() { + if (this.processing || this.dead) return; + + var handler = this.messageHandlers.shift(); + if (!handler) return; + + this.processing = true; + var self = this; + + handler(function() { + self.processing = false; + self.flush(); + }); +}; + +/** + * Apply extensions to message + * + */ +Receiver.prototype.applyExtensions = function(messageBuffer, fin, compressed, cb) { + var self = this; + if (compressed) { + this.extensions[PerMessageDeflate.extensionName].decompress(messageBuffer, fin, function(err, buffer) { + if (self.dead) return; + if (err) { + cb(new Error('invalid compressed data')); + return; + } + cb(null, buffer); + }); + } else { + cb(null, messageBuffer); + } +}; + +module.exports = Receiver; diff --git a/lib/internal/websockets/ReceiverHixie.js b/lib/internal/websockets/ReceiverHixie.js new file mode 100644 index 00000000000000..ab3ab618df840b --- /dev/null +++ b/lib/internal/websockets/ReceiverHixie.js @@ -0,0 +1,161 @@ +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +const util = require('util'); +// state constants +const EMPTY = 0; +const BODY = 1; +const BINARYLENGTH = 2; +const BINARYBODY = 3; + +/** + * Hixie Receiver implementation + */ +function Receiver () { + if (this instanceof Receiver === false) { + throw new TypeError("Classes can't be function-called"); + } + + this.state = EMPTY; + this.buffers = []; + this.messageEnd = -1; + this.spanLength = 0; + this.dead = false; + + this.onerror = function() {}; + this.ontext = function() {}; + this.onbinary = function() {}; + this.onclose = function() {}; + this.onping = function() {}; + this.onpong = function() {}; +} + +/** + * Add new data to the parser. + * + */ +Receiver.prototype.add = function(data) { + var self = this; + function doAdd() { + if (self.state === EMPTY) { + if (data.length == 2 && data[0] == 0xFF && data[1] == 0x00) { + self.reset(); + self.onclose(); + return; + } + if (data[0] === 0x80) { + self.messageEnd = 0; + self.state = BINARYLENGTH; + data = data.slice(1); + } else { + + if (data[0] !== 0x00) { + self.error('payload must start with 0x00 byte', true); + return; + } + data = data.slice(1); + self.state = BODY; + + } + } + if (self.state === BINARYLENGTH) { + var i = 0; + while ((i < data.length) && (data[i] & 0x80)) { + self.messageEnd = 128 * self.messageEnd + (data[i] & 0x7f); + ++i; + } + if (i < data.length) { + self.messageEnd = 128 * self.messageEnd + (data[i] & 0x7f); + self.state = BINARYBODY; + ++i; + } + if (i > 0) + data = data.slice(i); + } + if (self.state === BINARYBODY) { + var dataleft = self.messageEnd - self.spanLength; + if (data.length >= dataleft) { + // consume the whole buffer to finish the frame + self.buffers.push(data); + self.spanLength += dataleft; + self.messageEnd = dataleft; + return self.parse(); + } + // frame's not done even if we consume it all + self.buffers.push(data); + self.spanLength += data.length; + return; + } + self.buffers.push(data); + if ((self.messageEnd = bufferIndex(data, 0xFF)) != -1) { + self.spanLength += self.messageEnd; + return self.parse(); + } + else self.spanLength += data.length; + } + while(data) data = doAdd(); +}; + +/** + * Releases all resources used by the receiver. + * + */ +Receiver.prototype.cleanup = function() { + this.dead = true; + this.state = EMPTY; + this.buffers = []; +}; + +/** + * Process buffered data. + * + */ +Receiver.prototype.parse = function() { + var output = new Buffer(this.spanLength); + var outputIndex = 0; + for (var bi = 0, bl = this.buffers.length; bi < bl - 1; ++bi) { + var buffer = this.buffers[bi]; + buffer.copy(output, outputIndex); + outputIndex += buffer.length; + } + var lastBuffer = this.buffers[this.buffers.length - 1]; + if (this.messageEnd > 0) lastBuffer.copy(output, outputIndex, 0, this.messageEnd); + if (this.state !== BODY) --this.messageEnd; + var tail = null; + if (this.messageEnd < lastBuffer.length - 1) { + tail = lastBuffer.slice(this.messageEnd + 1); + } + this.reset(); + this.ontext(output.toString('utf8')); + return tail; +}; + +Receiver.prototype.error = function (reason, terminate) { + this.reset(); + this.onerror(reason, terminate); + return this; +}; + +/** + * Reset parser state + * + */ +Receiver.prototype.reset = function (reason) { + if (this.dead) return; + this.state = EMPTY; + this.buffers = []; + this.messageEnd = -1; + this.spanLength = 0; +}; + +function bufferIndex(buffer, byte) { + for (var i = 0, l = buffer.length; i < l; ++i) { + if (buffer[i] === byte) return i; + } + return -1; +} + +module.exports = Receiver; diff --git a/lib/internal/websockets/Sender.js b/lib/internal/websockets/Sender.js new file mode 100644 index 00000000000000..10eac539f79a21 --- /dev/null +++ b/lib/internal/websockets/Sender.js @@ -0,0 +1,317 @@ +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +const events = require('events'); +const util = require('util'); +const EventEmitter = events.EventEmitter; +const WSErrorCodes = require('websockets').WSErrorCodes; +const PerMessageDeflate = require('internal/websockets/PerMessageDeflate'); + +function writeUInt16BE(value, offset) { + this[offset] = (value & 0xff00)>>8; + this[offset+1] = value & 0xff; +} + +function writeUInt32BE(value, offset) { + this[offset] = (value & 0xff000000)>>24; + this[offset+1] = (value & 0xff0000)>>16; + this[offset+2] = (value & 0xff00)>>8; + this[offset+3] = value & 0xff; +} + +function getArrayBuffer(data) { + // data is either an ArrayBuffer or ArrayBufferView. + var array = new Uint8Array(data.buffer || data) + , l = data.byteLength || data.length + , o = data.byteOffset || 0 + , buffer = new Buffer(l); + for (var i = 0; i < l; ++i) { + buffer[i] = array[o+i]; + } + return buffer; +} + +function getRandomMask() { + return new Buffer([ + ~~(Math.random() * 255), + ~~(Math.random() * 255), + ~~(Math.random() * 255), + ~~(Math.random() * 255) + ]); +} + +function bufferUtilMask(source, mask, output, offset, length) { + var maskNum = mask.readUInt32LE(0, true); + var i = 0; + for (; i < length - 3; i += 4) { + var num = maskNum ^ source.readUInt32LE(i, true); + if (num < 0) num = 4294967296 + num; + output.writeUInt32LE(num, offset + i, true); + } + switch (length % 4) { + case 3: output[offset + i + 2] = source[i + 2] ^ mask[2]; + case 2: output[offset + i + 1] = source[i + 1] ^ mask[1]; + case 1: output[offset + i] = source[i] ^ mask[0]; + case 0:; + } +} + +/** + * HyBi Sender implementation + */ +function Sender(socket, extensions) { + if (this instanceof Sender === false) { + throw new TypeError("Classes can't be function-called"); + } + + events.EventEmitter.call(this); + + this._socket = socket; + this.extensions = extensions || {}; + this.firstFragment = true; + this.compress = false; + this.messageHandlers = []; + this.processing = false; +} +util.inherits(Sender, events.EventEmitter); + +/** + * Sends a close instruction to the remote party. + * + */ +Sender.prototype.close = function(code, data, mask, cb) { + if (typeof code !== 'undefined') { + if (typeof code !== 'number' || + !WSErrorCodes.isValidErrorCode(code)) throw new Error('first argument must be a valid error code number'); + } + code = code || 1000; + var dataBuffer = new Buffer(2 + (data ? Buffer.byteLength(data) : 0)); + writeUInt16BE.call(dataBuffer, code, 0); + if (dataBuffer.length > 2) dataBuffer.write(data, 2); + + var self = this; + this.messageHandlers.push(function(callback) { + self.frameAndSend(0x8, dataBuffer, true, mask); + callback(); + if (typeof cb == 'function') cb(); + }); + this.flush(); +}; + +/** + * Sends a ping message to the remote party. + * + */ +Sender.prototype.ping = function(data, options) { + var mask = options && options.mask; + var self = this; + this.messageHandlers.push(function(callback) { + self.frameAndSend(0x9, data || '', true, mask); + callback(); + }); + this.flush(); +}; + +/** + * Sends a pong message to the remote party. + * + */ +Sender.prototype.pong = function(data, options) { + var mask = options && options.mask; + var self = this; + this.messageHandlers.push(function(callback) { + self.frameAndSend(0xa, data || '', true, mask); + callback(); + }); + this.flush(); +}; + +/** + * Sends text or binary data to the remote party. + * + */ +Sender.prototype.send = function(data, options, cb) { + var finalFragment = options && options.fin === false ? false : true; + var mask = options && options.mask; + var compress = options && options.compress; + var opcode = options && options.binary ? 2 : 1; + if (this.firstFragment === false) { + opcode = 0; + compress = false; + } else { + this.firstFragment = false; + this.compress = compress; + } + if (finalFragment) this.firstFragment = true + + var compressFragment = this.compress; + + var self = this; + this.messageHandlers.push(function(callback) { + self.applyExtensions(data, finalFragment, compressFragment, function(err, data) { + if (err) { + if (typeof cb == 'function') cb(err); + else self.emit('error', err); + return; + } + self.frameAndSend(opcode, data, finalFragment, mask, compress, cb); + callback(); + }); + }); + this.flush(); +}; + +/** + * Frames and sends a piece of data according to the HyBi WebSocket protocol. + * + */ +Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData, compressed, cb) { + var canModifyData = false; + + if (!data) { + try { + this._socket.write(new Buffer([opcode | (finalFragment ? 0x80 : 0), 0 | (maskData ? 0x80 : 0)].concat(maskData ? [0, 0, 0, 0] : [])), 'binary', cb); + } + catch (e) { + if (typeof cb == 'function') cb(e); + else this.emit('error', e); + } + return; + } + + if (!Buffer.isBuffer(data)) { + canModifyData = true; + if (data && (typeof data.byteLength !== 'undefined' || typeof data.buffer !== 'undefined')) { + data = getArrayBuffer(data); + } else { + // If people want to send a number, this would allocate the number in + // bytes as memory size instead of storing the number as buffer value. So + // we need to transform it to string in order to prevent possible + // vulnerabilities / memory attacks. + if (typeof data === 'number') data = data.toString(); + + data = new Buffer(data); + } + } + + var dataLength = data.length + , dataOffset = maskData ? 6 : 2 + , secondByte = dataLength; + + if (dataLength >= 65536) { + dataOffset += 8; + secondByte = 127; + } + else if (dataLength > 125) { + dataOffset += 2; + secondByte = 126; + } + + var mergeBuffers = dataLength < 32768 || (maskData && !canModifyData); + var totalLength = mergeBuffers ? dataLength + dataOffset : dataOffset; + var outputBuffer = new Buffer(totalLength); + outputBuffer[0] = finalFragment ? opcode | 0x80 : opcode; + if (compressed) outputBuffer[0] |= 0x40; + + switch (secondByte) { + case 126: + writeUInt16BE.call(outputBuffer, dataLength, 2); + break; + case 127: + writeUInt32BE.call(outputBuffer, 0, 2); + writeUInt32BE.call(outputBuffer, dataLength, 6); + } + + if (maskData) { + outputBuffer[1] = secondByte | 0x80; + var mask = this._randomMask || (this._randomMask = getRandomMask()); + outputBuffer[dataOffset - 4] = mask[0]; + outputBuffer[dataOffset - 3] = mask[1]; + outputBuffer[dataOffset - 2] = mask[2]; + outputBuffer[dataOffset - 1] = mask[3]; + if (mergeBuffers) { + bufferUtilMask(data, mask, outputBuffer, dataOffset, dataLength); + try { + this._socket.write(outputBuffer, 'binary', cb); + } + catch (e) { + if (typeof cb == 'function') cb(e); + else this.emit('error', e); + } + } + else { + bufferUtilMask(data, mask, data, 0, dataLength); + try { + this._socket.write(outputBuffer, 'binary'); + this._socket.write(data, 'binary', cb); + } + catch (e) { + if (typeof cb == 'function') cb(e); + else this.emit('error', e); + } + } + } + else { + outputBuffer[1] = secondByte; + if (mergeBuffers) { + data.copy(outputBuffer, dataOffset); + try { + this._socket.write(outputBuffer, 'binary', cb); + } + catch (e) { + if (typeof cb == 'function') cb(e); + else this.emit('error', e); + } + } + else { + try { + this._socket.write(outputBuffer, 'binary'); + this._socket.write(data, 'binary', cb); + } + catch (e) { + if (typeof cb == 'function') cb(e); + else this.emit('error', e); + } + } + } +}; + +/** + * Execute message handler buffers + * + */ +Sender.prototype.flush = function() { + if (this.processing) return; + + var handler = this.messageHandlers.shift(); + if (!handler) return; + + this.processing = true; + + var self = this; + + handler(function() { + self.processing = false; + self.flush(); + }); +}; + +/** + * Apply extensions to message + * + */ +Sender.prototype.applyExtensions = function(data, fin, compress, callback) { + if (compress && data) { + if ((data.buffer || data) instanceof ArrayBuffer) { + data = getArrayBuffer(data); + } + this.extensions[PerMessageDeflate.extensionName].compress(data, fin, callback); + } else { + callback(null, data); + } +}; + +module.exports = Sender; diff --git a/lib/internal/websockets/SenderHixie.js b/lib/internal/websockets/SenderHixie.js new file mode 100644 index 00000000000000..3b1d13a789d01b --- /dev/null +++ b/lib/internal/websockets/SenderHixie.js @@ -0,0 +1,85 @@ +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +const events = require('events'); +const util = require('util'); +const EventEmitter = events.EventEmitter; + +function Sender(socket) { + if (this instanceof Sender === false) { + throw new TypeError("Classes can't be function-called"); + } + + events.EventEmitter.call(this); + + this.socket = socket; + this.continuationFrame = false; + this.isClosed = false; +} +util.inherits(Sender, events.EventEmitter); + +Sender.prototype.send = function(data, options, cb) { + if (this.isClosed) return; + + var isString = typeof data == 'string' + , length = isString ? Buffer.byteLength(data) : data.length + , lengthbytes = (length > 127) ? 2 : 1 // assume less than 2**14 bytes + , writeStartMarker = this.continuationFrame == false + , writeEndMarker = !options || !(typeof options.fin != 'undefined' && !options.fin) + , buffer = new Buffer((writeStartMarker ? ((options && options.binary) ? (1 + lengthbytes) : 1) : 0) + length + ((writeEndMarker && !(options && options.binary)) ? 1 : 0)) + , offset = writeStartMarker ? 1 : 0; + + if (writeStartMarker) { + if (options && options.binary) { + buffer.write('\x80', 'binary'); + // assume length less than 2**14 bytes + if (lengthbytes > 1) + buffer.write(String.fromCharCode(128+length/128), offset++, 'binary'); + buffer.write(String.fromCharCode(length&0x7f), offset++, 'binary'); + } else + buffer.write('\x00', 'binary'); + } + + if (isString) buffer.write(data, offset, 'utf8'); + else data.copy(buffer, offset, 0); + + if (writeEndMarker) { + if (options && options.binary) { + // sending binary, not writing end marker + } else + buffer.write('\xff', offset + length, 'binary'); + this.continuationFrame = false; + } + else this.continuationFrame = true; + + try { + this.socket.write(buffer, 'binary', cb); + } catch (e) { + this.error(e.toString()); + } +}; + +Sender.prototype.close = function(code, data, mask, cb) { + if (this.isClosed) return; + this.isClosed = true; + try { + if (this.continuationFrame) this.socket.write(new Buffer([0xff], 'binary')); + this.socket.write(new Buffer([0xff, 0x00]), 'binary', cb); + } catch (e) { + this.error(e.toString()); + } +}; + +Sender.prototype.ping = function(data, options) {}; + +Sender.prototype.pong = function(data, options) {}; + +Sender.prototype.error = function (reason) { + this.emit('error', reason); + return this; +}; + +module.exports = Sender; diff --git a/lib/internal/websockets/WebSocketServer.js b/lib/internal/websockets/WebSocketServer.js new file mode 100644 index 00000000000000..1373186afe55f1 --- /dev/null +++ b/lib/internal/websockets/WebSocketServer.js @@ -0,0 +1,495 @@ +'use strict' +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +const util = require('util'); +const events = require('events'); +const http = require('http'); +const crypto = require('crypto'); +const WebSocket = require('websockets'); +const Extensions = require('internal/websockets/Extensions'); +const PerMessageDeflate = require('internal/websockets/PerMessageDeflate'); +const tls = require('tls'); +const url = require('url'); + +/** +* Entirely private apis, +* which may or may not be bound to a sepcific WebSocket instance. +*/ +function handleHybiUpgrade(req, socket, upgradeHead, cb) { + // handle premature socket errors + let errorHandler = function() { + try { socket.destroy(); } catch (e) {} + } + socket.on('error', errorHandler); + + // verify key presence + if (!req.headers['sec-websocket-key']) { + abortConnection(socket, 400, 'Bad Request'); + return; + } + + // verify version + let version = parseInt(req.headers['sec-websocket-version']); + if ([8, 13].indexOf(version) === -1) { + abortConnection(socket, 400, 'Bad Request'); + return; + } + + // verify protocol + let protocols = req.headers['sec-websocket-protocol']; + + // verify client + let origin = version < 13 ? + req.headers['sec-websocket-origin'] : + req.headers['origin']; + + // handle extensions offer + let extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']); + + // handler to call when the connection sequence completes + let self = this; + let completeHybiUpgrade2 = function(protocol) { + + // calc key + let key = req.headers['sec-websocket-key']; + let shasum = crypto.createHash('sha1'); + shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + key = shasum.digest('base64'); + + let headers = [ + 'HTTP/1.1 101 Switching Protocols' + , 'Upgrade: websocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Accept: ' + key + ]; + + if (typeof protocol != 'undefined') { + headers.push('Sec-WebSocket-Protocol: ' + protocol); + } + + let extensions = {}; + try { + extensions = acceptExtensions.call(self, extensionsOffer); + } catch (err) { + abortConnection(socket, 400, 'Bad Request'); + return; + } + + if (Object.keys(extensions).length) { + let serverExtensions = {}; + Object.keys(extensions).forEach(function(token) { + serverExtensions[token] = [extensions[token].params] + }); + headers.push('Sec-WebSocket-Extensions: ' + Extensions.format(serverExtensions)); + } + + // allows external modification/inspection of handshake headers + self.emit('headers', headers); + + socket.setTimeout(0); + socket.setNoDelay(true); + try { + socket.write(headers.concat('', '').join('\r\n')); + } + catch (e) { + // if the upgrade write fails, shut the connection down hard + try { socket.destroy(); } catch (e) {} + return; + } + + let client = new WebSocket([req, socket, upgradeHead], { + protocolVersion: version, + protocol: protocol, + extensions: extensions + }); + + if (self.options.clientTracking) { + self.clients.push(client); + client.on('close', function() { + let index = self.clients.indexOf(client); + if (index != -1) { + self.clients.splice(index, 1); + } + }); + } + + // signal upgrade complete + socket.removeListener('error', errorHandler); + cb(client); + } + + // optionally call external protocol selection handler before + // calling completeHybiUpgrade2 + let completeHybiUpgrade1 = function() { + // choose from the sub-protocols + if (typeof self.options.handleProtocols == 'function') { + let protList = (protocols || "").split(/, */); + let cbCalled = false; + let res = self.options.handleProtocols(protList, function(result, protocol) { + cbCalled = true; + if (!result) abortConnection(socket, 401, 'Unauthorized'); + else completeHybiUpgrade2(protocol); + }); + if (!cbCalled) { + // the handleProtocols handler never called our cb + abortConnection(socket, 501, 'Could not process protocols'); + } + return; + } else { + if (typeof protocols !== 'undefined') { + completeHybiUpgrade2(protocols.split(/, */)[0]); + } + else { + completeHybiUpgrade2(); + } + } + } + + // optionally call external client verification handler + if (typeof this.options.verifyClient == 'function') { + let info = { + origin: origin, + secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined', + req: req + }; + if (this.options.verifyClient.length == 2) { + this.options.verifyClient(info, function(result, code, name) { + if (typeof code === 'undefined') code = 401; + if (typeof name === 'undefined') name = http.STATUS_CODES[code]; + + if (!result) abortConnection(socket, code, name); + else completeHybiUpgrade1(); + }); + return; + } + else if (!this.options.verifyClient(info)) { + abortConnection(socket, 401, 'Unauthorized'); + return; + } + } + + completeHybiUpgrade1(); +} + +function handleHixieUpgrade(req, socket, upgradeHead, cb) { + // handle premature socket errors + let errorHandler = function() { + try { socket.destroy(); } catch (e) {} + } + socket.on('error', errorHandler); + + // bail if options prevent hixie + if (this.options.disableHixie) { + abortConnection(socket, 401, 'Hixie support disabled'); + return; + } + + // verify key presence + if (!req.headers['sec-websocket-key2']) { + abortConnection(socket, 400, 'Bad Request'); + return; + } + + let origin = req.headers['origin'] + , self = this; + + // setup handshake completion to run after client has been verified + let onClientVerified = function() { + let wshost; + if (!req.headers['x-forwarded-host']) + wshost = req.headers.host; + else + wshost = req.headers['x-forwarded-host']; + let location = ((req.headers['x-forwarded-proto'] === 'https' || socket.encrypted) ? 'wss' : 'ws') + '://' + wshost + req.url + , protocol = req.headers['sec-websocket-protocol']; + + // handshake completion code to run once nonce has been successfully retrieved + let completeHandshake = function(nonce, rest) { + // calculate key + let k1 = req.headers['sec-websocket-key1'] + , k2 = req.headers['sec-websocket-key2'] + , md5 = crypto.createHash('md5'); + + [k1, k2].forEach(function (k) { + let n = parseInt(k.replace(/[^\d]/g, '')) + , spaces = k.replace(/[^ ]/g, '').length; + if (spaces === 0 || n % spaces !== 0){ + abortConnection(socket, 400, 'Bad Request'); + return; + } + n /= spaces; + md5.update(String.fromCharCode( + n >> 24 & 0xFF, + n >> 16 & 0xFF, + n >> 8 & 0xFF, + n & 0xFF)); + }); + md5.update(nonce.toString('binary')); + + let headers = [ + 'HTTP/1.1 101 Switching Protocols' + , 'Upgrade: WebSocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Location: ' + location + ]; + if (typeof protocol != 'undefined') headers.push('Sec-WebSocket-Protocol: ' + protocol); + if (typeof origin != 'undefined') headers.push('Sec-WebSocket-Origin: ' + origin); + + socket.setTimeout(0); + socket.setNoDelay(true); + try { + // merge header and hash buffer + let headerBuffer = new Buffer(headers.concat('', '').join('\r\n')); + let hashBuffer = new Buffer(md5.digest('binary'), 'binary'); + let handshakeBuffer = new Buffer(headerBuffer.length + hashBuffer.length); + headerBuffer.copy(handshakeBuffer, 0); + hashBuffer.copy(handshakeBuffer, headerBuffer.length); + + // do a single write, which - upon success - causes a new client websocket to be setup + socket.write(handshakeBuffer, 'binary', function(err) { + if (err) return; // do not create client if an error happens + let client = new WebSocket([req, socket, rest], { + protocolVersion: 'hixie-76', + protocol: protocol + }); + if (self.options.clientTracking) { + self.clients.push(client); + client.on('close', function() { + let index = self.clients.indexOf(client); + if (index != -1) { + self.clients.splice(index, 1); + } + }); + } + + // signal upgrade complete + socket.removeListener('error', errorHandler); + cb(client); + }); + } + catch (e) { + try { socket.destroy(); } catch (e) {} + return; + } + } + + // retrieve nonce + let nonceLength = 8; + if (upgradeHead && upgradeHead.length >= nonceLength) { + let nonce = upgradeHead.slice(0, nonceLength); + let rest = upgradeHead.length > nonceLength ? upgradeHead.slice(nonceLength) : null; + completeHandshake.call(self, nonce, rest); + } + else { + // nonce not present in upgradeHead, so we must wait for enough data + // data to arrive before continuing + let nonce = new Buffer(nonceLength); + upgradeHead.copy(nonce, 0); + let received = upgradeHead.length; + let rest = null; + let handler = function (data) { + let toRead = Math.min(data.length, nonceLength - received); + if (toRead === 0) return; + data.copy(nonce, received, 0, toRead); + received += toRead; + if (received == nonceLength) { + socket.removeListener('data', handler); + if (toRead < data.length) rest = data.slice(toRead); + completeHandshake.call(self, nonce, rest); + } + } + socket.on('data', handler); + } + } + + // verify client + if (typeof this.options.verifyClient == 'function') { + let info = { + origin: origin, + secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined', + req: req + }; + if (this.options.verifyClient.length == 2) { + let self = this; + this.options.verifyClient(info, function(result, code, name) { + if (typeof code === 'undefined') code = 401; + if (typeof name === 'undefined') name = http.STATUS_CODES[code]; + + if (!result) abortConnection(socket, code, name); + else onClientVerified.apply(self); + }); + return; + } + else if (!this.options.verifyClient(info)) { + abortConnection(socket, 401, 'Unauthorized'); + return; + } + } + + // no client verification required + onClientVerified(); +} + +function acceptExtensions(offer) { + let extensions = {}; + let options = this.options.perMessageDeflate; + if (options && offer[PerMessageDeflate.extensionName]) { + let perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true); + perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]); + extensions[PerMessageDeflate.extensionName] = perMessageDeflate; + } + return extensions; +} + +function abortConnection(socket, code, name) { + try { + let response = [ + 'HTTP/1.1 ' + code + ' ' + name, + 'Content-type: text/html' + ]; + socket.write(response.concat('', '').join('\r\n')); + } + catch (e) { /* ignore errors - we've aborted this connection */ } + finally { + // ensure that an early aborted connection is shut down completely + try { socket.destroy(); } catch (e) {} + } +} + +/** + * WebSocket Server implementation + */ + +function WebSocketServer(options, cb) { + if (this instanceof WebSocketServer === false) { + return new WebSocketServer(options, cb); + } + + events.EventEmitter.call(this); + + let opts = options + + opts.host = options.host || '0.0.0.0', + opts.port = options.port || null, + opts.server = options.server || null, + opts.verifyClient = options.verifyClient || null, + opts.handleProtocols = options.handleProtocols || null, + opts.path = options.path || null, + opts.noServer = options.noServer || false, + opts.disableHixie = options.disableHixie || false, + opts.clientTracking = options.clientTracking || true, + opts.perMessageDeflate = options.perMessageDeflate || true + + if (!opts.port && !opts.server && !opts.noServer) { + throw new TypeError('`port` or a `server` must be provided'); + } + + let self = this; + + if (opts.port) { + this._server = http.createServer(function (req, res) { + let body = http.STATUS_CODES[426]; + res.writeHead(426, { + 'Content-Length': body.length, + 'Content-Type': 'text/plain' + }); + res.end(body); + }); + this._server.allowHalfOpen = false; + this._server.listen(opts.port, opts.host, cb); + this._closeServer = function() { if (self._server) self._server.close(); }; + } + else if (opts.server) { + this._server = opts.server; + if (opts.path) { + // take note of the path, to avoid collisions when multiple websocket servers are + // listening on the same http server + if (this._server._webSocketPaths && opts.server._webSocketPaths[opts.path]) { + throw new Error('two instances of WebSocketServer cannot listen on the same http server path'); + } + if (typeof this._server._webSocketPaths !== 'object') { + this._server._webSocketPaths = {}; + } + this._server._webSocketPaths[opts.path] = 1; + } + } + if (this._server) this._server.once('listening', function() { self.emit('listening'); }); + + if (typeof this._server != 'undefined') { + this._server.on('error', function(error) { + self.emit('error', error) + }); + this._server.on('upgrade', function(req, socket, upgradeHead) { + //copy upgradeHead to avoid retention of large slab buffers used in node core + let head = new Buffer(upgradeHead.length); + upgradeHead.copy(head); + + self.handleUpgrade(req, socket, head, function(client) { + self.emit('connection'+req.url, client); + self.emit('connection', client); + }); + }); + } + + this.options = opts; + this.path = opts.path; + this.clients = []; +} +util.inherits(WebSocketServer, events.EventEmitter); + +WebSocketServer.prototype.close = function(cb) { + // terminate all associated clients + let error = null; + try { + for (let i = 0, l = this.clients.length; i < l; ++i) { + this.clients[i].terminate(); + } + } + catch (e) { + error = e; + } + + // remove path descriptor, if any + if (this.path && this._server._webSocketPaths) { + delete this._server._webSocketPaths[this.path]; + if (Object.keys(this._server._webSocketPaths).length == 0) { + delete this._server._webSocketPaths; + } + } + + // close the http server if it was internally created + try { + if (typeof this._closeServer !== 'undefined') { + this._closeServer(); + } + } + finally { + delete this._server; + } + if(cb) + cb(error); + else if(error) + throw error; +} + +WebSocketServer.prototype.handleUpgrade = function(req, socket, upgradeHead, cb) { + // check for wrong path + if (this.options.path) { + let u = url.parse(req.url); + if (u && u.pathname !== this.options.path) return; + } + + if (typeof req.headers.upgrade === 'undefined' || req.headers.upgrade.toLowerCase() !== 'websocket') { + abortConnection(socket, 400, 'Bad Request'); + return; + } + + if (req.headers['sec-websocket-key1']) handleHixieUpgrade.apply(this, arguments); + else handleHybiUpgrade.apply(this, arguments); +} + +module.exports = WebSocketServer; diff --git a/lib/websockets.js b/lib/websockets.js new file mode 100644 index 00000000000000..128f2600ec5792 --- /dev/null +++ b/lib/websockets.js @@ -0,0 +1,925 @@ +'use strict'; +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ +const url = require('url'); +const util = require('util'); +const http = require('http'); +const https = require('https'); +const crypto = require('crypto'); +const stream = require('stream'); +const Sender = require('internal/websockets/Sender'); +const Receiver = require('internal/websockets/Receiver'); +const SenderHixie = require('internal/websockets/SenderHixie'); +const ReceiverHixie = require('internal/websockets/ReceiverHixie'); +const Extensions = require('internal/websockets/Extensions'); +const PerMessageDeflate = require('internal/websockets/PerMessageDeflate'); +const EventEmitter = require('events').EventEmitter; +// Default protocol version +const protocolVersion = 13; +// Close timeout +const closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly +var has = Object.prototype.hasOwnProperty; + +const WSWSErrorCodes = { + 1000: 'normal', + 1001: 'going away', + 1002: 'protocol error', + 1003: 'unsupported data', + 1004: 'reserved', + 1005: 'reserved for extensions', + 1006: 'reserved for extensions', + 1007: 'inconsistent or invalid data', + 1008: 'policy violation', + 1009: 'message too big', + 1010: 'extension handshake missing', + 1011: 'an unexpected condition prevented the request from being fulfilled', + isValidErrorCode: function(code) { + return (code >= 1000 && code <= 1011 && code != 1004 && code != 1005 && code != 1006) || + (code >= 3000 && code <= 4999); + } +}; + + var _ultron_id = 0; + + function Ultron(ee) { + if (!(this instanceof Ultron)) return new Ultron(ee); + this.id = _ultron_id++; + this.ee = ee; + } + + Ultron.prototype.on = function on(event, fn, context) { + fn.__ultron = this.id; + this.ee.on(event, fn, context); + return this; + }; + + Ultron.prototype.once = function once(event, fn, context) { + fn.__ultron = this.id; + this.ee.once(event, fn, context); + return this; + }; + + Ultron.prototype.remove = function remove() { + var args = arguments + , event; + + if (args.length === 1 && 'string' === typeof args[0]) { + args = args[0].split(/[, ]+/); + } else if (!args.length) { + args = []; + + for (event in this.ee._events) { + if (has.call(this.ee._events, event)) args.push(event); + } + } + + for (var i = 0; i < args.length; i++) { + var listeners = this.ee.listeners(args[i]); + + for (var j = 0; j < listeners.length; j++) { + event = listeners[j]; + + if (event.listener) { + if (event.listener.__ultron !== this.id) continue; + delete event.listener.__ultron; + } else { + if (event.__ultron !== this.id) continue; + delete event.__ultron; + } + + this.ee.removeListener(args[i], event); + } + } + return this; + }; + + Ultron.prototype.destroy = function destroy() { + if (!this.ee) return false; + + this.remove(); + this.ee = null; + return true; + }; + +function WebSocket(address, protocols, options) { + if (this instanceof WebSocket === false) { + return new WebSocket(address, protocols, options); + } + EventEmitter.call(this); + + if (protocols && !Array.isArray(protocols) && 'object' === typeof protocols) { + // accept the "options" Object as the 2nd argument + options = protocols; + protocols = null; + } + + if ('string' === typeof protocols) { + protocols = [ protocols ]; + } + + if (!Array.isArray(protocols)) { + protocols = []; + } + + this.Sender = Sender + this.Receiver = Receiver + + this._socket = null; + this._ultron = null; + this._closeReceived = false; + this.bytesReceived = 0; + this.readyState = null; + this.supports = {}; + this.extensions = {}; + + if (Array.isArray(address)) { + initAsServerClient.apply(this, address.concat(options)); + } else { + initAsClient.apply(this, [address, protocols, options]); + } +} +util.inherits(WebSocket, EventEmitter); + +["CONNECTING", "OPEN", "CLOSING", "CLOSED"].forEach(function each(state, index) { + WebSocket.prototype[state] = WebSocket[state] = index; +}); + +WebSocket.prototype.createServer = function createServer(options, fn) { + var server = new WebSocket.Server(options); + + if (typeof fn === 'function') { + server.on('connection', fn); + } + return server; +}; + +WebSocket.prototype.connect = WebSocket.prototype.createConnection = function connect(address, fn) { + var client = new WebSocket(address); + + if (typeof fn === 'function') { + client.on('open', fn); + } + return client; +}; + +// Gracefully closes the connection, after sending a desc. message to the server +WebSocket.prototype.close = function close(code, data) { + if (this.readyState === WebSocket.CLOSED) return; + + if (this.readyState === WebSocket.CONNECTING) { + this.readyState = WebSocket.CLOSED; + return; + } + + if (this.readyState === WebSocket.CLOSING) { + if (this._closeReceived && this._isServer) { + this.terminate(); + } + return; + } + + var self = this; + try { + this.readyState = WebSocket.CLOSING; + this._closeCode = code; + this._closeMessage = data; + var mask = !this._isServer; + this._sender.close(code, data, mask, function(err) { + if (err) self.emit('error', err); + + if (self._closeReceived && self._isServer) { + self.terminate(); + } else { + // ensure that the connection is cleaned up even when no response of closing handshake. + clearTimeout(self._closeTimer); + self._closeTimer = setTimeout(cleanupWebsocketResources.bind(self, true), closeTimeout); + } + }); + } catch (e) { + this.emit('error', e); + } +}; + +WebSocket.prototype.pause = function pauser() { + if (this.readyState !== WebSocket.OPEN) throw new Error('not opened'); + return this._socket.pause(); +}; + +WebSocket.prototype.ping = function ping(data, options, dontFailWhenClosed) { + if (this.readyState !== WebSocket.OPEN) { + if (dontFailWhenClosed === true) return; + throw new Error('not opened'); + } + + options = options || {}; + + if (typeof options.mask === 'undefined') options.mask = !this._isServer; + this._sender.ping(data, options); +}; + +WebSocket.prototype.pong = function(data, options, dontFailWhenClosed) { + if (this.readyState !== WebSocket.OPEN) { + if (dontFailWhenClosed === true) return; + throw new Error('not opened'); + } + + options = options || {}; + + if (typeof options.mask === 'undefined') options.mask = !this._isServer; + this._sender.pong(data, options); +}; + +WebSocket.prototype.resume = function resume() { + if (this.readyState !== WebSocket.OPEN) throw new Error('not opened'); + return this._socket.resume(); +}; + +WebSocket.prototype.send = function send(data, options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } + + if (this.readyState !== WebSocket.OPEN) { + if (typeof cb === 'function') cb(new Error('not opened')); + else throw new Error('not opened'); + return; + } + + if (!data) data = ''; + if (this._queue) { + var self = this; + this._queue.push(function() { self.send(data, options, cb); }); + return; + } + + options = options || {}; + options.fin = true; + + if (typeof options.binary === 'undefined') { + options.binary = (data instanceof ArrayBuffer || data instanceof Buffer || + data instanceof Uint8Array || + data instanceof Uint16Array || + data instanceof Uint32Array || + data instanceof Int8Array || + data instanceof Int16Array || + data instanceof Int32Array || + data instanceof Float32Array || + data instanceof Float64Array); + } + + if (typeof options.mask === 'undefined') options.mask = !this._isServer; + if (typeof options.compress === 'undefined') options.compress = true; + if (!this.extensions[PerMessageDeflate.extensionName]) { + options.compress = false; + } + + var readable = typeof stream.Readable === 'function' + ? stream.Readable + : stream.Stream; + + if (data instanceof readable) { + startQueue(this); + var self = this; + + sendStream(this, data, options, function send(error) { + process.nextTick(function tock() { + executeQueueSends(self); + }); + + if (typeof cb === 'function') cb(error); + }); + } else { + this._sender.send(data, options, cb); + } +}; + +// Streams data through calls to a user supplied function +WebSocket.prototype.stream = function stream(options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } + + var self = this; + + if (typeof cb !== 'function') throw new Error('callback must be provided'); + + if (this.readyState !== WebSocket.OPEN) { + if (typeof cb === 'function') cb(new Error('not opened')); + else throw new Error('not opened'); + return; + } + + if (this._queue) { + this._queue.push(function () { self.stream(options, cb); }); + return; + } + + options = options || {}; + + if (typeof options.mask === 'undefined') options.mask = !this._isServer; + if (typeof options.compress === 'undefined') options.compress = true; + if (!this.extensions[PerMessageDeflate.extensionName]) { + options.compress = false; + } + + startQueue(this); + + function send(data, final) { + try { + if (self.readyState !== WebSocket.OPEN) throw new Error('not opened'); + options.fin = final === true; + self._sender.send(data, options); + if (!final) process.nextTick(cb.bind(null, null, send)); + else executeQueueSends(self); + } catch (e) { + if (typeof cb === 'function') cb(e); + else { + delete self._queue; + self.emit('error', e); + } + } + } + process.nextTick(cb.bind(null, null, send)); +}; + +WebSocket.prototype.terminate = function terminate() { + if (this.readyState === WebSocket.CLOSED) return; + + if (this._socket) { + this.readyState = WebSocket.CLOSING; + // End the connection + try { this._socket.end(); } + catch (e) { + // Socket error during end() call, so just destroy it right now + cleanupWebsocketResources.call(this, true); + return; + } + // Add a timeout to ensure that the connection is completely + // cleaned up within 30 seconds, even if the clean close procedure + // fails for whatever reason + // First cleanup any pre-existing timeout from an earlier "terminate" call, + // if one exists. Otherwise terminate calls in quick succession will leak timeouts + // and hold the program open for `closeTimout` time. + if (this._closeTimer) { clearTimeout(this._closeTimer); } + this._closeTimer = setTimeout(cleanupWebsocketResources.bind(this, true), closeTimeout); + } else if (this.readyState === WebSocket.CONNECTING) { + cleanupWebsocketResources.call(this, true); + } +}; +// Expose bufferedAmount +Object.defineProperty(WebSocket.prototype, 'bufferedAmount', { + get: function get() { + var amount = 0; + if (this._socket) { + amount = this._socket.bufferSize || 0; + } + return amount; + } +}); + +// Emulates the W3C Browser based WebSocket interface using function members. +// @see http://dev.w3.org/html5/websockets/#the-websocket-interface +['open', 'error', 'close', 'message'].forEach(function(method) { + Object.defineProperty(WebSocket.prototype, 'on' + method, { + // Returns the current listener + get: function get() { + var listener = this.listeners(method)[0]; + return listener ? (listener._listener ? listener._listener : listener) : undefined; + }, + // Start listening for events + set: function set(listener) { + this.removeAllListeners(method); + this.addEventListener(method, listener); + } + }); +}); + +// Emulates the W3C Browser based WebSocket interface using addEventListener. +// @see https://developer.mozilla.org/en/DOM/element.addEventListener +// @see http://dev.w3.org/html5/websockets/#the-websocket-interface +WebSocket.prototype.addEventListener = function(method, listener) { + var target = this; + + function onMessage (data, flags) { + listener.call(target, new MessageEvent(data, !!flags.binary, target)); + } + + function onClose (code, message) { + listener.call(target, new CloseEvent(code, message, target)); + } + + function onError (event) { + event.type = 'error'; + event.target = target; + listener.call(target, event); + } + + function onOpen () { + listener.call(target, new OpenEvent(target)); + } + + if (typeof listener === 'function') { + if (method === 'message') { + // store a reference so we can return the original function from the + // addEventListener hook + onMessage._listener = listener; + this.on(method, onMessage); + } else if (method === 'close') { + // store a reference so we can return the original function from the + // addEventListener hook + onClose._listener = listener; + this.on(method, onClose); + } else if (method === 'error') { + // store a reference so we can return the original function from the + // addEventListener hook + onError._listener = listener; + this.on(method, onError); + } else if (method === 'open') { + // store a reference so we can return the original function from the + // addEventListener hook + onOpen._listener = listener; + this.on(method, onOpen); + } else { + this.on(method, listener); + } + } +}; + +// @see http://www.w3.org/TR/html5/comms.html +function MessageEvent(dataArg, isBinary, target) { + this.type = 'message'; + this.data = dataArg; + this.target = target; + this.binary = isBinary; // non-standard. +} + +// @see http://www.w3.org/TR/html5/comms.html +function CloseEvent(code, reason, target) { + this.type = 'close'; + this.wasClean = (typeof code === 'undefined' || code === 1000); + this.code = code; + this.reason = reason; + this.target = target; +} + +// @see http://www.w3.org/TR/html5/comms.html +function OpenEvent(target) { + this.type = 'open'; + this.target = target; +} +// Append port number to Host header, only if specified in the url +// and non-default +function buildHostHeader(isSecure, hostname, port) { + var headerHost = hostname; + if (hostname) { + if ((isSecure && (port != 443)) || (!isSecure && (port != 80))){ + headerHost = headerHost + ':' + port; + } + } + return headerHost; +} + +function initAsServerClient(req, socket, upgradeHead, options) { + let opts = options + opts = {} + + opts.protocolVersion = options.protocolVersion || protocolVersion + opts.protocol = options.protocol || undefined + opts.extensions = options.extensions || {} + // expose state properties + this.protocol = opts.protocol; + this.protocolVersion = opts.protocolVersion; + this.extensions = opts.extensions; + this.supports.binary = (this.protocolVersion !== 'hixie-76'); + this.upgradeReq = req; + this.readyState = WebSocket.CONNECTING; + this._isServer = true; + // establish connection + if (opts.protocolVersion === 'hixie-76') { + establishConnection.call(this, ReceiverHixie, SenderHixie, socket, upgradeHead); + } else { + establishConnection.call(this, Receiver, Sender, socket, upgradeHead); + } +} + +function initAsClient(address, protocols, options) { + let opts = options || {} + + opts.origin = opts.origin || null + opts.protocolVersion = opts.protocolVersion || protocolVersion + opts.host = opts.host || null + opts.headers = opts.headers || null + opts.protocol = opts.protocol || protocols.join(',') + opts.agent = opts.agent || null + // ssl-related options + opts.pfx = opts.pfx || null + opts.key = opts.key || null + opts.passphrase = opts.passphrase || null + opts.cert = opts.cert || null + opts.ca = opts.ca || null + opts.ciphers = opts.ciphers || null + opts.rejectUnauthorized = opts.rejectUnauthorized || null + // TODO(eljefedelrodeodeljefe): failing test / assertion + // at test/WebSocket.test.js:1940:61 -> is enabled by default + opts.perMessageDeflate = opts.perMessageDeflate || null + opts.localAddress = opts.localAddress || null + + if (opts.protocolVersion !== 8 && opts.protocolVersion !== 13) { + throw new Error('unsupported protocol version'); + } + // verify URL and establish http class + var serverUrl = url.parse(address); + var isUnixSocket = serverUrl.protocol === 'ws+unix:'; + if (!serverUrl.host && !isUnixSocket) throw new Error('invalid url'); + var isSecure = serverUrl.protocol === 'wss:' || serverUrl.protocol === 'https:'; + var httpObj = isSecure ? https : http; + var port = serverUrl.port || (isSecure ? 443 : 80); + var auth = serverUrl.auth; + // prepare extensions + var extensionsOffer = {}; + var perMessageDeflate; + if (opts.perMessageDeflate) { + perMessageDeflate = new PerMessageDeflate(typeof opts.perMessageDeflate !== true ? opts.perMessageDeflate : {}, false); + extensionsOffer[PerMessageDeflate.extensionName] = perMessageDeflate.offer(); + } + // expose state properties + this._isServer = false; + this.url = address; + this.protocolVersion = opts.protocolVersion; + this.supports.binary = (this.protocolVersion !== 'hixie-76'); + // begin handshake + var key = new Buffer(opts.protocolVersion + '-' + Date.now()).toString('base64'); + var shasum = crypto.createHash('sha1'); + shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'); + var expectedServerKey = shasum.digest('base64'); + + var agent = opts.agent; + var headerHost = buildHostHeader(isSecure, serverUrl.hostname, port) + + var requestOptions = { + port: port, + host: serverUrl.hostname, + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Host': headerHost, + 'Sec-WebSocket-Version': opts.protocolVersion, + 'Sec-WebSocket-Key': key + } + }; + // If we have basic auth. + if (auth) { + requestOptions.headers.Authorization = 'Basic ' + new Buffer(auth).toString('base64'); + } + + if (opts.protocol) { + requestOptions.headers['Sec-WebSocket-Protocol'] = opts.protocol; + } + + if (opts.host) { + requestOptions.headers.Host = opts.host; + } + + if (opts.headers) { + for (var header in opts.headers) { + if (opts.headers.hasOwnProperty(header)) { + requestOptions.headers[header] = opts.headers[header]; + } + } + } + + if (Object.keys(extensionsOffer).length) { + requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format(extensionsOffer); + } + + if (opts.pfx + || opts.key + || opts.passphrase + || opts.cert + || opts.ca + || opts.ciphers + || opts.rejectUnauthorized) { + + if (opts.pfx) requestOptions.pfx = opts.pfx; + if (opts.key) requestOptions.key = opts.key; + if (opts.passphrase) requestOptions.passphrase = opts.passphrase; + if (opts.cert) requestOptions.cert = opts.cert; + if (opts.ca) requestOptions.ca = opts.ca; + if (opts.ciphers) requestOptions.ciphers = opts.ciphers; + if (opts.rejectUnauthorized) requestOptions.rejectUnauthorized = opts.rejectUnauthorized; + + if (!agent) { + // global agent ignores client side certificates + agent = new httpObj.Agent(requestOptions); + } + } + + requestOptions.path = serverUrl.path || '/'; + + if (agent) { + requestOptions.agent = agent; + } + + if (isUnixSocket) { + requestOptions.socketPath = serverUrl.pathname; + } + + if (opts.localAddress) { + requestOptions.localAddress = opts.localAddress; + } + + if (opts.origin) { + if (opts.protocolVersion < 13) requestOptions.headers['Sec-WebSocket-Origin'] = opts.origin; + else requestOptions.headers.Origin = opts.origin; + } + + var self = this; + var req = httpObj.request(requestOptions); + + req.on('error', function onerror(error) { + self.emit('error', error); + cleanupWebsocketResources.call(self, error); + }); + + req.once('response', function response(res) { + var error; + + if (!self.emit('unexpected-response', req, res)) { + error = new Error('unexpected server response (' + res.statusCode + ')'); + req.abort(); + self.emit('error', error); + } + + cleanupWebsocketResources.call(self, error); + }); + + req.once('upgrade', function upgrade(res, socket, upgradeHead) { + if (self.readyState === WebSocket.CLOSED) { + // client closed before server accepted connection + self.emit('close'); + self.removeAllListeners(); + socket.end(); + return; + } + + var serverKey = res.headers['sec-websocket-accept']; + if (typeof serverKey === 'undefined' || serverKey !== expectedServerKey) { + self.emit('error', 'invalid server key'); + self.removeAllListeners(); + socket.end(); + return; + } + + var serverProt = res.headers['sec-websocket-protocol']; + var protList = (opts.protocol || "").split(/, */); + var protError = null; + + if (!opts.protocol && serverProt) { + protError = 'server sent a subprotocol even though none requested'; + } else if (opts.protocol && !serverProt) { + protError = 'server sent no subprotocol even though requested'; + } else if (serverProt && protList.indexOf(serverProt) === -1) { + protError = 'server responded with an invalid protocol'; + } + + if (protError) { + self.emit('error', protError); + self.removeAllListeners(); + socket.end(); + return; + } else if (serverProt) { + self.protocol = serverProt; + } + + var serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']); + if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) { + try { + perMessageDeflate.accept(serverExtensions[PerMessageDeflate.extensionName]); + } catch (err) { + self.emit('error', 'invalid extension parameter'); + self.removeAllListeners(); + socket.end(); + return; + } + self.extensions[PerMessageDeflate.extensionName] = perMessageDeflate; + } + + establishConnection.call(self, Receiver, Sender, socket, upgradeHead); + + // perform cleanup on http resources + req.removeAllListeners(); + req = null; + agent = null; + }); + + req.end(); + this.readyState = WebSocket.CONNECTING; +} + +function establishConnection(ReceiverClass, SenderClass, socket, upgradeHead) { + var ultron = this._ultron = new Ultron(socket) + , called = false + , self = this; + + socket.setTimeout(0); + socket.setNoDelay(true); + + this._receiver = new ReceiverClass(this.extensions); + this._socket = socket; + + // socket cleanup handlers + ultron.on('end', cleanupWebsocketResources.bind(this)); + ultron.on('close', cleanupWebsocketResources.bind(this)); + ultron.on('error', cleanupWebsocketResources.bind(this)); + + // ensure that the upgradeHead is added to the receiver + function firstHandler(data) { + if (called || self.readyState === WebSocket.CLOSED) return; + + called = true; + socket.removeListener('data', firstHandler); + ultron.on('data', realHandler); + + if (upgradeHead && upgradeHead.length > 0) { + realHandler(upgradeHead); + upgradeHead = null; + } + + if (data) realHandler(data); + } + + // subsequent packets are pushed straight to the receiver + function realHandler(data) { + self.bytesReceived += data.length; + self._receiver.add(data); + } + ultron.on('data', firstHandler); + // if data was passed along with the http upgrade, + // this will schedule a push of that on to the receiver. + // this has to be done on next tick, since the caller + // hasn't had a chance to set event handlers on this client + // object yet. + process.nextTick(firstHandler); + // receiver event handlers + self._receiver.ontext = function ontext(data, flags) { + flags = flags || {}; + + self.emit('message', data, flags); + }; + + self._receiver.onbinary = function onbinary(data, flags) { + flags = flags || {}; + + flags.binary = true; + self.emit('message', data, flags); + }; + + self._receiver.onping = function onping(data, flags) { + flags = flags || {}; + + self.pong(data, { + mask: !self._isServer, + binary: flags.binary === true + }, true); + + self.emit('ping', data, flags); + }; + + self._receiver.onpong = function onpong(data, flags) { + self.emit('pong', data, flags || {}); + }; + + self._receiver.onclose = function onclose(code, data, flags) { + flags = flags || {}; + + self._closeReceived = true; + self.close(code, data); + }; + + self._receiver.onerror = function onerror(reason, errorCode) { + // close the connection when the receiver reports a HyBi error code + self.close(typeof errorCode !== 'undefined' ? errorCode : 1002, ''); + self.emit('error', reason, errorCode); + }; + + // finalize the client + this._sender = new SenderClass(socket, this.extensions); + this._sender.on('error', function onerror(error) { + self.close(1002, ''); + self.emit('error', error); + }); + + this.readyState = WebSocket.OPEN; + this.emit('open'); +} + +function startQueue(instance) { + instance._queue = instance._queue || []; +} + +function executeQueueSends(instance) { + var queue = instance._queue; + if (typeof queue === 'undefined') return; + + delete instance._queue; + for (var i = 0, l = queue.length; i < l; ++i) { + queue[i](); + } +} + +function sendStream(instance, stream, options, cb) { + stream.on('data', function incoming(data) { + if (instance.readyState !== WebSocket.OPEN) { + if (typeof cb === 'function') cb(new Error('not opened')); + else { + delete instance._queue; + instance.emit('error', new Error('not opened')); + } + return; + } + options.fin = false; + instance._sender.send(data, options); + }); + + stream.on('end', function end() { + if (instance.readyState !== WebSocket.OPEN) { + if (typeof cb === 'function') cb(new Error('not opened')); + else { + delete instance._queue; + instance.emit('error', new Error('not opened')); + } + return; + } + options.fin = true; + instance._sender.send(null, options); + + if (typeof cb === 'function') cb(null); + }); +} + +function cleanupWebsocketResources(error) { + if (this.readyState === WebSocket.CLOSED) return; + + var emitClose = this.readyState !== WebSocket.CONNECTING; + this.readyState = WebSocket.CLOSED; + + clearTimeout(this._closeTimer); + this._closeTimer = null; + + if (emitClose) { + // If the connection was closed abnormally (with an error), or if + // the close control frame was not received then the close code + // must default to 1006. + if (error || !this._closeReceived) { + this._closeCode = 1006; + } + this.emit('close', this._closeCode || 1000, this._closeMessage || ''); + } + + if (this._socket) { + if (this._ultron) this._ultron.destroy(); + this._socket.on('error', function onerror() { + try { this.destroy(); } + catch (e) {} + }); + + try { + if (!error) this._socket.end(); + else this._socket.destroy(); + } catch (e) { /* Ignore termination errors */ } + + this._socket = null; + this._ultron = null; + } + + if (this._sender) { + this._sender.removeAllListeners(); + this._sender = null; + } + + if (this._receiver) { + this._receiver.cleanup(); + this._receiver = null; + } + + if (this.extensions[PerMessageDeflate.extensionName]) { + this.extensions[PerMessageDeflate.extensionName].cleanup(); + } + + this.extensions = null; + + this.removeAllListeners(); + this.on('error', function onerror() {}); // catch all errors after this + delete this._queue; +} + +module.exports = WebSocket; +// TODO(eljefedelrodeodeljefe): do we need to expose this via module? +module.exports.Server = require('internal/websockets/WebSocketServer'); +module.exports.WSWSErrorCodes = WSWSErrorCodes +module.exports.buildHostHeader = buildHostHeader diff --git a/node.gyp b/node.gyp index 672c3ce6691b9f..8fbb6704272d73 100644 --- a/node.gyp +++ b/node.gyp @@ -68,6 +68,7 @@ 'lib/util.js', 'lib/v8.js', 'lib/vm.js', + 'lib/websockets.js', 'lib/zlib.js', 'lib/internal/child_process.js', 'lib/internal/cluster.js', @@ -81,6 +82,17 @@ 'lib/internal/v8_prof_polyfill.js', 'lib/internal/v8_prof_processor.js', 'lib/internal/streams/lazy_transform.js', + 'lib/internal/websockets/BufferPool.js', + 'lib/internal/websockets/BufferUtil.js', + 'lib/internal/websockets/WSErrorCodes.js', + 'lib/internal/websockets/Extensions.js', + 'lib/internal/websockets/Opcodes.js', + 'lib/internal/websockets/PerMessageDeflate.js', + 'lib/internal/websockets/Receiver.js', + 'lib/internal/websockets/ReceiverHixie.js', + 'lib/internal/websockets/Sender.js', + 'lib/internal/websockets/SenderHixie.js', + 'lib/internal/websockets/WebSocketServer.js', 'deps/v8/tools/splaytree.js', 'deps/v8/tools/codemap.js', 'deps/v8/tools/consarray.js', diff --git a/test/common-websockets.js b/test/common-websockets.js new file mode 100644 index 00000000000000..904159960db875 --- /dev/null +++ b/test/common-websockets.js @@ -0,0 +1,295 @@ +'use strict'; +const http = require('http'); +const util = require('util'); +const crypto = require('crypto'); +const events = require('events'); +const Sender = require('../lib/internal/websockets/Sender'); +const Receiver = require('../lib/internal/websockets/Receiver'); + +/** + * Returns a Buffer from a "ff 00 ff"-type hex string. + */ + + function getBufferFromHexString (byteStr) { + var bytes = byteStr.split(' '); + var buf = new Buffer(bytes.length); + for (var i = 0; i < bytes.length; ++i) { + buf[i] = parseInt(bytes[i], 16); + } + return buf; +} + +/** + * Returns a hex string from a Buffer. + */ + + function getHexStringFromBuffer (data) { + var s = ''; + for (var i = 0; i < data.length; ++i) { + s += padl(data[i].toString(16), 2, '0') + ' '; + } + return s.trim(); +} + +/** + * Splits a buffer in two parts. + */ + +function splitBuffer (buffer) { + var b1 = new Buffer(Math.ceil(buffer.length / 2)); + buffer.copy(b1, 0, 0, b1.length); + var b2 = new Buffer(Math.floor(buffer.length / 2)); + buffer.copy(b2, 0, b1.length, b1.length + b2.length); + return [b1, b2]; +} + +/** + * Performs hybi07+ type masking on a hex string or buffer. + */ + + function mask (buf, maskString) { + if (typeof buf == 'string') buf = new Buffer(buf); + var mask = getBufferFromHexString(maskString || '34 83 a8 68'); + for (var i = 0; i < buf.length; ++i) { + buf[i] ^= mask[i % 4]; + } + return buf; +} + +/** + * Returns a hex string representing the length of a message + */ + + function getHybiLengthAsHexString (len, masked) { + if (len < 126) { + var buf = new Buffer(1); + buf[0] = (masked ? 0x80 : 0) | len; + } + else if (len < 65536) { + var buf = new Buffer(3); + buf[0] = (masked ? 0x80 : 0) | 126; + getBufferFromHexString(pack(4, len)).copy(buf, 1); + } + else { + var buf = new Buffer(9); + buf[0] = (masked ? 0x80 : 0) | 127; + getBufferFromHexString(pack(16, len)).copy(buf, 1); + } + return getHexStringFromBuffer(buf); +} + +/** + * Unpacks a Buffer into a number. + */ + +function unpack (buffer) { + var n = 0; + for (var i = 0; i < buffer.length; ++i) { + n = (i == 0) ? buffer[i] : (n * 256) + buffer[i]; + } + return n; +} + +/** + * Returns a hex string, representing a specific byte count 'length', from a number. + */ + + function pack (length, number) { + return padl(number.toString(16), length, '0').replace(/([0-9a-f][0-9a-f])/gi, '$1 ').trim(); +} + +/** + * Left pads the string 's' to a total length of 'n' with char 'c'. + */ + +function padl (s, n, c) { + return new Array(1 + n - s.length).join(c) + s; +} + +/** + * Test strategies + */ + +function validServer(server, req, socket) { + if (typeof req.headers.upgrade === 'undefined' || + req.headers.upgrade.toLowerCase() !== 'websocket') { + throw new Error('invalid headers'); + return; + } + + if (!req.headers['sec-websocket-key']) { + socket.end(); + throw new Error('websocket key is missing'); + } + + // calc key + var key = req.headers['sec-websocket-key']; + var shasum = crypto.createHash('sha1'); + shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + key = shasum.digest('base64'); + + var headers = [ + 'HTTP/1.1 101 Switching Protocols' + , 'Upgrade: websocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Accept: ' + key + ]; + + socket.write(headers.concat('', '').join('\r\n')); + socket.setTimeout(0); + socket.setNoDelay(true); + + var sender = new Sender(socket); + var receiver = new Receiver(); + receiver.ontext = function (message, flags) { + server.emit('message', message, flags); + sender.send(message); + }; + receiver.onbinary = function (message, flags) { + flags = flags || {}; + flags.binary = true; + server.emit('message', message, flags); + sender.send(message, {binary: true}); + }; + receiver.onping = function (message, flags) { + flags = flags || {}; + server.emit('ping', message, flags); + }; + receiver.onpong = function (message, flags) { + flags = flags || {}; + server.emit('pong', message, flags); + }; + receiver.onclose = function (code, message, flags) { + flags = flags || {}; + sender.close(code, message, false, function(err) { + server.emit('close', code, message, flags); + socket.end(); + }); + }; + socket.on('data', function (data) { + receiver.add(data); + }); + socket.on('end', function() { + socket.end(); + }); +} + +function invalidRequestHandler(server, req, socket) { + if (typeof req.headers.upgrade === 'undefined' || + req.headers.upgrade.toLowerCase() !== 'websocket') { + throw new Error('invalid headers'); + return; + } + + if (!req.headers['sec-websocket-key']) { + socket.end(); + throw new Error('websocket key is missing'); + } + + // calc key + var key = req.headers['sec-websocket-key']; + var shasum = crypto.createHash('sha1'); + shasum.update(key + "bogus"); + key = shasum.digest('base64'); + + var headers = [ + 'HTTP/1.1 101 Switching Protocols' + , 'Upgrade: websocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Accept: ' + key + ]; + + socket.write(headers.concat('', '').join('\r\n')); + socket.end(); +} + +function closeAfterConnectHandler(server, req, socket) { + if (typeof req.headers.upgrade === 'undefined' || + req.headers.upgrade.toLowerCase() !== 'websocket') { + throw new Error('invalid headers'); + return; + } + + if (!req.headers['sec-websocket-key']) { + socket.end(); + throw new Error('websocket key is missing'); + } + + // calc key + var key = req.headers['sec-websocket-key']; + var shasum = crypto.createHash('sha1'); + shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + key = shasum.digest('base64'); + + var headers = [ + 'HTTP/1.1 101 Switching Protocols' + , 'Upgrade: websocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Accept: ' + key + ]; + + socket.write(headers.concat('', '').join('\r\n')); + socket.end(); +} + + +function return401(server, req, socket) { + var headers = [ + 'HTTP/1.1 401 Unauthorized' + , 'Content-type: text/html' + ]; + + socket.write(headers.concat('', '').join('\r\n')); + socket.write('Not allowed!'); + socket.end(); +} + +/** + * Server object, which will do the actual emitting + */ + +function Server(webServer) { + this.webServer = webServer; +} + +util.inherits(Server, events.EventEmitter); + +Server.prototype.close = function() { + this.webServer.close(); + if (this._socket) this._socket.end(); +} + + +exports.handlers = { + valid: validServer, + invalidKey: invalidRequestHandler, + closeAfterConnect: closeAfterConnectHandler, + return401: return401 + } + +exports.createServer = function createServer (port, handler, cb) { + if (handler && !cb) { + cb = handler; + handler = null; + } + var webServer = http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('okay'); + }); + var srv = new Server(webServer); + webServer.on('upgrade', function(req, socket) { + webServer._socket = socket; + (handler || validServer)(srv, req, socket); + }); + webServer.listen(port, '127.0.0.1', function() { cb(srv); }); +} + + +exports.getBufferFromHexString = getBufferFromHexString +exports.getHexStringFromBuffer = getHexStringFromBuffer +exports.splitBuffer = splitBuffer +exports.mask = mask +exports.getHybiLengthAsHexString = getHybiLengthAsHexString +exports.unpack = unpack +exports.pack = pack +exports.padl = padl diff --git a/test/fixtures/websockets/agent1-cert.pem b/test/fixtures/websockets/agent1-cert.pem new file mode 100644 index 00000000000000..cccb9fb4d35dfb --- /dev/null +++ b/test/fixtures/websockets/agent1-cert.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICbjCCAdcCCQCVvok5oeLpqzANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV +UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO +BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA +dGlueWNsb3Vkcy5vcmcwHhcNMTMwMzA4MDAzMDIyWhcNNDAwNzIzMDAzMDIyWjB9 +MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK +EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDzANBgNVBAMTBmFnZW50MTEgMB4G +CSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBAL6GwKosYb0Yc3Qo0OtQVlCJ4208Idw11ij+t2W5sfYbCil5tyQo +jnhGM1CJhEXynQpXXwjKJuIeTQCkeUibTyFKa0bs8+li2FiGoKYbb4G81ovnqkmE +2iDVb8Gw3rrM4zeZ0ZdFnjMsAZac8h6+C4sB/pS9BiMOo6qTl15RQlcJAgMBAAEw +DQYJKoZIhvcNAQEFBQADgYEAOtmLo8DwTPnI4wfQbQ3hWlTS/9itww6IsxH2ODt9 +ggB7wi7N3uAdIWRZ54ke0NEAO5CW1xNTwsWcxQbiHrDOqX1vfVCjIenI76jVEEap +/Ay53ydHNBKdsKkib61Me14Mu0bA3lUul57VXwmH4NUEFB3w973Q60PschUhOEXj +7DY= +-----END CERTIFICATE----- diff --git a/test/fixtures/websockets/agent1-key.pem b/test/fixtures/websockets/agent1-key.pem new file mode 100644 index 00000000000000..cbd5f0c26ae366 --- /dev/null +++ b/test/fixtures/websockets/agent1-key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC+hsCqLGG9GHN0KNDrUFZQieNtPCHcNdYo/rdlubH2Gwopebck +KI54RjNQiYRF8p0KV18IyibiHk0ApHlIm08hSmtG7PPpYthYhqCmG2+BvNaL56pJ +hNog1W/BsN66zOM3mdGXRZ4zLAGWnPIevguLAf6UvQYjDqOqk5deUUJXCQIDAQAB +AoGANu/CBA+SCyVOvRK70u4yRTzNMAUjukxnuSBhH1rg/pajYnwvG6T6F6IeT72n +P0gKkh3JUE6B0bds+p9yPUZTFUXghxjcF33wlIY44H6gFE4K5WutsFJ9c450wtuu +8rXZTsIg7lAXWjTFVmdtOEPetcGlO2Hpi1O7ZzkzHgB2w9ECQQDksCCYx78or1zY +ZSokm8jmpIjG3VLKdvI9HAoJRN40ldnwFoigrFa1AHwsFtWNe8bKyVRPDoLDUjpB +dkPWgweVAkEA1UfgqguQ2KIkbtp9nDBionu3QaajksrRHwIa8vdfRfLxszfHk2fh +NGY3dkRZF8HUAbzYLrd9poVhCBAEjWekpQJASOM6AHfpnXYHCZF01SYx6hEW5wsz +kARJQODm8f1ZNTlttO/5q/xBxn7ZFNRSTD3fJlL05B2j380ddC/Vf1FT4QJAP1BC +GliqnBSuGhZUWYxni3KMeTm9rzL0F29pjpzutHYlWB2D6ndY/FQnvL0XcZ0Bka58 +womIDGnl3x3aLBwLXQJBAJv6h5CHbXHx7VyDJAcNfppAqZGcEaiVg8yf2F33iWy2 +FLthhJucx7df7SO2aw5h06bRDRAhb9br0R9/3mLr7RE= +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/websockets/ca1-cert.pem b/test/fixtures/websockets/ca1-cert.pem new file mode 100644 index 00000000000000..1d0c0d68882081 --- /dev/null +++ b/test/fixtures/websockets/ca1-cert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICazCCAdQCCQC9/g69HtxXRzANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV +UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO +BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA +dGlueWNsb3Vkcy5vcmcwHhcNMTMwMzA4MDAzMDIyWhcNNDAwNzIzMDAzMDIyWjB6 +MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK +EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqG +SIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0A +MIGJAoGBAKxr1mARUcv7zaqx5y4AxJPK6c1jdbSg7StcL4vg8klaPAlfNO6o+/Cl +w5CdQD3ukaVUwUOJ4T/+b3Xf7785XcWBC33GdjVQkfbHATJYcka7j7JDw3qev5Jk +1rAbRw48hF6rYlSGcx1mccAjoLoa3I8jgxCNAYHIjUQXgdmU893rAgMBAAEwDQYJ +KoZIhvcNAQEFBQADgYEAis05yxjCtJRuv8uX/DK6TX/j9C9Lzp1rKDNFTaTZ0iRw +KCw1EcNx4OXSj9gNblW4PWxpDvygrt1AmH9h2cb8K859NSHa9JOBFw6MA5C2A4Sj +NQfNATqUl4T6cdORlcDEZwHtT8b6D4A6Er31G/eJF4Sen0TUFpjdjd+l9RBjHlo= +-----END CERTIFICATE----- diff --git a/test/fixtures/websockets/ca1-key.pem b/test/fixtures/websockets/ca1-key.pem new file mode 100644 index 00000000000000..df1495083e8ecc --- /dev/null +++ b/test/fixtures/websockets/ca1-key.pem @@ -0,0 +1,17 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIFeWxJE1BrRECAggA +MBQGCCqGSIb3DQMHBAgu9PlMSQ+BOASCAoDEZN2tX0xWo/N+Jg+PrvCrFDk3P+3x +5xG/PEDjtMCAWPBEwbnaYHDzYmhNcAmxzGqEHGMDiWYs46LbO560VS3uMvFbEWPo +KYYVb13vkxl2poXdonCb5cHZA5GUYzTIVVJFptl4LHwBczHoMHtA4FqAhKlYvlWw +EOrdLB8XcwMmGPFabbbGxno0+EWWM27uNjlogfoxj35mQqSW4rOlhZ460XjOB1Zx +LjXMuZeONojkGYQRG5EUMchBoctQpCOM6cAi9r1B9BvtFCBpDV1c1zEZBzTEUd8o +kLn6tjLmY+QpTdylFjEWc7U3ppLY/pkoTBv4r85a2sEMWqkhSJboLaTboWzDJcU3 +Ke61pMpovt/3yCUd3TKgwduVwwQtDVTlBe0p66aN9QVj3CrFy/bKAGO3vxlli24H +aIjZf+OVoBY21ESlW3jLvNlBf7Ezf///2E7j4SCDLyZSFMTpFoAG/jDRyvi+wTKX +Kh485Bptnip6DCSuoH4u2SkOqwz3gJS/6s02YKe4m311QT4Pzne5/FwOFaS/HhQg +Xvyh2/d00OgJ0Y0PYQsHILPRgTUCKUXvj1O58opn3fxSacsPxIXwj6Z4FYAjUTaV +2B85k1lpant/JJEilDqMjqzx4pHZ/Z3Uto1lSM1JZs9SNL/0UR+6F0TXZTULVU9V +w8jYzz4sPr7LEyrrTbzmjQgnQFVbhAN/eKgRZK/SpLjxpmBV5MfpbPKsPUZqT4UC +4nXa8a/NYUQ9e+QKK8enq9E599c2W442W7Z1uFRZTWReMx/lF8wwA6G8zOPG0bdj +d+T5Gegzd5mvRiXMBklCo8RLxOOvgxun1n3PY4a63aH6mqBhdfhiLp5j +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/websockets/certificate.pem b/test/fixtures/websockets/certificate.pem new file mode 100644 index 00000000000000..0efc2ef5b71820 --- /dev/null +++ b/test/fixtures/websockets/certificate.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICATCCAWoCCQDPufXH86n2QzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJu +bzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTEyMDEwMTE0NDQwMFoXDTIwMDMxOTE0NDQwMFowRTELMAkG +A1UEBhMCbm8xEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtrQ7 ++r//2iV/B6F+4boH0XqFn7alcV9lpjvAmwRXNKnxAoa0f97AjYPGNLKrjpkNXXhB +JROIdbRbZnCNeC5fzX1a+JCo7KStzBXuGSZr27TtFmcV4H+9gIRIcNHtZmJLnxbJ +sIhkGR8yVYdmJZe4eT5ldk1zoB1adgPF1hZhCBMCAwEAATANBgkqhkiG9w0BAQUF +AAOBgQCeWBEHYJ4mCB5McwSSUox0T+/mJ4W48L/ZUE4LtRhHasU9hiW92xZkTa7E +QLcoJKQiWfiLX2ysAro0NX4+V8iqLziMqvswnPzz5nezaOLE/9U/QvH3l8qqNkXu +rNbsW1h/IO6FV8avWFYVFoutUwOaZ809k7iMh2F2JMgXQ5EymQ== +-----END CERTIFICATE----- diff --git a/test/fixtures/websockets/key.pem b/test/fixtures/websockets/key.pem new file mode 100644 index 00000000000000..176fe320bb725b --- /dev/null +++ b/test/fixtures/websockets/key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEChrR/ +3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0WZxXg +f72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwIDAQAB +AoGAAlVY8sHi/aE+9xT77twWX3mGHV0SzdjfDnly40fx6S1Gc7bOtVdd9DC7pk6l +3ENeJVR02IlgU8iC5lMHq4JEHPE272jtPrLlrpWLTGmHEqoVFv9AITPqUDLhB9Kk +Hjl7h8NYBKbr2JHKICr3DIPKOT+RnXVb1PD4EORbJ3ooYmkCQQDfknUnVxPgxUGs +ouABw1WJIOVgcCY/IFt4Ihf6VWTsxBgzTJKxn3HtgvE0oqTH7V480XoH0QxHhjLq +DrgobWU9AkEA0TRJ8/ouXGnFEPAXjWr9GdPQRZ1Use2MrFjneH2+Sxc0CmYtwwqL +Kr5kS6mqJrxprJeluSjBd+3/ElxURrEXjwJAUvmlN1OPEhXDmRHd92mKnlkyKEeX +OkiFCiIFKih1S5Y/sRJTQ0781nyJjtJqO7UyC3pnQu1oFEePL+UEniRztQJAMfav +AtnpYKDSM+1jcp7uu9BemYGtzKDTTAYfoiNF42EzSJiGrWJDQn4eLgPjY0T0aAf/ +yGz3Z9ErbhMm/Ysl+QJBAL4kBxRT8gM4ByJw4sdOvSeCCANFq8fhbgm8pGWlCPb5 +JGmX3/GHFM8x2tbWMGpyZP1DLtiNEFz7eCGktWK5rqE= +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/websockets/request.pem b/test/fixtures/websockets/request.pem new file mode 100644 index 00000000000000..51bc7f6254e196 --- /dev/null +++ b/test/fixtures/websockets/request.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBhDCB7gIBADBFMQswCQYDVQQGEwJubzETMBEGA1UECAwKU29tZS1TdGF0ZTEh +MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEC +hrR/3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0W +ZxXgf72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwID +AQABoAAwDQYJKoZIhvcNAQEFBQADgYEAjsUXEARgfxZNkMjuUcudgU2w4JXS0gGI +JQ0U1LmU0vMDSKwqndMlvCbKzEgPbJnGJDI8D4MeINCJHa5Ceyb8c+jaJYUcCabl +lQW5Psn3+eWp8ncKlIycDRj1Qk615XuXtV0fhkrgQM2ZCm9LaQ1O1Gd/CzLihLjF +W0MmgMKMMRk= +-----END CERTIFICATE REQUEST----- diff --git a/test/fixtures/websockets/textfile b/test/fixtures/websockets/textfile new file mode 100644 index 00000000000000..a10483b0ecc0d9 --- /dev/null +++ b/test/fixtures/websockets/textfile @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam egestas, massa at aliquam luctus, sapien erat viverra elit, nec pulvinar turpis eros sagittis urna. Pellentesque imperdiet tempor varius. Pellentesque blandit, ipsum in imperdiet venenatis, mi elit faucibus odio, id condimentum ante enim sed lectus. Aliquam et odio non odio pellentesque pulvinar. Vestibulum a erat dolor. Integer pretium risus sit amet nisl volutpat nec venenatis magna egestas. Ut bibendum felis eu tellus laoreet eleifend. Nam pulvinar auctor tortor, eu iaculis leo vestibulum quis. In euismod risus ac purus vehicula et fermentum ligula consectetur. Vivamus condimentum tempus lacinia. + +Curabitur sodales condimentum urna id dictum. Sed quis justo sit amet quam ultrices tincidunt vel laoreet nulla. Nullam quis ipsum sed nisi mollis bibendum at sit amet nisi. Donec laoreet consequat velit sit amet mollis. Nam sed sapien a massa iaculis dapibus. Sed dui nunc, ultricies et pellentesque ullamcorper, aliquet vitae ligula. Integer eu velit in neque iaculis venenatis. Ut rhoncus cursus est, ac dignissim leo vehicula a. Nulla ullamcorper vulputate mauris id blandit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eleifend, nisi a tempor sollicitudin, odio massa pretium urna, quis congue sapien elit at tortor. Curabitur ipsum orci, vehicula non commodo molestie, laoreet id enim. Pellentesque convallis ultrices congue. Pellentesque nec iaculis lorem. In sagittis pharetra ipsum eget sodales. + +Fusce id nulla odio. Nunc nibh justo, placerat vel tincidunt sed, ornare et enim. Nulla vel urna vel ante commodo bibendum in vitae metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis erat nunc, semper eget sagittis sit amet, ullamcorper eget lacus. Donec hendrerit ipsum vitae eros vestibulum eu gravida neque tincidunt. Ut molestie lacinia nulla. Donec mattis odio at magna egestas at pellentesque eros accumsan. Praesent interdum sem sit amet nibh commodo dignissim. Duis laoreet, enim ultricies fringilla suscipit, enim libero cursus nulla, sollicitudin adipiscing erat velit ut dui. Nulla eleifend mauris at velit fringilla a molestie lorem venenatis. + +Donec sit amet scelerisque metus. Cras ac felis a nulla venenatis vulputate. Duis porttitor eros ac neque rhoncus eget aliquet neque egestas. Quisque sed nunc est, vitae dapibus quam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In vehicula, est vitae posuere ultricies, diam purus pretium sapien, nec rhoncus dolor nisl eget arcu. Aliquam et nisi vitae risus tincidunt auctor. In vehicula, erat a cursus adipiscing, lorem orci congue est, nec ultricies elit dui in nunc. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +Duis congue tempus elit sit amet auctor. Duis dignissim, risus ut sollicitudin ultricies, dolor ligula gravida odio, nec congue orci purus ut ligula. Fusce pretium dictum lectus at volutpat. Sed non auctor mauris. Etiam placerat vestibulum massa id blandit. Quisque consequat lacus ut nulla euismod facilisis. Sed aliquet ipsum nec mi imperdiet viverra. Pellentesque ullamcorper, lectus nec varius gravida, odio justo cursus risus, eu sagittis metus arcu quis felis. Phasellus consectetur vehicula libero, at condimentum orci euismod vel. Nunc purus tortor, suscipit nec fringilla nec, vulputate et nibh. Nam porta vehicula neque. Praesent porttitor, sapien eu auctor euismod, arcu quam elementum urna, sed hendrerit magna augue sed quam. \ No newline at end of file diff --git a/test/parallel/test-websockets-bufferpool.js b/test/parallel/test-websockets-bufferpool.js new file mode 100644 index 00000000000000..be093d31e2af64 --- /dev/null +++ b/test/parallel/test-websockets-bufferpool.js @@ -0,0 +1,83 @@ +'use strict'; +const BufferPool = require('../../lib/internal/websockets/BufferPool'); +const assert = require('assert'); + +/*'BufferPool'*/ +{ + /*'#ctor'*/ + { + /* 'allocates pool'*/ + { + var db = new BufferPool(1000); + assert.equal(db.size, 1000); + } + /*'throws TypeError when called without new'*/ + { + try { + var db = BufferPool(1000); + } catch (e) { + assert.ok(e instanceof TypeError); + } + } + } + /*'#get'*/ + { + /*'grows the pool if necessary'*/ + { + var db = new BufferPool(1000); + var buf = db.get(2000); + assert.ok(db.size > 1000); + assert.equal(db.used, 2000); + assert.equal(buf.length, 2000); + } + /*'grows the pool after the first call, if necessary'*/ + { + var db = new BufferPool(1000); + var buf = db.get(1000); + assert.equal(db.used, 1000); + assert.equal(db.size, 1000); + assert.equal(buf.length, 1000); + var buf2 = db.get(1000); + assert.equal(db.used, 2000); + assert.ok(db.size > 1000); + assert.equal(buf2.length, 1000); + } + /*'grows the pool according to the growStrategy if necessary'*/ + { + var db = new BufferPool(1000, function(db, length) { + return db.size + 2345; + }); + var buf = db.get(2000); + assert.equal(db.size, 3345); + assert.equal(buf.length, 2000); + } + /*'doesnt grow the pool if theres enough room available'*/ + { + var db = new BufferPool(1000); + var buf = db.get(1000); + assert.equal(db.size, 1000); + assert.equal(buf.length, 1000); + } + } + /*'#reset'*/ + { + /*'shinks the pool'*/ + { + var db = new BufferPool(1000); + var buf = db.get(2000); + db.reset(true); + assert.equal(db.size, 1000); + } + /*'shrinks the pool according to the shrinkStrategy'*/ + { + var db = new BufferPool(1000, function(db, length) { + return db.used + length; + }, function(db) { + return 0; + }); + var buf = db.get(2000); + db.reset(true); + assert.equal(db.size, 0); + } + } +} diff --git a/test/parallel/test-websockets-extensions.js b/test/parallel/test-websockets-extensions.js new file mode 100644 index 00000000000000..b5ad984e539d28 --- /dev/null +++ b/test/parallel/test-websockets-extensions.js @@ -0,0 +1,64 @@ +'use strict'; +const Extensions = require('../../lib/internal/websockets/Extensions'); +const assert = require('assert'); + +/*'Extensions'*/ +{ + /*'parse'*/ + { + /*'should parse'*/ + { + var extensions = Extensions.parse('foo'); + assert.deepEqual(extensions, { foo: [{}] }); + } + + /*'should parse params'*/ + { + var extensions = Extensions.parse('foo; bar; baz=1; bar=2'); + assert.deepEqual(extensions, { + foo: [{ bar: [true, '2'], baz: ['1'] }] + }); + } + + /*'should parse multiple extensions'*/ + { + var extensions = Extensions.parse('foo, bar; baz, foo; baz'); + assert.deepEqual(extensions, { + foo: [{}, { baz: [true] }], + bar: [{ baz: [true] }] + }); + } + + /*'should parse quoted params'*/ + { + var extensions = Extensions.parse('foo; bar="hi"'); + assert.deepEqual(extensions, { + foo: [{ bar: ['hi'] }] + }); + } + } + + /*'format'*/ + { + /*'should format'*/ + { + var extensions = Extensions.format({ foo: {} }); + assert.deepEqual(extensions, 'foo'); + } + + /*'should format params'*/ + { + var extensions = Extensions.format({ foo: { bar: [true, 2], baz: 1 } }); + assert.deepEqual(extensions, 'foo; bar; bar=2; baz=1'); + } + + /*'should format multiple extensions'*/ + { + var extensions = Extensions.format({ + foo: [{}, { baz: true }], + bar: { baz: true } + }); + assert.deepEqual(extensions, 'foo, foo; baz, bar; baz'); + } + } +} diff --git a/test/parallel/test-websockets-permessagedeflate.js b/test/parallel/test-websockets-permessagedeflate.js new file mode 100644 index 00000000000000..773dd4e496fcb7 --- /dev/null +++ b/test/parallel/test-websockets-permessagedeflate.js @@ -0,0 +1,317 @@ +'use strict'; +const PerMessageDeflate = require('../../lib/internal/websockets/PerMessageDeflate'); +const Extensions = require('../../lib/internal/websockets/Extensions'); +const assert = require('assert'); + +/*'PerMessageDeflate'*/ +{ + /*'#ctor'*/ + { + /*'throws TypeError when called without new'*/ + { + try { + var perMessageDeflate = PerMessageDeflate(); + } + catch (e) { + assert.ok(e instanceof TypeError); + } + } + } + + /*'#offer'*/ + { + /*'should create default params'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + assert.deepEqual(perMessageDeflate.offer(), { client_max_window_bits: true }); + } + + /*'should create params from options'*/ + { + var perMessageDeflate = new PerMessageDeflate({ + serverNoContextTakeover: true, + clientNoContextTakeover: true, + serverMaxWindowBits: 10, + clientMaxWindowBits: 11 + }); + assert.deepEqual(perMessageDeflate.offer(), { + server_no_context_takeover: true, + client_no_context_takeover: true, + server_max_window_bits: 10, + client_max_window_bits: 11 + }); + } + } + + /*'#accept'*/ + { + /*'as server'*/ + { + /*'should accept empty offer'*/ + { + var perMessageDeflate = new PerMessageDeflate({}, true); + assert.deepEqual(perMessageDeflate.accept([{}]), {}); + } + + /*'should accept offer'*/ + { + var perMessageDeflate = new PerMessageDeflate({}, true); + var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11'); + assert.deepEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { + server_no_context_takeover: true, + client_no_context_takeover: true, + server_max_window_bits: 10, + client_max_window_bits: 11 + }); + } + + /*'should prefer configuration than offer'*/ + { + var perMessageDeflate = new PerMessageDeflate({ + serverNoContextTakeover: true, + clientNoContextTakeover: true, + serverMaxWindowBits: 12, + clientMaxWindowBits: 11 + }, true); + var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=14; client_max_window_bits=13'); + assert.deepEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { + server_no_context_takeover: true, + client_no_context_takeover: true, + server_max_window_bits: 12, + client_max_window_bits: 11 + }); + } + + /*'should fallback'*/ + { + var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true); + var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10, permessage-deflate'); + assert.deepEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { + server_max_window_bits: 11 + }); + } + + /*'should throw an error if server_no_context_takeover is unsupported'*/ + { + var perMessageDeflate = new PerMessageDeflate({ serverNoContextTakeover: false }, true); + var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })) + } + + /*'should throw an error if server_max_window_bits is unsupported'*/ + { + var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: false }, true); + var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if server_max_window_bits is less than configuration'*/ + { + var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true); + var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if client_max_window_bits is unsupported on client'*/ + { + var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }, true); + var extensions = Extensions.parse('permessage-deflate'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + } + + /*'as client'*/ + { + /*'should accept empty response'*/ + { + var perMessageDeflate = new PerMessageDeflate({}); + assert.deepEqual(perMessageDeflate.accept([{}]), {}); + } + + /*'should accept response parameter'*/ + { + var perMessageDeflate = new PerMessageDeflate({}); + var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11'); + assert.deepEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { + server_no_context_takeover: true, + client_no_context_takeover: true, + server_max_window_bits: 10, + client_max_window_bits: 11 + }); + } + + /*'should throw an error if client_no_context_takeover is unsupported'*/ + { + var perMessageDeflate = new PerMessageDeflate({ clientNoContextTakeover: false }); + var extensions = Extensions.parse('permessage-deflate; client_no_context_takeover'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if client_max_window_bits is unsupported'*/ + { + var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: false }); + var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=10'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if client_max_window_bits is greater than configuration'*/ + { + var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }); + var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=11'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + } + + /*'validate parameters'*/ + { + /*'should throw an error if a parameter has multiple values'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; server_no_context_takeover'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if a parameter is undefined'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + var extensions = Extensions.parse('permessage-deflate; foo;'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if server_no_context_takeover has a value'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover=10'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if client_no_context_takeover has a value'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + var extensions = Extensions.parse('permessage-deflate; client_no_context_takeover=10'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if server_max_window_bits has an invalid value'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=7'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + + /*'should throw an error if client_max_window_bits has an invalid value'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=16'); + assert.throws((function() { + perMessageDeflate.accept(extensions['permessage-deflate']); + })); + } + } + } + + /*'#compress/#decompress'*/ + { + /*'should compress/decompress data'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + perMessageDeflate.compress(new Buffer([1, 2, 3]), true, function(err, compressed) { + if (err) return done(err); + perMessageDeflate.decompress(compressed, true, function(err, data) { + if (err) return; + assert.deepEqual(data, new Buffer([1, 2, 3])); + }); + }); + } + + /*'should compress/decompress fragments'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + var buf = new Buffer([1, 2, 3, 4]); + perMessageDeflate.compress(buf.slice(0, 2), false, function(err, compressed1) { + if (err) return done(err); + perMessageDeflate.compress(buf.slice(2), true, function(err, compressed2) { + if (err) return; + perMessageDeflate.decompress(compressed1, false, function(err, data1) { + if (err) return; + perMessageDeflate.decompress(compressed2, true, function(err, data2) { + if (err) return; + assert.deepEqual(new Buffer.concat([data1, data2]), new Buffer([1, 2, 3, 4])); + }); + }); + }); + }); + } + + /*'should compress/decompress data with parameters'*/ + { + function done() {} + (function () { + var perMessageDeflate = new PerMessageDeflate({ memLevel: 5 }); + var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11'); + perMessageDeflate.accept(extensions['permessage-deflate']); + perMessageDeflate.compress(new Buffer([1, 2, 3]), true, function(err, compressed) { + if (err) return done(err); + perMessageDeflate.decompress(compressed, true, function(err, data) { + if (err) return done(err); + assert.deepEqual(data, new Buffer([1, 2, 3])); + done() + }); + }); + }()) + } + + /*'should compress/decompress data with no context takeover'*/ + { + function done() {} + (function () { + var perMessageDeflate = new PerMessageDeflate(); + var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover'); + perMessageDeflate.accept(extensions['permessage-deflate']); + var buf = new Buffer('foofoo'); + perMessageDeflate.compress(buf, true, function(err, compressed1) { + if (err) return done(err); + perMessageDeflate.decompress(compressed1, true, function(err, data) { + if (err) return done(err); + perMessageDeflate.compress(data, true, function(err, compressed2) { + if (err) return done(err); + perMessageDeflate.decompress(compressed2, true, function(err, data) { + if (err) return done(err); + assert.deepEqual(compressed2.length, compressed1.length); + assert.deepEqual(data, buf); + done() + }); + }); + }); + }); + }()) + } + } +} diff --git a/test/parallel/test-websockets-receiver.js b/test/parallel/test-websockets-receiver.js new file mode 100644 index 00000000000000..2dd2dd45cbc411 --- /dev/null +++ b/test/parallel/test-websockets-receiver.js @@ -0,0 +1,351 @@ +'use strict'; +const assert = require('assert'); +const Receiver = require('../../lib/internal/websockets/Receiver'); +const PerMessageDeflate = require('../../lib/internal/websockets/PerMessageDeflate'); +const ws_common = require('../common-websockets'); + +/*'Receiver'*/ +{ + /*'#ctor'*/ + { + /*'throws TypeError when called without new'*/ + { + try { + var p = Receiver(); + } + catch (e) { + assert.ok(e instanceof TypeError); + } + } + } + + /*'can parse unmasked text message'*/ + { + var p = new Receiver(); + var packet = '81 05 48 65 6c 6c 6f'; + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal('Hello', data); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotData); + } + /*'can parse close message'*/ + { + var p = new Receiver(); + var packet = '88 00'; + + var gotClose = false; + p.onclose = function(data) { + gotClose = true; + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotClose); + } + /*'can parse masked text message'*/ + { + var p = new Receiver(); + var packet = '81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5'; + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal('5:::{"name":"echo"}', data); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotData); + } + /*'can parse a masked text message longer than 125 bytes'*/ + { + var p = new Receiver(); + var message = 'A'; + for (var i = 0; i < 300; ++i) message += (i % 5).toString(); + var packet = '81 FE ' + ws_common.pack(4, message.length) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(message, '34 83 a8 68')); + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal(message, data); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotData); + } + /*'can parse a really long masked text message'*/ + { + var p = new Receiver(); + var message = 'A'; + for (var i = 0; i < 64*1024; ++i) message += (i % 5).toString(); + var packet = '81 FF ' + ws_common.pack(16, message.length) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(message, '34 83 a8 68')); + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal(message, data); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotData); + } + /*'can parse a fragmented masked text message of 300 bytes'*/ + { + var p = new Receiver(); + var message = 'A'; + for (var i = 0; i < 300; ++i) message += (i % 5).toString(); + var msgpiece1 = message.substr(0, 150); + var msgpiece2 = message.substr(150); + var packet1 = '01 FE ' + ws_common.pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(msgpiece1, '34 83 a8 68')); + var packet2 = '80 FE ' + ws_common.pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(msgpiece2, '34 83 a8 68')); + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal(message, data); + }; + + p.add(ws_common.getBufferFromHexString(packet1)); + p.add(ws_common.getBufferFromHexString(packet2)); + assert.ok(gotData); + } + /*'can parse a ping message'*/ + { + var p = new Receiver(); + var message = 'Hello'; + var packet = '89 ' + ws_common.getHybiLengthAsHexString(message.length, true) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(message, '34 83 a8 68')); + + var gotPing = false; + p.onping = function(data) { + gotPing = true; + assert.equal(message, data); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotPing); + } + /*'can parse a ping with no data'*/ + { + var p = new Receiver(); + var packet = '89 00'; + + var gotPing = false; + p.onping = function(data) { + gotPing = true; + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotPing); + } + /*'can parse a fragmented masked text message of 300 bytes with a ping in the middle'*/ + { + var p = new Receiver(); + var message = 'A'; + for (var i = 0; i < 300; ++i) message += (i % 5).toString(); + + var msgpiece1 = message.substr(0, 150); + var packet1 = '01 FE ' + ws_common.pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(msgpiece1, '34 83 a8 68')); + + var pingMessage = 'Hello'; + var pingPacket = '89 ' + ws_common.getHybiLengthAsHexString(pingMessage.length, true) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(pingMessage, '34 83 a8 68')); + + var msgpiece2 = message.substr(150); + var packet2 = '80 FE ' + ws_common.pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(msgpiece2, '34 83 a8 68')); + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal(message, data); + }; + var gotPing = false; + p.onping = function(data) { + gotPing = true; + assert.equal(pingMessage, data); + }; + + p.add(ws_common.getBufferFromHexString(packet1)); + p.add(ws_common.getBufferFromHexString(pingPacket)); + p.add(ws_common.getBufferFromHexString(packet2)); + assert.ok(gotData); + assert.ok(gotPing); + } + /*'can parse a fragmented masked text message of 300 bytes with a ping in the middle, which is delievered over sevaral tcp packets'*/ + { + var p = new Receiver(); + var message = 'A'; + for (var i = 0; i < 300; ++i) message += (i % 5).toString(); + + var msgpiece1 = message.substr(0, 150); + var packet1 = '01 FE ' + ws_common.pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(msgpiece1, '34 83 a8 68')); + + var pingMessage = 'Hello'; + var pingPacket = '89 ' + ws_common.getHybiLengthAsHexString(pingMessage.length, true) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(pingMessage, '34 83 a8 68')); + + var msgpiece2 = message.substr(150); + var packet2 = '80 FE ' + ws_common.pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(msgpiece2, '34 83 a8 68')); + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal(message, data); + }; + var gotPing = false; + p.onping = function(data) { + gotPing = true; + assert.equal(pingMessage, data); + }; + + var buffers = []; + buffers = buffers.concat(ws_common.splitBuffer(ws_common.getBufferFromHexString(packet1))); + buffers = buffers.concat(ws_common.splitBuffer(ws_common.getBufferFromHexString(pingPacket))); + buffers = buffers.concat(ws_common.splitBuffer(ws_common.getBufferFromHexString(packet2))); + for (var i = 0; i < buffers.length; ++i) { + p.add(buffers[i]); + } + assert.ok(gotData); + assert.ok(gotPing); + } + /*'can parse a 100 byte long masked binary message'*/ + { + var p = new Receiver(); + var length = 100; + var message = new Buffer(length); + for (var i = 0; i < length; ++i) message[i] = i % 256; + var originalMessage = ws_common.getHexStringFromBuffer(message); + var packet = '82 ' + ws_common.getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(message, '34 83 a8 68')); + + var gotData = false; + p.onbinary = function(data) { + gotData = true; + assert.equal(originalMessage, ws_common.getHexStringFromBuffer(data)); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotData); + } + /*'can parse a 256 byte long masked binary message'*/ + { + var p = new Receiver(); + var length = 256; + var message = new Buffer(length); + for (var i = 0; i < length; ++i) message[i] = i % 256; + var originalMessage = ws_common.getHexStringFromBuffer(message); + var packet = '82 ' + ws_common.getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(message, '34 83 a8 68')); + + var gotData = false; + p.onbinary = function(data) { + gotData = true; + assert.equal(originalMessage, ws_common.getHexStringFromBuffer(data)); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotData); + } + /*'can parse a 200kb long masked binary message'*/ + { + var p = new Receiver(); + var length = 200 * 1024; + var message = new Buffer(length); + for (var i = 0; i < length; ++i) message[i] = i % 256; + var originalMessage = ws_common.getHexStringFromBuffer(message); + var packet = '82 ' + ws_common.getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + ws_common.getHexStringFromBuffer(ws_common.mask(message, '34 83 a8 68')); + + var gotData = false; + p.onbinary = function(data) { + gotData = true; + assert.equal(originalMessage, ws_common.getHexStringFromBuffer(data)); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotData); + } + /*'can parse a 200kb long unmasked binary message'*/ + { + var p = new Receiver(); + var length = 200 * 1024; + var message = new Buffer(length); + for (var i = 0; i < length; ++i) message[i] = i % 256; + var originalMessage = ws_common.getHexStringFromBuffer(message); + var packet = '82 ' + ws_common.getHybiLengthAsHexString(length, false) + ' ' + ws_common.getHexStringFromBuffer(message); + + var gotData = false; + p.onbinary = function(data) { + gotData = true; + assert.equal(originalMessage, ws_common.getHexStringFromBuffer(data)); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.ok(gotData); + } + /*'can parse compressed message'*/ + { + function done () {} + (function () { + var perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + var p = new Receiver({ 'permessage-deflate': perMessageDeflate }); + var buf = new Buffer('Hello'); + + p.ontext = function(data) { + assert.equal('Hello', data); + done(); + }; + + perMessageDeflate.compress(buf, true, function(err, compressed) { + if (err) return done(err); + p.add(new Buffer([0xc1, compressed.length])); + p.add(compressed); + }); + }()) + } + /*'can parse compressed fragments'*/ + { + var perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + var p = new Receiver({ 'permessage-deflate': perMessageDeflate }); + var buf1 = new Buffer('foo'); + var buf2 = new Buffer('bar'); + + p.ontext = function(data) { + assert.equal('foobar', data); + }; + + perMessageDeflate.compress(buf1, false, function(err, compressed1) { + if (err) return done(err); + p.add(new Buffer([0x41, compressed1.length])); + p.add(compressed1); + + perMessageDeflate.compress(buf2, true, function(err, compressed2) { + p.add(new Buffer([0x80, compressed2.length])); + p.add(compressed2); + }); + }); + } + /*'can cleanup during consuming data'*/ + { + function done () {} + (function () { + var perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + var p = new Receiver({ 'permessage-deflate': perMessageDeflate }); + var buf = new Buffer('Hello'); + + perMessageDeflate.compress(buf, true, function(err, compressed) { + if (err) return done(err); + var data = Buffer.concat([new Buffer([0xc1, compressed.length]), compressed]); + p.add(data); + p.add(data); + p.add(data); + p.cleanup(); + // TODO(eljefedelrodeodeljefe): long tinmeout + setTimeout(done, 1000); + }); + }()) + } +} diff --git a/test/parallel/test-websockets-receiverhixie.js b/test/parallel/test-websockets-receiverhixie.js new file mode 100644 index 00000000000000..2e19730cf8f819 --- /dev/null +++ b/test/parallel/test-websockets-receiverhixie.js @@ -0,0 +1,177 @@ +'use strict'; +const assert = require('assert'); +const Receiver = require('../../lib/internal/websockets/ReceiverHixie'); +const ws_common = require('../common-websockets'); + +/*'Receiver'*/ +{ + /*'#ctor'*/ + { + /*'throws TypeError when called without new'*/ + try { + var p = Receiver(); + } + catch (e) { + assert.ok(e instanceof TypeError); + } + } + + /*'can parse text message'*/ + { + var p = new Receiver(); + var packet = '00 48 65 6c 6c 6f ff'; + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal('Hello', data); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.equal(gotData, true); + } + + /*'can parse multiple text messages'*/ + { + var p = new Receiver(); + var packet = '00 48 65 6c 6c 6f ff 00 48 65 6c 6c 6f ff'; + + var gotData = false; + var messages = []; + p.ontext = function(data) { + gotData = true; + messages.push(data); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.equal(gotData, true); + for (var i = 0; i < 2; ++i) { + assert.equal(messages[i], 'Hello'); + } + } + + /*'can parse empty message'*/ + { + var p = new Receiver(); + var packet = '00 ff'; + + var gotData = false; + p.ontext = function(data) { + gotData = true; + assert.equal('', data); + }; + + p.add(ws_common.getBufferFromHexString(packet)); + assert.deepEqual(gotData, true); + } + + /*'can parse text messages delivered over multiple frames'*/ + { + var p = new Receiver(); + var packets = [ + '00 48', + '65 6c 6c', + '6f ff 00 48', + '65', + '6c 6c 6f', + 'ff' + ]; + + var gotData = false; + var messages = []; + p.ontext = function(data) { + gotData = true; + messages.push(data); + }; + + for (var i = 0; i < packets.length; ++i) { + p.add(ws_common.getBufferFromHexString(packets[i])); + } + assert.equal(gotData, true); + for (var i = 0; i < 2; ++i) { + assert.equal(messages[i], 'Hello'); + } + } + + /*'emits an error if a payload doesnt start with 0x00'*/ + { + var p = new Receiver(); + var packets = [ + '00 6c ff', + '00 6c ff ff', + 'ff 00 6c ff 00 6c ff', + '00', + '6c 6c 6f', + 'ff' + ]; + + var gotData = false; + var gotError = false; + var messages = []; + p.ontext = function(data) { + gotData = true; + messages.push(data); + }; + p.onerror = function(reason, code) { + gotError = code == true; + }; + + for (var i = 0; i < packets.length && !gotError; ++i) { + p.add(ws_common.getBufferFromHexString(packets[i])); + } + assert.equal(gotError, true); + assert.equal(messages[0], 'l'); + assert.equal(messages[1], 'l'); + assert.equal(messages.length, 2); + } + + /*'can parse close messages'*/ + { + var p = new Receiver(); + var packets = [ + 'ff 00' + ]; + + var gotClose = false; + var gotError = false; + p.onclose = function() { + gotClose = true; + }; + p.onerror = function(reason, code) { + gotError = code == true; + }; + + for (var i = 0; i < packets.length && !gotError; ++i) { + p.add(ws_common.getBufferFromHexString(packets[i])); + } + assert.equal(gotClose, true); + assert.equal(gotError, false); + } + + /*'can parse binary messages delivered over multiple frames'*/ + { + var p = new Receiver(); + var packets = [ + '80 05 48', + '65 6c 6c', + '6f 80 80 05 48', + '65', + '6c 6c 6f' + ]; + + var gotData = false; + var messages = []; + p.ontext = function(data) { + gotData = true; + messages.push(data); + }; + + for (var i = 0; i < packets.length; ++i) { + p.add(ws_common.getBufferFromHexString(packets[i])); + } + assert.equal(gotData, true); + for (var i = 0; i < 2; ++i) { + assert.equal(messages[i], 'Hello'); + } + } +} diff --git a/test/parallel/test-websockets-sender.js b/test/parallel/test-websockets-sender.js new file mode 100644 index 00000000000000..29f7ad50f6442a --- /dev/null +++ b/test/parallel/test-websockets-sender.js @@ -0,0 +1,87 @@ +'use strict'; +const Sender = require('../../lib/internal/websockets/Sender'); +const PerMessageDeflate = require('../../lib/internal/websockets/PerMessageDeflate'); +const assert = require('assert') + +/*'Sender'*/ +{ + /*'#ctor'*/ + { + /*'throws TypeError when called without new'*/ + try { + var sender = Sender({ write: function() {} }); + } + catch (e) { + assert.ok(e instanceof TypeError); + } + } + + /*'#frameAndSend'*/ + { + /*'does not modify a masked binary buffer'*/ + { + var sender = new Sender({ write: function() {} }); + var buf = new Buffer([1, 2, 3, 4, 5]); + sender.frameAndSend(2, buf, true, true); + assert.equal(buf[0], 1); + assert.equal(buf[1], 2); + assert.equal(buf[2], 3); + assert.equal(buf[3], 4); + assert.equal(buf[4], 5); + } + + /*'does not modify a masked text buffer'*/ + { + var sender = new Sender({ write: function() {} }); + var text = 'hi there'; + sender.frameAndSend(1, text, true, true); + assert.equal(text, 'hi there'); + } + + /*'sets rsv1 flag if compressed'*/ + var sender = new Sender({ + write: function(data) { + assert.equal((data[0] & 0x40), 0x40); + } + }); + sender.frameAndSend(1, 'hi', true, false, true); + } + + /*'#send'*/ + { + /*'compresses data if compress option is enabled'*/ + var perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + var sender = new Sender({ + write: function(data) { + assert.equal((data[0] & 0x40), 0x40); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + sender.send('hi', { compress: true }); + } + + /*'#close'*/ + { + /*should consume all data before closing'*/ + var perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + var count = 0; + var sender = new Sender({ + write: function(data) { + count++; + } + }, { + 'permessage-deflate': perMessageDeflate + }); + sender.send('foo', {compress: true}); + sender.send('bar', {compress: true}); + sender.send('baz', {compress: true}); + sender.close(1000, null, false, function(err) { + assert.equal(count, 4); + }); + } +} diff --git a/test/parallel/test-websockets-senderhixie.js b/test/parallel/test-websockets-senderhixie.js new file mode 100644 index 00000000000000..d76c5eec612062 --- /dev/null +++ b/test/parallel/test-websockets-senderhixie.js @@ -0,0 +1,182 @@ +'use strict'; +const assert = require('assert'); +const Sender = require('../../lib/internal/websockets/SenderHixie'); + +/*'Sender'*/ +{ + /*'#ctor'*/ + { + /*'throws TypeError when called without new'*/ + { + function done () {} + (function () { + try { + var sender = Sender({ write: function() {} }); + } + catch (e) { + assert.ok(e instanceof TypeError); + done(); + } + done() + }()) + } + } + + /*'#send'*/ + { + /*'frames and sends a text message'*/ + { + function done () {} + (function () { + var message = 'Hello world'; + var received; + var socket = { + write: function(data, encoding, cb) { + received = data; + process.nextTick(cb); + } + }; + var sender = new Sender(socket, {}); + sender.send(message, {}, function() { + assert.equal(received.toString('utf8'), '\u0000' + message + '\ufffd'); + done(); + }); + }()) + } + + /*'frames and sends an empty message'*/ + { + function done () {} + (function () { + var socket = { + write: function(data, encoding, cb) { + done(); + } + }; + var sender = new Sender(socket, {}); + sender.send('', {}, function() {}); + }()) + } + + /*'frames and sends a buffer'*/ + { + function done () {} + (function () { + var received; + var socket = { + write: function(data, encoding, cb) { + received = data; + process.nextTick(cb); + } + }; + var sender = new Sender(socket, {}); + sender.send(new Buffer('foobar'), {}, function() { + assert.equal(received.toString('utf8'), '\u0000foobar\ufffd'); + done(); + }); + }()) + } + + /*'frames and sends a binary message'*/ + { + function done () {} + (function () { + var message = 'Hello world'; + var received; + var socket = { + write: function(data, encoding, cb) { + received = data; + process.nextTick(cb); + } + }; + var sender = new Sender(socket, {}); + sender.send(message, {binary: true}, function() { + assert.equal(received.toString('hex'), + // 0x80 0x0b H e l l o w o r l d + '800b48656c6c6f20776f726c64'); + done(); + }); + }()) + } +/* + it('throws an exception for binary data', function(done) { + var socket = { + write: function(data, encoding, cb) { + process.nextTick(cb); + } + }; + var sender = new Sender(socket, {}); + sender.on('error', function() { + done(); + }); + sender.send(new Buffer(100), {binary: true}, function() {}); + }); +*/ + /*'can fauxe stream data'*/ + { + function done () {} + (function () { + var received = []; + var socket = { + write: function(data, encoding, cb) { + received.push(data); + process.nextTick(cb); + } + }; + var sender = new Sender(socket, {}); + sender.send(new Buffer('foobar'), { fin: false }, function() {}); + sender.send('bazbar', { fin: false }, function() {}); + sender.send(new Buffer('end'), { fin: true }, function() { + assert.equal(received[0].toString('utf8'), '\u0000foobar'); + assert.equal(received[1].toString('utf8'), 'bazbar'); + assert.equal(received[2].toString('utf8'), 'end\ufffd'); + done(); + }); + }()) + } + } + + /*'#close'*/ + { + /*'sends a hixie close frame'*/ + { + function done () {} + (function () { + var received; + var socket = { + write: function(data, encoding, cb) { + received = data; + process.nextTick(cb); + } + }; + var sender = new Sender(socket, {}); + sender.close(null, null, null, function() { + assert.equal(received.toString('utf8'), '\ufffd\u0000'); + done(); + }); + }()) + } + + /*'sends a message end marker if fauxe streaming has started, before hixie close frame'*/ + { + function done () {} + (function () { + var received = []; + var socket = { + write: function(data, encoding, cb) { + received.push(data); + if (cb) process.nextTick(cb); + } + }; + var sender = new Sender(socket, {}); + sender.send(new Buffer('foobar'), { fin: false }, function() {}); + sender.close(null, null, null, function() { + assert.equal(received[0].toString('utf8'), '\u0000foobar'); + assert.equal(received[1].toString('utf8'), '\ufffd'); + assert.equal(received[2].toString('utf8'), '\ufffd\u0000'); + done(); + }); + }()) + } + } +} diff --git a/test/parallel/test-websockets-websocket.js b/test/parallel/test-websockets-websocket.js new file mode 100644 index 00000000000000..2b91ba92fb5c41 --- /dev/null +++ b/test/parallel/test-websockets-websocket.js @@ -0,0 +1,2455 @@ +'use strict'; +const assert = require('assert'); +const https = require('https'); +const http = require('http'); +const WebSocket = require('websockets'); +const WebSocketServer = require('websockets').Server; +const fs = require('fs'); +const os = require('os'); +const crypto = require('crypto'); + +const ws_common = require('../common-websockets') + +var port = 30000; + +function getArrayBuffer(buf) { + var l = buf.length; + var arrayBuf = new ArrayBuffer(l); + var uint8View = new Uint8Array(arrayBuf); + for (var i = 0; i < l; i++) { + uint8View[i] = buf[i]; + } + return uint8View.buffer; +} + + +function areArraysEqual(x, y) { + if (x.length != y.length) return false; + for (var i = 0, l = x.length; i < l; ++i) { + if (x[i] !== y[i]) return false; + } + return true; +} + +/*'WebSocket'*/ +{ + /*'#ctor'*/ + { + /*'throws exception for invalid url'*/ + { + assert.throws( + function() { + var ws = new WebSocket('echo.websocket.org'); + }, + Error + ); + } + + /*'should return a new instance if called without new'*/ + // { + // let p = ++port + // var ws = WebSocket('ws://localhost:' + p); + // assert.ok(ws instanceof WebSocket); + // } + } + + /*'options'*/ + { + /*'should accept an `agent` option'*/ + { + function done () {} + (function () { + const p1 = ++port + var wss = new WebSocketServer({port: p1}, function() { + var agent = { + addRequest: function() { + wss.close(); + done(); + } + }; + const p2 = ++port + var ws = new WebSocket('ws://localhost:' + p2, { agent: agent }); + }); + }()) + } + // GH-227 + /*'should accept the `options` object as the 3rd argument'*/ + { + function done () {} + (function () { + const p = ++port + var wss = new WebSocketServer({port: p}, function() { + var agent = { + addRequest: function() { + wss.close(); + done(); + } + }; + var ws = new WebSocket('ws://localhost:' + p, [], { agent: agent }); + }); + }()) + } + + /*'should accept the localAddress option'*/ + // { + // // explore existing interfaces + // var devs = os.networkInterfaces() + // , localAddresses = [] + // , j, ifc, dev, devname; + // for ( devname in devs ) { + // dev = devs[devname]; + // for ( j=0;j 0) break; + ws.send((new Array(10000)).join('hello')); + } + ws.terminate(); + ws.on('close', function() { + wss.close(); + done(); + }); + }); + }()) + } + } + + /*'Custom headers'*/ + { + /*'request has an authorization header'*/ + { + function done () {} + (function () { + var auth = 'test:testpass'; + var srv = http.createServer(function (req, res) {}); + var wss = new WebSocketServer({server: srv}); + let p = ++port + srv.listen(p); + var ws = new WebSocket('ws://' + auth + '@localhost:' + p); + srv.on('upgrade', function (req, socket, head) { + assert(req.headers.authorization, 'auth header exists'); + assert.equal(req.headers.authorization, 'Basic ' + new Buffer(auth).toString('base64')); + ws.terminate(); + ws.on('close', function () { + srv.close(); + wss.close(); + done(); + }); + }); + }()) + } + + /*'accepts custom headers'*/ + { + function done () {} + (function () { + var srv = http.createServer(function (req, res) {}); + var wss = new WebSocketServer({server: srv}); + let p = ++port + srv.listen(p); + + var ws = new WebSocket('ws://localhost:' + p, { + headers: { + 'Cookie': 'foo=bar' + } + }); + + srv.on('upgrade', function (req, socket, head) { + assert(req.headers.cookie, 'auth header exists'); + assert.equal(req.headers.cookie, 'foo=bar'); + + ws.terminate(); + ws.on('close', function () { + srv.close(); + wss.close(); + done(); + }); + }); + }()) + } + } + + /*'#readyState'*/ + { + /*'defaults to connecting'*/ + { + function done () {} + (function () { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + assert.equal(WebSocket.CONNECTING, ws.readyState); + ws.terminate(); + ws.on('close', function() { + srv.close(); + done(); + }); + }); + }()) + } + + /*'set to open once connection is established'*/ + // { + // function done () {} + // (function () { + // let p = ++port + // ws_common.createServer(p, function(srv) { + // var ws = new WebSocket('ws://localhost:' + p); + // ws.on('open', function() { + // assert.equal(WebSocket.OPEN, ws.readyState); + // srv.close(); + // done(); + // }); + // }); + // }()) + // } + + /*'set to closed once connection is closed'*/ + { + function done () {} + (function () { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.close(1001); + ws.on('close', function() { + assert.equal(WebSocket.CLOSED, ws.readyState); + srv.close(); + done(); + }); + }); + }()) + } + + /*'set to closed once connection is terminated'*/ + { + function done () {} + (function () { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.terminate(); + ws.on('close', function() { + assert.equal(WebSocket.CLOSED, ws.readyState); + srv.close(); + done(); + }); + }); + }()) + } + } + + /* + * Ready state constants + */ + + // var readyStates = { + // CONNECTING: 0, + // OPEN: 1, + // CLOSING: 2, + // CLOSED: 3 + // }; + // + // /* + // * Ready state constant tests + // */ + // // TODO(eljefedelrodeodeljefe): look into this nasty thing + // Object.keys(readyStates).forEach(function(state) { + // /*'.' + state*/ + // { + // /*'is enumerable property of class'*/ + // { + // var propertyDescripter = Object.getOwnPropertyDescriptor(WebSocket, state) + // assert.equal(readyStates[state], propertyDescripter.value); + // assert.equal(true, propertyDescripter.enumerable); + // } + // } + // }); + // + // let p = ++port + // ws_common.createServer(p, function(srv) { + // var ws = new WebSocket('ws://localhost:' + p); + // Object.keys(readyStates).forEach(function(state) { + // /*'.' + state*/ + // { + // /*'is property of instance'*/ + // { + // assert.equal(readyStates[state], ws[state]); + // } + // } + // }); + // }); + } + + /*'events'*/ + { + /*'emits a ping event'*/ + { + + let p = ++port + let wss = new WebSocketServer({port: p}); + wss.on('connection', function(client) { + client.ping(); + }); + let ws = new WebSocket('ws://localhost:' + p); + ws.on('ping', function() { + ws.terminate(); + wss.close(); + }); + + } + + /*'emits a pong event'*/ + { + let p = ++port + var wss = new WebSocketServer({port: p}); + wss.on('connection', function(client) { + client.pong(); + }); + var ws = new WebSocket('ws://localhost:' + p); + ws.on('pong', function() { + ws.terminate(); + wss.close(); + }); + } + } + + /*'connection establishing'*/ + { + /*'can disconnect before connection is established'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.terminate(); + ws.on('open', function() { + assert.fail('connect shouldnt be raised here'); + }); + ws.on('close', function() { + srv.close(); + }); + }); + } + + /*'can close before connection is established'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.close(1001); + ws.on('open', function() { + assert.fail('connect shouldnt be raised here'); + }); + ws.on('close', function() { + srv.close(); + }); + }); + } + + /*'can handle error before request is upgraded'*/ + { + // Here, we don't create a server, to guarantee that the connection will + // fail before the request is upgraded + let p = ++port + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + assert.fail('connect shouldnt be raised here'); + }); + ws.on('close', function() { + assert.fail('close shouldnt be raised here'); + }); + ws.on('error', function() { + setTimeout(function() { + assert.equal(ws.readyState, WebSocket.CLOSED); + }, 50) + }); + } + + /*'invalid server key is denied'*/ + { + let p = ++port + ws_common.createServer(p, ws_common.handlers.invalidKey, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('error', function() { + srv.close(); + }); + }); + } + + /*'close event is raised when server closes connection'*/ + { + let p = ++port + ws_common.createServer(p, ws_common.handlers.closeAfterConnect, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('close', function() { + srv.close(); + }); + }); + } + + /*'error is emitted if server aborts connection'*/ + { + let p = ++port + ws_common.createServer(p, ws_common.handlers.return401, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + assert.fail('connect shouldnt be raised here'); + }); + ws.on('error', function() { + srv.close(); + }); + }); + } + + /*'unexpected response can be read when sent by server'*/ + // { + // let p = ++port + // ws_common.createServer(p, ws_common.handlers.return401, function(srv) { + // var ws = new WebSocket('ws://localhost:' + p); + // ws.on('open', function() { + // assert.fail('connect shouldnt be raised here'); + // }); + // ws.on('unexpected-response', function(req, res) { + // assert.equal(res.statusCode, 401); + // + // var data = ''; + // + // res.on('data', function (v) { + // data += v; + // }); + // + // res.on('end', function () { + // assert.equal(data, 'Not allowed!'); + // }); + // }); + // ws.on('error', function () { + // assert.fail('error shouldnt be raised here'); + // }); + // }); + // } + + /*'request can be aborted when unexpected response is sent by server'*/ + { + let p = ++port + ws_common.createServer(p, ws_common.handlers.return401, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + assert.fail('connect shouldnt be raised here'); + }); + ws.on('unexpected-response', function(req, res) { + assert.equal(res.statusCode, 401); + + res.on('end', function () { + srv.close(); + }); + + req.abort(); + }); + ws.on('error', function () { + assert.fail('error shouldnt be raised here'); + }); + }); + } + } + + /*'#pause and #resume'*/ + { + /*'pauses the underlying stream'*/ + { + function done () {} + (function () { + // this test is sort-of racecondition'y, since an unlikely slow connection + // to localhost can cause the test to succeed even when the stream pausing + // isn't working as intended. that is an extremely unlikely scenario, though + // and an acceptable risk for the test. + var client; + var serverClient; + var openCount = 0; + function onOpen() { + if (++openCount == 2) { + var paused = true; + serverClient.on('message', function() { + assert.ifError(paused); + wss.close(); + done(); + }); + serverClient.pause(); + setTimeout(function() { + paused = false; + serverClient.resume(); + }, 200); + client.send('foo'); + } + } + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + var ws = new WebSocket('ws://localhost:' + p); + serverClient = ws; + serverClient.on('open', onOpen); + }); + wss.on('connection', function(ws) { + client = ws; + onOpen(); + }); + }()) + } + } + + /*'#ping'*/ + { + /*'before connect should fail'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('error', function() {}); + try { + ws.ping(); + } + catch (e) { + srv.close(); + ws.terminate(); + } + }); + } + + /*'before connect can silently fail'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('error', function() {}); + ws.ping('', {}, true); + srv.close(); + ws.terminate(); + }); + } + + /*'without message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.ping(); + }); + srv.on('ping', function(message) { + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.ping('hi'); + }); + srv.on('ping', function(message) { + assert.equal('hi', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'can send safely receive numbers as ping payload'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + + ws.on('open', function() { + ws.ping(200); + }); + + srv.on('ping', function(message) { + assert.equal('200', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with encoded message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.ping('hi', {mask: true}); + }); + srv.on('ping', function(message, flags) { + assert.ok(flags.masked); + assert.equal('hi', message); + srv.close(); + ws.terminate(); + }); + }); + } + } + + /*'#pong'*/ + { + /*'before connect should fail'*/ + { + let p = ++port + ws_common.createServer(port, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('error', function() {}); + try { + ws.pong(); + } + catch (e) { + srv.close(); + ws.terminate(); + } + }); + } + + /*'before connect can silently fail'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('error', function() {}); + ws.pong('', {}, true); + srv.close(); + ws.terminate(); + }); + } + + /*'without message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.pong(); + }); + srv.on('pong', function(message) { + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(port, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.pong('hi'); + }); + srv.on('pong', function(message) { + assert.equal('hi', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with encoded message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.pong('hi', {mask: true}); + }); + srv.on('pong', function(message, flags) { + assert.ok(flags.masked); + assert.equal('hi', message); + srv.close(); + ws.terminate(); + }); + }); + } + } + + /*'#send'*/ + { + /*'very long binary data can be sent and received (with echoing server)'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var array = new Float32Array(5 * 1024 * 1024); + for (var i = 0; i < array.length; ++i) array[i] = i / 5; + ws.on('open', function() { + ws.send(array, {binary: true}); + }); + ws.on('message', function(message, flags) { + assert.ok(flags.binary); + assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); + ws.terminate(); + srv.close(); + }); + }); + } + + /*'can send and receive text data'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.send('hi'); + }); + ws.on('message', function(message, flags) { + assert.equal('hi', message); + ws.terminate(); + srv.close(); + }); + }); + } + + /*'send and receive binary data as an array'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var array = new Float32Array(6); + for (var i = 0; i < array.length; ++i) array[i] = i / 2; + var partial = array.subarray(2, 5); + ws.on('open', function() { + ws.send(partial, {binary: true}); + }); + ws.on('message', function(message, flags) { + assert.ok(flags.binary); + assert.ok(areArraysEqual(partial, new Float32Array(getArrayBuffer(message)))); + ws.terminate(); + srv.close(); + }); + }); + } + + /*'binary data can be sent and received as buffer'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var buf = new Buffer('foobar'); + ws.on('open', function() { + ws.send(buf, {binary: true}); + }); + ws.on('message', function(message, flags) { + assert.ok(flags.binary); + assert.ok(areArraysEqual(buf, message)); + ws.terminate(); + srv.close(); + }); + }); + } + + /*'ArrayBuffer is auto-detected without binary flag'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var array = new Float32Array(5); + for (var i = 0; i < array.length; ++i) array[i] = i / 2; + ws.on('open', function() { + ws.send(array.buffer); + }); + ws.onmessage = function (event) { + assert.ok(event.binary); + assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(event.data)))); + ws.terminate(); + srv.close(); + }; + }); + } + + /*'Buffer is auto-detected without binary flag'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var buf = new Buffer('foobar'); + ws.on('open', function() { + ws.send(buf); + }); + ws.onmessage = function (event) { + assert.ok(event.binary); + assert.ok(areArraysEqual(event.data, buf)); + ws.terminate(); + srv.close(); + }; + }); + } + + /*'before connect should fail'*/ + { + let p =++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('error', function() {}); + try { + ws.send('hi'); + } + catch (e) { + ws.terminate(); + srv.close(); + } + }); + } + + /*'before connect should pass error through callback, if present'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('error', function() {}); + ws.send('hi', function(error) { + assert.ok(error instanceof Error); + ws.terminate(); + srv.close(); + }); + }); + } + + /*'without data should be successful'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.send(); + }); + srv.on('message', function(message, flags) { + assert.equal('', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'calls optional callback when flushed'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.send('hi', function() { + srv.close(); + ws.terminate(); + }); + }); + }); + } + + /*'with unencoded message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.send('hi'); + }); + srv.on('message', function(message, flags) { + assert.equal('hi', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with encoded message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.send('hi', {mask: true}); + }); + srv.on('message', function(message, flags) { + assert.ok(flags.masked); + assert.equal('hi', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with unencoded binary message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var array = new Float32Array(5); + for (var i = 0; i < array.length; ++i) array[i] = i / 2; + ws.on('open', function() { + ws.send(array, {binary: true}); + }); + srv.on('message', function(message, flags) { + assert.ok(flags.binary); + assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with encoded binary message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var array = new Float32Array(5); + for (var i = 0; i < array.length; ++i) array[i] = i / 2; + ws.on('open', function() { + ws.send(array, {mask: true, binary: true}); + }); + srv.on('message', function(message, flags) { + assert.ok(flags.binary); + assert.ok(flags.masked); + assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with binary stream will send fragmented data'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var callbackFired = false; + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.bufferSize = 100; + ws.send(fileStream, {binary: true}, function(error) { + assert.equal(null, error); + callbackFired = true; + }); + }); + srv.on('message', function(data, flags) { + assert.ok(flags.binary); + assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/websockets/textfile'), data)); + ws.terminate(); + }); + ws.on('close', function() { + assert.ok(callbackFired); + srv.close(); + }); + }); + } + + /*'with text stream will send fragmented data'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var callbackFired = false; + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.setEncoding('utf8'); + fileStream.bufferSize = 100; + ws.send(fileStream, {binary: false}, function(error) { + assert.equal(null, error); + callbackFired = true; + }); + }); + srv.on('message', function(data, flags) { + assert.ok(!flags.binary); + assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/websockets/textfile', 'utf8'), data)); + ws.terminate(); + }); + ws.on('close', function() { + assert.ok(callbackFired); + srv.close(); + }); + }); + } + + /*'will cause intermittent send to be delayed in order'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.setEncoding('utf8'); + fileStream.bufferSize = 100; + ws.send(fileStream); + ws.send('foobar'); + ws.send('baz'); + }); + var receivedIndex = 0; + srv.on('message', function(data, flags) { + ++receivedIndex; + if (receivedIndex == 1) { + assert.ok(!flags.binary); + assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/websockets/textfile', 'utf8'), data)); + } + else if (receivedIndex == 2) { + assert.ok(!flags.binary); + assert.equal('foobar', data); + } + else { + assert.ok(!flags.binary); + assert.equal('baz', data); + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'will cause intermittent stream to be delayed in order'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.setEncoding('utf8'); + fileStream.bufferSize = 100; + ws.send(fileStream); + var i = 0; + ws.stream(function(error, send) { + assert.ok(!error); + if (++i == 1) send('foo'); + else send('bar', true); + }); + }); + var receivedIndex = 0; + srv.on('message', function(data, flags) { + ++receivedIndex; + if (receivedIndex == 1) { + assert.ok(!flags.binary); + assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/websockets/textfile', 'utf8'), data)); + } + else if (receivedIndex == 2) { + assert.ok(!flags.binary); + assert.equal('foobar', data); + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'will cause intermittent ping to be delivered'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.setEncoding('utf8'); + fileStream.bufferSize = 100; + ws.send(fileStream); + ws.ping('foobar'); + }); + var receivedIndex = 0; + srv.on('message', function(data, flags) { + assert.ok(!flags.binary); + assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/websockets/textfile', 'utf8'), data)); + if (++receivedIndex == 2) { + srv.close(); + ws.terminate(); + } + }); + srv.on('ping', function(data) { + assert.equal('foobar', data); + if (++receivedIndex == 2) { + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'will cause intermittent pong to be delivered'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.setEncoding('utf8'); + fileStream.bufferSize = 100; + ws.send(fileStream); + ws.pong('foobar'); + }); + var receivedIndex = 0; + srv.on('message', function(data, flags) { + assert.ok(!flags.binary); + assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/websockets/textfile', 'utf8'), data)); + if (++receivedIndex == 2) { + srv.close(); + ws.terminate(); + } + }); + srv.on('pong', function(data) { + assert.equal('foobar', data); + if (++receivedIndex == 2) { + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'will cause intermittent close to be delivered'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.setEncoding('utf8'); + fileStream.bufferSize = 100; + ws.send(fileStream); + ws.close(1000, 'foobar'); + }); + ws.on('close', function() { + srv.close(); + ws.terminate(); + }); + ws.on('error', function() { /* That's quite alright -- a send was attempted after close */ }); + srv.on('message', function(data, flags) { + assert.ok(!flags.binary); + assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/websockets/textfile', 'utf8'), data)); + }); + srv.on('close', function(code, data) { + assert.equal(1000, code); + assert.equal('foobar', data); + }); + }); + } + } + + /*'#stream'*/ + { + /*'very long binary data can be streamed'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var buffer = new Buffer(10 * 1024); + for (var i = 0; i < buffer.length; ++i) buffer[i] = i % 0xff; + ws.on('open', function() { + var i = 0; + var blockSize = 800; + var bufLen = buffer.length; + ws.stream({binary: true}, function(error, send) { + assert.ok(!error); + var start = i * blockSize; + var toSend = Math.min(blockSize, bufLen - (i * blockSize)); + var end = start + toSend; + var isFinal = toSend < blockSize; + send(buffer.slice(start, end), isFinal); + i += 1; + }); + }); + srv.on('message', function(data, flags) { + assert.ok(flags.binary); + assert.ok(areArraysEqual(buffer, data)); + ws.terminate(); + srv.close(); + }); + }); + } + + /*'before connect should pass error through callback'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('error', function() {}); + ws.stream(function(error) { + assert.ok(error instanceof Error); + ws.terminate(); + srv.close(); + }); + }); + } + + /*'without callback should fail'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var payload = 'HelloWorld'; + ws.on('open', function() { + try { + ws.stream(); + } + catch (e) { + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'will cause intermittent send to be delayed in order'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var payload = 'HelloWorld'; + ws.on('open', function() { + var i = 0; + ws.stream(function(error, send) { + assert.ok(!error); + if (++i == 1) { + send(payload.substr(0, 5)); + ws.send('foobar'); + ws.send('baz'); + } + else { + send(payload.substr(5, 5), true); + } + }); + }); + var receivedIndex = 0; + srv.on('message', function(data, flags) { + ++receivedIndex; + if (receivedIndex == 1) { + assert.ok(!flags.binary); + assert.equal(payload, data); + } + else if (receivedIndex == 2) { + assert.ok(!flags.binary); + assert.equal('foobar', data); + } + else { + assert.ok(!flags.binary); + assert.equal('baz', data); + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'will cause intermittent stream to be delayed in order'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var payload = 'HelloWorld'; + ws.on('open', function() { + var i = 0; + ws.stream(function(error, send) { + assert.ok(!error); + if (++i == 1) { + send(payload.substr(0, 5)); + var i2 = 0; + ws.stream(function(error, send) { + assert.ok(!error); + if (++i2 == 1) send('foo'); + else send('bar', true); + }); + ws.send('baz'); + } + else send(payload.substr(5, 5), true); + }); + }); + var receivedIndex = 0; + srv.on('message', function(data, flags) { + ++receivedIndex; + if (receivedIndex == 1) { + assert.ok(!flags.binary); + assert.equal(payload, data); + } + else if (receivedIndex == 2) { + assert.ok(!flags.binary); + assert.equal('foobar', data); + } + else if (receivedIndex == 3){ + assert.ok(!flags.binary); + assert.equal('baz', data); + setTimeout(function() { + srv.close(); + ws.terminate(); + }, 1000); + } + else throw new Error('more messages than we actually sent just arrived'); + }); + }); + } + + /*'will cause intermittent ping to be delivered'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var payload = 'HelloWorld'; + ws.on('open', function() { + var i = 0; + ws.stream(function(error, send) { + assert.ok(!error); + if (++i == 1) { + send(payload.substr(0, 5)); + ws.ping('foobar'); + } + else { + send(payload.substr(5, 5), true); + } + }); + }); + var receivedIndex = 0; + srv.on('message', function(data, flags) { + assert.ok(!flags.binary); + assert.equal(payload, data); + if (++receivedIndex == 2) { + srv.close(); + ws.terminate(); + } + }); + srv.on('ping', function(data) { + assert.equal('foobar', data); + if (++receivedIndex == 2) { + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'will cause intermittent pong to be delivered'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var payload = 'HelloWorld'; + ws.on('open', function() { + var i = 0; + ws.stream(function(error, send) { + assert.ok(!error); + if (++i == 1) { + send(payload.substr(0, 5)); + ws.pong('foobar'); + } + else { + send(payload.substr(5, 5), true); + } + }); + }); + var receivedIndex = 0; + srv.on('message', function(data, flags) { + assert.ok(!flags.binary); + assert.equal(payload, data); + if (++receivedIndex == 2) { + srv.close(); + ws.terminate(); + } + }); + srv.on('pong', function(data) { + assert.equal('foobar', data); + if (++receivedIndex == 2) { + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'will cause intermittent close to be delivered'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var payload = 'HelloWorld'; + var errorGiven = false; + ws.on('open', function() { + var i = 0; + ws.stream(function(error, send) { + if (++i == 1) { + send(payload.substr(0, 5)); + ws.close(1000, 'foobar'); + } + else if(i == 2) { + send(payload.substr(5, 5), true); + } + else if (i == 3) { + assert.ok(error); + errorGiven = true; + } + }); + }); + ws.on('close', function() { + assert.ok(errorGiven); + srv.close(); + ws.terminate(); + }); + srv.on('message', function(data, flags) { + assert.ok(!flags.binary); + assert.equal(payload, data); + }); + srv.on('close', function(code, data) { + assert.equal(1000, code); + assert.equal('foobar', data); + }); + }); + } + } + + /*'#close'*/ + { + /*'will raise error callback, if any, if called during send stream'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var errorGiven = false; + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.setEncoding('utf8'); + fileStream.bufferSize = 100; + ws.send(fileStream, function(error) { + errorGiven = error != null; + }); + ws.close(1000, 'foobar'); + }); + ws.on('close', function() { + setTimeout(function() { + assert.ok(errorGiven); + srv.close(); + ws.terminate(); + }, 1000); + }); + }); + } + + /*'without invalid first argument throws exception'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + try { + ws.close('error'); + } + catch (e) { + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'without reserved error code 1004 throws exception'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + try { + ws.close(1004); + } + catch (e) { + srv.close(); + ws.terminate(); + } + }); + }); + } + + /*'without message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.close(1000); + }); + srv.on('close', function(code, message, flags) { + assert.equal('', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.close(1000, 'some reason'); + }); + srv.on('close', function(code, message, flags) { + assert.ok(flags.masked); + assert.equal('some reason', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'with encoded message is successfully transmitted to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.on('open', function() { + ws.close(1000, 'some reason', {mask: true}); + }); + srv.on('close', function(code, message, flags) { + assert.ok(flags.masked); + assert.equal('some reason', message); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'ends connection to the server'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var connectedOnce = false; + ws.on('open', function() { + connectedOnce = true; + ws.close(1000, 'some reason', {mask: true}); + }); + ws.on('close', function() { + assert.ok(connectedOnce); + srv.close(); + ws.terminate(); + }); + }); + } + + /*'consumes all data when the server socket closed'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + wss.on('connection', function(conn) { + conn.send('foo'); + conn.send('bar'); + conn.send('baz'); + conn.close(); + }); + var ws = new WebSocket('ws://localhost:' + p); + var messages = []; + ws.on('message', function (message) { + messages.push(message); + if (messages.length === 3) { + assert.deepEqual(messages, ['foo', 'bar', 'baz']); + wss.close(); + ws.terminate(); + done(); + } + }); + }); + }()) + } + } + + /*'W3C API emulation'*/ + { + /*'should not throw errors when getting and setting'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var listener = function () {}; + + ws.onmessage = listener; + ws.onerror = listener; + ws.onclose = listener; + ws.onopen = listener; + + assert.ok(ws.onopen === listener); + assert.ok(ws.onmessage === listener); + assert.ok(ws.onclose === listener); + assert.ok(ws.onerror === listener); + + srv.close(); + ws.terminate(); + }); + } + + /*'should work the same as the EventEmitter api'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + var listener = function() {}; + var message = 0; + var close = 0; + var open = 0; + + ws.onmessage = function(messageEvent) { + assert.ok(!!messageEvent.data); + ++message; + ws.close(); + }; + + ws.onopen = function() { + ++open; + } + + ws.onclose = function() { + ++close; + } + + ws.on('open', function() { + ws.send('foo'); + }); + + ws.on('close', function() { + process.nextTick(function() { + assert.ok(message === 1); + assert.ok(open === 1); + assert.ok(close === 1); + + srv.close(); + ws.terminate(); + }); + }); + }); + } + + /*'should receive text data wrapped in a MessageEvent when using addEventListener'*/ + { + let p = ++port + ws_common.createServer(p, function(srv) { + var ws = new WebSocket('ws://localhost:' + p); + ws.addEventListener('open', function() { + ws.send('hi'); + }); + ws.addEventListener('message', function(messageEvent) { + assert.equal('hi', messageEvent.data); + ws.terminate(); + srv.close(); + }); + }); + } + + /*'should receive valid CloseEvent when server closes with code 1000'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + var ws = new WebSocket('ws://localhost:' + p); + ws.addEventListener('close', function(closeEvent) { + assert.equal(true, closeEvent.wasClean); + assert.equal(1000, closeEvent.code); + ws.terminate(); + wss.close(); + done(); + }); + }); + wss.on('connection', function(client) { + client.close(1000); + }); + }()) + } + + /*'should receive valid CloseEvent when server closes with code 1001'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + var ws = new WebSocket('ws://localhost:' + p); + ws.addEventListener('close', function(closeEvent) { + assert.equal(false, closeEvent.wasClean); + assert.equal(1001, closeEvent.code); + assert.equal('some daft reason', closeEvent.reason); + ws.terminate(); + wss.close(); + done(); + }); + }); + wss.on('connection', function(client) { + client.close(1001, 'some daft reason'); + }); + }()) + } + + /*'should have target set on Events'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + var ws = new WebSocket('ws://localhost:' + p); + ws.addEventListener('open', function(openEvent) { + assert.equal(ws, openEvent.target); + }); + ws.addEventListener('message', function(messageEvent) { + assert.equal(ws, messageEvent.target); + wss.close(); + }); + ws.addEventListener('close', function(closeEvent) { + assert.equal(ws, closeEvent.target); + ws.emit('error', new Error('forced')); + }); + ws.addEventListener('error', function(errorEvent) { + assert.equal(errorEvent.message, 'forced'); + assert.equal(ws, errorEvent.target); + ws.terminate(); + done(); + }); + }); + wss.on('connection', function(client) { + client.send('hi') + }); + }()) + } + + /*'should have type set on Events'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + var ws = new WebSocket('ws://localhost:' + p); + ws.addEventListener('open', function(openEvent) { + assert.equal('open', openEvent.type); + }); + ws.addEventListener('message', function(messageEvent) { + assert.equal('message', messageEvent.type); + wss.close(); + }); + ws.addEventListener('close', function(closeEvent) { + assert.equal('close', closeEvent.type); + ws.emit('error', new Error('forced')); + }); + ws.addEventListener('error', function(errorEvent) { + assert.equal(errorEvent.message, 'forced'); + assert.equal('error', errorEvent.type); + ws.terminate(); + done(); + }); + }); + wss.on('connection', function(client) { + client.send('hi') + }); + }()) + } + } + // + // /*'ssl'*/ + { + /*'can connect to secure websocket server'*/ + { + function done () {} + (function () { + var options = { + key: fs.readFileSync('test/fixtures/websockets/key.pem'), + cert: fs.readFileSync('test/fixtures/websockets/certificate.pem') + }; + var app = https.createServer(options, function (req, res) { + res.writeHead(200); + res.end(); + }); + var wss = new WebSocketServer({server: app}); + let p = ++port + app.listen(p, function() { + var ws = new WebSocket('wss://localhost:' + p); + }); + wss.on('connection', function(ws) { + app.close(); + ws.terminate(); + wss.close(); + done(); + }); + }()) + } + + /*'can connect to secure websocket server with client side certificate'*/ + { + function done () {} + (function () { + var options = { + key: fs.readFileSync('test/fixtures/websockets/key.pem'), + cert: fs.readFileSync('test/fixtures/websockets/certificate.pem'), + ca: [fs.readFileSync('test/fixtures/websockets/ca1-cert.pem')], + requestCert: true + }; + var clientOptions = { + key: fs.readFileSync('test/fixtures/websockets/agent1-key.pem'), + cert: fs.readFileSync('test/fixtures/websockets/agent1-cert.pem') + }; + var app = https.createServer(options, function (req, res) { + res.writeHead(200); + res.end(); + }); + var success = false; + var wss = new WebSocketServer({ + server: app, + verifyClient: function(info) { + success = !!info.req.client.authorized; + return true; + } + }); + let p = ++port + app.listen(p, function() { + var ws = new WebSocket('wss://localhost:' + p, clientOptions); + }); + wss.on('connection', function(ws) { + app.close(); + ws.terminate(); + wss.close(); + assert.ok(success); + done(); + }); + }()) + } + + /*'cannot connect to secure websocket server via ws://'*/ + { + function done () {} + (function () { + var options = { + key: fs.readFileSync('test/fixtures/websockets/key.pem'), + cert: fs.readFileSync('test/fixtures/websockets/certificate.pem') + }; + var app = https.createServer(options, function (req, res) { + res.writeHead(200); + res.end(); + }); + var wss = new WebSocketServer({server: app}); + let p = ++port + app.listen(p, function() { + var ws = new WebSocket('ws://localhost:' + p, { rejectUnauthorized :false }); + ws.on('error', function() { + app.close(); + ws.terminate(); + wss.close(); + done(); + }); + }); + }()) + } + + /*'can send and receive text data'*/ + { + function done () {} + (function () { + var options = { + key: fs.readFileSync('test/fixtures/websockets/key.pem'), + cert: fs.readFileSync('test/fixtures/websockets/certificate.pem') + }; + var app = https.createServer(options, function (req, res) { + res.writeHead(200); + res.end(); + }); + var wss = new WebSocketServer({server: app}); + let p = ++port + app.listen(p, function() { + var ws = new WebSocket('wss://localhost:' + p); + ws.on('open', function() { + ws.send('foobar'); + }); + }); + wss.on('connection', function(ws) { + ws.on('message', function(message, flags) { + assert.equal(message, 'foobar'); + app.close(); + ws.terminate(); + wss.close(); + done(); + }); + }); + }()) + } + + /*'can send and receive very long binary data'*/ + { + function done () {} + (function () { + var options = { + key: fs.readFileSync('test/fixtures/websockets/key.pem'), + cert: fs.readFileSync('test/fixtures/websockets/certificate.pem') + } + var app = https.createServer(options, function (req, res) { + res.writeHead(200); + res.end(); + }); + crypto.randomBytes(5 * 1024 * 1024, function(ex, buf) { + if (ex) throw ex; + var wss = new WebSocketServer({server: app}); + let p = ++port + app.listen(p, function() { + var ws = new WebSocket('wss://localhost:' + p); + ws.on('open', function() { + ws.send(buf, {binary: true}); + }); + ws.on('message', function(message, flags) { + assert.ok(flags.binary); + assert.ok(areArraysEqual(buf, message)); + app.close(); + ws.terminate(); + wss.close(); + done(); + }); + }); + wss.on('connection', function(ws) { + ws.on('message', function(message, flags) { + ws.send(message, {binary: true}); + }); + }); + }); + }()) + } + } + + /*'protocol support discovery'*/ + { + /*'#supports'*/ + { + /*'#binary'*/ + { + /*'returns true for hybi transport'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + var ws = new WebSocket('ws://localhost:' + p); + }); + wss.on('connection', function(client) { + assert.equal(true, client.supports.binary); + wss.close(); + done(); + }); + }()) + } + + /*'returns false for hixie transport'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + var options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + } + }; + var req = http.request(options); + req.write('WjN}|M(6'); + req.end(); + }); + wss.on('connection', function(client) { + assert.equal(false, client.supports.binary); + wss.close(); + done(); + }); + }()) + } + } + } + } + + /*'host and origin headers'*/ + { + /*'includes the host header with port number'*/ + // { + // function done () {} + // (function () { + // var srv = http.createServer(); + // let p1 = ++port + // srv.listen(p1, function(){ + // srv.on('upgrade', function(req, socket, upgradeHeade) { + // assert.equal('localhost:' + p1, req.headers['host']); + // srv.close(); + // done(); + // }); + // var ws = new WebSocket('ws://localhost:' + p1); + // }); + // }()) + // } + + /*'lacks default origin header'*/ + // { + // function done () {} + // (function () { + // var srv = http.createServer(); + // let p2 = ++port + // srv.listen(p2, function() { + // srv.on('upgrade', function(req, socket, upgradeHeade) { + // assert.ifError(req.headers.hasOwnProperty('origin')); + // srv.close(); + // done(); + // }); + // var ws = new WebSocket('ws://localhost:' + p2); + // }); + // }()) + // } + + /*'honors origin set in options'*/ + // { + // function done () {} + // (function () { + // var srv = http.createServer(); + // let p3 = ++port + // srv.listen(p3, function() { + // var options = {origin: 'https://example.com:8000'} + // srv.on('upgrade', function(req, socket, upgradeHeade) { + // assert.equal(options.origin, req.headers['origin']); + // srv.close(); + // done(); + // }); + // var ws = new WebSocket('ws://localhost:' + p3, options); + // }); + // }()) + // } + + /*'excludes default ports from host header'*/ + { + function done () {} + (function () { + // can't create a server listening on ports 80 or 443 + // so we need to expose the method that does this + var buildHostHeader = WebSocket.buildHostHeader + var host = buildHostHeader(false, 'localhost', 80) + assert.equal('localhost', host); + host = buildHostHeader(false, 'localhost', 88) + assert.equal('localhost:88', host); + host = buildHostHeader(true, 'localhost', 443) + assert.equal('localhost', host); + host = buildHostHeader(true, 'localhost', 8443) + assert.equal('localhost:8443', host); + done() + }()) + } + } + + /*'permessage-deflate'*/ + { + /*'is enabled by default'*/ + // { + // function done () {} + // (function () { + // var srv = http.createServer(function (req, res) {}); + // var wss = new WebSocketServer({server: srv, perMessageDeflate: true}); + // let p = ++port + // srv.listen(p, function() { + // var ws = new WebSocket('ws://localhost:' + p); + // srv.on('upgrade', function(req, socket, head) { + // // assert.ok(~req.headers['sec-websocket-extensions'].indexOf('permessage-deflate')); + // }); + // ws.on('open', function() { + // // assert.ok(ws.extensions['permessage-deflate']); + // ws.terminate(); + // wss.close(); + // done(); + // }); + // }); + // }()) + // } + + /*'can be disabled'*/ + // { + // function done () {} + // (function () { + // var srv = http.createServer(function (req, res) {}); + // var wss = new WebSocketServer({server: srv, perMessageDeflate: true}); + // let p = ++port + // srv.listen(p, function() { + // var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: false}); + // srv.on('upgrade', function(req, socket, head) { + // assert.ok(!req.headers['sec-websocket-extensions']); + // ws.terminate(); + // wss.close(); + // done(); + // }); + // }); + // }()) + // } + + /*'can send extension parameters'*/ + // { + // function done () {} + // (function () { + // var srv = http.createServer(function (req, res) {}); + // var wss = new WebSocketServer({server: srv, perMessageDeflate: true}); + // let p = ++port + // srv.listen(p, function() { + // var ws = new WebSocket('ws://localhost:' + p, { + // perMessageDeflate: { + // serverNoContextTakeover: true, + // clientNoContextTakeover: true, + // serverMaxWindowBits: 10, + // clientMaxWindowBits: true + // } + // }); + // srv.on('upgrade', function(req, socket, head) { + // var extensions = req.headers['sec-websocket-extensions']; + // assert.ok(~extensions.indexOf('permessage-deflate')); + // assert.ok(~extensions.indexOf('server_no_context_takeover')); + // assert.ok(~extensions.indexOf('client_no_context_takeover')); + // assert.ok(~extensions.indexOf('server_max_window_bits=10')); + // assert.ok(~extensions.indexOf('client_max_window_bits')); + // ws.terminate(); + // wss.close(); + // done(); + // }); + // }); + // }()) + // } + + /*'can send and receive text data'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p, perMessageDeflate: true}, function() { + var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: true}); + ws.on('open', function() { + ws.send('hi', {compress: true}); + }); + ws.on('message', function(message, flags) { + assert.equal('hi', message); + ws.terminate(); + wss.close(); + done(); + }); + }); + wss.on('connection', function(ws) { + ws.on('message', function(message, flags) { + ws.send(message, {compress: true}); + }); + }); + }()) + } + + /*'can send and receive a typed array'*/ + { + function done () {} + (function () { + var array = new Float32Array(5); + for (var i = 0; i < array.length; i++) array[i] = i / 2; + let p = ++port + var wss = new WebSocketServer({port: p, perMessageDeflate: true}, function() { + var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: true}); + ws.on('open', function() { + ws.send(array, {compress: true}); + }); + ws.on('message', function(message, flags) { + assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); + ws.terminate(); + wss.close(); + done(); + }); + }); + wss.on('connection', function(ws) { + ws.on('message', function(message, flags) { + ws.send(message, {compress: true}); + }); + }); + }()) + } + + /*'can send and receive ArrayBuffer'*/ + { + function done () {} + (function () { + var array = new Float32Array(5); + for (var i = 0; i < array.length; i++) array[i] = i / 2; + let p = ++port + var wss = new WebSocketServer({port: p, perMessageDeflate: true}, function() { + var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: true}); + ws.on('open', function() { + ws.send(array.buffer, {compress: true}); + }); + ws.on('message', function(message, flags) { + assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); + ws.terminate(); + wss.close(); + done(); + }); + }); + wss.on('connection', function(ws) { + ws.on('message', function(message, flags) { + ws.send(message, {compress: true}); + }); + }); + }()) + } + + /*'with binary stream will send fragmented data'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p, perMessageDeflate: true}, function() { + var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: true}); + var callbackFired = false; + ws.on('open', function() { + var fileStream = fs.createReadStream('test/fixtures/websockets/textfile'); + fileStream.bufferSize = 100; + ws.send(fileStream, {binary: true, compress: true}, function(error) { + assert.equal(null, error); + callbackFired = true; + }); + }); + ws.on('close', function() { + assert.ok(callbackFired); + wss.close(); + done(); + }); + }); + wss.on('connection', function(ws) { + ws.on('message', function(data, flags) { + assert.ok(flags.binary); + assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/websockets/textfile'), data)); + ws.terminate(); + }); + }); + }()) + } + + /*'#send'*/ + { + /*'can set the compress option true when perMessageDeflate is disabled'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p}, function() { + var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: false}); + ws.on('open', function() { + ws.send('hi', {compress: true}); + }); + ws.on('message', function(message, flags) { + assert.equal('hi', message); + ws.terminate(); + wss.close(); + done(); + }); + }); + wss.on('connection', function(ws) { + ws.on('message', function(message, flags) { + ws.send(message, {compress: true}); + }); + }); + }()) + } + } + + /*'#close'*/ + { + /*'should not raise error callback, if any, if called during send data'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p, perMessageDeflate: true}, function() { + var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: true}); + var errorGiven = false; + ws.on('open', function() { + ws.send('hi', function(error) { + errorGiven = error != null; + }); + ws.close(); + }); + ws.on('close', function() { + setTimeout(function() { + assert.ok(!errorGiven); + wss.close(); + ws.terminate(); + done(); + }, 1000); + }); + }); + }()) + } + } + + /*'#terminate'*/ + { + /*'will raise error callback, if any, if called during send data'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p, perMessageDeflate: true}, function() { + var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: true}); + var errorGiven = false; + ws.on('open', function() { + ws.send('hi', function(error) { + errorGiven = error != null; + }); + ws.terminate(); + }); + ws.on('close', function() { + setTimeout(function() { + assert.ok(errorGiven); + wss.close(); + ws.terminate(); + done(); + }, 1000); + }); + }); + }()) + } + + /*'can call during receiving data'*/ + { + function done () {} + (function () { + let p = ++port + var wss = new WebSocketServer({port: p, perMessageDeflate: true}, function() { + var ws = new WebSocket('ws://localhost:' + p, {perMessageDeflate: true}); + wss.on('connection', function(client) { + for (var i = 0; i < 10; i++) { + client.send('hi'); + } + client.send('hi', function() { + ws.terminate(); + }); + }); + ws.on('close', function() { + setTimeout(function() { + wss.close(); + done(); + }, 1000); + }); + }); + }()) + } + } + } +} diff --git a/test/parallel/test-websockets-websocketserver.js b/test/parallel/test-websockets-websocketserver.js new file mode 100644 index 00000000000000..3e0a675597b1e5 --- /dev/null +++ b/test/parallel/test-websockets-websocketserver.js @@ -0,0 +1,1410 @@ +'use strict'; +const http = require('http'); +const https = require('https'); +const WebSocket = require('websockets'); +const WebSocketServer = require('websockets').Server; +const fs = require('fs'); +const assert = require('assert'); + +var port = 40000; + +function getArrayBuffer(buf) { + var l = buf.length; + var arrayBuf = new ArrayBuffer(l); + for (var i = 0; i < l; ++i) { + arrayBuf[i] = buf[i]; + } + return arrayBuf; +} + +function areArraysEqual(x, y) { + if (x.length != y.length) return false; + for (var i = 0, l = x.length; i < l; ++i) { + if (x[i] !== y[i]) return false; + } + return true; +} + +/*'WebSocketServer'*/ +{ + /*'#ctor'*/ + { + /*'should return a new instance if called without new'*/ + { + var ws = WebSocketServer({noServer: true}); + assert.ok(ws instanceof WebSocketServer); + } + + /*'throws an error if no option object is passed'*/ + { + var gotException = false; + try { + let wss = new WebSocketServer(); + } + catch (e) { + gotException = true; + } + assert.ok(gotException); + } + + /*'throws an error if no port or server is specified'*/ + { + var gotException = false; + try { + let wss = new WebSocketServer({}); + } + catch (e) { + gotException = true; + } + assert.ok(gotException); + } + + /*'does not throw an error if no port or server is specified, when the noServer option is true'*/ + { + var gotException = false; + try { + let wss = new WebSocketServer({noServer: true}); + } + catch (e) { + gotException = true; + } + assert.equal(gotException, false); + } + + /*'emits an error if http server bind fails'*/ + { + var wss1 = new WebSocketServer({port: 50003}); + var wss2 = new WebSocketServer({port: 50003}); + wss2.on('error', function() { + wss1.close(); + }); + } + + /*'starts a server on a given port'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p}, function() { + let ws = new WebSocket('ws://localhost:' + p); + }); + wss.on('connection', function(client) { + wss.close(); + }); + } + + /*'uses a precreated http server', function*/ + { + let srv = http.createServer(); + let p = ++port + srv.listen(p, function () { + let wss = new WebSocketServer({server: srv}); + let ws = new WebSocket('ws://localhost:' + p); + + wss.on('connection', function(client) { + wss.close(); + srv.close(); + }); + }); + } + + /*'426s for non-Upgrade requests'*/ + { + let p = ++port + let wss = new WebSocketServer({ port: p }, function () { + http.get('http://localhost:' + p, function (res) { + var body = ''; + + assert.equal(res.statusCode, 426); + res.on('data', function (chunk) { body += chunk; }); + res.on('end', function () { + assert.equal(body, http.STATUS_CODES[426]); + wss.close(); + }); + }); + }); + } + + // Don't test this on Windows. It throws errors for obvious reasons. + if(!/^win/i.test(process.platform)) { + /*'uses a precreated http server listening on unix socket'*/ + { + let srv = http.createServer(); + let p = ++port + var sockPath = '/tmp/ws_socket_'+ new Date().getTime() + '.' + Math.floor(Math.random() * 1000); + srv.listen(sockPath, function () { + let wss = new WebSocketServer({server: srv}); + var ws = new WebSocket('ws+unix://' + sockPath); + + wss.on('connection', function(client) { + wss.close(); + srv.close(); + }); + }); + } + } + + /*'emits path specific connection event'*/ + { + let srv = http.createServer(); + let p = ++port + srv.listen(p, function () { + let wss = new WebSocketServer({server: srv}); + var ws = new WebSocket('ws://localhost:' + p +'/endpointName'); + + wss.on('connection/endpointName', function(client) { + wss.close(); + srv.close(); + }); + }); + } + + /*'can have two different instances listening on the same http server with two different paths'*/ + { + let srv = http.createServer(); + let p = ++port + srv.listen(p, function () { + var wss1 = new WebSocketServer({server: srv, path: '/wss1'}) + , wss2 = new WebSocketServer({server: srv, path: '/wss2'}); + var doneCount = 0; + wss1.on('connection', function(client) { + wss1.close(); + if (++doneCount == 2) { + srv.close(); + } + }); + wss2.on('connection', function(client) { + wss2.close(); + if (++doneCount == 2) { + srv.close(); + } + }); + var ws1 = new WebSocket('ws://localhost:' + p + '/wss1'); + var ws2 = new WebSocket('ws://localhost:' + p + '/wss2?foo=1'); + }); + } + + /*'cannot have two different instances listening on the same http server with the same path'*/ + { + let srv = http.createServer(); + let p = ++port + srv.listen(p, function () { + var wss1 = new WebSocketServer({server: srv, path: '/wss1'}); + try { + var wss2 = new WebSocketServer({server: srv, path: '/wss1'}); + } + catch (e) { + wss1.close(); + srv.close(); + } + }); + } + } + // + // /*'#close'*/ + // { + // /*'does not thrown when called twice'*/ + // { + // let wss = new WebSocketServer({port: ++port}, function() { + // wss.close(); + // wss.close(); + // wss.close(); + // + // done(); + // }); + // } + // + // /*'will close all clients'*/ + // { + // let wss = new WebSocketServer({port: ++port}, function() { + // var ws = new WebSocket('ws://localhost:' + port); + // ws.on('close', function() { + // if (++closes == 2) done(); + // }); + // }); + // var closes = 0; + // wss.on('connection', function(client) { + // client.on('close', function() { + // if (++closes == 2) done(); + // }); + // wss.close(); + // }); + // } + // + // /*'does not close a precreated server'*/ + // { + // let srv = http.createServer(); + // var realClose = srv.close; + // srv.close = function() { + // assert.fail('must not close pre-created server'); + // } + // srv.listen(++port, function () { + // let wss = new WebSocketServer({server: srv}); + // var ws = new WebSocket('ws://localhost:' + port); + // wss.on('connection', function(client) { + // wss.close(); + // srv.close = realClose; + // srv.close(); + // done(); + // }); + // }); + // } + // + // /*'cleans up websocket data on a precreated server'*/ + // { + // let srv = http.createServer(); + // srv.listen(++port, function () { + // var wss1 = new WebSocketServer({server: srv, path: '/wss1'}) + // , wss2 = new WebSocketServer({server: srv, path: '/wss2'}); + // assert.equal((typeof srv._webSocketPaths), 'object'); + // assert.equal(Object.keys(srv._webSocketPaths).length, 2); + // wss1.close(); + // assert.equal(Object.keys(srv._webSocketPaths).length, 1); + // wss2.close(); + // assert.equal((typeof srv._webSocketPaths), 'undefined'); + // srv.close(); + // done(); + // }); + // } + // } + // + // /*'#clients'*/ + // { + // /*'returns a list of connected clients'*/ + // { + // let wss = new WebSocketServer({port: ++port}, function() { + // assert.equal(wss.clients.length, 0); + // var ws = new WebSocket('ws://localhost:' + port); + // }); + // wss.on('connection', function(client) { + // assert.equal(wss.clients.length, 1); + // wss.close(); + // done(); + // }); + // } + // + // // TODO(eljefedelrodeodeljefe): this is failing due to unknown reason + // /*it('can be disabled'*/ + // { + // // let wss = new WebSocketServer({port: ++port, clientTracking: false}, function() { + // // assert.equal(wss.clients.length, 0); + // // var ws = new WebSocket('ws://localhost:' + port); + // // }); + // // wss.on('connection', function(client) { + // // assert.equal(wss.clients.length, 0); + // // wss.close(); + // // done(); + // // }); + // // }); + // + // /*'is updated when client terminates the connection'*/ + // { + // var ws; + // let wss = new WebSocketServer({port: ++port}, function() { + // ws = new WebSocket('ws://localhost:' + port); + // }); + // wss.on('connection', function(client) { + // client.on('close', function() { + // assert.equal(wss.clients.length, 0); + // wss.close(); + // done(); + // }); + // ws.terminate(); + // }); + // } + // + // /*'is updated when client closes the connection'*/ + // { + // var ws; + // let wss = new WebSocketServer({port: ++port}, function() { + // ws = new WebSocket('ws://localhost:' + port); + // }); + // wss.on('connection', function(client) { + // client.on('close', function() { + // assert.equal(wss.clients.length, 0); + // wss.close(); + // done(); + // }); + // ws.close(); + // }); + // } + // } + // + /*'#options'*/ + { + /*'exposes options passed to constructor'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p}, function() { + assert.equal(wss.options.port, p); + wss.close(); + }); + } + } + + /*'#handleUpgrade'*/ + { + /*'can be used for a pre-existing server'*/ + { + let srv = http.createServer(); + let p = ++port + srv.listen(p, function () { + let wss = new WebSocketServer({noServer: true}); + srv.on('upgrade', function(req, socket, upgradeHead) { + wss.handleUpgrade(req, socket, upgradeHead, function(client) { + client.send('hello'); + }); + }); + var ws = new WebSocket('ws://localhost:' + p); + ws.on('message', function(message) { + assert.equal(message, 'hello'); + wss.close(); + srv.close(); + }); + }); + } + } + + /*'hybi mode'*/ + { + /*'connection establishing'*/ + { + /*'does not accept connections with no sec-websocket-key'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket' + } + }; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 400); + wss.close(); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'does not accept connections with no sec-websocket-version'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==' + } + }; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 400); + wss.close(); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'does not accept connections with invalid sec-websocket-version'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 12 + } + }; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 400); + wss.close(); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'client can be denied'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, verifyClient: function(o) { + return false; + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 8, + 'Sec-WebSocket-Origin': 'http://foobar.com' + } + }; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 401); + process.nextTick(function() { + wss.close(); + }); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'client can be accepted'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, verifyClient: function(o) { + return true; + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Origin': 'http://foobar.com' + } + }; + let req = http.request(options); + req.end(); + }); + wss.on('connection', function(ws) { + ws.terminate(); + wss.close(); + }); + wss.on('error', function() {}); + } + + /*'verifyClient gets client origin'*/ + { + let p = ++port + let verifyClientCalled = false; + let wss = new WebSocketServer({port: p, verifyClient: function(info) { + assert.equal(info.origin, 'http://foobarbaz.com'); + verifyClientCalled = true; + return false; + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Origin': 'http://foobarbaz.com' + } + }; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.ok(verifyClientCalled); + wss.close(); + }); + }); + wss.on('error', function() {}); + } + + /*'verifyClient gets original request'*/ + { + let p = ++port + let verifyClientCalled = false; + let wss = new WebSocketServer({port: p, verifyClient: function(info) { + assert.equal(info.req.headers['sec-websocket-key'], 'dGhlIHNhbXBsZSBub25jZQ=='); + verifyClientCalled = true; + return false; + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Origin': 'http://foobarbaz.com' + } + }; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.ok(verifyClientCalled); + wss.close(); + }); + }); + wss.on('error', function() {}); + } + + /*'verifyClient has secure:true for ssl connections'*/ + { + let options = { + key: fs.readFileSync('test/fixtures/websockets/key.pem'), + cert: fs.readFileSync('test/fixtures/websockets/certificate.pem') + }; + var app = https.createServer(options, function (req, res) { + res.writeHead(200); + res.end(); + }); + var success = false; + let wss = new WebSocketServer({ + server: app, + verifyClient: function(info) { + success = info.secure === true; + return true; + } + }); + let p = ++port + app.listen(p, function() { + var ws = new WebSocket('wss://localhost:' + p); + }); + wss.on('connection', function(ws) { + app.close(); + ws.terminate(); + wss.close(); + assert.ok(success); + }); + } + + /*'verifyClient has secure:false for non-ssl connections'*/ + // { + // var app = http.createServer(function (req, res) { + // res.writeHead(200); + // res.end(); + // }); + // var success = false; + // let wss = new WebSocketServer({ + // server: app, + // verifyClient: function(info) { + // success = info.secure === false; + // return true; + // } + // }); + // let p = ++port + // app.listen(p, function() { + // var ws = new WebSocket('ws://localhost:' + p); + // }); + // wss.on('connection', function(ws) { + // app.close(); + // ws.terminate(); + // wss.close(); + // assert.ok(success); + // }); + // } + + /*'client can be denied asynchronously'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, verifyClient: function(o, cb) { + process.nextTick(function() { + cb(false); + }); + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 8, + 'Sec-WebSocket-Origin': 'http://foobar.com' + } + }; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 401); + process.nextTick(function() { + wss.close(); + }); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'client can be denied asynchronously with custom response code'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, verifyClient: function(o, cb) { + process.nextTick(function() { + cb(false, 404); + }); + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 8, + 'Sec-WebSocket-Origin': 'http://foobar.com' + } + }; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 404); + process.nextTick(function() { + wss.close(); + }); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'client can be accepted asynchronously'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, verifyClient: function(o, cb) { + process.nextTick(function() { + cb(true); + }); + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Origin': 'http://foobar.com' + } + }; + let req = http.request(options); + req.end(); + }); + wss.on('connection', function(ws) { + ws.terminate(); + wss.close(); + }); + wss.on('error', function() {}); + } + + /*'handles messages passed along with the upgrade request (upgrade head)'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, verifyClient: function(o) { + return true; + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Origin': 'http://foobar.com' + } + }; + let req = http.request(options); + req.write(new Buffer([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], 'binary')); + req.end(); + }); + wss.on('connection', function(ws) { + ws.on('message', function(data) { + assert.equal(data, 'Hello'); + ws.terminate(); + wss.close(); + }); + }); + wss.on('error', function() {}); + } + + /*'selects the first protocol by default'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p}, function() { + let ws = new WebSocket('ws://localhost:' + p, ['prot1', 'prot2']); + ws.on('open', function(client) { + assert.equal(ws.protocol, 'prot1'); + wss.close(); + }); + }); + } + + /*'selects the last protocol via protocol handler'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, handleProtocols: function(ps, cb) { + cb(true, ps[ps.length-1]); }}, function() { + let ws = new WebSocket('ws://localhost:' + p, ['prot1', 'prot2']); + ws.on('open', function(client) { + assert.equal(ws.protocol, 'prot2'); + wss.close(); + }); + }); + } + + /*'client detects invalid server protocol'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p, handleProtocols: function(ps, cb) { + // cb(true, 'prot3'); }}, function() { + // var ws = new WebSocket('ws://localhost:' + p, ['prot1', 'prot2']); + // ws.on('open', function(client) { + // // done(new Error('connection must not be established')); + // }); + // ws.on('error', function() {}); + // }); + // } + + /*'client detects no server protocol'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p, handleProtocols: function(ps, cb) { + // cb(true); }}, function() { + // var ws = new WebSocket('ws://localhost:' + p, ['prot1', 'prot2']); + // ws.on('open', function(client) { + // // done(new Error('connection must not be established')); + // }); + // ws.on('error', function() {}); + // }); + // } + + /*'client refuses server protocols'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p, handleProtocols: function(ps, cb) { + // cb(false); }}, function() { + // var ws = new WebSocket('ws://localhost:' + p, ['prot1', 'prot2']); + // ws.on('open', function(client) { + // // done(new Error('connection must not be established')); + // }); + // ws.on('error', function() {}); + // }); + // } + + /*'server detects unauthorized protocol handler'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, handleProtocols: function(ps, cb) { + cb(false); + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Origin': 'http://foobar.com' + } + }; + options.port = p; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 401); + wss.close(); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'server detects invalid protocol handler'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, handleProtocols: function(ps, cb) { + // not calling callback is an error and shouldn't timeout + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Origin': 'http://foobar.com' + } + }; + options.port = p; + let req = http.request(options); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 501); + wss.close(); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'accept connections with sec-websocket-extensions'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Extensions': 'permessage-foo; x=10' + } + }; + let req = http.request(options); + req.end(); + }); + wss.on('connection', function(ws) { + ws.terminate(); + wss.close(); + }); + wss.on('error', function() {}); + } + } + + /*'messaging'*/ + { + /*'can send and receive data'*/ + { + let data = new Array(65*1024); + for (var i = 0; i < data.length; ++i) { + data[i] = String.fromCharCode(65 + ~~(25 * Math.random())); + } + data = data.join(''); + let p = ++port + let wss = new WebSocketServer({port: p}, function() { + let ws = new WebSocket('ws://localhost:' + p); + ws.on('message', function(message, flags) { + ws.send(message); + }); + }); + wss.on('connection', function(client) { + client.on('message', function(message) { + assert.equal(message, data); + wss.close(); + }); + client.send(data); + }); + } + } + } + + /*'hixie mode'*/ + { + /*'can be disabled'*/ + { + let wss = new WebSocketServer({port: ++port, disableHixie: true}, function() { + let options = { + port: port, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + } + }; + let req = http.request(options); + req.write('WjN}|M(6'); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 401); + process.nextTick(function() { + wss.close(); + }); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'connection establishing'*/ + { + // /*'does not accept connections with no sec-websocket-key1'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'WebSocket', + // 'Sec-WebSocket-Key1': '3e6b263 4 17 80' + // } + // }; + // let req = http.request(options); + // req.end(); + // req.on('response', function(res) { + // assert.equal(res.statusCode, 400); + // wss.close(); + // }); + // }); + // wss.on('connection', function(ws) { + // // done(new Error('connection must not be established')); + // }); + // wss.on('error', function() {}); + // } + + /*'does not accept connections with no sec-websocket-key2'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'WebSocket', + // 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + // } + // }; + // let req = http.request(options); + // req.end(); + // req.on('response', function(res) { + // assert.equal(res.statusCode, 400); + // wss.close(); + // }); + // }); + // wss.on('connection', function(ws) { + // // done(new Error('connection must not be established')); + // }); + // wss.on('error', function() {}); + // } + + /*'accepts connections with valid handshake'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'WebSocket', + // 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + // 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + // } + // }; + // let req = http.request(options); + // req.write('WjN}|M(6'); + // req.end(); + // }); + // wss.on('connection', function(ws) { + // ws.terminate(); + // wss.close(); + // }); + // wss.on('error', function() {}); + // } + + /*'client can be denied'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, verifyClient: function(o) { + return false; + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + } + }; + let req = http.request(options); + req.write('WjN}|M(6'); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 401); + process.nextTick(function() { + wss.close(); + }); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'client can be accepted'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p, verifyClient: function(o) { + // return true; + // }}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'WebSocket', + // 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + // 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + // } + // }; + // let req = http.request(options); + // req.write('WjN}|M(6'); + // req.end(); + // }); + // wss.on('connection', function(ws) { + // ws.terminate(); + // wss.close(); + // }); + // wss.on('error', function() {}); + // } + + /*'verifyClient gets client origin'*/ + // { + // let p = ++port + // var verifyClientCalled = false; + // let wss = new WebSocketServer({port: p, verifyClient: function(info) { + // assert.equal(info.origin, 'http://foobarbaz.com'); + // verifyClientCalled = true; + // return false; + // }}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'WebSocket', + // 'Origin': 'http://foobarbaz.com', + // 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + // 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + // } + // }; + // let req = http.request(options); + // req.write('WjN}|M(6'); + // req.end(); + // req.on('response', function(res) { + // assert.ok(verifyClientCalled); + // wss.close(); + // }); + // }); + // wss.on('error', function() {}); + // } + + /*'verifyClient gets original request'*/ + { + let p = ++port + let verifyClientCalled = false; + let wss = new WebSocketServer({port: p, verifyClient: function(info) { + assert.equal(info.req.headers['sec-websocket-key1'], '3e6b263 4 17 80'); + verifyClientCalled = true; + return false; + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'WebSocket', + 'Origin': 'http://foobarbaz.com', + 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + } + }; + let req = http.request(options); + req.write('WjN}|M(6'); + req.end(); + req.on('response', function(res) { + assert.ok(verifyClientCalled); + wss.close(); + }); + }); + wss.on('error', function() {}); + } + + /*'client can be denied asynchronously'*/ + { + let p = ++port + let wss = new WebSocketServer({port: p, verifyClient: function(o, cb) { + cb(false); + }}, function() { + let options = { + port: p, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'WebSocket', + 'Origin': 'http://foobarbaz.com', + 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + } + }; + let req = http.request(options); + req.write('WjN}|M(6'); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 401); + process.nextTick(function() { + wss.close(); + }); + }); + }); + wss.on('connection', function(ws) { + // done(new Error('connection must not be established')); + }); + wss.on('error', function() {}); + } + + /*'client can be denied asynchronously with custom response code'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p, verifyClient: function(o, cb) { + // cb(false, 404, 'Not Found'); + // }}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'WebSocket', + // 'Origin': 'http://foobarbaz.com', + // 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + // 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + // } + // }; + // let req = http.request(options); + // req.write('WjN}|M(6'); + // req.end(); + // req.on('response', function(res) { + // assert.equal(res.statusCode, 404); + // process.nextTick(function() { + // wss.close(); + // }); + // }); + // }); + // wss.on('connection', function(ws) { + // // done(new Error('connection must not be established')); + // }); + // wss.on('error', function() {}); + // } + + /*'client can be accepted asynchronously'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p, verifyClient: function(o, cb) { + // cb(true); + // }}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'WebSocket', + // 'Origin': 'http://foobarbaz.com', + // 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + // 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' + // } + // }; + // let req = http.request(options); + // req.write('WjN}|M(6'); + // req.end(); + // }); + // wss.on('connection', function(ws) { + // wss.close(); + // }); + // wss.on('error', function() {}); + // } + + /*'handles messages passed along with the upgrade request (upgrade head)'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p, verifyClient: function(o) { + // return true; + // }}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'WebSocket', + // 'Sec-WebSocket-Key1': '3e6b263 4 17 80', + // 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90', + // 'Origin': 'http://foobar.com' + // } + // }; + // let req = http.request(options); + // req.write('WjN}|M(6'); + // req.write(new Buffer([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], 'binary')); + // req.end(); + // }); + // wss.on('connection', function(ws) { + // ws.on('message', function(data) { + // assert.equal(data, 'Hello'); + // ws.terminate(); + // wss.close(); + // }); + // }); + // wss.on('error', function() {}); + // } + } + } + + /*'client properties'*/ + { + /*'protocol is exposed'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // var ws = new WebSocket('ws://localhost:' + p, 'hi'); + // }); + // wss.on('connection', function(client) { + // assert.equal(client.protocol, 'hi'); + // wss.close(); + // }); + // } + + /*'protocolVersion is exposed'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // var ws = new WebSocket('ws://localhost:' + p, {protocolVersion: 8}); + // }); + // wss.on('connection', function(client) { + // assert.equal(client.protocolVersion, 8); + // wss.close(); + // }); + // } + + /*'upgradeReq is the original request object'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // var ws = new WebSocket('ws://localhost:' + p, {protocolVersion: 8}); + // }); + // wss.on('connection', function(client) { + // assert.equal(client.upgradeReq.httpVersion, '1.1'); + // wss.close(); + // }); + // } + } + + /*'permessage-deflate'*/ + { + /*'accept connections with permessage-deflate extension'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'websocket', + // 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + // 'Sec-WebSocket-Version': 13, + // 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits=8; server_max_window_bits=8; client_no_context_takeover; server_no_context_takeover' + // } + // }; + // let req = http.request(options); + // req.end(); + // }); + // wss.on('connection', function(ws) { + // ws.terminate(); + // wss.close(); + // }); + // wss.on('error', function() {}); + // } + + /*'does not accept connections with not defined extension parameter'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'websocket', + // 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + // 'Sec-WebSocket-Version': 13, + // 'Sec-WebSocket-Extensions': 'permessage-deflate; foo=15' + // } + // }; + // let req = http.request(options); + // req.end(); + // req.on('response', function(res) { + // assert.equal(res.statusCode, 400); + // wss.close(); + // }); + // }); + // wss.on('connection', function(ws) { + // // done(new Error('connection must not be established')); + // }); + // wss.on('error', function() {}); + // } + + /*'does not accept connections with invalid extension parameter'*/ + // { + // let p = ++port + // let wss = new WebSocketServer({port: p}, function() { + // let options = { + // port: p, + // host: '127.0.0.1', + // headers: { + // 'Connection': 'Upgrade', + // 'Upgrade': 'websocket', + // 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + // 'Sec-WebSocket-Version': 13, + // 'Sec-WebSocket-Extensions': 'permessage-deflate; server_max_window_bits=foo' + // } + // }; + // let req = http.request(options); + // req.end(); + // req.on('response', function(res) { + // assert.equal(res.statusCode, 400); + // wss.close(); + // }); + // }); + // wss.on('connection', function(ws) { + // // done(new Error('connection must not be established')); + // }); + // wss.on('error', function() {}); + // } + } + }