Websocket
[VSoRC/.git] / node_modules / websocket / lib / WebSocketRequest.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 crypto = require('crypto');
18 var util = require('util');
19 var url = require('url');
20 var EventEmitter = require('events').EventEmitter;
21 var WebSocketConnection = require('./WebSocketConnection');
22
23 var headerValueSplitRegExp = /,\s*/;
24 var headerParamSplitRegExp = /;\s*/;
25 var headerSanitizeRegExp = /[\r\n]/g;
26 var xForwardedForSeparatorRegExp = /,\s*/;
27 var separators = [
28     '(', ')', '<', '>', '@',
29     ',', ';', ':', '\\', '\"',
30     '/', '[', ']', '?', '=',
31     '{', '}', ' ', String.fromCharCode(9)
32 ];
33 var controlChars = [String.fromCharCode(127) /* DEL */];
34 for (var i=0; i < 31; i ++) {
35     /* US-ASCII Control Characters */
36     controlChars.push(String.fromCharCode(i));
37 }
38
39 var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
40 var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;
41 var cookieValueDQuoteValidateRegEx = /^"[^"]*"$/;
42 var controlCharsAndSemicolonRegEx = /[\x00-\x20\x3b]/g;
43
44 var cookieSeparatorRegEx = /[;,] */;
45
46 var httpStatusDescriptions = {
47     100: 'Continue',
48     101: 'Switching Protocols',
49     200: 'OK',
50     201: 'Created',
51     203: 'Non-Authoritative Information',
52     204: 'No Content',
53     205: 'Reset Content',
54     206: 'Partial Content',
55     300: 'Multiple Choices',
56     301: 'Moved Permanently',
57     302: 'Found',
58     303: 'See Other',
59     304: 'Not Modified',
60     305: 'Use Proxy',
61     307: 'Temporary Redirect',
62     400: 'Bad Request',
63     401: 'Unauthorized',
64     402: 'Payment Required',
65     403: 'Forbidden',
66     404: 'Not Found',
67     406: 'Not Acceptable',
68     407: 'Proxy Authorization Required',
69     408: 'Request Timeout',
70     409: 'Conflict',
71     410: 'Gone',
72     411: 'Length Required',
73     412: 'Precondition Failed',
74     413: 'Request Entity Too Long',
75     414: 'Request-URI Too Long',
76     415: 'Unsupported Media Type',
77     416: 'Requested Range Not Satisfiable',
78     417: 'Expectation Failed',
79     426: 'Upgrade Required',
80     500: 'Internal Server Error',
81     501: 'Not Implemented',
82     502: 'Bad Gateway',
83     503: 'Service Unavailable',
84     504: 'Gateway Timeout',
85     505: 'HTTP Version Not Supported'
86 };
87
88 function WebSocketRequest(socket, httpRequest, serverConfig) {
89     // Superclass Constructor
90     EventEmitter.call(this);
91
92     this.socket = socket;
93     this.httpRequest = httpRequest;
94     this.resource = httpRequest.url;
95     this.remoteAddress = socket.remoteAddress;
96     this.remoteAddresses = [this.remoteAddress];
97     this.serverConfig = serverConfig;
98     
99     // Watch for the underlying TCP socket closing before we call accept
100     this._socketIsClosing = false;
101     this._socketCloseHandler = this._handleSocketCloseBeforeAccept.bind(this);
102     this.socket.on('end', this._socketCloseHandler);
103     this.socket.on('close', this._socketCloseHandler);
104     
105     this._resolved = false;
106 }
107
108 util.inherits(WebSocketRequest, EventEmitter);
109
110 WebSocketRequest.prototype.readHandshake = function() {
111     var self = this;
112     var request = this.httpRequest;
113
114     // Decode URL
115     this.resourceURL = url.parse(this.resource, true);
116
117     this.host = request.headers['host'];
118     if (!this.host) {
119         throw new Error('Client must provide a Host header.');
120     }
121
122     this.key = request.headers['sec-websocket-key'];
123     if (!this.key) {
124         throw new Error('Client must provide a value for Sec-WebSocket-Key.');
125     }
126
127     this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10);
128
129     if (!this.webSocketVersion || isNaN(this.webSocketVersion)) {
130         throw new Error('Client must provide a value for Sec-WebSocket-Version.');
131     }
132
133     switch (this.webSocketVersion) {
134         case 8:
135         case 13:
136             break;
137         default:
138             var e = new Error('Unsupported websocket client version: ' + this.webSocketVersion +
139                               'Only versions 8 and 13 are supported.');
140             e.httpCode = 426;
141             e.headers = {
142                 'Sec-WebSocket-Version': '13'
143             };
144             throw e;
145     }
146
147     if (this.webSocketVersion === 13) {
148         this.origin = request.headers['origin'];
149     }
150     else if (this.webSocketVersion === 8) {
151         this.origin = request.headers['sec-websocket-origin'];
152     }
153
154     // Protocol is optional.
155     var protocolString = request.headers['sec-websocket-protocol'];
156     this.protocolFullCaseMap = {};
157     this.requestedProtocols = [];
158     if (protocolString) {
159         var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp);
160         requestedProtocolsFullCase.forEach(function(protocol) {
161             var lcProtocol = protocol.toLocaleLowerCase();
162             self.requestedProtocols.push(lcProtocol);
163             self.protocolFullCaseMap[lcProtocol] = protocol;
164         });
165     }
166
167     if (!this.serverConfig.ignoreXForwardedFor &&
168         request.headers['x-forwarded-for']) {
169         var immediatePeerIP = this.remoteAddress;
170         this.remoteAddresses = request.headers['x-forwarded-for']
171             .split(xForwardedForSeparatorRegExp);
172         this.remoteAddresses.push(immediatePeerIP);
173         this.remoteAddress = this.remoteAddresses[0];
174     }
175
176     // Extensions are optional.
177     var extensionsString = request.headers['sec-websocket-extensions'];
178     this.requestedExtensions = this.parseExtensions(extensionsString);
179
180     // Cookies are optional
181     var cookieString = request.headers['cookie'];
182     this.cookies = this.parseCookies(cookieString);
183 };
184
185 WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
186     if (!extensionsString || extensionsString.length === 0) {
187         return [];
188     }
189     var extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
190     extensions.forEach(function(extension, index, array) {
191         var params = extension.split(headerParamSplitRegExp);
192         var extensionName = params[0];
193         var extensionParams = params.slice(1);
194         extensionParams.forEach(function(rawParam, index, array) {
195             var arr = rawParam.split('=');
196             var obj = {
197                 name: arr[0],
198                 value: arr[1]
199             };
200             array.splice(index, 1, obj);
201         });
202         var obj = {
203             name: extensionName,
204             params: extensionParams
205         };
206         array.splice(index, 1, obj);
207     });
208     return extensions;
209 };
210
211 // This function adapted from node-cookie
212 // https://github.com/shtylman/node-cookie
213 WebSocketRequest.prototype.parseCookies = function(str) {
214     // Sanity Check
215     if (!str || typeof(str) !== 'string') {
216         return [];
217     }
218
219     var cookies = [];
220     var pairs = str.split(cookieSeparatorRegEx);
221
222     pairs.forEach(function(pair) {
223         var eq_idx = pair.indexOf('=');
224         if (eq_idx === -1) {
225             cookies.push({
226                 name: pair,
227                 value: null
228             });
229             return;
230         }
231
232         var key = pair.substr(0, eq_idx).trim();
233         var val = pair.substr(++eq_idx, pair.length).trim();
234
235         // quoted values
236         if ('"' === val[0]) {
237             val = val.slice(1, -1);
238         }
239
240         cookies.push({
241             name: key,
242             value: decodeURIComponent(val)
243         });
244     });
245
246     return cookies;
247 };
248
249 WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
250     this._verifyResolution();
251     
252     // TODO: Handle extensions
253
254     var protocolFullCase;
255
256     if (acceptedProtocol) {
257         protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()];
258         if (typeof(protocolFullCase) === 'undefined') {
259             protocolFullCase = acceptedProtocol;
260         }
261     }
262     else {
263         protocolFullCase = acceptedProtocol;
264     }
265     this.protocolFullCaseMap = null;
266
267     // Create key validation hash
268     var sha1 = crypto.createHash('sha1');
269     sha1.update(this.key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
270     var acceptKey = sha1.digest('base64');
271
272     var response = 'HTTP/1.1 101 Switching Protocols\r\n' +
273                    'Upgrade: websocket\r\n' +
274                    'Connection: Upgrade\r\n' +
275                    'Sec-WebSocket-Accept: ' + acceptKey + '\r\n';
276
277     if (protocolFullCase) {
278         // validate protocol
279         for (var i=0; i < protocolFullCase.length; i++) {
280             var charCode = protocolFullCase.charCodeAt(i);
281             var character = protocolFullCase.charAt(i);
282             if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
283                 this.reject(500);
284                 throw new Error('Illegal character "' + String.fromCharCode(character) + '" in subprotocol.');
285             }
286         }
287         if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
288             this.reject(500);
289             throw new Error('Specified protocol was not requested by the client.');
290         }
291
292         protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, '');
293         response += 'Sec-WebSocket-Protocol: ' + protocolFullCase + '\r\n';
294     }
295     this.requestedProtocols = null;
296
297     if (allowedOrigin) {
298         allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
299         if (this.webSocketVersion === 13) {
300             response += 'Origin: ' + allowedOrigin + '\r\n';
301         }
302         else if (this.webSocketVersion === 8) {
303             response += 'Sec-WebSocket-Origin: ' + allowedOrigin + '\r\n';
304         }
305     }
306
307     if (cookies) {
308         if (!Array.isArray(cookies)) {
309             this.reject(500);
310             throw new Error('Value supplied for "cookies" argument must be an array.');
311         }
312         var seenCookies = {};
313         cookies.forEach(function(cookie) {
314             if (!cookie.name || !cookie.value) {
315                 this.reject(500);
316                 throw new Error('Each cookie to set must at least provide a "name" and "value"');
317             }
318
319             // Make sure there are no \r\n sequences inserted
320             cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, '');
321             cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, '');
322
323             if (seenCookies[cookie.name]) {
324                 this.reject(500);
325                 throw new Error('You may not specify the same cookie name twice.');
326             }
327             seenCookies[cookie.name] = true;
328
329             // token (RFC 2616, Section 2.2)
330             var invalidChar = cookie.name.match(cookieNameValidateRegEx);
331             if (invalidChar) {
332                 this.reject(500);
333                 throw new Error('Illegal character ' + invalidChar[0] + ' in cookie name');
334             }
335
336             // RFC 6265, Section 4.1.1
337             // *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
338             if (cookie.value.match(cookieValueDQuoteValidateRegEx)) {
339                 invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx);
340             } else {
341                 invalidChar = cookie.value.match(cookieValueValidateRegEx);
342             }
343             if (invalidChar) {
344                 this.reject(500);
345                 throw new Error('Illegal character ' + invalidChar[0] + ' in cookie value');
346             }
347
348             var cookieParts = [cookie.name + '=' + cookie.value];
349
350             // RFC 6265, Section 4.1.1
351             // 'Path=' path-value | <any CHAR except CTLs or ';'>
352             if(cookie.path){
353                 invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx);
354                 if (invalidChar) {
355                     this.reject(500);
356                     throw new Error('Illegal character ' + invalidChar[0] + ' in cookie path');
357                 }
358                 cookieParts.push('Path=' + cookie.path);
359             }
360
361             // RFC 6265, Section 4.1.2.3
362             // 'Domain=' subdomain
363             if (cookie.domain) {
364                 if (typeof(cookie.domain) !== 'string') {
365                     this.reject(500);
366                     throw new Error('Domain must be specified and must be a string.');
367                 }
368                 invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx);
369                 if (invalidChar) {
370                     this.reject(500);
371                     throw new Error('Illegal character ' + invalidChar[0] + ' in cookie domain');
372                 }
373                 cookieParts.push('Domain=' + cookie.domain.toLowerCase());
374             }
375
376             // RFC 6265, Section 4.1.1
377             //'Expires=' sane-cookie-date | Force Date object requirement by using only epoch
378             if (cookie.expires) {
379                 if (!(cookie.expires instanceof Date)){
380                     this.reject(500);
381                     throw new Error('Value supplied for cookie "expires" must be a vaild date object');
382                 }
383                 cookieParts.push('Expires=' + cookie.expires.toGMTString());
384             }
385
386             // RFC 6265, Section 4.1.1
387             //'Max-Age=' non-zero-digit *DIGIT
388             if (cookie.maxage) {
389                 var maxage = cookie.maxage;
390                 if (typeof(maxage) === 'string') {
391                     maxage = parseInt(maxage, 10);
392                 }
393                 if (isNaN(maxage) || maxage <= 0 ) {
394                     this.reject(500);
395                     throw new Error('Value supplied for cookie "maxage" must be a non-zero number');
396                 }
397                 maxage = Math.round(maxage);
398                 cookieParts.push('Max-Age=' + maxage.toString(10));
399             }
400
401             // RFC 6265, Section 4.1.1
402             //'Secure;'
403             if (cookie.secure) {
404                 if (typeof(cookie.secure) !== 'boolean') {
405                     this.reject(500);
406                     throw new Error('Value supplied for cookie "secure" must be of type boolean');
407                 }
408                 cookieParts.push('Secure');
409             }
410
411             // RFC 6265, Section 4.1.1
412             //'HttpOnly;'
413             if (cookie.httponly) {
414                 if (typeof(cookie.httponly) !== 'boolean') {
415                     this.reject(500);
416                     throw new Error('Value supplied for cookie "httponly" must be of type boolean');
417                 }
418                 cookieParts.push('HttpOnly');
419             }
420
421             response += ('Set-Cookie: ' + cookieParts.join(';') + '\r\n');
422         }.bind(this));
423     }
424
425     // TODO: handle negotiated extensions
426     // if (negotiatedExtensions) {
427     //     response += 'Sec-WebSocket-Extensions: ' + negotiatedExtensions.join(', ') + '\r\n';
428     // }
429     
430     // Mark the request resolved now so that the user can't call accept or
431     // reject a second time.
432     this._resolved = true;
433     this.emit('requestResolved', this);
434     
435     response += '\r\n';
436
437     var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);
438     connection.webSocketVersion = this.webSocketVersion;
439     connection.remoteAddress = this.remoteAddress;
440     connection.remoteAddresses = this.remoteAddresses;
441     
442     var self = this;
443     
444     if (this._socketIsClosing) {
445         // Handle case when the client hangs up before we get a chance to
446         // accept the connection and send our side of the opening handshake.
447         cleanupFailedConnection(connection);
448     }
449     else {
450         this.socket.write(response, 'ascii', function(error) {
451             if (error) {
452                 cleanupFailedConnection(connection);
453                 return;
454             }
455             
456             self._removeSocketCloseListeners();
457             connection._addSocketEventListeners();
458         });
459     }
460
461     this.emit('requestAccepted', connection);
462     return connection;
463 };
464
465 WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
466     this._verifyResolution();
467     
468     // Mark the request resolved now so that the user can't call accept or
469     // reject a second time.
470     this._resolved = true;
471     this.emit('requestResolved', this);
472     
473     if (typeof(status) !== 'number') {
474         status = 403;
475     }
476     var response = 'HTTP/1.1 ' + status + ' ' + httpStatusDescriptions[status] + '\r\n' +
477                    'Connection: close\r\n';
478     if (reason) {
479         reason = reason.replace(headerSanitizeRegExp, '');
480         response += 'X-WebSocket-Reject-Reason: ' + reason + '\r\n';
481     }
482
483     if (extraHeaders) {
484         for (var key in extraHeaders) {
485             var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
486             var sanitizedKey = key.replace(headerSanitizeRegExp, '');
487             response += (sanitizedKey + ': ' + sanitizedValue + '\r\n');
488         }
489     }
490
491     response += '\r\n';
492     this.socket.end(response, 'ascii');
493
494     this.emit('requestRejected', this);
495 };
496
497 WebSocketRequest.prototype._handleSocketCloseBeforeAccept = function() {
498     this._socketIsClosing = true;
499     this._removeSocketCloseListeners();
500 };
501
502 WebSocketRequest.prototype._removeSocketCloseListeners = function() {
503     this.socket.removeListener('end', this._socketCloseHandler);
504     this.socket.removeListener('close', this._socketCloseHandler);
505 };
506
507 WebSocketRequest.prototype._verifyResolution = function() {
508     if (this._resolved) {
509         throw new Error('WebSocketRequest may only be accepted or rejected one time.');
510     }
511 };
512
513 function cleanupFailedConnection(connection) {
514     // Since we have to return a connection object even if the socket is
515     // already dead in order not to break the API, we schedule a 'close'
516     // event on the connection object to occur immediately.
517     process.nextTick(function() {
518         // WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
519         // Third param: Skip sending the close frame to a dead socket
520         connection.drop(1006, 'TCP connection lost before handshake completed.', true);
521     });
522 }
523
524 module.exports = WebSocketRequest;