@fairwords/websocket 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,361 @@
1
+ /************************************************************************
2
+ * Copyright 2010-2015 Brian McKelvey.
3
+ *
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
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
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
+ ***********************************************************************/
16
+
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;
27
+
28
+ var protocolSeparators = [
29
+ '(', ')', '<', '>', '@',
30
+ ',', ';', ':', '\\', '\"',
31
+ '/', '[', ']', '?', '=',
32
+ '{', '}', ' ', String.fromCharCode(9)
33
+ ];
34
+
35
+ var excludedTlsOptions = ['hostname','port','method','path','headers'];
36
+
37
+ function WebSocketClient(config) {
38
+ // Superclass Constructor
39
+ EventEmitter.call(this);
40
+
41
+ // TODO: Implement extensions
42
+
43
+ this.config = {
44
+ // 1MiB max frame size.
45
+ maxReceivedFrameSize: 0x100000,
46
+
47
+ // 8MiB max message size, only applicable if
48
+ // assembleFragments is true
49
+ maxReceivedMessageSize: 0x800000,
50
+
51
+ // Outgoing messages larger than fragmentationThreshold will be
52
+ // split into multiple fragments.
53
+ fragmentOutgoingMessages: true,
54
+
55
+ // Outgoing frames are fragmented if they exceed this threshold.
56
+ // Default is 16KiB
57
+ fragmentationThreshold: 0x4000,
58
+
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.
64
+ webSocketVersion: 13,
65
+
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,
74
+
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,
82
+
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.
86
+ closeTimeout: 5000,
87
+
88
+ // Options to pass to https.connect if connecting via TLS
89
+ tlsOptions: {}
90
+ };
91
+
92
+ if (config) {
93
+ var tlsOptions;
94
+ if (config.tlsOptions) {
95
+ tlsOptions = config.tlsOptions;
96
+ delete config.tlsOptions;
97
+ }
98
+ else {
99
+ tlsOptions = {};
100
+ }
101
+ extend(this.config, config);
102
+ extend(this.config.tlsOptions, tlsOptions);
103
+ }
104
+
105
+ this._req = null;
106
+
107
+ switch (this.config.webSocketVersion) {
108
+ case 8:
109
+ case 13:
110
+ break;
111
+ default:
112
+ throw new Error('Requested webSocketVersion is not supported. Allowed values are 8 and 13.');
113
+ }
114
+ }
115
+
116
+ util.inherits(WebSocketClient, EventEmitter);
117
+
118
+ WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, headers, extraRequestOptions) {
119
+ var self = this;
120
+
121
+ if (typeof(protocols) === 'string') {
122
+ if (protocols.length > 0) {
123
+ protocols = [protocols];
124
+ }
125
+ else {
126
+ protocols = [];
127
+ }
128
+ }
129
+ if (!(protocols instanceof Array)) {
130
+ protocols = [];
131
+ }
132
+ this.protocols = protocols;
133
+ this.origin = origin;
134
+
135
+ if (typeof(requestUrl) === 'string') {
136
+ this.url = url.parse(requestUrl);
137
+ }
138
+ else {
139
+ this.url = requestUrl; // in case an already parsed url is passed in.
140
+ }
141
+ if (!this.url.protocol) {
142
+ throw new Error('You must specify a full WebSocket URL, including protocol.');
143
+ }
144
+ if (!this.url.host) {
145
+ throw new Error('You must specify a full WebSocket URL, including hostname. Relative URLs are not supported.');
146
+ }
147
+
148
+ this.secure = (this.url.protocol === 'wss:');
149
+
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) + '"');
157
+ }
158
+ }
159
+ });
160
+
161
+ var defaultPorts = {
162
+ 'ws:': '80',
163
+ 'wss:': '443'
164
+ };
165
+
166
+ if (!this.url.port) {
167
+ this.url.port = defaultPorts[this.url.protocol];
168
+ }
169
+
170
+ var nonce = bufferAllocUnsafe(16);
171
+ for (var i=0; i < 16; i++) {
172
+ nonce[i] = Math.round(Math.random()*0xFF);
173
+ }
174
+ this.base64nonce = nonce.toString('base64');
175
+
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);
180
+ }
181
+
182
+ var reqHeaders = {};
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);
186
+ }
187
+ if (headers) {
188
+ // Explicitly provided headers take priority over any from tlsOptions
189
+ extend(reqHeaders, headers);
190
+ }
191
+ extend(reqHeaders, {
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
197
+ });
198
+
199
+ if (this.protocols.length > 0) {
200
+ reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', ');
201
+ }
202
+ if (this.origin) {
203
+ if (this.config.webSocketVersion === 13) {
204
+ reqHeaders['Origin'] = this.origin;
205
+ }
206
+ else if (this.config.webSocketVersion === 8) {
207
+ reqHeaders['Sec-WebSocket-Origin'] = this.origin;
208
+ }
209
+ }
210
+
211
+ // TODO: Implement extensions
212
+
213
+ var pathAndQuery;
214
+ // Ensure it begins with '/'.
215
+ if (this.url.pathname) {
216
+ pathAndQuery = this.url.path;
217
+ }
218
+ else if (this.url.path) {
219
+ pathAndQuery = '/' + this.url.path;
220
+ }
221
+ else {
222
+ pathAndQuery = '/';
223
+ }
224
+
225
+ function handleRequestError(error) {
226
+ self._req = null;
227
+ self.emit('connectFailed', error);
228
+ }
229
+
230
+ var requestOptions = {
231
+ agent: false
232
+ };
233
+ if (extraRequestOptions) {
234
+ extend(requestOptions, extraRequestOptions);
235
+ }
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,
240
+ port: this.url.port,
241
+ method: 'GET',
242
+ path: pathAndQuery,
243
+ headers: reqHeaders
244
+ });
245
+ if (this.secure) {
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];
250
+ }
251
+ }
252
+ }
253
+
254
+ var req = this._req = (this.secure ? https : http).request(requestOptions);
255
+ req.on('upgrade', function handleRequestUpgrade(response, socket, head) {
256
+ self._req = null;
257
+ req.removeListener('error', handleRequestError);
258
+ self.socket = socket;
259
+ self.response = response;
260
+ self.firstDataChunk = head;
261
+ self.validateHandshake();
262
+ });
263
+ req.on('error', handleRequestError);
264
+
265
+ req.on('response', function(response) {
266
+ self._req = null;
267
+ if (utils.eventEmitterListenerCount(self, 'httpResponse') > 0) {
268
+ self.emit('httpResponse', response, self);
269
+ if (response.socket) {
270
+ response.socket.end();
271
+ }
272
+ }
273
+ else {
274
+ var headerDumpParts = [];
275
+ for (var headerName in response.headers) {
276
+ headerDumpParts.push(headerName + ': ' + response.headers[headerName]);
277
+ }
278
+ self.failHandshake(
279
+ 'Server responded with a non-101 status: ' +
280
+ response.statusCode + ' ' + response.statusMessage +
281
+ '\nResponse Headers Follow:\n' +
282
+ headerDumpParts.join('\n') + '\n'
283
+ );
284
+ }
285
+ });
286
+ req.end();
287
+ };
288
+
289
+ WebSocketClient.prototype.validateHandshake = function() {
290
+ var headers = this.response.headers;
291
+
292
+ if (this.protocols.length > 0) {
293
+ this.protocol = headers['sec-websocket-protocol'];
294
+ if (this.protocol) {
295
+ if (this.protocols.indexOf(this.protocol) === -1) {
296
+ this.failHandshake('Server did not respond with a requested protocol.');
297
+ return;
298
+ }
299
+ }
300
+ else {
301
+ this.failHandshake('Expected a Sec-WebSocket-Protocol header.');
302
+ return;
303
+ }
304
+ }
305
+
306
+ if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) {
307
+ this.failHandshake('Expected a Connection: Upgrade header from the server');
308
+ return;
309
+ }
310
+
311
+ if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) {
312
+ this.failHandshake('Expected an Upgrade: websocket header from the server');
313
+ return;
314
+ }
315
+
316
+ var sha1 = crypto.createHash('sha1');
317
+ sha1.update(this.base64nonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
318
+ var expectedKey = sha1.digest('base64');
319
+
320
+ if (!headers['sec-websocket-accept']) {
321
+ this.failHandshake('Expected Sec-WebSocket-Accept header from server');
322
+ return;
323
+ }
324
+
325
+ if (headers['sec-websocket-accept'] !== expectedKey) {
326
+ this.failHandshake('Sec-WebSocket-Accept header from server didn\'t match expected value of ' + expectedKey);
327
+ return;
328
+ }
329
+
330
+ // TODO: Support extensions
331
+
332
+ this.succeedHandshake();
333
+ };
334
+
335
+ WebSocketClient.prototype.failHandshake = function(errorDescription) {
336
+ if (this.socket && this.socket.writable) {
337
+ this.socket.end();
338
+ }
339
+ this.emit('connectFailed', new Error(errorDescription));
340
+ };
341
+
342
+ WebSocketClient.prototype.succeedHandshake = function() {
343
+ var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config);
344
+
345
+ connection.webSocketVersion = this.config.webSocketVersion;
346
+ connection._addSocketEventListeners();
347
+
348
+ this.emit('connect', connection);
349
+ if (this.firstDataChunk.length > 0) {
350
+ connection.handleSocketData(this.firstDataChunk);
351
+ }
352
+ this.firstDataChunk = null;
353
+ };
354
+
355
+ WebSocketClient.prototype.abort = function() {
356
+ if (this._req) {
357
+ this._req.abort();
358
+ }
359
+ };
360
+
361
+ module.exports = WebSocketClient;