Websocket
[VSoRC/.git] / node_modules / websocket / lib / WebSocketClient.js
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;