braid/node_modules/websocket/lib/WebSocketClient.js

362 lines
12 KiB
JavaScript

/************************************************************************
* 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 bufferAllocUnsafe = utils.bufferAllocUnsafe;
var protocolSeparators = [
'(', ')', '<', '>', '@',
',', ';', ':', '\\', '\"',
'/', '[', ']', '?', '=',
'{', '}', ' ', String.fromCharCode(9)
];
var excludedTlsOptions = ['hostname','port','method','path','headers'];
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 = bufferAllocUnsafe(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 = {};
if (this.secure && this.config.tlsOptions.hasOwnProperty('headers')) {
// Allow for additional headers to be provided when connecting via HTTPS
extend(reqHeaders, this.config.tlsOptions.headers);
}
if (headers) {
// Explicitly provided headers take priority over any from tlsOptions
extend(reqHeaders, headers);
}
extend(reqHeaders, {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Version': this.config.webSocketVersion.toString(10),
'Sec-WebSocket-Key': this.base64nonce,
'Host': reqHeaders.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) {
var tlsOptions = this.config.tlsOptions;
for (var key in tlsOptions) {
if (tlsOptions.hasOwnProperty(key) && excludedTlsOptions.indexOf(key) === -1) {
requestOptions[key] = 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 + ' ' + response.statusMessage +
'\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;