1 /************************************************************************
2 * Copyright 2010-2015 Brian McKelvey.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 ***********************************************************************/
17 var utils = require('./utils');
18 var extend = utils.extend;
19 var util = require('util');
20 var EventEmitter = require('events').EventEmitter;
21 var http = require('http');
22 var https = require('https');
23 var url = require('url');
24 var crypto = require('crypto');
25 var WebSocketConnection = require('./WebSocketConnection');
26 var bufferAllocUnsafe = utils.bufferAllocUnsafe;
28 var protocolSeparators = [
29 '(', ')', '<', '>', '@',
30 ',', ';', ':', '\\', '\"',
31 '/', '[', ']', '?', '=',
32 '{', '}', ' ', String.fromCharCode(9)
35 var excludedTlsOptions = ['hostname','port','method','path','headers'];
37 function WebSocketClient(config) {
38 // Superclass Constructor
39 EventEmitter.call(this);
41 // TODO: Implement extensions
44 // 1MiB max frame size.
45 maxReceivedFrameSize: 0x100000,
47 // 8MiB max message size, only applicable if
48 // assembleFragments is true
49 maxReceivedMessageSize: 0x800000,
51 // Outgoing messages larger than fragmentationThreshold will be
52 // split into multiple fragments.
53 fragmentOutgoingMessages: true,
55 // Outgoing frames are fragmented if they exceed this threshold.
57 fragmentationThreshold: 0x4000,
59 // Which version of the protocol to use for this session. This
60 // option will be removed once the protocol is finalized by the IETF
61 // It is only available to ease the transition through the
62 // intermediate draft protocol versions.
63 // At present, it only affects the name of the Origin header.
66 // If true, fragmented messages will be automatically assembled
67 // and the full message will be emitted via a 'message' event.
68 // If false, each frame will be emitted via a 'frame' event and
69 // the application will be responsible for aggregating multiple
70 // fragmented frames. Single-frame messages will emit a 'message'
71 // event in addition to the 'frame' event.
72 // Most users will want to leave this set to 'true'
73 assembleFragments: true,
75 // The Nagle Algorithm makes more efficient use of network resources
76 // by introducing a small delay before sending small packets so that
77 // multiple messages can be batched together before going onto the
78 // wire. This however comes at the cost of latency, so the default
79 // is to disable it. If you don't need low latency and are streaming
80 // lots of small messages, you can change this to 'false'
81 disableNagleAlgorithm: true,
83 // The number of milliseconds to wait after sending a close frame
84 // for an acknowledgement to come back before giving up and just
85 // closing the socket.
88 // Options to pass to https.connect if connecting via TLS
94 if (config.tlsOptions) {
95 tlsOptions = config.tlsOptions;
96 delete config.tlsOptions;
101 extend(this.config, config);
102 extend(this.config.tlsOptions, tlsOptions);
107 switch (this.config.webSocketVersion) {
112 throw new Error('Requested webSocketVersion is not supported. Allowed values are 8 and 13.');
116 util.inherits(WebSocketClient, EventEmitter);
118 WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, headers, extraRequestOptions) {
121 if (typeof(protocols) === 'string') {
122 if (protocols.length > 0) {
123 protocols = [protocols];
129 if (!(protocols instanceof Array)) {
132 this.protocols = protocols;
133 this.origin = origin;
135 if (typeof(requestUrl) === 'string') {
136 this.url = url.parse(requestUrl);
139 this.url = requestUrl; // in case an already parsed url is passed in.
141 if (!this.url.protocol) {
142 throw new Error('You must specify a full WebSocket URL, including protocol.');
144 if (!this.url.host) {
145 throw new Error('You must specify a full WebSocket URL, including hostname. Relative URLs are not supported.');
148 this.secure = (this.url.protocol === 'wss:');
150 // validate protocol characters:
151 this.protocols.forEach(function(protocol) {
152 for (var i=0; i < protocol.length; i ++) {
153 var charCode = protocol.charCodeAt(i);
154 var character = protocol.charAt(i);
155 if (charCode < 0x0021 || charCode > 0x007E || protocolSeparators.indexOf(character) !== -1) {
156 throw new Error('Protocol list contains invalid character "' + String.fromCharCode(charCode) + '"');
166 if (!this.url.port) {
167 this.url.port = defaultPorts[this.url.protocol];
170 var nonce = bufferAllocUnsafe(16);
171 for (var i=0; i < 16; i++) {
172 nonce[i] = Math.round(Math.random()*0xFF);
174 this.base64nonce = nonce.toString('base64');
176 var hostHeaderValue = this.url.hostname;
177 if ((this.url.protocol === 'ws:' && this.url.port !== '80') ||
178 (this.url.protocol === 'wss:' && this.url.port !== '443')) {
179 hostHeaderValue += (':' + this.url.port);
183 if (this.secure && this.config.tlsOptions.hasOwnProperty('headers')) {
184 // Allow for additional headers to be provided when connecting via HTTPS
185 extend(reqHeaders, this.config.tlsOptions.headers);
188 // Explicitly provided headers take priority over any from tlsOptions
189 extend(reqHeaders, headers);
192 'Upgrade': 'websocket',
193 'Connection': 'Upgrade',
194 'Sec-WebSocket-Version': this.config.webSocketVersion.toString(10),
195 'Sec-WebSocket-Key': this.base64nonce,
196 'Host': reqHeaders.Host || hostHeaderValue
199 if (this.protocols.length > 0) {
200 reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', ');
203 if (this.config.webSocketVersion === 13) {
204 reqHeaders['Origin'] = this.origin;
206 else if (this.config.webSocketVersion === 8) {
207 reqHeaders['Sec-WebSocket-Origin'] = this.origin;
211 // TODO: Implement extensions
214 // Ensure it begins with '/'.
215 if (this.url.pathname) {
216 pathAndQuery = this.url.path;
218 else if (this.url.path) {
219 pathAndQuery = '/' + this.url.path;
225 function handleRequestError(error) {
227 self.emit('connectFailed', error);
230 var requestOptions = {
233 if (extraRequestOptions) {
234 extend(requestOptions, extraRequestOptions);
236 // These options are always overridden by the library. The user is not
237 // allowed to specify these directly.
238 extend(requestOptions, {
239 hostname: this.url.hostname,
246 var tlsOptions = this.config.tlsOptions;
247 for (var key in tlsOptions) {
248 if (tlsOptions.hasOwnProperty(key) && excludedTlsOptions.indexOf(key) === -1) {
249 requestOptions[key] = tlsOptions[key];
254 var req = this._req = (this.secure ? https : http).request(requestOptions);
255 req.on('upgrade', function handleRequestUpgrade(response, socket, head) {
257 req.removeListener('error', handleRequestError);
258 self.socket = socket;
259 self.response = response;
260 self.firstDataChunk = head;
261 self.validateHandshake();
263 req.on('error', handleRequestError);
265 req.on('response', function(response) {
267 if (utils.eventEmitterListenerCount(self, 'httpResponse') > 0) {
268 self.emit('httpResponse', response, self);
269 if (response.socket) {
270 response.socket.end();
274 var headerDumpParts = [];
275 for (var headerName in response.headers) {
276 headerDumpParts.push(headerName + ': ' + response.headers[headerName]);
279 'Server responded with a non-101 status: ' +
280 response.statusCode + ' ' + response.statusMessage +
281 '\nResponse Headers Follow:\n' +
282 headerDumpParts.join('\n') + '\n'
289 WebSocketClient.prototype.validateHandshake = function() {
290 var headers = this.response.headers;
292 if (this.protocols.length > 0) {
293 this.protocol = headers['sec-websocket-protocol'];
295 if (this.protocols.indexOf(this.protocol) === -1) {
296 this.failHandshake('Server did not respond with a requested protocol.');
301 this.failHandshake('Expected a Sec-WebSocket-Protocol header.');
306 if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) {
307 this.failHandshake('Expected a Connection: Upgrade header from the server');
311 if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) {
312 this.failHandshake('Expected an Upgrade: websocket header from the server');
316 var sha1 = crypto.createHash('sha1');
317 sha1.update(this.base64nonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
318 var expectedKey = sha1.digest('base64');
320 if (!headers['sec-websocket-accept']) {
321 this.failHandshake('Expected Sec-WebSocket-Accept header from server');
325 if (headers['sec-websocket-accept'] !== expectedKey) {
326 this.failHandshake('Sec-WebSocket-Accept header from server didn\'t match expected value of ' + expectedKey);
330 // TODO: Support extensions
332 this.succeedHandshake();
335 WebSocketClient.prototype.failHandshake = function(errorDescription) {
336 if (this.socket && this.socket.writable) {
339 this.emit('connectFailed', new Error(errorDescription));
342 WebSocketClient.prototype.succeedHandshake = function() {
343 var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config);
345 connection.webSocketVersion = this.config.webSocketVersion;
346 connection._addSocketEventListeners();
348 this.emit('connect', connection);
349 if (this.firstDataChunk.length > 0) {
350 connection.handleSocketData(this.firstDataChunk);
352 this.firstDataChunk = null;
355 WebSocketClient.prototype.abort = function() {
361 module.exports = WebSocketClient;