summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/BufferUtil.fallback.js52
-rw-r--r--lib/BufferUtil.js17
-rw-r--r--lib/Deprecation.js32
-rw-r--r--lib/Validation.fallback.js12
-rw-r--r--lib/Validation.js17
-rw-r--r--lib/W3CWebSocket.js257
-rw-r--r--lib/WebSocketClient.js348
-rw-r--r--lib/WebSocketConnection.js889
-rw-r--r--lib/WebSocketFrame.js279
-rw-r--r--lib/WebSocketRequest.js524
-rw-r--r--lib/WebSocketRouter.js157
-rw-r--r--lib/WebSocketRouterRequest.js54
-rw-r--r--lib/WebSocketServer.js245
-rw-r--r--lib/browser.js36
-rw-r--r--lib/utils.js60
-rw-r--r--lib/version.js1
-rw-r--r--lib/websocket.js11
17 files changed, 2991 insertions, 0 deletions
diff --git a/lib/BufferUtil.fallback.js b/lib/BufferUtil.fallback.js
new file mode 100644
index 0000000..de18bfb
--- /dev/null
+++ b/lib/BufferUtil.fallback.js
@@ -0,0 +1,52 @@
+/*!
+ * Copied from:
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+/* jshint -W086 */
+
+module.exports.BufferUtil = {
+ merge: function(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;
+ }
+ },
+ mask: function(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:
+ }
+ },
+ unmask: function(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:
+ }
+ }
+};
+
+/* jshint +W086 */ \ No newline at end of file
diff --git a/lib/BufferUtil.js b/lib/BufferUtil.js
new file mode 100644
index 0000000..fa37c80
--- /dev/null
+++ b/lib/BufferUtil.js
@@ -0,0 +1,17 @@
+/*!
+ * Copied from:
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+try {
+ module.exports = require('../build/Release/bufferutil');
+} catch (e) { try {
+ module.exports = require('../build/default/bufferutil');
+} catch (e) { try {
+ module.exports = require('./BufferUtil.fallback');
+} catch (e) {
+ console.error('bufferutil.node seems to not have been built. Run npm install.');
+ throw e;
+}}}
diff --git a/lib/Deprecation.js b/lib/Deprecation.js
new file mode 100644
index 0000000..094f160
--- /dev/null
+++ b/lib/Deprecation.js
@@ -0,0 +1,32 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var Deprecation = {
+ disableWarnings: false,
+
+ deprecationWarningMap: {
+
+ },
+
+ warn: function(deprecationName) {
+ if (!this.disableWarnings && this.deprecationWarningMap[deprecationName]) {
+ console.warn('DEPRECATION WARNING: ' + this.deprecationWarningMap[deprecationName]);
+ this.deprecationWarningMap[deprecationName] = false;
+ }
+ }
+};
+
+module.exports = Deprecation;
diff --git a/lib/Validation.fallback.js b/lib/Validation.fallback.js
new file mode 100644
index 0000000..6160f88
--- /dev/null
+++ b/lib/Validation.fallback.js
@@ -0,0 +1,12 @@
+/*!
+ * UTF-8 Validation Fallback Code originally from:
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+module.exports.Validation = {
+ isValidUTF8: function() {
+ return true;
+ }
+};
diff --git a/lib/Validation.js b/lib/Validation.js
new file mode 100644
index 0000000..b4106e8
--- /dev/null
+++ b/lib/Validation.js
@@ -0,0 +1,17 @@
+/*!
+ * UTF-8 Validation Code originally from:
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+try {
+ module.exports = require('../build/Release/validation');
+} catch (e) { try {
+ module.exports = require('../build/default/validation');
+} catch (e) { try {
+ module.exports = require('./Validation.fallback');
+} catch (e) {
+ console.error('validation.node seems not to have been built. Run npm install.');
+ throw e;
+}}}
diff --git a/lib/W3CWebSocket.js b/lib/W3CWebSocket.js
new file mode 100644
index 0000000..4305fb6
--- /dev/null
+++ b/lib/W3CWebSocket.js
@@ -0,0 +1,257 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var WebSocketClient = require('./WebSocketClient');
+var toBuffer = require('typedarray-to-buffer');
+var yaeti = require('yaeti');
+
+
+const CONNECTING = 0;
+const OPEN = 1;
+const CLOSING = 2;
+const CLOSED = 3;
+
+
+module.exports = W3CWebSocket;
+
+
+function W3CWebSocket(url, protocols, origin, headers, requestOptions, clientConfig) {
+ // Make this an EventTarget.
+ yaeti.EventTarget.call(this);
+
+ // Sanitize clientConfig.
+ clientConfig = clientConfig || {};
+ clientConfig.assembleFragments = true; // Required in the W3C API.
+
+ var self = this;
+
+ this._url = url;
+ this._readyState = CONNECTING;
+ this._protocol = undefined;
+ this._extensions = '';
+ this._bufferedAmount = 0; // Hack, always 0.
+ this._binaryType = 'arraybuffer'; // TODO: Should be 'blob' by default, but Node has no Blob.
+
+ // The WebSocketConnection instance.
+ this._connection = undefined;
+
+ // WebSocketClient instance.
+ this._client = new WebSocketClient(clientConfig);
+
+ this._client.on('connect', function(connection) {
+ onConnect.call(self, connection);
+ });
+
+ this._client.on('connectFailed', function() {
+ onConnectFailed.call(self);
+ });
+
+ this._client.connect(url, protocols, origin, headers, requestOptions);
+}
+
+
+// Expose W3C read only attributes.
+Object.defineProperties(W3CWebSocket.prototype, {
+ url: { get: function() { return this._url; } },
+ readyState: { get: function() { return this._readyState; } },
+ protocol: { get: function() { return this._protocol; } },
+ extensions: { get: function() { return this._extensions; } },
+ bufferedAmount: { get: function() { return this._bufferedAmount; } }
+});
+
+
+// Expose W3C write/read attributes.
+Object.defineProperties(W3CWebSocket.prototype, {
+ binaryType: {
+ get: function() {
+ return this._binaryType;
+ },
+ set: function(type) {
+ // TODO: Just 'arraybuffer' supported.
+ if (type !== 'arraybuffer') {
+ throw new SyntaxError('just "arraybuffer" type allowed for "binaryType" attribute');
+ }
+ this._binaryType = type;
+ }
+ }
+});
+
+
+// Expose W3C readyState constants into the WebSocket instance as W3C states.
+[['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) {
+ Object.defineProperty(W3CWebSocket.prototype, property[0], {
+ get: function() { return property[1]; }
+ });
+});
+
+// Also expone W3C readyState constants into the WebSocket class (not defined by the W3C,
+// but there are so many libs relying on them).
+[['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) {
+ Object.defineProperty(W3CWebSocket, property[0], {
+ get: function() { return property[1]; }
+ });
+});
+
+
+W3CWebSocket.prototype.send = function(data) {
+ if (this._readyState !== OPEN) {
+ throw new Error('cannot call send() while not connected');
+ }
+
+ // Text.
+ if (typeof data === 'string' || data instanceof String) {
+ this._connection.sendUTF(data);
+ }
+ // Binary.
+ else {
+ // Node Buffer.
+ if (data instanceof Buffer) {
+ this._connection.sendBytes(data);
+ }
+ // If ArrayBuffer or ArrayBufferView convert it to Node Buffer.
+ else if (data.byteLength || data.byteLength === 0) {
+ data = toBuffer(data);
+ this._connection.sendBytes(data);
+ }
+ else {
+ throw new Error('unknown binary data:', data);
+ }
+ }
+};
+
+
+W3CWebSocket.prototype.close = function(code, reason) {
+ switch(this._readyState) {
+ case CONNECTING:
+ // NOTE: We don't have the WebSocketConnection instance yet so no
+ // way to close the TCP connection.
+ // Artificially invoke the onConnectFailed event.
+ onConnectFailed.call(this);
+ // And close if it connects after a while.
+ this._client.on('connect', function(connection) {
+ if (code) {
+ connection.close(code, reason);
+ } else {
+ connection.close();
+ }
+ });
+ break;
+ case OPEN:
+ this._readyState = CLOSING;
+ if (code) {
+ this._connection.close(code, reason);
+ } else {
+ this._connection.close();
+ }
+ break;
+ case CLOSING:
+ case CLOSED:
+ break;
+ }
+};
+
+
+/**
+ * Private API.
+ */
+
+
+function createCloseEvent(code, reason) {
+ var event = new yaeti.Event('close');
+
+ event.code = code;
+ event.reason = reason;
+ event.wasClean = (typeof code === 'undefined' || code === 1000);
+
+ return event;
+}
+
+
+function createMessageEvent(data) {
+ var event = new yaeti.Event('message');
+
+ event.data = data;
+
+ return event;
+}
+
+
+function onConnect(connection) {
+ var self = this;
+
+ this._readyState = OPEN;
+ this._connection = connection;
+ this._protocol = connection.protocol;
+ this._extensions = connection.extensions;
+
+ this._connection.on('close', function(code, reason) {
+ onClose.call(self, code, reason);
+ });
+
+ this._connection.on('message', function(msg) {
+ onMessage.call(self, msg);
+ });
+
+ this.dispatchEvent(new yaeti.Event('open'));
+}
+
+
+function onConnectFailed() {
+ destroy.call(this);
+ this._readyState = CLOSED;
+
+ try {
+ this.dispatchEvent(new yaeti.Event('error'));
+ } finally {
+ this.dispatchEvent(createCloseEvent(1006, 'connection failed'));
+ }
+}
+
+
+function onClose(code, reason) {
+ destroy.call(this);
+ this._readyState = CLOSED;
+
+ this.dispatchEvent(createCloseEvent(code, reason || ''));
+}
+
+
+function onMessage(message) {
+ if (message.utf8Data) {
+ this.dispatchEvent(createMessageEvent(message.utf8Data));
+ }
+ else if (message.binaryData) {
+ // Must convert from Node Buffer to ArrayBuffer.
+ // TODO: or to a Blob (which does not exist in Node!).
+ if (this.binaryType === 'arraybuffer') {
+ var buffer = message.binaryData;
+ var arraybuffer = new ArrayBuffer(buffer.length);
+ var view = new Uint8Array(arraybuffer);
+ for (var i=0, len=buffer.length; i<len; ++i) {
+ view[i] = buffer[i];
+ }
+ this.dispatchEvent(createMessageEvent(arraybuffer));
+ }
+ }
+}
+
+
+function destroy() {
+ this._client.removeAllListeners();
+ if (this._connection) {
+ this._connection.removeAllListeners();
+ }
+}
diff --git a/lib/WebSocketClient.js b/lib/WebSocketClient.js
new file mode 100644
index 0000000..4b4abb2
--- /dev/null
+++ b/lib/WebSocketClient.js
@@ -0,0 +1,348 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var utils = require('./utils');
+var extend = utils.extend;
+var util = require('util');
+var EventEmitter = require('events').EventEmitter;
+var http = require('http');
+var https = require('https');
+var url = require('url');
+var crypto = require('crypto');
+var WebSocketConnection = require('./WebSocketConnection');
+
+var protocolSeparators = [
+ '(', ')', '<', '>', '@',
+ ',', ';', ':', '\\', '\"',
+ '/', '[', ']', '?', '=',
+ '{', '}', ' ', String.fromCharCode(9)
+];
+
+function WebSocketClient(config) {
+ // Superclass Constructor
+ EventEmitter.call(this);
+
+ // TODO: Implement extensions
+
+ this.config = {
+ // 1MiB max frame size.
+ maxReceivedFrameSize: 0x100000,
+
+ // 8MiB max message size, only applicable if
+ // assembleFragments is true
+ maxReceivedMessageSize: 0x800000,
+
+ // Outgoing messages larger than fragmentationThreshold will be
+ // split into multiple fragments.
+ fragmentOutgoingMessages: true,
+
+ // Outgoing frames are fragmented if they exceed this threshold.
+ // Default is 16KiB
+ fragmentationThreshold: 0x4000,
+
+ // Which version of the protocol to use for this session. This
+ // option will be removed once the protocol is finalized by the IETF
+ // It is only available to ease the transition through the
+ // intermediate draft protocol versions.
+ // At present, it only affects the name of the Origin header.
+ webSocketVersion: 13,
+
+ // If true, fragmented messages will be automatically assembled
+ // and the full message will be emitted via a 'message' event.
+ // If false, each frame will be emitted via a 'frame' event and
+ // the application will be responsible for aggregating multiple
+ // fragmented frames. Single-frame messages will emit a 'message'
+ // event in addition to the 'frame' event.
+ // Most users will want to leave this set to 'true'
+ assembleFragments: true,
+
+ // The Nagle Algorithm makes more efficient use of network resources
+ // by introducing a small delay before sending small packets so that
+ // multiple messages can be batched together before going onto the
+ // wire. This however comes at the cost of latency, so the default
+ // is to disable it. If you don't need low latency and are streaming
+ // lots of small messages, you can change this to 'false'
+ disableNagleAlgorithm: true,
+
+ // The number of milliseconds to wait after sending a close frame
+ // for an acknowledgement to come back before giving up and just
+ // closing the socket.
+ closeTimeout: 5000,
+
+ // Options to pass to https.connect if connecting via TLS
+ tlsOptions: {}
+ };
+
+ if (config) {
+ var tlsOptions;
+ if (config.tlsOptions) {
+ tlsOptions = config.tlsOptions;
+ delete config.tlsOptions;
+ }
+ else {
+ tlsOptions = {};
+ }
+ extend(this.config, config);
+ extend(this.config.tlsOptions, tlsOptions);
+ }
+
+ this._req = null;
+
+ switch (this.config.webSocketVersion) {
+ case 8:
+ case 13:
+ break;
+ default:
+ throw new Error('Requested webSocketVersion is not supported. Allowed values are 8 and 13.');
+ }
+}
+
+util.inherits(WebSocketClient, EventEmitter);
+
+WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, headers, extraRequestOptions) {
+ var self = this;
+ if (typeof(protocols) === 'string') {
+ if (protocols.length > 0) {
+ protocols = [protocols];
+ }
+ else {
+ protocols = [];
+ }
+ }
+ if (!(protocols instanceof Array)) {
+ protocols = [];
+ }
+ this.protocols = protocols;
+ this.origin = origin;
+
+ if (typeof(requestUrl) === 'string') {
+ this.url = url.parse(requestUrl);
+ }
+ else {
+ this.url = requestUrl; // in case an already parsed url is passed in.
+ }
+ if (!this.url.protocol) {
+ throw new Error('You must specify a full WebSocket URL, including protocol.');
+ }
+ if (!this.url.host) {
+ throw new Error('You must specify a full WebSocket URL, including hostname. Relative URLs are not supported.');
+ }
+
+ this.secure = (this.url.protocol === 'wss:');
+
+ // validate protocol characters:
+ this.protocols.forEach(function(protocol) {
+ for (var i=0; i < protocol.length; i ++) {
+ var charCode = protocol.charCodeAt(i);
+ var character = protocol.charAt(i);
+ if (charCode < 0x0021 || charCode > 0x007E || protocolSeparators.indexOf(character) !== -1) {
+ throw new Error('Protocol list contains invalid character "' + String.fromCharCode(charCode) + '"');
+ }
+ }
+ });
+
+ var defaultPorts = {
+ 'ws:': '80',
+ 'wss:': '443'
+ };
+
+ if (!this.url.port) {
+ this.url.port = defaultPorts[this.url.protocol];
+ }
+
+ var nonce = new Buffer(16);
+ for (var i=0; i < 16; i++) {
+ nonce[i] = Math.round(Math.random()*0xFF);
+ }
+ this.base64nonce = nonce.toString('base64');
+
+ var hostHeaderValue = this.url.hostname;
+ if ((this.url.protocol === 'ws:' && this.url.port !== '80') ||
+ (this.url.protocol === 'wss:' && this.url.port !== '443')) {
+ hostHeaderValue += (':' + this.url.port);
+ }
+
+ var reqHeaders = headers || {};
+ extend(reqHeaders, {
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Version': this.config.webSocketVersion.toString(10),
+ 'Sec-WebSocket-Key': this.base64nonce,
+ 'Host': hostHeaderValue
+ });
+
+ if (this.protocols.length > 0) {
+ reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', ');
+ }
+ if (this.origin) {
+ if (this.config.webSocketVersion === 13) {
+ reqHeaders['Origin'] = this.origin;
+ }
+ else if (this.config.webSocketVersion === 8) {
+ reqHeaders['Sec-WebSocket-Origin'] = this.origin;
+ }
+ }
+
+ // TODO: Implement extensions
+
+ var pathAndQuery;
+ // Ensure it begins with '/'.
+ if (this.url.pathname) {
+ pathAndQuery = this.url.path;
+ }
+ else if (this.url.path) {
+ pathAndQuery = '/' + this.url.path;
+ }
+ else {
+ pathAndQuery = '/';
+ }
+
+ function handleRequestError(error) {
+ self._req = null;
+ self.emit('connectFailed', error);
+ }
+
+ var requestOptions = {
+ agent: false
+ };
+ if (extraRequestOptions) {
+ extend(requestOptions, extraRequestOptions);
+ }
+ // These options are always overridden by the library. The user is not
+ // allowed to specify these directly.
+ extend(requestOptions, {
+ hostname: this.url.hostname,
+ port: this.url.port,
+ method: 'GET',
+ path: pathAndQuery,
+ headers: reqHeaders
+ });
+ if (this.secure) {
+ for (var key in self.config.tlsOptions) {
+ if (self.config.tlsOptions.hasOwnProperty(key)) {
+ requestOptions[key] = self.config.tlsOptions[key];
+ }
+ }
+ }
+
+ var req = this._req = (this.secure ? https : http).request(requestOptions);
+ req.on('upgrade', function handleRequestUpgrade(response, socket, head) {
+ self._req = null;
+ req.removeListener('error', handleRequestError);
+ self.socket = socket;
+ self.response = response;
+ self.firstDataChunk = head;
+ self.validateHandshake();
+ });
+ req.on('error', handleRequestError);
+
+ req.on('response', function(response) {
+ self._req = null;
+ if (utils.eventEmitterListenerCount(self, 'httpResponse') > 0) {
+ self.emit('httpResponse', response, self);
+ if (response.socket) {
+ response.socket.end();
+ }
+ }
+ else {
+ var headerDumpParts = [];
+ for (var headerName in response.headers) {
+ headerDumpParts.push(headerName + ': ' + response.headers[headerName]);
+ }
+ self.failHandshake(
+ 'Server responded with a non-101 status: ' +
+ response.statusCode +
+ '\nResponse Headers Follow:\n' +
+ headerDumpParts.join('\n') + '\n'
+ );
+ }
+ });
+ req.end();
+};
+
+WebSocketClient.prototype.validateHandshake = function() {
+ var headers = this.response.headers;
+
+ if (this.protocols.length > 0) {
+ this.protocol = headers['sec-websocket-protocol'];
+ if (this.protocol) {
+ if (this.protocols.indexOf(this.protocol) === -1) {
+ this.failHandshake('Server did not respond with a requested protocol.');
+ return;
+ }
+ }
+ else {
+ this.failHandshake('Expected a Sec-WebSocket-Protocol header.');
+ return;
+ }
+ }
+
+ if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) {
+ this.failHandshake('Expected a Connection: Upgrade header from the server');
+ return;
+ }
+
+ if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) {
+ this.failHandshake('Expected an Upgrade: websocket header from the server');
+ return;
+ }
+
+ var sha1 = crypto.createHash('sha1');
+ sha1.update(this.base64nonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
+ var expectedKey = sha1.digest('base64');
+
+ if (!headers['sec-websocket-accept']) {
+ this.failHandshake('Expected Sec-WebSocket-Accept header from server');
+ return;
+ }
+
+ if (headers['sec-websocket-accept'] !== expectedKey) {
+ this.failHandshake('Sec-WebSocket-Accept header from server didn\'t match expected value of ' + expectedKey);
+ return;
+ }
+
+ // TODO: Support extensions
+
+ this.succeedHandshake();
+};
+
+WebSocketClient.prototype.failHandshake = function(errorDescription) {
+ if (this.socket && this.socket.writable) {
+ this.socket.end();
+ }
+ this.emit('connectFailed', new Error(errorDescription));
+};
+
+WebSocketClient.prototype.succeedHandshake = function() {
+ var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config);
+
+ connection.webSocketVersion = this.config.webSocketVersion;
+ connection._addSocketEventListeners();
+
+ this.emit('connect', connection);
+ if (this.firstDataChunk.length > 0) {
+ connection.handleSocketData(this.firstDataChunk);
+ }
+ this.firstDataChunk = null;
+};
+
+WebSocketClient.prototype.abort = function() {
+ if (this._req) {
+ this._req.abort();
+ }
+};
+
+module.exports = WebSocketClient;
diff --git a/lib/WebSocketConnection.js b/lib/WebSocketConnection.js
new file mode 100644
index 0000000..fd87264
--- /dev/null
+++ b/lib/WebSocketConnection.js
@@ -0,0 +1,889 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var util = require('util');
+var utils = require('./utils');
+var EventEmitter = require('events').EventEmitter;
+var WebSocketFrame = require('./WebSocketFrame');
+var BufferList = require('../vendor/FastBufferList');
+var Validation = require('./Validation').Validation;
+
+// Connected, fully-open, ready to send and receive frames
+const STATE_OPEN = 'open';
+// Received a close frame from the remote peer
+const STATE_PEER_REQUESTED_CLOSE = 'peer_requested_close';
+// Sent close frame to remote peer. No further data can be sent.
+const STATE_ENDING = 'ending';
+// Connection is fully closed. No further data can be sent or received.
+const STATE_CLOSED = 'closed';
+
+var setImmediateImpl = ('setImmediate' in global) ?
+ global.setImmediate.bind(global) :
+ process.nextTick.bind(process);
+
+var idCounter = 0;
+
+function WebSocketConnection(socket, extensions, protocol, maskOutgoingPackets, config) {
+ this._debug = utils.BufferingLogger('websocket:connection', ++idCounter);
+ this._debug('constructor');
+
+ if (this._debug.enabled) {
+ instrumentSocketForDebugging(this, socket);
+ }
+
+ // Superclass Constructor
+ EventEmitter.call(this);
+
+ this._pingListenerCount = 0;
+ this.on('newListener', function(ev) {
+ if (ev === 'ping'){
+ this._pingListenerCount++;
+ }
+ }).on('removeListener', function(ev) {
+ if (ev === 'ping') {
+ this._pingListenerCount--;
+ }
+ });
+
+ this.config = config;
+ this.socket = socket;
+ this.protocol = protocol;
+ this.extensions = extensions;
+ this.remoteAddress = socket.remoteAddress;
+ this.closeReasonCode = -1;
+ this.closeDescription = null;
+ this.closeEventEmitted = false;
+
+ // We have to mask outgoing packets if we're acting as a WebSocket client.
+ this.maskOutgoingPackets = maskOutgoingPackets;
+
+ // We re-use the same buffers for the mask and frame header for all frames
+ // received on each connection to avoid a small memory allocation for each
+ // frame.
+ this.maskBytes = new Buffer(4);
+ this.frameHeader = new Buffer(10);
+
+ // the BufferList will handle the data streaming in
+ this.bufferList = new BufferList();
+
+ // Prepare for receiving first frame
+ this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
+ this.fragmentationSize = 0; // data received so far...
+ this.frameQueue = [];
+
+ // Various bits of connection state
+ this.connected = true;
+ this.state = STATE_OPEN;
+ this.waitingForCloseResponse = false;
+ // Received TCP FIN, socket's readable stream is finished.
+ this.receivedEnd = false;
+
+ this.closeTimeout = this.config.closeTimeout;
+ this.assembleFragments = this.config.assembleFragments;
+ this.maxReceivedMessageSize = this.config.maxReceivedMessageSize;
+
+ this.outputBufferFull = false;
+ this.inputPaused = false;
+ this.receivedDataHandler = this.processReceivedData.bind(this);
+ this._closeTimerHandler = this.handleCloseTimer.bind(this);
+
+ // Disable nagle algorithm?
+ this.socket.setNoDelay(this.config.disableNagleAlgorithm);
+
+ // Make sure there is no socket inactivity timeout
+ this.socket.setTimeout(0);
+
+ if (this.config.keepalive && !this.config.useNativeKeepalive) {
+ if (typeof(this.config.keepaliveInterval) !== 'number') {
+ throw new Error('keepaliveInterval must be specified and numeric ' +
+ 'if keepalive is true.');
+ }
+ this._keepaliveTimerHandler = this.handleKeepaliveTimer.bind(this);
+ this.setKeepaliveTimer();
+
+ if (this.config.dropConnectionOnKeepaliveTimeout) {
+ if (typeof(this.config.keepaliveGracePeriod) !== 'number') {
+ throw new Error('keepaliveGracePeriod must be specified and ' +
+ 'numeric if dropConnectionOnKeepaliveTimeout ' +
+ 'is true.');
+ }
+ this._gracePeriodTimerHandler = this.handleGracePeriodTimer.bind(this);
+ }
+ }
+ else if (this.config.keepalive && this.config.useNativeKeepalive) {
+ if (!('setKeepAlive' in this.socket)) {
+ throw new Error('Unable to use native keepalive: unsupported by ' +
+ 'this version of Node.');
+ }
+ this.socket.setKeepAlive(true, this.config.keepaliveInterval);
+ }
+
+ // The HTTP Client seems to subscribe to socket error events
+ // and re-dispatch them in such a way that doesn't make sense
+ // for users of our client, so we want to make sure nobody
+ // else is listening for error events on the socket besides us.
+ this.socket.removeAllListeners('error');
+}
+
+WebSocketConnection.CLOSE_REASON_NORMAL = 1000;
+WebSocketConnection.CLOSE_REASON_GOING_AWAY = 1001;
+WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR = 1002;
+WebSocketConnection.CLOSE_REASON_UNPROCESSABLE_INPUT = 1003;
+WebSocketConnection.CLOSE_REASON_RESERVED = 1004; // Reserved value. Undefined meaning.
+WebSocketConnection.CLOSE_REASON_NOT_PROVIDED = 1005; // Not to be used on the wire
+WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006; // Not to be used on the wire
+WebSocketConnection.CLOSE_REASON_INVALID_DATA = 1007;
+WebSocketConnection.CLOSE_REASON_POLICY_VIOLATION = 1008;
+WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG = 1009;
+WebSocketConnection.CLOSE_REASON_EXTENSION_REQUIRED = 1010;
+WebSocketConnection.CLOSE_REASON_INTERNAL_SERVER_ERROR = 1011;
+WebSocketConnection.CLOSE_REASON_TLS_HANDSHAKE_FAILED = 1015; // Not to be used on the wire
+
+WebSocketConnection.CLOSE_DESCRIPTIONS = {
+ 1000: 'Normal connection closure',
+ 1001: 'Remote peer is going away',
+ 1002: 'Protocol error',
+ 1003: 'Unprocessable input',
+ 1004: 'Reserved',
+ 1005: 'Reason not provided',
+ 1006: 'Abnormal closure, no further detail available',
+ 1007: 'Invalid data received',
+ 1008: 'Policy violation',
+ 1009: 'Message too big',
+ 1010: 'Extension requested by client is required',
+ 1011: 'Internal Server Error',
+ 1015: 'TLS Handshake Failed'
+};
+
+function validateCloseReason(code) {
+ if (code < 1000) {
+ // Status codes in the range 0-999 are not used
+ return false;
+ }
+ if (code >= 1000 && code <= 2999) {
+ // Codes from 1000 - 2999 are reserved for use by the protocol. Only
+ // a few codes are defined, all others are currently illegal.
+ return [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011].indexOf(code) !== -1;
+ }
+ if (code >= 3000 && code <= 3999) {
+ // Reserved for use by libraries, frameworks, and applications.
+ // Should be registered with IANA. Interpretation of these codes is
+ // undefined by the WebSocket protocol.
+ return true;
+ }
+ if (code >= 4000 && code <= 4999) {
+ // Reserved for private use. Interpretation of these codes is
+ // undefined by the WebSocket protocol.
+ return true;
+ }
+ if (code >= 5000) {
+ return false;
+ }
+}
+
+util.inherits(WebSocketConnection, EventEmitter);
+
+WebSocketConnection.prototype._addSocketEventListeners = function() {
+ this.socket.on('error', this.handleSocketError.bind(this));
+ this.socket.on('end', this.handleSocketEnd.bind(this));
+ this.socket.on('close', this.handleSocketClose.bind(this));
+ this.socket.on('drain', this.handleSocketDrain.bind(this));
+ this.socket.on('pause', this.handleSocketPause.bind(this));
+ this.socket.on('resume', this.handleSocketResume.bind(this));
+ this.socket.on('data', this.handleSocketData.bind(this));
+};
+
+// set or reset the keepalive timer when data is received.
+WebSocketConnection.prototype.setKeepaliveTimer = function() {
+ this._debug('setKeepaliveTimer');
+ if (!this.config.keepalive) { return; }
+ this.clearKeepaliveTimer();
+ this.clearGracePeriodTimer();
+ this._keepaliveTimeoutID = setTimeout(this._keepaliveTimerHandler, this.config.keepaliveInterval);
+};
+
+WebSocketConnection.prototype.clearKeepaliveTimer = function() {
+ if (this._keepaliveTimeoutID) {
+ clearTimeout(this._keepaliveTimeoutID);
+ }
+};
+
+// No data has been received within config.keepaliveTimeout ms.
+WebSocketConnection.prototype.handleKeepaliveTimer = function() {
+ this._debug('handleKeepaliveTimer');
+ this._keepaliveTimeoutID = null;
+ this.ping();
+
+ // If we are configured to drop connections if the client doesn't respond
+ // then set the grace period timer.
+ if (this.config.dropConnectionOnKeepaliveTimeout) {
+ this.setGracePeriodTimer();
+ }
+ else {
+ // Otherwise reset the keepalive timer to send the next ping.
+ this.setKeepaliveTimer();
+ }
+};
+
+WebSocketConnection.prototype.setGracePeriodTimer = function() {
+ this._debug('setGracePeriodTimer');
+ this.clearGracePeriodTimer();
+ this._gracePeriodTimeoutID = setTimeout(this._gracePeriodTimerHandler, this.config.keepaliveGracePeriod);
+};
+
+WebSocketConnection.prototype.clearGracePeriodTimer = function() {
+ if (this._gracePeriodTimeoutID) {
+ clearTimeout(this._gracePeriodTimeoutID);
+ }
+};
+
+WebSocketConnection.prototype.handleGracePeriodTimer = function() {
+ this._debug('handleGracePeriodTimer');
+ // If this is called, the client has not responded and is assumed dead.
+ this._gracePeriodTimeoutID = null;
+ this.drop(WebSocketConnection.CLOSE_REASON_ABNORMAL, 'Peer not responding.', true);
+};
+
+WebSocketConnection.prototype.handleSocketData = function(data) {
+ this._debug('handleSocketData');
+ // Reset the keepalive timer when receiving data of any kind.
+ this.setKeepaliveTimer();
+
+ // Add received data to our bufferList, which efficiently holds received
+ // data chunks in a linked list of Buffer objects.
+ this.bufferList.write(data);
+
+ this.processReceivedData();
+};
+
+WebSocketConnection.prototype.processReceivedData = function() {
+ this._debug('processReceivedData');
+ // If we're not connected, we should ignore any data remaining on the buffer.
+ if (!this.connected) { return; }
+
+ // Receiving/parsing is expected to be halted when paused.
+ if (this.inputPaused) { return; }
+
+ var frame = this.currentFrame;
+
+ // WebSocketFrame.prototype.addData returns true if all data necessary to
+ // parse the frame was available. It returns false if we are waiting for
+ // more data to come in on the wire.
+ if (!frame.addData(this.bufferList)) { this._debug('-- insufficient data for frame'); return; }
+
+ var self = this;
+
+ // Handle possible parsing errors
+ if (frame.protocolError) {
+ // Something bad happened.. get rid of this client.
+ this._debug('-- protocol error');
+ process.nextTick(function() {
+ self.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, frame.dropReason);
+ });
+ return;
+ }
+ else if (frame.frameTooLarge) {
+ this._debug('-- frame too large');
+ process.nextTick(function() {
+ self.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG, frame.dropReason);
+ });
+ return;
+ }
+
+ // For now since we don't support extensions, all RSV bits are illegal
+ if (frame.rsv1 || frame.rsv2 || frame.rsv3) {
+ this._debug('-- illegal rsv flag');
+ process.nextTick(function() {
+ self.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
+ 'Unsupported usage of rsv bits without negotiated extension.');
+ });
+ return;
+ }
+
+ if (!this.assembleFragments) {
+ this._debug('-- emitting frame');
+ process.nextTick(function() { self.emit('frame', frame); });
+ }
+
+ process.nextTick(function() { self.processFrame(frame); });
+
+ this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
+
+ // If there's data remaining, schedule additional processing, but yield
+ // for now so that other connections have a chance to have their data
+ // processed. We use setImmediate here instead of process.nextTick to
+ // explicitly indicate that we wish for other I/O to be handled first.
+ if (this.bufferList.length > 0) {
+ setImmediateImpl(this.receivedDataHandler);
+ }
+};
+
+WebSocketConnection.prototype.handleSocketError = function(error) {
+ this._debug('handleSocketError: %j', error);
+ this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL;
+ this.closeDescription = 'Socket Error: ' + error.syscall + ' ' + error.code;
+ this.connected = false;
+ this.state = STATE_CLOSED;
+ this.fragmentationSize = 0;
+ if (utils.eventEmitterListenerCount(this, 'error') > 0) {
+ this.emit('error', error);
+ }
+ this.socket.destroy(error);
+ this._debug.printOutput();
+};
+
+WebSocketConnection.prototype.handleSocketEnd = function() {
+ this._debug('handleSocketEnd: received socket end. state = %s', this.state);
+ this.receivedEnd = true;
+ if (this.state === STATE_CLOSED) {
+ // When using the TLS module, sometimes the socket will emit 'end'
+ // after it emits 'close'. I don't think that's correct behavior,
+ // but we should deal with it gracefully by ignoring it.
+ this._debug(' --- Socket \'end\' after \'close\'');
+ return;
+ }
+ if (this.state !== STATE_PEER_REQUESTED_CLOSE &&
+ this.state !== STATE_ENDING) {
+ this._debug(' --- UNEXPECTED socket end.');
+ this.socket.end();
+ }
+};
+
+WebSocketConnection.prototype.handleSocketClose = function(hadError) {
+ this._debug('handleSocketClose: received socket close');
+ this.socketHadError = hadError;
+ this.connected = false;
+ this.state = STATE_CLOSED;
+ // If closeReasonCode is still set to -1 at this point then we must
+ // not have received a close frame!!
+ if (this.closeReasonCode === -1) {
+ this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL;
+ this.closeDescription = 'Connection dropped by remote peer.';
+ }
+ this.clearCloseTimer();
+ this.clearKeepaliveTimer();
+ this.clearGracePeriodTimer();
+ if (!this.closeEventEmitted) {
+ this.closeEventEmitted = true;
+ this._debug('-- Emitting WebSocketConnection close event');
+ this.emit('close', this.closeReasonCode, this.closeDescription);
+ }
+};
+
+WebSocketConnection.prototype.handleSocketDrain = function() {
+ this._debug('handleSocketDrain: socket drain event');
+ this.outputBufferFull = false;
+ this.emit('drain');
+};
+
+WebSocketConnection.prototype.handleSocketPause = function() {
+ this._debug('handleSocketPause: socket pause event');
+ this.inputPaused = true;
+ this.emit('pause');
+};
+
+WebSocketConnection.prototype.handleSocketResume = function() {
+ this._debug('handleSocketResume: socket resume event');
+ this.inputPaused = false;
+ this.emit('resume');
+ this.processReceivedData();
+};
+
+WebSocketConnection.prototype.pause = function() {
+ this._debug('pause: pause requested');
+ this.socket.pause();
+};
+
+WebSocketConnection.prototype.resume = function() {
+ this._debug('resume: resume requested');
+ this.socket.resume();
+};
+
+WebSocketConnection.prototype.close = function(reasonCode, description) {
+ if (this.connected) {
+ this._debug('close: Initating clean WebSocket close sequence.');
+ if ('number' !== typeof reasonCode) {
+ reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
+ }
+ if (!validateCloseReason(reasonCode)) {
+ throw new Error('Close code ' + reasonCode + ' is not valid.');
+ }
+ if ('string' !== typeof description) {
+ description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode];
+ }
+ this.closeReasonCode = reasonCode;
+ this.closeDescription = description;
+ this.setCloseTimer();
+ this.sendCloseFrame(this.closeReasonCode, this.closeDescription);
+ this.state = STATE_ENDING;
+ this.connected = false;
+ }
+};
+
+WebSocketConnection.prototype.drop = function(reasonCode, description, skipCloseFrame) {
+ this._debug('drop');
+ if (typeof(reasonCode) !== 'number') {
+ reasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
+ }
+
+ if (typeof(description) !== 'string') {
+ // If no description is provided, try to look one up based on the
+ // specified reasonCode.
+ description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode];
+ }
+
+ this._debug('Forcefully dropping connection. skipCloseFrame: %s, code: %d, description: %s',
+ skipCloseFrame, reasonCode, description
+ );
+
+ this.closeReasonCode = reasonCode;
+ this.closeDescription = description;
+ this.frameQueue = [];
+ this.fragmentationSize = 0;
+ if (!skipCloseFrame) {
+ this.sendCloseFrame(reasonCode, description);
+ }
+ this.connected = false;
+ this.state = STATE_CLOSED;
+ this.clearCloseTimer();
+ this.clearKeepaliveTimer();
+ this.clearGracePeriodTimer();
+
+ if (!this.closeEventEmitted) {
+ this.closeEventEmitted = true;
+ this._debug('Emitting WebSocketConnection close event');
+ this.emit('close', this.closeReasonCode, this.closeDescription);
+ }
+
+ this._debug('Drop: destroying socket');
+ this.socket.destroy();
+};
+
+WebSocketConnection.prototype.setCloseTimer = function() {
+ this._debug('setCloseTimer');
+ this.clearCloseTimer();
+ this._debug('Setting close timer');
+ this.waitingForCloseResponse = true;
+ this.closeTimer = setTimeout(this._closeTimerHandler, this.closeTimeout);
+};
+
+WebSocketConnection.prototype.clearCloseTimer = function() {
+ this._debug('clearCloseTimer');
+ if (this.closeTimer) {
+ this._debug('Clearing close timer');
+ clearTimeout(this.closeTimer);
+ this.waitingForCloseResponse = false;
+ this.closeTimer = null;
+ }
+};
+
+WebSocketConnection.prototype.handleCloseTimer = function() {
+ this._debug('handleCloseTimer');
+ this.closeTimer = null;
+ if (this.waitingForCloseResponse) {
+ this._debug('Close response not received from client. Forcing socket end.');
+ this.waitingForCloseResponse = false;
+ this.state = STATE_CLOSED;
+ this.socket.end();
+ }
+};
+
+WebSocketConnection.prototype.processFrame = function(frame) {
+ this._debug('processFrame');
+ this._debug(' -- frame: %s', frame);
+
+ // Any non-control opcode besides 0x00 (continuation) received in the
+ // middle of a fragmented message is illegal.
+ if (this.frameQueue.length !== 0 && (frame.opcode > 0x00 && frame.opcode < 0x08)) {
+ this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
+ 'Illegal frame opcode 0x' + frame.opcode.toString(16) + ' ' +
+ 'received in middle of fragmented message.');
+ return;
+ }
+
+ switch(frame.opcode) {
+ case 0x02: // WebSocketFrame.BINARY_FRAME
+ this._debug('-- Binary Frame');
+ if (this.assembleFragments) {
+ if (frame.fin) {
+ // Complete single-frame message received
+ this._debug('---- Emitting \'message\' event');
+ this.emit('message', {
+ type: 'binary',
+ binaryData: frame.binaryPayload
+ });
+ }
+ else {
+ // beginning of a fragmented message
+ this.frameQueue.push(frame);
+ this.fragmentationSize = frame.length;
+ }
+ }
+ break;
+ case 0x01: // WebSocketFrame.TEXT_FRAME
+ this._debug('-- Text Frame');
+ if (this.assembleFragments) {
+ if (frame.fin) {
+ if (!Validation.isValidUTF8(frame.binaryPayload)) {
+ this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
+ 'Invalid UTF-8 Data Received');
+ return;
+ }
+ // Complete single-frame message received
+ this._debug('---- Emitting \'message\' event');
+ this.emit('message', {
+ type: 'utf8',
+ utf8Data: frame.binaryPayload.toString('utf8')
+ });
+ }
+ else {
+ // beginning of a fragmented message
+ this.frameQueue.push(frame);
+ this.fragmentationSize = frame.length;
+ }
+ }
+ break;
+ case 0x00: // WebSocketFrame.CONTINUATION
+ this._debug('-- Continuation Frame');
+ if (this.assembleFragments) {
+ if (this.frameQueue.length === 0) {
+ this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
+ 'Unexpected Continuation Frame');
+ return;
+ }
+
+ this.fragmentationSize += frame.length;
+
+ if (this.fragmentationSize > this.maxReceivedMessageSize) {
+ this.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG,
+ 'Maximum message size exceeded.');
+ return;
+ }
+
+ this.frameQueue.push(frame);
+
+ if (frame.fin) {
+ // end of fragmented message, so we process the whole
+ // message now. We also have to decode the utf-8 data
+ // for text frames after combining all the fragments.
+ var bytesCopied = 0;
+ var binaryPayload = new Buffer(this.fragmentationSize);
+ var opcode = this.frameQueue[0].opcode;
+ this.frameQueue.forEach(function (currentFrame) {
+ currentFrame.binaryPayload.copy(binaryPayload, bytesCopied);
+ bytesCopied += currentFrame.binaryPayload.length;
+ });
+ this.frameQueue = [];
+ this.fragmentationSize = 0;
+
+ switch (opcode) {
+ case 0x02: // WebSocketOpcode.BINARY_FRAME
+ this.emit('message', {
+ type: 'binary',
+ binaryData: binaryPayload
+ });
+ break;
+ case 0x01: // WebSocketOpcode.TEXT_FRAME
+ if (!Validation.isValidUTF8(binaryPayload)) {
+ this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
+ 'Invalid UTF-8 Data Received');
+ return;
+ }
+ this.emit('message', {
+ type: 'utf8',
+ utf8Data: binaryPayload.toString('utf8')
+ });
+ break;
+ default:
+ this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
+ 'Unexpected first opcode in fragmentation sequence: 0x' + opcode.toString(16));
+ return;
+ }
+ }
+ }
+ break;
+ case 0x09: // WebSocketFrame.PING
+ this._debug('-- Ping Frame');
+
+ if (this._pingListenerCount > 0) {
+ // logic to emit the ping frame: this is only done when a listener is known to exist
+ // Expose a function allowing the user to override the default ping() behavior
+ var cancelled = false;
+ var cancel = function() {
+ cancelled = true;
+ };
+ this.emit('ping', cancel, frame.binaryPayload);
+
+ // Only send a pong if the client did not indicate that he would like to cancel
+ if (!cancelled) {
+ this.pong(frame.binaryPayload);
+ }
+ }
+ else {
+ this.pong(frame.binaryPayload);
+ }
+
+ break;
+ case 0x0A: // WebSocketFrame.PONG
+ this._debug('-- Pong Frame');
+ this.emit('pong', frame.binaryPayload);
+ break;
+ case 0x08: // WebSocketFrame.CONNECTION_CLOSE
+ this._debug('-- Close Frame');
+ if (this.waitingForCloseResponse) {
+ // Got response to our request to close the connection.
+ // Close is complete, so we just hang up.
+ this._debug('---- Got close response from peer. Completing closing handshake.');
+ this.clearCloseTimer();
+ this.waitingForCloseResponse = false;
+ this.state = STATE_CLOSED;
+ this.socket.end();
+ return;
+ }
+
+ this._debug('---- Closing handshake initiated by peer.');
+ // Got request from other party to close connection.
+ // Send back acknowledgement and then hang up.
+ this.state = STATE_PEER_REQUESTED_CLOSE;
+ var respondCloseReasonCode;
+
+ // Make sure the close reason provided is legal according to
+ // the protocol spec. Providing no close status is legal.
+ // WebSocketFrame sets closeStatus to -1 by default, so if it
+ // is still -1, then no status was provided.
+ if (frame.invalidCloseFrameLength) {
+ this.closeReasonCode = 1005; // 1005 = No reason provided.
+ respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
+ }
+ else if (frame.closeStatus === -1 || validateCloseReason(frame.closeStatus)) {
+ this.closeReasonCode = frame.closeStatus;
+ respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
+ }
+ else {
+ this.closeReasonCode = frame.closeStatus;
+ respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
+ }
+
+ // If there is a textual description in the close frame, extract it.
+ if (frame.binaryPayload.length > 1) {
+ if (!Validation.isValidUTF8(frame.binaryPayload)) {
+ this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
+ 'Invalid UTF-8 Data Received');
+ return;
+ }
+ this.closeDescription = frame.binaryPayload.toString('utf8');
+ }
+ else {
+ this.closeDescription = WebSocketConnection.CLOSE_DESCRIPTIONS[this.closeReasonCode];
+ }
+ this._debug(
+ '------ Remote peer %s - code: %d - %s - close frame payload length: %d',
+ this.remoteAddress, this.closeReasonCode,
+ this.closeDescription, frame.length
+ );
+ this._debug('------ responding to remote peer\'s close request.');
+ this.sendCloseFrame(respondCloseReasonCode, null);
+ this.connected = false;
+ break;
+ default:
+ this._debug('-- Unrecognized Opcode %d', frame.opcode);
+ this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
+ 'Unrecognized Opcode: 0x' + frame.opcode.toString(16));
+ break;
+ }
+};
+
+WebSocketConnection.prototype.send = function(data, cb) {
+ this._debug('send');
+ if (Buffer.isBuffer(data)) {
+ this.sendBytes(data, cb);
+ }
+ else if (typeof(data['toString']) === 'function') {
+ this.sendUTF(data, cb);
+ }
+ else {
+ throw new Error('Data provided must either be a Node Buffer or implement toString()');
+ }
+};
+
+WebSocketConnection.prototype.sendUTF = function(data, cb) {
+ data = new Buffer(data.toString(), 'utf8');
+ this._debug('sendUTF: %d bytes', data.length);
+ var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
+ frame.opcode = 0x01; // WebSocketOpcode.TEXT_FRAME
+ frame.binaryPayload = data;
+ this.fragmentAndSend(frame, cb);
+};
+
+WebSocketConnection.prototype.sendBytes = function(data, cb) {
+ this._debug('sendBytes');
+ if (!Buffer.isBuffer(data)) {
+ throw new Error('You must pass a Node Buffer object to WebSocketConnection.prototype.sendBytes()');
+ }
+ var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
+ frame.opcode = 0x02; // WebSocketOpcode.BINARY_FRAME
+ frame.binaryPayload = data;
+ this.fragmentAndSend(frame, cb);
+};
+
+WebSocketConnection.prototype.ping = function(data) {
+ this._debug('ping');
+ var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
+ frame.opcode = 0x09; // WebSocketOpcode.PING
+ frame.fin = true;
+ if (data) {
+ if (!Buffer.isBuffer(data)) {
+ data = new Buffer(data.toString(), 'utf8');
+ }
+ if (data.length > 125) {
+ this._debug('WebSocket: Data for ping is longer than 125 bytes. Truncating.');
+ data = data.slice(0,124);
+ }
+ frame.binaryPayload = data;
+ }
+ this.sendFrame(frame);
+};
+
+// Pong frames have to echo back the contents of the data portion of the
+// ping frame exactly, byte for byte.
+WebSocketConnection.prototype.pong = function(binaryPayload) {
+ this._debug('pong');
+ var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
+ frame.opcode = 0x0A; // WebSocketOpcode.PONG
+ if (Buffer.isBuffer(binaryPayload) && binaryPayload.length > 125) {
+ this._debug('WebSocket: Data for pong is longer than 125 bytes. Truncating.');
+ binaryPayload = binaryPayload.slice(0,124);
+ }
+ frame.binaryPayload = binaryPayload;
+ frame.fin = true;
+ this.sendFrame(frame);
+};
+
+WebSocketConnection.prototype.fragmentAndSend = function(frame, cb) {
+ this._debug('fragmentAndSend');
+ if (frame.opcode > 0x07) {
+ throw new Error('You cannot fragment control frames.');
+ }
+
+ var threshold = this.config.fragmentationThreshold;
+ var length = frame.binaryPayload.length;
+
+ // Send immediately if fragmentation is disabled or the message is not
+ // larger than the fragmentation threshold.
+ if (!this.config.fragmentOutgoingMessages || (frame.binaryPayload && length <= threshold)) {
+ frame.fin = true;
+ this.sendFrame(frame, cb);
+ return;
+ }
+
+ var numFragments = Math.ceil(length / threshold);
+ var sentFragments = 0;
+ var sentCallback = function fragmentSentCallback(err) {
+ if (err) {
+ if (typeof cb === 'function') {
+ // pass only the first error
+ cb(err);
+ cb = null;
+ }
+ return;
+ }
+ ++sentFragments;
+ if ((sentFragments === numFragments) && (typeof cb === 'function')) {
+ cb();
+ }
+ };
+ for (var i=1; i <= numFragments; i++) {
+ var currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
+
+ // continuation opcode except for first frame.
+ currentFrame.opcode = (i === 1) ? frame.opcode : 0x00;
+
+ // fin set on last frame only
+ currentFrame.fin = (i === numFragments);
+
+ // length is likely to be shorter on the last fragment
+ var currentLength = (i === numFragments) ? length - (threshold * (i-1)) : threshold;
+ var sliceStart = threshold * (i-1);
+
+ // Slice the right portion of the original payload
+ currentFrame.binaryPayload = frame.binaryPayload.slice(sliceStart, sliceStart + currentLength);
+
+ this.sendFrame(currentFrame, sentCallback);
+ }
+};
+
+WebSocketConnection.prototype.sendCloseFrame = function(reasonCode, description, cb) {
+ if (typeof(reasonCode) !== 'number') {
+ reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
+ }
+
+ this._debug('sendCloseFrame state: %s, reasonCode: %d, description: %s', this.state, reasonCode, description);
+
+ if (this.state !== STATE_OPEN && this.state !== STATE_PEER_REQUESTED_CLOSE) { return; }
+
+ var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
+ frame.fin = true;
+ frame.opcode = 0x08; // WebSocketOpcode.CONNECTION_CLOSE
+ frame.closeStatus = reasonCode;
+ if (typeof(description) === 'string') {
+ frame.binaryPayload = new Buffer(description, 'utf8');
+ }
+
+ this.sendFrame(frame, cb);
+ this.socket.end();
+};
+
+WebSocketConnection.prototype.sendFrame = function(frame, cb) {
+ this._debug('sendFrame');
+ frame.mask = this.maskOutgoingPackets;
+ var flushed = this.socket.write(frame.toBuffer(), cb);
+ this.outputBufferFull = !flushed;
+ return flushed;
+};
+
+module.exports = WebSocketConnection;
+
+
+
+function instrumentSocketForDebugging(connection, socket) {
+ /* jshint loopfunc: true */
+ if (!connection._debug.enabled) { return; }
+
+ var originalSocketEmit = socket.emit;
+ socket.emit = function(event) {
+ connection._debug('||| Socket Event \'%s\'', event);
+ originalSocketEmit.apply(this, arguments);
+ };
+
+ for (var key in socket) {
+ if ('function' !== typeof(socket[key])) { continue; }
+ if (['emit'].indexOf(key) !== -1) { continue; }
+ (function(key) {
+ var original = socket[key];
+ if (key === 'on') {
+ socket[key] = function proxyMethod__EventEmitter__On() {
+ connection._debug('||| Socket method called: %s (%s)', key, arguments[0]);
+ return original.apply(this, arguments);
+ };
+ return;
+ }
+ socket[key] = function proxyMethod() {
+ connection._debug('||| Socket method called: %s', key);
+ return original.apply(this, arguments);
+ };
+ })(key);
+ }
+}
diff --git a/lib/WebSocketFrame.js b/lib/WebSocketFrame.js
new file mode 100644
index 0000000..859e879
--- /dev/null
+++ b/lib/WebSocketFrame.js
@@ -0,0 +1,279 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var bufferUtil = require('./BufferUtil').BufferUtil;
+
+const DECODE_HEADER = 1;
+const WAITING_FOR_16_BIT_LENGTH = 2;
+const WAITING_FOR_64_BIT_LENGTH = 3;
+const WAITING_FOR_MASK_KEY = 4;
+const WAITING_FOR_PAYLOAD = 5;
+const COMPLETE = 6;
+
+// WebSocketConnection will pass shared buffer objects for maskBytes and
+// frameHeader into the constructor to avoid tons of small memory allocations
+// for each frame we have to parse. This is only used for parsing frames
+// we receive off the wire.
+function WebSocketFrame(maskBytes, frameHeader, config) {
+ this.maskBytes = maskBytes;
+ this.frameHeader = frameHeader;
+ this.config = config;
+ this.maxReceivedFrameSize = config.maxReceivedFrameSize;
+ this.protocolError = false;
+ this.frameTooLarge = false;
+ this.invalidCloseFrameLength = false;
+ this.parseState = DECODE_HEADER;
+ this.closeStatus = -1;
+}
+
+WebSocketFrame.prototype.addData = function(bufferList) {
+ if (this.parseState === DECODE_HEADER) {
+ if (bufferList.length >= 2) {
+ bufferList.joinInto(this.frameHeader, 0, 0, 2);
+ bufferList.advance(2);
+ var firstByte = this.frameHeader[0];
+ var secondByte = this.frameHeader[1];
+
+ this.fin = Boolean(firstByte & 0x80);
+ this.rsv1 = Boolean(firstByte & 0x40);
+ this.rsv2 = Boolean(firstByte & 0x20);
+ this.rsv3 = Boolean(firstByte & 0x10);
+ this.mask = Boolean(secondByte & 0x80);
+
+ this.opcode = firstByte & 0x0F;
+ this.length = secondByte & 0x7F;
+
+ // Control frame sanity check
+ if (this.opcode >= 0x08) {
+ if (this.length > 125) {
+ this.protocolError = true;
+ this.dropReason = 'Illegal control frame longer than 125 bytes.';
+ return true;
+ }
+ if (!this.fin) {
+ this.protocolError = true;
+ this.dropReason = 'Control frames must not be fragmented.';
+ return true;
+ }
+ }
+
+ if (this.length === 126) {
+ this.parseState = WAITING_FOR_16_BIT_LENGTH;
+ }
+ else if (this.length === 127) {
+ this.parseState = WAITING_FOR_64_BIT_LENGTH;
+ }
+ else {
+ this.parseState = WAITING_FOR_MASK_KEY;
+ }
+ }
+ }
+ if (this.parseState === WAITING_FOR_16_BIT_LENGTH) {
+ if (bufferList.length >= 2) {
+ bufferList.joinInto(this.frameHeader, 2, 0, 2);
+ bufferList.advance(2);
+ this.length = this.frameHeader.readUInt16BE(2, true);
+ this.parseState = WAITING_FOR_MASK_KEY;
+ }
+ }
+ else if (this.parseState === WAITING_FOR_64_BIT_LENGTH) {
+ if (bufferList.length >= 8) {
+ bufferList.joinInto(this.frameHeader, 2, 0, 8);
+ bufferList.advance(8);
+ var lengthPair = [
+ this.frameHeader.readUInt32BE(2, true),
+ this.frameHeader.readUInt32BE(2+4, true)
+ ];
+
+ if (lengthPair[0] !== 0) {
+ this.protocolError = true;
+ this.dropReason = 'Unsupported 64-bit length frame received';
+ return true;
+ }
+ this.length = lengthPair[1];
+ this.parseState = WAITING_FOR_MASK_KEY;
+ }
+ }
+
+ if (this.parseState === WAITING_FOR_MASK_KEY) {
+ if (this.mask) {
+ if (bufferList.length >= 4) {
+ bufferList.joinInto(this.maskBytes, 0, 0, 4);
+ bufferList.advance(4);
+ this.parseState = WAITING_FOR_PAYLOAD;
+ }
+ }
+ else {
+ this.parseState = WAITING_FOR_PAYLOAD;
+ }
+ }
+
+ if (this.parseState === WAITING_FOR_PAYLOAD) {
+ if (this.length > this.maxReceivedFrameSize) {
+ this.frameTooLarge = true;
+ this.dropReason = 'Frame size of ' + this.length.toString(10) +
+ ' bytes exceeds maximum accepted frame size';
+ return true;
+ }
+
+ if (this.length === 0) {
+ this.binaryPayload = new Buffer(0);
+ this.parseState = COMPLETE;
+ return true;
+ }
+ if (bufferList.length >= this.length) {
+ this.binaryPayload = bufferList.take(this.length);
+ bufferList.advance(this.length);
+ if (this.mask) {
+ bufferUtil.unmask(this.binaryPayload, this.maskBytes);
+ // xor(this.binaryPayload, this.maskBytes, 0);
+ }
+
+ if (this.opcode === 0x08) { // WebSocketOpcode.CONNECTION_CLOSE
+ if (this.length === 1) {
+ // Invalid length for a close frame. Must be zero or at least two.
+ this.binaryPayload = new Buffer(0);
+ this.invalidCloseFrameLength = true;
+ }
+ if (this.length >= 2) {
+ this.closeStatus = this.binaryPayload.readUInt16BE(0, true);
+ this.binaryPayload = this.binaryPayload.slice(2);
+ }
+ }
+
+ this.parseState = COMPLETE;
+ return true;
+ }
+ }
+ return false;
+};
+
+WebSocketFrame.prototype.throwAwayPayload = function(bufferList) {
+ if (bufferList.length >= this.length) {
+ bufferList.advance(this.length);
+ this.parseState = COMPLETE;
+ return true;
+ }
+ return false;
+};
+
+WebSocketFrame.prototype.toBuffer = function(nullMask) {
+ var maskKey;
+ var headerLength = 2;
+ var data;
+ var outputPos;
+ var firstByte = 0x00;
+ var secondByte = 0x00;
+
+ if (this.fin) {
+ firstByte |= 0x80;
+ }
+ if (this.rsv1) {
+ firstByte |= 0x40;
+ }
+ if (this.rsv2) {
+ firstByte |= 0x20;
+ }
+ if (this.rsv3) {
+ firstByte |= 0x10;
+ }
+ if (this.mask) {
+ secondByte |= 0x80;
+ }
+
+ firstByte |= (this.opcode & 0x0F);
+
+ // the close frame is a special case because the close reason is
+ // prepended to the payload data.
+ if (this.opcode === 0x08) {
+ this.length = 2;
+ if (this.binaryPayload) {
+ this.length += this.binaryPayload.length;
+ }
+ data = new Buffer(this.length);
+ data.writeUInt16BE(this.closeStatus, 0, true);
+ if (this.length > 2) {
+ this.binaryPayload.copy(data, 2);
+ }
+ }
+ else if (this.binaryPayload) {
+ data = this.binaryPayload;
+ this.length = data.length;
+ }
+ else {
+ this.length = 0;
+ }
+
+ if (this.length <= 125) {
+ // encode the length directly into the two-byte frame header
+ secondByte |= (this.length & 0x7F);
+ }
+ else if (this.length > 125 && this.length <= 0xFFFF) {
+ // Use 16-bit length
+ secondByte |= 126;
+ headerLength += 2;
+ }
+ else if (this.length > 0xFFFF) {
+ // Use 64-bit length
+ secondByte |= 127;
+ headerLength += 8;
+ }
+
+ var output = new Buffer(this.length + headerLength + (this.mask ? 4 : 0));
+
+ // write the frame header
+ output[0] = firstByte;
+ output[1] = secondByte;
+
+ outputPos = 2;
+
+ if (this.length > 125 && this.length <= 0xFFFF) {
+ // write 16-bit length
+ output.writeUInt16BE(this.length, outputPos, true);
+ outputPos += 2;
+ }
+ else if (this.length > 0xFFFF) {
+ // write 64-bit length
+ output.writeUInt32BE(0x00000000, outputPos, true);
+ output.writeUInt32BE(this.length, outputPos + 4, true);
+ outputPos += 8;
+ }
+
+ if (this.mask) {
+ maskKey = nullMask ? 0 : (Math.random()*0xFFFFFFFF) | 0;
+ this.maskBytes.writeUInt32BE(maskKey, 0, true);
+
+ // write the mask key
+ this.maskBytes.copy(output, outputPos);
+ outputPos += 4;
+
+ if (data) {
+ bufferUtil.mask(data, this.maskBytes, output, outputPos, this.length);
+ }
+ }
+ else if (data) {
+ data.copy(output, outputPos);
+ }
+
+ return output;
+};
+
+WebSocketFrame.prototype.toString = function() {
+ return 'Opcode: ' + this.opcode + ', fin: ' + this.fin + ', length: ' + this.length + ', hasPayload: ' + Boolean(this.binaryPayload) + ', masked: ' + this.mask;
+};
+
+
+module.exports = WebSocketFrame;
diff --git a/lib/WebSocketRequest.js b/lib/WebSocketRequest.js
new file mode 100644
index 0000000..f4d9655
--- /dev/null
+++ b/lib/WebSocketRequest.js
@@ -0,0 +1,524 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var crypto = require('crypto');
+var util = require('util');
+var url = require('url');
+var EventEmitter = require('events').EventEmitter;
+var WebSocketConnection = require('./WebSocketConnection');
+
+var headerValueSplitRegExp = /,\s*/;
+var headerParamSplitRegExp = /;\s*/;
+var headerSanitizeRegExp = /[\r\n]/g;
+var xForwardedForSeparatorRegExp = /,\s*/;
+var separators = [
+ '(', ')', '<', '>', '@',
+ ',', ';', ':', '\\', '\"',
+ '/', '[', ']', '?', '=',
+ '{', '}', ' ', String.fromCharCode(9)
+];
+var controlChars = [String.fromCharCode(127) /* DEL */];
+for (var i=0; i < 31; i ++) {
+ /* US-ASCII Control Characters */
+ controlChars.push(String.fromCharCode(i));
+}
+
+var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
+var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;
+var cookieValueDQuoteValidateRegEx = /^"[^"]*"$/;
+var controlCharsAndSemicolonRegEx = /[\x00-\x20\x3b]/g;
+
+var cookieSeparatorRegEx = /[;,] */;
+
+var httpStatusDescriptions = {
+ 100: 'Continue',
+ 101: 'Switching Protocols',
+ 200: 'OK',
+ 201: 'Created',
+ 203: 'Non-Authoritative Information',
+ 204: 'No Content',
+ 205: 'Reset Content',
+ 206: 'Partial Content',
+ 300: 'Multiple Choices',
+ 301: 'Moved Permanently',
+ 302: 'Found',
+ 303: 'See Other',
+ 304: 'Not Modified',
+ 305: 'Use Proxy',
+ 307: 'Temporary Redirect',
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 402: 'Payment Required',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 406: 'Not Acceptable',
+ 407: 'Proxy Authorization Required',
+ 408: 'Request Timeout',
+ 409: 'Conflict',
+ 410: 'Gone',
+ 411: 'Length Required',
+ 412: 'Precondition Failed',
+ 413: 'Request Entity Too Long',
+ 414: 'Request-URI Too Long',
+ 415: 'Unsupported Media Type',
+ 416: 'Requested Range Not Satisfiable',
+ 417: 'Expectation Failed',
+ 426: 'Upgrade Required',
+ 500: 'Internal Server Error',
+ 501: 'Not Implemented',
+ 502: 'Bad Gateway',
+ 503: 'Service Unavailable',
+ 504: 'Gateway Timeout',
+ 505: 'HTTP Version Not Supported'
+};
+
+function WebSocketRequest(socket, httpRequest, serverConfig) {
+ // Superclass Constructor
+ EventEmitter.call(this);
+
+ this.socket = socket;
+ this.httpRequest = httpRequest;
+ this.resource = httpRequest.url;
+ this.remoteAddress = socket.remoteAddress;
+ this.remoteAddresses = [this.remoteAddress];
+ this.serverConfig = serverConfig;
+
+ // Watch for the underlying TCP socket closing before we call accept
+ this._socketIsClosing = false;
+ this._socketCloseHandler = this._handleSocketCloseBeforeAccept.bind(this);
+ this.socket.on('end', this._socketCloseHandler);
+ this.socket.on('close', this._socketCloseHandler);
+
+ this._resolved = false;
+}
+
+util.inherits(WebSocketRequest, EventEmitter);
+
+WebSocketRequest.prototype.readHandshake = function() {
+ var self = this;
+ var request = this.httpRequest;
+
+ // Decode URL
+ this.resourceURL = url.parse(this.resource, true);
+
+ this.host = request.headers['host'];
+ if (!this.host) {
+ throw new Error('Client must provide a Host header.');
+ }
+
+ this.key = request.headers['sec-websocket-key'];
+ if (!this.key) {
+ throw new Error('Client must provide a value for Sec-WebSocket-Key.');
+ }
+
+ this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10);
+
+ if (!this.webSocketVersion || isNaN(this.webSocketVersion)) {
+ throw new Error('Client must provide a value for Sec-WebSocket-Version.');
+ }
+
+ switch (this.webSocketVersion) {
+ case 8:
+ case 13:
+ break;
+ default:
+ var e = new Error('Unsupported websocket client version: ' + this.webSocketVersion +
+ 'Only versions 8 and 13 are supported.');
+ e.httpCode = 426;
+ e.headers = {
+ 'Sec-WebSocket-Version': '13'
+ };
+ throw e;
+ }
+
+ if (this.webSocketVersion === 13) {
+ this.origin = request.headers['origin'];
+ }
+ else if (this.webSocketVersion === 8) {
+ this.origin = request.headers['sec-websocket-origin'];
+ }
+
+ // Protocol is optional.
+ var protocolString = request.headers['sec-websocket-protocol'];
+ this.protocolFullCaseMap = {};
+ this.requestedProtocols = [];
+ if (protocolString) {
+ var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp);
+ requestedProtocolsFullCase.forEach(function(protocol) {
+ var lcProtocol = protocol.toLocaleLowerCase();
+ self.requestedProtocols.push(lcProtocol);
+ self.protocolFullCaseMap[lcProtocol] = protocol;
+ });
+ }
+
+ if (!this.serverConfig.ignoreXForwardedFor &&
+ request.headers['x-forwarded-for']) {
+ var immediatePeerIP = this.remoteAddress;
+ this.remoteAddresses = request.headers['x-forwarded-for']
+ .split(xForwardedForSeparatorRegExp);
+ this.remoteAddresses.push(immediatePeerIP);
+ this.remoteAddress = this.remoteAddresses[0];
+ }
+
+ // Extensions are optional.
+ var extensionsString = request.headers['sec-websocket-extensions'];
+ this.requestedExtensions = this.parseExtensions(extensionsString);
+
+ // Cookies are optional
+ var cookieString = request.headers['cookie'];
+ this.cookies = this.parseCookies(cookieString);
+};
+
+WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
+ if (!extensionsString || extensionsString.length === 0) {
+ return [];
+ }
+ var extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
+ extensions.forEach(function(extension, index, array) {
+ var params = extension.split(headerParamSplitRegExp);
+ var extensionName = params[0];
+ var extensionParams = params.slice(1);
+ extensionParams.forEach(function(rawParam, index, array) {
+ var arr = rawParam.split('=');
+ var obj = {
+ name: arr[0],
+ value: arr[1]
+ };
+ array.splice(index, 1, obj);
+ });
+ var obj = {
+ name: extensionName,
+ params: extensionParams
+ };
+ array.splice(index, 1, obj);
+ });
+ return extensions;
+};
+
+// This function adapted from node-cookie
+// https://github.com/shtylman/node-cookie
+WebSocketRequest.prototype.parseCookies = function(str) {
+ // Sanity Check
+ if (!str || typeof(str) !== 'string') {
+ return [];
+ }
+
+ var cookies = [];
+ var pairs = str.split(cookieSeparatorRegEx);
+
+ pairs.forEach(function(pair) {
+ var eq_idx = pair.indexOf('=');
+ if (eq_idx === -1) {
+ cookies.push({
+ name: pair,
+ value: null
+ });
+ return;
+ }
+
+ var key = pair.substr(0, eq_idx).trim();
+ var val = pair.substr(++eq_idx, pair.length).trim();
+
+ // quoted values
+ if ('"' === val[0]) {
+ val = val.slice(1, -1);
+ }
+
+ cookies.push({
+ name: key,
+ value: decodeURIComponent(val)
+ });
+ });
+
+ return cookies;
+};
+
+WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
+ this._verifyResolution();
+
+ // TODO: Handle extensions
+
+ var protocolFullCase;
+
+ if (acceptedProtocol) {
+ protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()];
+ if (typeof(protocolFullCase) === 'undefined') {
+ protocolFullCase = acceptedProtocol;
+ }
+ }
+ else {
+ protocolFullCase = acceptedProtocol;
+ }
+ this.protocolFullCaseMap = null;
+
+ // Create key validation hash
+ var sha1 = crypto.createHash('sha1');
+ sha1.update(this.key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
+ var acceptKey = sha1.digest('base64');
+
+ var response = 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ 'Sec-WebSocket-Accept: ' + acceptKey + '\r\n';
+
+ if (protocolFullCase) {
+ // validate protocol
+ for (var i=0; i < protocolFullCase.length; i++) {
+ var charCode = protocolFullCase.charCodeAt(i);
+ var character = protocolFullCase.charAt(i);
+ if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
+ this.reject(500);
+ throw new Error('Illegal character "' + String.fromCharCode(character) + '" in subprotocol.');
+ }
+ }
+ if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
+ this.reject(500);
+ throw new Error('Specified protocol was not requested by the client.');
+ }
+
+ protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, '');
+ response += 'Sec-WebSocket-Protocol: ' + protocolFullCase + '\r\n';
+ }
+ this.requestedProtocols = null;
+
+ if (allowedOrigin) {
+ allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
+ if (this.webSocketVersion === 13) {
+ response += 'Origin: ' + allowedOrigin + '\r\n';
+ }
+ else if (this.webSocketVersion === 8) {
+ response += 'Sec-WebSocket-Origin: ' + allowedOrigin + '\r\n';
+ }
+ }
+
+ if (cookies) {
+ if (!Array.isArray(cookies)) {
+ this.reject(500);
+ throw new Error('Value supplied for "cookies" argument must be an array.');
+ }
+ var seenCookies = {};
+ cookies.forEach(function(cookie) {
+ if (!cookie.name || !cookie.value) {
+ this.reject(500);
+ throw new Error('Each cookie to set must at least provide a "name" and "value"');
+ }
+
+ // Make sure there are no \r\n sequences inserted
+ cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, '');
+ cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, '');
+
+ if (seenCookies[cookie.name]) {
+ this.reject(500);
+ throw new Error('You may not specify the same cookie name twice.');
+ }
+ seenCookies[cookie.name] = true;
+
+ // token (RFC 2616, Section 2.2)
+ var invalidChar = cookie.name.match(cookieNameValidateRegEx);
+ if (invalidChar) {
+ this.reject(500);
+ throw new Error('Illegal character ' + invalidChar[0] + ' in cookie name');
+ }
+
+ // RFC 6265, Section 4.1.1
+ // *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+ if (cookie.value.match(cookieValueDQuoteValidateRegEx)) {
+ invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx);
+ } else {
+ invalidChar = cookie.value.match(cookieValueValidateRegEx);
+ }
+ if (invalidChar) {
+ this.reject(500);
+ throw new Error('Illegal character ' + invalidChar[0] + ' in cookie value');
+ }
+
+ var cookieParts = [cookie.name + '=' + cookie.value];
+
+ // RFC 6265, Section 4.1.1
+ // 'Path=' path-value | <any CHAR except CTLs or ';'>
+ if(cookie.path){
+ invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx);
+ if (invalidChar) {
+ this.reject(500);
+ throw new Error('Illegal character ' + invalidChar[0] + ' in cookie path');
+ }
+ cookieParts.push('Path=' + cookie.path);
+ }
+
+ // RFC 6265, Section 4.1.2.3
+ // 'Domain=' subdomain
+ if (cookie.domain) {
+ if (typeof(cookie.domain) !== 'string') {
+ this.reject(500);
+ throw new Error('Domain must be specified and must be a string.');
+ }
+ invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx);
+ if (invalidChar) {
+ this.reject(500);
+ throw new Error('Illegal character ' + invalidChar[0] + ' in cookie domain');
+ }
+ cookieParts.push('Domain=' + cookie.domain.toLowerCase());
+ }
+
+ // RFC 6265, Section 4.1.1
+ //'Expires=' sane-cookie-date | Force Date object requirement by using only epoch
+ if (cookie.expires) {
+ if (!(cookie.expires instanceof Date)){
+ this.reject(500);
+ throw new Error('Value supplied for cookie "expires" must be a vaild date object');
+ }
+ cookieParts.push('Expires=' + cookie.expires.toGMTString());
+ }
+
+ // RFC 6265, Section 4.1.1
+ //'Max-Age=' non-zero-digit *DIGIT
+ if (cookie.maxage) {
+ var maxage = cookie.maxage;
+ if (typeof(maxage) === 'string') {
+ maxage = parseInt(maxage, 10);
+ }
+ if (isNaN(maxage) || maxage <= 0 ) {
+ this.reject(500);
+ throw new Error('Value supplied for cookie "maxage" must be a non-zero number');
+ }
+ maxage = Math.round(maxage);
+ cookieParts.push('Max-Age=' + maxage.toString(10));
+ }
+
+ // RFC 6265, Section 4.1.1
+ //'Secure;'
+ if (cookie.secure) {
+ if (typeof(cookie.secure) !== 'boolean') {
+ this.reject(500);
+ throw new Error('Value supplied for cookie "secure" must be of type boolean');
+ }
+ cookieParts.push('Secure');
+ }
+
+ // RFC 6265, Section 4.1.1
+ //'HttpOnly;'
+ if (cookie.httponly) {
+ if (typeof(cookie.httponly) !== 'boolean') {
+ this.reject(500);
+ throw new Error('Value supplied for cookie "httponly" must be of type boolean');
+ }
+ cookieParts.push('HttpOnly');
+ }
+
+ response += ('Set-Cookie: ' + cookieParts.join(';') + '\r\n');
+ }.bind(this));
+ }
+
+ // TODO: handle negotiated extensions
+ // if (negotiatedExtensions) {
+ // response += 'Sec-WebSocket-Extensions: ' + negotiatedExtensions.join(', ') + '\r\n';
+ // }
+
+ // Mark the request resolved now so that the user can't call accept or
+ // reject a second time.
+ this._resolved = true;
+ this.emit('requestResolved', this);
+
+ response += '\r\n';
+
+ var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);
+ connection.webSocketVersion = this.webSocketVersion;
+ connection.remoteAddress = this.remoteAddress;
+ connection.remoteAddresses = this.remoteAddresses;
+
+ var self = this;
+
+ if (this._socketIsClosing) {
+ // Handle case when the client hangs up before we get a chance to
+ // accept the connection and send our side of the opening handshake.
+ cleanupFailedConnection(connection);
+ }
+ else {
+ this.socket.write(response, 'ascii', function(error) {
+ if (error) {
+ cleanupFailedConnection(connection);
+ return;
+ }
+
+ self._removeSocketCloseListeners();
+ connection._addSocketEventListeners();
+ });
+ }
+
+ this.emit('requestAccepted', connection);
+ return connection;
+};
+
+WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
+ this._verifyResolution();
+
+ // Mark the request resolved now so that the user can't call accept or
+ // reject a second time.
+ this._resolved = true;
+ this.emit('requestResolved', this);
+
+ if (typeof(status) !== 'number') {
+ status = 403;
+ }
+ var response = 'HTTP/1.1 ' + status + ' ' + httpStatusDescriptions[status] + '\r\n' +
+ 'Connection: close\r\n';
+ if (reason) {
+ reason = reason.replace(headerSanitizeRegExp, '');
+ response += 'X-WebSocket-Reject-Reason: ' + reason + '\r\n';
+ }
+
+ if (extraHeaders) {
+ for (var key in extraHeaders) {
+ var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
+ var sanitizedKey = key.replace(headerSanitizeRegExp, '');
+ response += (sanitizedKey + ': ' + sanitizedValue + '\r\n');
+ }
+ }
+
+ response += '\r\n';
+ this.socket.end(response, 'ascii');
+
+ this.emit('requestRejected', this);
+};
+
+WebSocketRequest.prototype._handleSocketCloseBeforeAccept = function() {
+ this._socketIsClosing = true;
+ this._removeSocketCloseListeners();
+};
+
+WebSocketRequest.prototype._removeSocketCloseListeners = function() {
+ this.socket.removeListener('end', this._socketCloseHandler);
+ this.socket.removeListener('close', this._socketCloseHandler);
+};
+
+WebSocketRequest.prototype._verifyResolution = function() {
+ if (this._resolved) {
+ throw new Error('WebSocketRequest may only be accepted or rejected one time.');
+ }
+};
+
+function cleanupFailedConnection(connection) {
+ // Since we have to return a connection object even if the socket is
+ // already dead in order not to break the API, we schedule a 'close'
+ // event on the connection object to occur immediately.
+ process.nextTick(function() {
+ // WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
+ // Third param: Skip sending the close frame to a dead socket
+ connection.drop(1006, 'TCP connection lost before handshake completed.', true);
+ });
+}
+
+module.exports = WebSocketRequest;
diff --git a/lib/WebSocketRouter.js b/lib/WebSocketRouter.js
new file mode 100644
index 0000000..35bced9
--- /dev/null
+++ b/lib/WebSocketRouter.js
@@ -0,0 +1,157 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var extend = require('./utils').extend;
+var util = require('util');
+var EventEmitter = require('events').EventEmitter;
+var WebSocketRouterRequest = require('./WebSocketRouterRequest');
+
+function WebSocketRouter(config) {
+ // Superclass Constructor
+ EventEmitter.call(this);
+
+ this.config = {
+ // The WebSocketServer instance to attach to.
+ server: null
+ };
+ if (config) {
+ extend(this.config, config);
+ }
+ this.handlers = [];
+
+ this._requestHandler = this.handleRequest.bind(this);
+ if (this.config.server) {
+ this.attachServer(this.config.server);
+ }
+}
+
+util.inherits(WebSocketRouter, EventEmitter);
+
+WebSocketRouter.prototype.attachServer = function(server) {
+ if (server) {
+ this.server = server;
+ this.server.on('request', this._requestHandler);
+ }
+ else {
+ throw new Error('You must specify a WebSocketServer instance to attach to.');
+ }
+};
+
+WebSocketRouter.prototype.detachServer = function() {
+ if (this.server) {
+ this.server.removeListener('request', this._requestHandler);
+ this.server = null;
+ }
+ else {
+ throw new Error('Cannot detach from server: not attached.');
+ }
+};
+
+WebSocketRouter.prototype.mount = function(path, protocol, callback) {
+ if (!path) {
+ throw new Error('You must specify a path for this handler.');
+ }
+ if (!protocol) {
+ protocol = '____no_protocol____';
+ }
+ if (!callback) {
+ throw new Error('You must specify a callback for this handler.');
+ }
+
+ path = this.pathToRegExp(path);
+ if (!(path instanceof RegExp)) {
+ throw new Error('Path must be specified as either a string or a RegExp.');
+ }
+ var pathString = path.toString();
+
+ // normalize protocol to lower-case
+ protocol = protocol.toLocaleLowerCase();
+
+ if (this.findHandlerIndex(pathString, protocol) !== -1) {
+ throw new Error('You may only mount one handler per path/protocol combination.');
+ }
+
+ this.handlers.push({
+ 'path': path,
+ 'pathString': pathString,
+ 'protocol': protocol,
+ 'callback': callback
+ });
+};
+WebSocketRouter.prototype.unmount = function(path, protocol) {
+ var index = this.findHandlerIndex(this.pathToRegExp(path).toString(), protocol);
+ if (index !== -1) {
+ this.handlers.splice(index, 1);
+ }
+ else {
+ throw new Error('Unable to find a route matching the specified path and protocol.');
+ }
+};
+
+WebSocketRouter.prototype.findHandlerIndex = function(pathString, protocol) {
+ protocol = protocol.toLocaleLowerCase();
+ for (var i=0, len=this.handlers.length; i < len; i++) {
+ var handler = this.handlers[i];
+ if (handler.pathString === pathString && handler.protocol === protocol) {
+ return i;
+ }
+ }
+ return -1;
+};
+
+WebSocketRouter.prototype.pathToRegExp = function(path) {
+ if (typeof(path) === 'string') {
+ if (path === '*') {
+ path = /^.*$/;
+ }
+ else {
+ path = path.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+ path = new RegExp('^' + path + '$');
+ }
+ }
+ return path;
+};
+
+WebSocketRouter.prototype.handleRequest = function(request) {
+ var requestedProtocols = request.requestedProtocols;
+ if (requestedProtocols.length === 0) {
+ requestedProtocols = ['____no_protocol____'];
+ }
+
+ // Find a handler with the first requested protocol first
+ for (var i=0; i < requestedProtocols.length; i++) {
+ var requestedProtocol = requestedProtocols[i].toLocaleLowerCase();
+
+ // find the first handler that can process this request
+ for (var j=0, len=this.handlers.length; j < len; j++) {
+ var handler = this.handlers[j];
+ if (handler.path.test(request.resourceURL.pathname)) {
+ if (requestedProtocol === handler.protocol ||
+ handler.protocol === '*')
+ {
+ var routerRequest = new WebSocketRouterRequest(request, requestedProtocol);
+ handler.callback(routerRequest);
+ return;
+ }
+ }
+ }
+ }
+
+ // If we get here we were unable to find a suitable handler.
+ request.reject(404, 'No handler is available for the given request.');
+};
+
+module.exports = WebSocketRouter;
diff --git a/lib/WebSocketRouterRequest.js b/lib/WebSocketRouterRequest.js
new file mode 100644
index 0000000..d3e3745
--- /dev/null
+++ b/lib/WebSocketRouterRequest.js
@@ -0,0 +1,54 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var util = require('util');
+var EventEmitter = require('events').EventEmitter;
+
+function WebSocketRouterRequest(webSocketRequest, resolvedProtocol) {
+ // Superclass Constructor
+ EventEmitter.call(this);
+
+ this.webSocketRequest = webSocketRequest;
+ if (resolvedProtocol === '____no_protocol____') {
+ this.protocol = null;
+ }
+ else {
+ this.protocol = resolvedProtocol;
+ }
+ this.origin = webSocketRequest.origin;
+ this.resource = webSocketRequest.resource;
+ this.resourceURL = webSocketRequest.resourceURL;
+ this.httpRequest = webSocketRequest.httpRequest;
+ this.remoteAddress = webSocketRequest.remoteAddress;
+ this.webSocketVersion = webSocketRequest.webSocketVersion;
+ this.requestedExtensions = webSocketRequest.requestedExtensions;
+ this.cookies = webSocketRequest.cookies;
+}
+
+util.inherits(WebSocketRouterRequest, EventEmitter);
+
+WebSocketRouterRequest.prototype.accept = function(origin, cookies) {
+ var connection = this.webSocketRequest.accept(this.protocol, origin, cookies);
+ this.emit('requestAccepted', connection);
+ return connection;
+};
+
+WebSocketRouterRequest.prototype.reject = function(status, reason, extraHeaders) {
+ this.webSocketRequest.reject(status, reason, extraHeaders);
+ this.emit('requestRejected', this);
+};
+
+module.exports = WebSocketRouterRequest;
diff --git a/lib/WebSocketServer.js b/lib/WebSocketServer.js
new file mode 100644
index 0000000..c27d967
--- /dev/null
+++ b/lib/WebSocketServer.js
@@ -0,0 +1,245 @@
+/************************************************************************
+ * Copyright 2010-2015 Brian McKelvey.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ***********************************************************************/
+
+var extend = require('./utils').extend;
+var utils = require('./utils');
+var util = require('util');
+var debug = require('debug')('websocket:server');
+var EventEmitter = require('events').EventEmitter;
+var WebSocketRequest = require('./WebSocketRequest');
+
+var WebSocketServer = function WebSocketServer(config) {
+ // Superclass Constructor
+ EventEmitter.call(this);
+
+ this._handlers = {
+ upgrade: this.handleUpgrade.bind(this),
+ requestAccepted: this.handleRequestAccepted.bind(this),
+ requestResolved: this.handleRequestResolved.bind(this)
+ };
+ this.connections = [];
+ this.pendingRequests = [];
+ if (config) {
+ this.mount(config);
+ }
+};
+
+util.inherits(WebSocketServer, EventEmitter);
+
+WebSocketServer.prototype.mount = function(config) {
+ this.config = {
+ // The http server instance to attach to. Required.
+ httpServer: null,
+
+ // 64KiB max frame size.
+ maxReceivedFrameSize: 0x10000,
+
+ // 1MiB max message size, only applicable if
+ // assembleFragments is true
+ maxReceivedMessageSize: 0x100000,
+
+ // Outgoing messages larger than fragmentationThreshold will be
+ // split into multiple fragments.
+ fragmentOutgoingMessages: true,
+
+ // Outgoing frames are fragmented if they exceed this threshold.
+ // Default is 16KiB
+ fragmentationThreshold: 0x4000,
+
+ // If true, the server will automatically send a ping to all
+ // clients every 'keepaliveInterval' milliseconds. The timer is
+ // reset on any received data from the client.
+ keepalive: true,
+
+ // The interval to send keepalive pings to connected clients if the
+ // connection is idle. Any received data will reset the counter.
+ keepaliveInterval: 20000,
+
+ // If true, the server will consider any connection that has not
+ // received any data within the amount of time specified by
+ // 'keepaliveGracePeriod' after a keepalive ping has been sent to
+ // be dead, and will drop the connection.
+ // Ignored if keepalive is false.
+ dropConnectionOnKeepaliveTimeout: true,
+
+ // The amount of time to wait after sending a keepalive ping before
+ // closing the connection if the connected peer does not respond.
+ // Ignored if keepalive is false.
+ keepaliveGracePeriod: 10000,
+
+ // Whether to use native TCP keep-alive instead of WebSockets ping
+ // and pong packets. Native TCP keep-alive sends smaller packets
+ // on the wire and so uses bandwidth more efficiently. This may
+ // be more important when talking to mobile devices.
+ // If this value is set to true, then these values will be ignored:
+ // keepaliveGracePeriod
+ // dropConnectionOnKeepaliveTimeout
+ useNativeKeepalive: false,
+
+ // If true, fragmented messages will be automatically assembled
+ // and the full message will be emitted via a 'message' event.
+ // If false, each frame will be emitted via a 'frame' event and
+ // the application will be responsible for aggregating multiple
+ // fragmented frames. Single-frame messages will emit a 'message'
+ // event in addition to the 'frame' event.
+ // Most users will want to leave this set to 'true'
+ assembleFragments: true,
+
+ // If this is true, websocket connections will be accepted
+ // regardless of the path and protocol specified by the client.
+ // The protocol accepted will be the first that was requested
+ // by the client. Clients from any origin will be accepted.
+ // This should only be used in the simplest of cases. You should
+ // probably leave this set to 'false' and inspect the request
+ // object to make sure it's acceptable before accepting it.
+ autoAcceptConnections: false,
+
+ // Whether or not the X-Forwarded-For header should be respected.
+ // It's important to set this to 'true' when accepting connections
+ // from untrusted clients, as a malicious client could spoof its
+ // IP address by simply setting this header. It's meant to be added
+ // by a trusted proxy or other intermediary within your own
+ // infrastructure.
+ // See: http://en.wikipedia.org/wiki/X-Forwarded-For
+ ignoreXForwardedFor: false,
+
+ // The Nagle Algorithm makes more efficient use of network resources
+ // by introducing a small delay before sending small packets so that
+ // multiple messages can be batched together before going onto the
+ // wire. This however comes at the cost of latency, so the default
+ // is to disable it. If you don't need low latency and are streaming
+ // lots of small messages, you can change this to 'false'
+ disableNagleAlgorithm: true,
+
+ // The number of milliseconds to wait after sending a close frame
+ // for an acknowledgement to come back before giving up and just
+ // closing the socket.
+ closeTimeout: 5000
+ };
+ extend(this.config, config);
+
+ if (this.config.httpServer) {
+ if (!Array.isArray(this.config.httpServer)) {
+ this.config.httpServer = [this.config.httpServer];
+ }
+ var upgradeHandler = this._handlers.upgrade;
+ this.config.httpServer.forEach(function(httpServer) {
+ httpServer.on('upgrade', upgradeHandler);
+ });
+ }
+ else {
+ throw new Error('You must specify an httpServer on which to mount the WebSocket server.');
+ }
+};
+
+WebSocketServer.prototype.unmount = function() {
+ var upgradeHandler = this._handlers.upgrade;
+ this.config.httpServer.forEach(function(httpServer) {
+ httpServer.removeListener('upgrade', upgradeHandler);
+ });
+};
+
+WebSocketServer.prototype.closeAllConnections = function() {
+ this.connections.forEach(function(connection) {
+ connection.close();
+ });
+ this.pendingRequests.forEach(function(request) {
+ process.nextTick(function() {
+ request.reject(503); // HTTP 503 Service Unavailable
+ });
+ });
+};
+
+WebSocketServer.prototype.broadcast = function(data) {
+ if (Buffer.isBuffer(data)) {
+ this.broadcastBytes(data);
+ }
+ else if (typeof(data.toString) === 'function') {
+ this.broadcastUTF(data);
+ }
+};
+
+WebSocketServer.prototype.broadcastUTF = function(utfData) {
+ this.connections.forEach(function(connection) {
+ connection.sendUTF(utfData);
+ });
+};
+
+WebSocketServer.prototype.broadcastBytes = function(binaryData) {
+ this.connections.forEach(function(connection) {
+ connection.sendBytes(binaryData);
+ });
+};
+
+WebSocketServer.prototype.shutDown = function() {
+ this.unmount();
+ this.closeAllConnections();
+};
+
+WebSocketServer.prototype.handleUpgrade = function(request, socket) {
+ var wsRequest = new WebSocketRequest(socket, request, this.config);
+ try {
+ wsRequest.readHandshake();
+ }
+ catch(e) {
+ wsRequest.reject(
+ e.httpCode ? e.httpCode : 400,
+ e.message,
+ e.headers
+ );
+ debug('Invalid handshake: %s', e.message);
+ return;
+ }
+
+ this.pendingRequests.push(wsRequest);
+
+ wsRequest.once('requestAccepted', this._handlers.requestAccepted);
+ wsRequest.once('requestResolved', this._handlers.requestResolved);
+
+ if (!this.config.autoAcceptConnections && utils.eventEmitterListenerCount(this, 'request') > 0) {
+ this.emit('request', wsRequest);
+ }
+ else if (this.config.autoAcceptConnections) {
+ wsRequest.accept(wsRequest.requestedProtocols[0], wsRequest.origin);
+ }
+ else {
+ wsRequest.reject(404, 'No handler is configured to accept the connection.');
+ }
+};
+
+WebSocketServer.prototype.handleRequestAccepted = function(connection) {
+ var self = this;
+ connection.once('close', function(closeReason, description) {
+ self.handleConnectionClose(connection, closeReason, description);
+ });
+ this.connections.push(connection);
+ this.emit('connect', connection);
+};
+
+WebSocketServer.prototype.handleConnectionClose = function(connection, closeReason, description) {
+ var index = this.connections.indexOf(connection);
+ if (index !== -1) {
+ this.connections.splice(index, 1);
+ }
+ this.emit('close', connection, closeReason, description);
+};
+
+WebSocketServer.prototype.handleRequestResolved = function(request) {
+ var index = this.pendingRequests.indexOf(request);
+ if (index !== -1) { this.pendingRequests.splice(index, 1); }
+};
+
+module.exports = WebSocketServer;
diff --git a/lib/browser.js b/lib/browser.js
new file mode 100644
index 0000000..16c641b
--- /dev/null
+++ b/lib/browser.js
@@ -0,0 +1,36 @@
+var _global = (function() { return this; })();
+var nativeWebSocket = _global.WebSocket || _global.MozWebSocket;
+var websocket_version = require('./version');
+
+
+/**
+ * Expose a W3C WebSocket class with just one or two arguments.
+ */
+function W3CWebSocket(uri, protocols) {
+ var native_instance;
+
+ if (protocols) {
+ native_instance = new nativeWebSocket(uri, protocols);
+ }
+ else {
+ native_instance = new nativeWebSocket(uri);
+ }
+
+ /**
+ * 'native_instance' is an instance of nativeWebSocket (the browser's WebSocket
+ * class). Since it is an Object it will be returned as it is when creating an
+ * instance of W3CWebSocket via 'new W3CWebSocket()'.
+ *
+ * ECMAScript 5: http://bclary.com/2004/11/07/#a-13.2.2
+ */
+ return native_instance;
+}
+
+
+/**
+ * Module exports.
+ */
+module.exports = {
+ 'w3cwebsocket' : nativeWebSocket ? W3CWebSocket : null,
+ 'version' : websocket_version
+};
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..6506dc9
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,60 @@
+var noop = exports.noop = function(){};
+
+exports.extend = function extend(dest, source) {
+ for (var prop in source) {
+ dest[prop] = source[prop];
+ }
+};
+
+exports.eventEmitterListenerCount =
+ require('events').EventEmitter.listenerCount ||
+ function(emitter, type) { return emitter.listeners(type).length; };
+
+
+
+
+
+exports.BufferingLogger = function createBufferingLogger(identifier, uniqueID) {
+ var logFunction = require('debug')(identifier);
+ if (logFunction.enabled) {
+ var logger = new BufferingLogger(identifier, uniqueID, logFunction);
+ var debug = logger.log.bind(logger);
+ debug.printOutput = logger.printOutput.bind(logger);
+ debug.enabled = logFunction.enabled;
+ return debug;
+ }
+ logFunction.printOutput = noop;
+ return logFunction;
+};
+
+function BufferingLogger(identifier, uniqueID, logFunction) {
+ this.logFunction = logFunction;
+ this.identifier = identifier;
+ this.uniqueID = uniqueID;
+ this.buffer = [];
+}
+
+BufferingLogger.prototype.log = function() {
+ this.buffer.push([ new Date(), Array.prototype.slice.call(arguments) ]);
+ return this;
+};
+
+BufferingLogger.prototype.clear = function() {
+ this.buffer = [];
+ return this;
+};
+
+BufferingLogger.prototype.printOutput = function(logFunction) {
+ if (!logFunction) { logFunction = this.logFunction; }
+ var uniqueID = this.uniqueID;
+ this.buffer.forEach(function(entry) {
+ var date = entry[0].toLocaleString();
+ var args = entry[1].slice();
+ var formatString = args[0];
+ if (formatString !== (void 0) && formatString !== null) {
+ formatString = '%s - %s - ' + formatString.toString();
+ args.splice(0, 1, formatString, date, uniqueID);
+ logFunction.apply(global, args);
+ }
+ });
+};
diff --git a/lib/version.js b/lib/version.js
new file mode 100644
index 0000000..81f6e78
--- /dev/null
+++ b/lib/version.js
@@ -0,0 +1 @@
+module.exports = require('../package.json').version;
diff --git a/lib/websocket.js b/lib/websocket.js
new file mode 100644
index 0000000..6242d56
--- /dev/null
+++ b/lib/websocket.js
@@ -0,0 +1,11 @@
+module.exports = {
+ 'server' : require('./WebSocketServer'),
+ 'client' : require('./WebSocketClient'),
+ 'router' : require('./WebSocketRouter'),
+ 'frame' : require('./WebSocketFrame'),
+ 'request' : require('./WebSocketRequest'),
+ 'connection' : require('./WebSocketConnection'),
+ 'w3cwebsocket' : require('./W3CWebSocket'),
+ 'deprecation' : require('./Deprecation'),
+ 'version' : require('./version')
+};