123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- /************************************************************************
- * 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;
|