11132Smax.romanov@nginx.com/************************************************************************ 21132Smax.romanov@nginx.com * Copyright 2010-2015 Brian McKelvey. 31132Smax.romanov@nginx.com * 41132Smax.romanov@nginx.com * Licensed under the Apache License, Version 2.0 (the "License"); 51132Smax.romanov@nginx.com * you may not use this file except in compliance with the License. 61132Smax.romanov@nginx.com * You may obtain a copy of the License at 71132Smax.romanov@nginx.com * 81132Smax.romanov@nginx.com * http://www.apache.org/licenses/LICENSE-2.0 91132Smax.romanov@nginx.com * 101132Smax.romanov@nginx.com * Unless required by applicable law or agreed to in writing, software 111132Smax.romanov@nginx.com * distributed under the License is distributed on an "AS IS" BASIS, 121132Smax.romanov@nginx.com * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 131132Smax.romanov@nginx.com * See the License for the specific language governing permissions and 141132Smax.romanov@nginx.com * limitations under the License. 151132Smax.romanov@nginx.com ***********************************************************************/ 161132Smax.romanov@nginx.com 171132Smax.romanov@nginx.comvar util = require('util'); 181132Smax.romanov@nginx.comvar url = require('url'); 191132Smax.romanov@nginx.comvar EventEmitter = require('events').EventEmitter; 201132Smax.romanov@nginx.comvar WebSocketConnection = require('./websocket_connection'); 211132Smax.romanov@nginx.com 221132Smax.romanov@nginx.comvar headerValueSplitRegExp = /,\s*/; 231132Smax.romanov@nginx.comvar headerParamSplitRegExp = /;\s*/; 241132Smax.romanov@nginx.comvar headerSanitizeRegExp = /[\r\n]/g; 251132Smax.romanov@nginx.comvar xForwardedForSeparatorRegExp = /,\s*/; 261132Smax.romanov@nginx.comvar separators = [ 271132Smax.romanov@nginx.com '(', ')', '<', '>', '@', 281132Smax.romanov@nginx.com ',', ';', ':', '\\', '\"', 291132Smax.romanov@nginx.com '/', '[', ']', '?', '=', 301132Smax.romanov@nginx.com '{', '}', ' ', String.fromCharCode(9) 311132Smax.romanov@nginx.com]; 321132Smax.romanov@nginx.comvar controlChars = [String.fromCharCode(127) /* DEL */]; 331132Smax.romanov@nginx.comfor (var i=0; i < 31; i ++) { 341132Smax.romanov@nginx.com /* US-ASCII Control Characters */ 351132Smax.romanov@nginx.com controlChars.push(String.fromCharCode(i)); 361132Smax.romanov@nginx.com} 371132Smax.romanov@nginx.com 381132Smax.romanov@nginx.comvar cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/; 391132Smax.romanov@nginx.comvar cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/; 401132Smax.romanov@nginx.comvar cookieValueDQuoteValidateRegEx = /^"[^"]*"$/; 411132Smax.romanov@nginx.comvar controlCharsAndSemicolonRegEx = /[\x00-\x20\x3b]/g; 421132Smax.romanov@nginx.com 431132Smax.romanov@nginx.comvar cookieSeparatorRegEx = /[;,] */; 441132Smax.romanov@nginx.com 451132Smax.romanov@nginx.comvar httpStatusDescriptions = { 461132Smax.romanov@nginx.com 100: 'Continue', 471132Smax.romanov@nginx.com 101: 'Switching Protocols', 481132Smax.romanov@nginx.com 200: 'OK', 491132Smax.romanov@nginx.com 201: 'Created', 501132Smax.romanov@nginx.com 203: 'Non-Authoritative Information', 511132Smax.romanov@nginx.com 204: 'No Content', 521132Smax.romanov@nginx.com 205: 'Reset Content', 531132Smax.romanov@nginx.com 206: 'Partial Content', 541132Smax.romanov@nginx.com 300: 'Multiple Choices', 551132Smax.romanov@nginx.com 301: 'Moved Permanently', 561132Smax.romanov@nginx.com 302: 'Found', 571132Smax.romanov@nginx.com 303: 'See Other', 581132Smax.romanov@nginx.com 304: 'Not Modified', 591132Smax.romanov@nginx.com 305: 'Use Proxy', 601132Smax.romanov@nginx.com 307: 'Temporary Redirect', 611132Smax.romanov@nginx.com 400: 'Bad Request', 621132Smax.romanov@nginx.com 401: 'Unauthorized', 631132Smax.romanov@nginx.com 402: 'Payment Required', 641132Smax.romanov@nginx.com 403: 'Forbidden', 651132Smax.romanov@nginx.com 404: 'Not Found', 661132Smax.romanov@nginx.com 406: 'Not Acceptable', 671132Smax.romanov@nginx.com 407: 'Proxy Authorization Required', 681132Smax.romanov@nginx.com 408: 'Request Timeout', 691132Smax.romanov@nginx.com 409: 'Conflict', 701132Smax.romanov@nginx.com 410: 'Gone', 711132Smax.romanov@nginx.com 411: 'Length Required', 721132Smax.romanov@nginx.com 412: 'Precondition Failed', 731132Smax.romanov@nginx.com 413: 'Request Entity Too Long', 741132Smax.romanov@nginx.com 414: 'Request-URI Too Long', 751132Smax.romanov@nginx.com 415: 'Unsupported Media Type', 761132Smax.romanov@nginx.com 416: 'Requested Range Not Satisfiable', 771132Smax.romanov@nginx.com 417: 'Expectation Failed', 781132Smax.romanov@nginx.com 426: 'Upgrade Required', 791132Smax.romanov@nginx.com 500: 'Internal Server Error', 801132Smax.romanov@nginx.com 501: 'Not Implemented', 811132Smax.romanov@nginx.com 502: 'Bad Gateway', 821132Smax.romanov@nginx.com 503: 'Service Unavailable', 831132Smax.romanov@nginx.com 504: 'Gateway Timeout', 841132Smax.romanov@nginx.com 505: 'HTTP Version Not Supported' 851132Smax.romanov@nginx.com}; 861132Smax.romanov@nginx.com 871132Smax.romanov@nginx.comfunction WebSocketRequest(socket, httpRequest, serverConfig) { 881132Smax.romanov@nginx.com // Superclass Constructor 891132Smax.romanov@nginx.com EventEmitter.call(this); 901132Smax.romanov@nginx.com 911132Smax.romanov@nginx.com this.socket = socket; 921132Smax.romanov@nginx.com this.httpRequest = httpRequest; 931132Smax.romanov@nginx.com this.resource = httpRequest.url; 941132Smax.romanov@nginx.com this.remoteAddress = socket.remoteAddress; 951132Smax.romanov@nginx.com this.remoteAddresses = [this.remoteAddress]; 961132Smax.romanov@nginx.com this.serverConfig = serverConfig; 971132Smax.romanov@nginx.com 981132Smax.romanov@nginx.com // Watch for the underlying TCP socket closing before we call accept 991132Smax.romanov@nginx.com this._socketIsClosing = false; 1001132Smax.romanov@nginx.com this._socketCloseHandler = this._handleSocketCloseBeforeAccept.bind(this); 1011132Smax.romanov@nginx.com this.socket.on('end', this._socketCloseHandler); 1021132Smax.romanov@nginx.com this.socket.on('close', this._socketCloseHandler); 1031132Smax.romanov@nginx.com 1041132Smax.romanov@nginx.com this._resolved = false; 1051132Smax.romanov@nginx.com} 1061132Smax.romanov@nginx.com 1071132Smax.romanov@nginx.comutil.inherits(WebSocketRequest, EventEmitter); 1081132Smax.romanov@nginx.com 1091132Smax.romanov@nginx.comWebSocketRequest.prototype.readHandshake = function() { 1101132Smax.romanov@nginx.com var self = this; 1111132Smax.romanov@nginx.com var request = this.httpRequest; 1121132Smax.romanov@nginx.com 1131132Smax.romanov@nginx.com // Decode URL 1141132Smax.romanov@nginx.com this.resourceURL = url.parse(this.resource, true); 1151132Smax.romanov@nginx.com 1161132Smax.romanov@nginx.com this.host = request.headers['host']; 1171132Smax.romanov@nginx.com if (!this.host) { 1181132Smax.romanov@nginx.com throw new Error('Client must provide a Host header.'); 1191132Smax.romanov@nginx.com } 1201132Smax.romanov@nginx.com 1211132Smax.romanov@nginx.com this.key = request.headers['sec-websocket-key']; 1221132Smax.romanov@nginx.com if (!this.key) { 1231132Smax.romanov@nginx.com throw new Error('Client must provide a value for Sec-WebSocket-Key.'); 1241132Smax.romanov@nginx.com } 1251132Smax.romanov@nginx.com 1261132Smax.romanov@nginx.com this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10); 1271132Smax.romanov@nginx.com 1281132Smax.romanov@nginx.com if (!this.webSocketVersion || isNaN(this.webSocketVersion)) { 1291132Smax.romanov@nginx.com throw new Error('Client must provide a value for Sec-WebSocket-Version.'); 1301132Smax.romanov@nginx.com } 1311132Smax.romanov@nginx.com 1321132Smax.romanov@nginx.com switch (this.webSocketVersion) { 1331132Smax.romanov@nginx.com case 8: 1341132Smax.romanov@nginx.com case 13: 1351132Smax.romanov@nginx.com break; 1361132Smax.romanov@nginx.com default: 1371132Smax.romanov@nginx.com var e = new Error('Unsupported websocket client version: ' + this.webSocketVersion + 1381132Smax.romanov@nginx.com 'Only versions 8 and 13 are supported.'); 1391132Smax.romanov@nginx.com e.httpCode = 426; 1401132Smax.romanov@nginx.com e.headers = { 1411132Smax.romanov@nginx.com 'Sec-WebSocket-Version': '13' 1421132Smax.romanov@nginx.com }; 1431132Smax.romanov@nginx.com throw e; 1441132Smax.romanov@nginx.com } 1451132Smax.romanov@nginx.com 1461132Smax.romanov@nginx.com if (this.webSocketVersion === 13) { 1471132Smax.romanov@nginx.com this.origin = request.headers['origin']; 1481132Smax.romanov@nginx.com } 1491132Smax.romanov@nginx.com else if (this.webSocketVersion === 8) { 1501132Smax.romanov@nginx.com this.origin = request.headers['sec-websocket-origin']; 1511132Smax.romanov@nginx.com } 1521132Smax.romanov@nginx.com 1531132Smax.romanov@nginx.com // Protocol is optional. 1541132Smax.romanov@nginx.com var protocolString = request.headers['sec-websocket-protocol']; 1551132Smax.romanov@nginx.com this.protocolFullCaseMap = {}; 1561132Smax.romanov@nginx.com this.requestedProtocols = []; 1571132Smax.romanov@nginx.com if (protocolString) { 1581132Smax.romanov@nginx.com var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp); 1591132Smax.romanov@nginx.com requestedProtocolsFullCase.forEach(function(protocol) { 1601132Smax.romanov@nginx.com var lcProtocol = protocol.toLocaleLowerCase(); 1611132Smax.romanov@nginx.com self.requestedProtocols.push(lcProtocol); 1621132Smax.romanov@nginx.com self.protocolFullCaseMap[lcProtocol] = protocol; 1631132Smax.romanov@nginx.com }); 1641132Smax.romanov@nginx.com } 1651132Smax.romanov@nginx.com 1661132Smax.romanov@nginx.com if (!this.serverConfig.ignoreXForwardedFor && 1671132Smax.romanov@nginx.com request.headers['x-forwarded-for']) { 1681132Smax.romanov@nginx.com var immediatePeerIP = this.remoteAddress; 1691132Smax.romanov@nginx.com this.remoteAddresses = request.headers['x-forwarded-for'] 1701132Smax.romanov@nginx.com .split(xForwardedForSeparatorRegExp); 1711132Smax.romanov@nginx.com this.remoteAddresses.push(immediatePeerIP); 1721132Smax.romanov@nginx.com this.remoteAddress = this.remoteAddresses[0]; 1731132Smax.romanov@nginx.com } 1741132Smax.romanov@nginx.com 1751132Smax.romanov@nginx.com // Extensions are optional. 1761132Smax.romanov@nginx.com var extensionsString = request.headers['sec-websocket-extensions']; 1771132Smax.romanov@nginx.com this.requestedExtensions = this.parseExtensions(extensionsString); 1781132Smax.romanov@nginx.com 1791132Smax.romanov@nginx.com // Cookies are optional 1801132Smax.romanov@nginx.com var cookieString = request.headers['cookie']; 1811132Smax.romanov@nginx.com this.cookies = this.parseCookies(cookieString); 1821132Smax.romanov@nginx.com}; 1831132Smax.romanov@nginx.com 1841132Smax.romanov@nginx.comWebSocketRequest.prototype.parseExtensions = function(extensionsString) { 1851132Smax.romanov@nginx.com if (!extensionsString || extensionsString.length === 0) { 1861132Smax.romanov@nginx.com return []; 1871132Smax.romanov@nginx.com } 1881132Smax.romanov@nginx.com var extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp); 1891132Smax.romanov@nginx.com extensions.forEach(function(extension, index, array) { 1901132Smax.romanov@nginx.com var params = extension.split(headerParamSplitRegExp); 1911132Smax.romanov@nginx.com var extensionName = params[0]; 1921132Smax.romanov@nginx.com var extensionParams = params.slice(1); 1931132Smax.romanov@nginx.com extensionParams.forEach(function(rawParam, index, array) { 1941132Smax.romanov@nginx.com var arr = rawParam.split('='); 1951132Smax.romanov@nginx.com var obj = { 1961132Smax.romanov@nginx.com name: arr[0], 1971132Smax.romanov@nginx.com value: arr[1] 1981132Smax.romanov@nginx.com }; 1991132Smax.romanov@nginx.com array.splice(index, 1, obj); 2001132Smax.romanov@nginx.com }); 2011132Smax.romanov@nginx.com var obj = { 2021132Smax.romanov@nginx.com name: extensionName, 2031132Smax.romanov@nginx.com params: extensionParams 2041132Smax.romanov@nginx.com }; 2051132Smax.romanov@nginx.com array.splice(index, 1, obj); 2061132Smax.romanov@nginx.com }); 2071132Smax.romanov@nginx.com return extensions; 2081132Smax.romanov@nginx.com}; 2091132Smax.romanov@nginx.com 2101132Smax.romanov@nginx.com// This function adapted from node-cookie 2111132Smax.romanov@nginx.com// https://github.com/shtylman/node-cookie 2121132Smax.romanov@nginx.comWebSocketRequest.prototype.parseCookies = function(str) { 2131132Smax.romanov@nginx.com // Sanity Check 2141132Smax.romanov@nginx.com if (!str || typeof(str) !== 'string') { 2151132Smax.romanov@nginx.com return []; 2161132Smax.romanov@nginx.com } 2171132Smax.romanov@nginx.com 2181132Smax.romanov@nginx.com var cookies = []; 2191132Smax.romanov@nginx.com var pairs = str.split(cookieSeparatorRegEx); 2201132Smax.romanov@nginx.com 2211132Smax.romanov@nginx.com pairs.forEach(function(pair) { 2221132Smax.romanov@nginx.com var eq_idx = pair.indexOf('='); 2231132Smax.romanov@nginx.com if (eq_idx === -1) { 2241132Smax.romanov@nginx.com cookies.push({ 2251132Smax.romanov@nginx.com name: pair, 2261132Smax.romanov@nginx.com value: null 2271132Smax.romanov@nginx.com }); 2281132Smax.romanov@nginx.com return; 2291132Smax.romanov@nginx.com } 2301132Smax.romanov@nginx.com 2311132Smax.romanov@nginx.com var key = pair.substr(0, eq_idx).trim(); 2321132Smax.romanov@nginx.com var val = pair.substr(++eq_idx, pair.length).trim(); 2331132Smax.romanov@nginx.com 2341132Smax.romanov@nginx.com // quoted values 2351132Smax.romanov@nginx.com if ('"' === val[0]) { 2361132Smax.romanov@nginx.com val = val.slice(1, -1); 2371132Smax.romanov@nginx.com } 2381132Smax.romanov@nginx.com 2391132Smax.romanov@nginx.com cookies.push({ 2401132Smax.romanov@nginx.com name: key, 2411132Smax.romanov@nginx.com value: decodeURIComponent(val) 2421132Smax.romanov@nginx.com }); 2431132Smax.romanov@nginx.com }); 2441132Smax.romanov@nginx.com 2451132Smax.romanov@nginx.com return cookies; 2461132Smax.romanov@nginx.com}; 2471132Smax.romanov@nginx.com 2481132Smax.romanov@nginx.comWebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) { 2491132Smax.romanov@nginx.com this._verifyResolution(); 250*2617Szelenkov@nginx.com 2511132Smax.romanov@nginx.com // TODO: Handle extensions 2521132Smax.romanov@nginx.com 2531132Smax.romanov@nginx.com var protocolFullCase; 2541132Smax.romanov@nginx.com 2551132Smax.romanov@nginx.com if (acceptedProtocol) { 2561132Smax.romanov@nginx.com protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()]; 2571132Smax.romanov@nginx.com if (typeof(protocolFullCase) === 'undefined') { 2581132Smax.romanov@nginx.com protocolFullCase = acceptedProtocol; 2591132Smax.romanov@nginx.com } 2601132Smax.romanov@nginx.com } 2611132Smax.romanov@nginx.com else { 2621132Smax.romanov@nginx.com protocolFullCase = acceptedProtocol; 2631132Smax.romanov@nginx.com } 2641132Smax.romanov@nginx.com this.protocolFullCaseMap = null; 2651132Smax.romanov@nginx.com 2661132Smax.romanov@nginx.com var response = this.httpRequest._response; 2671132Smax.romanov@nginx.com response.statusCode = 101; 2681132Smax.romanov@nginx.com 2691132Smax.romanov@nginx.com if (protocolFullCase) { 2701132Smax.romanov@nginx.com // validate protocol 2711132Smax.romanov@nginx.com for (var i=0; i < protocolFullCase.length; i++) { 2721132Smax.romanov@nginx.com var charCode = protocolFullCase.charCodeAt(i); 2731132Smax.romanov@nginx.com var character = protocolFullCase.charAt(i); 2741132Smax.romanov@nginx.com if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) { 2751132Smax.romanov@nginx.com this.reject(500); 2761132Smax.romanov@nginx.com throw new Error('Illegal character "' + String.fromCharCode(character) + '" in subprotocol.'); 2771132Smax.romanov@nginx.com } 2781132Smax.romanov@nginx.com } 2791132Smax.romanov@nginx.com if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) { 2801132Smax.romanov@nginx.com this.reject(500); 2811132Smax.romanov@nginx.com throw new Error('Specified protocol was not requested by the client.'); 2821132Smax.romanov@nginx.com } 2831132Smax.romanov@nginx.com 2841132Smax.romanov@nginx.com protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, ''); 2851132Smax.romanov@nginx.com response += 'Sec-WebSocket-Protocol: ' + protocolFullCase + '\r\n'; 2861132Smax.romanov@nginx.com } 2871132Smax.romanov@nginx.com this.requestedProtocols = null; 2881132Smax.romanov@nginx.com 2891132Smax.romanov@nginx.com if (allowedOrigin) { 2901132Smax.romanov@nginx.com allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, ''); 2911132Smax.romanov@nginx.com if (this.webSocketVersion === 13) { 2921132Smax.romanov@nginx.com response.setHeader('Origin', allowedOrigin); 2931132Smax.romanov@nginx.com } 2941132Smax.romanov@nginx.com else if (this.webSocketVersion === 8) { 2951132Smax.romanov@nginx.com response.setHeader('Sec-WebSocket-Origin', allowedOrigin); 2961132Smax.romanov@nginx.com } 2971132Smax.romanov@nginx.com } 2981132Smax.romanov@nginx.com 2991132Smax.romanov@nginx.com if (cookies) { 3001132Smax.romanov@nginx.com if (!Array.isArray(cookies)) { 3011132Smax.romanov@nginx.com this.reject(500); 3021132Smax.romanov@nginx.com throw new Error('Value supplied for "cookies" argument must be an array.'); 3031132Smax.romanov@nginx.com } 3041132Smax.romanov@nginx.com var seenCookies = {}; 3051132Smax.romanov@nginx.com cookies.forEach(function(cookie) { 3061132Smax.romanov@nginx.com if (!cookie.name || !cookie.value) { 3071132Smax.romanov@nginx.com this.reject(500); 3081132Smax.romanov@nginx.com throw new Error('Each cookie to set must at least provide a "name" and "value"'); 3091132Smax.romanov@nginx.com } 3101132Smax.romanov@nginx.com 3111132Smax.romanov@nginx.com // Make sure there are no \r\n sequences inserted 3121132Smax.romanov@nginx.com cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, ''); 3131132Smax.romanov@nginx.com cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, ''); 3141132Smax.romanov@nginx.com 3151132Smax.romanov@nginx.com if (seenCookies[cookie.name]) { 3161132Smax.romanov@nginx.com this.reject(500); 3171132Smax.romanov@nginx.com throw new Error('You may not specify the same cookie name twice.'); 3181132Smax.romanov@nginx.com } 3191132Smax.romanov@nginx.com seenCookies[cookie.name] = true; 3201132Smax.romanov@nginx.com 3211132Smax.romanov@nginx.com // token (RFC 2616, Section 2.2) 3221132Smax.romanov@nginx.com var invalidChar = cookie.name.match(cookieNameValidateRegEx); 3231132Smax.romanov@nginx.com if (invalidChar) { 3241132Smax.romanov@nginx.com this.reject(500); 3251132Smax.romanov@nginx.com throw new Error('Illegal character ' + invalidChar[0] + ' in cookie name'); 3261132Smax.romanov@nginx.com } 3271132Smax.romanov@nginx.com 3281132Smax.romanov@nginx.com // RFC 6265, Section 4.1.1 3291132Smax.romanov@nginx.com // *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E 3301132Smax.romanov@nginx.com if (cookie.value.match(cookieValueDQuoteValidateRegEx)) { 3311132Smax.romanov@nginx.com invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx); 3321132Smax.romanov@nginx.com } else { 3331132Smax.romanov@nginx.com invalidChar = cookie.value.match(cookieValueValidateRegEx); 3341132Smax.romanov@nginx.com } 3351132Smax.romanov@nginx.com if (invalidChar) { 3361132Smax.romanov@nginx.com this.reject(500); 3371132Smax.romanov@nginx.com throw new Error('Illegal character ' + invalidChar[0] + ' in cookie value'); 3381132Smax.romanov@nginx.com } 3391132Smax.romanov@nginx.com 3401132Smax.romanov@nginx.com var cookieParts = [cookie.name + '=' + cookie.value]; 3411132Smax.romanov@nginx.com 3421132Smax.romanov@nginx.com // RFC 6265, Section 4.1.1 3431132Smax.romanov@nginx.com // 'Path=' path-value | <any CHAR except CTLs or ';'> 3441132Smax.romanov@nginx.com if(cookie.path){ 3451132Smax.romanov@nginx.com invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx); 3461132Smax.romanov@nginx.com if (invalidChar) { 3471132Smax.romanov@nginx.com this.reject(500); 3481132Smax.romanov@nginx.com throw new Error('Illegal character ' + invalidChar[0] + ' in cookie path'); 3491132Smax.romanov@nginx.com } 3501132Smax.romanov@nginx.com cookieParts.push('Path=' + cookie.path); 3511132Smax.romanov@nginx.com } 3521132Smax.romanov@nginx.com 3531132Smax.romanov@nginx.com // RFC 6265, Section 4.1.2.3 3541132Smax.romanov@nginx.com // 'Domain=' subdomain 3551132Smax.romanov@nginx.com if (cookie.domain) { 3561132Smax.romanov@nginx.com if (typeof(cookie.domain) !== 'string') { 3571132Smax.romanov@nginx.com this.reject(500); 3581132Smax.romanov@nginx.com throw new Error('Domain must be specified and must be a string.'); 3591132Smax.romanov@nginx.com } 3601132Smax.romanov@nginx.com invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx); 3611132Smax.romanov@nginx.com if (invalidChar) { 3621132Smax.romanov@nginx.com this.reject(500); 3631132Smax.romanov@nginx.com throw new Error('Illegal character ' + invalidChar[0] + ' in cookie domain'); 3641132Smax.romanov@nginx.com } 3651132Smax.romanov@nginx.com cookieParts.push('Domain=' + cookie.domain.toLowerCase()); 3661132Smax.romanov@nginx.com } 3671132Smax.romanov@nginx.com 3681132Smax.romanov@nginx.com // RFC 6265, Section 4.1.1 3691132Smax.romanov@nginx.com //'Expires=' sane-cookie-date | Force Date object requirement by using only epoch 3701132Smax.romanov@nginx.com if (cookie.expires) { 3711132Smax.romanov@nginx.com if (!(cookie.expires instanceof Date)){ 3721132Smax.romanov@nginx.com this.reject(500); 3731132Smax.romanov@nginx.com throw new Error('Value supplied for cookie "expires" must be a vaild date object'); 3741132Smax.romanov@nginx.com } 3751132Smax.romanov@nginx.com cookieParts.push('Expires=' + cookie.expires.toGMTString()); 3761132Smax.romanov@nginx.com } 3771132Smax.romanov@nginx.com 3781132Smax.romanov@nginx.com // RFC 6265, Section 4.1.1 3791132Smax.romanov@nginx.com //'Max-Age=' non-zero-digit *DIGIT 3801132Smax.romanov@nginx.com if (cookie.maxage) { 3811132Smax.romanov@nginx.com var maxage = cookie.maxage; 3821132Smax.romanov@nginx.com if (typeof(maxage) === 'string') { 3831132Smax.romanov@nginx.com maxage = parseInt(maxage, 10); 3841132Smax.romanov@nginx.com } 3851132Smax.romanov@nginx.com if (isNaN(maxage) || maxage <= 0 ) { 3861132Smax.romanov@nginx.com this.reject(500); 3871132Smax.romanov@nginx.com throw new Error('Value supplied for cookie "maxage" must be a non-zero number'); 3881132Smax.romanov@nginx.com } 3891132Smax.romanov@nginx.com maxage = Math.round(maxage); 3901132Smax.romanov@nginx.com cookieParts.push('Max-Age=' + maxage.toString(10)); 3911132Smax.romanov@nginx.com } 3921132Smax.romanov@nginx.com 3931132Smax.romanov@nginx.com // RFC 6265, Section 4.1.1 3941132Smax.romanov@nginx.com //'Secure;' 3951132Smax.romanov@nginx.com if (cookie.secure) { 3961132Smax.romanov@nginx.com if (typeof(cookie.secure) !== 'boolean') { 3971132Smax.romanov@nginx.com this.reject(500); 3981132Smax.romanov@nginx.com throw new Error('Value supplied for cookie "secure" must be of type boolean'); 3991132Smax.romanov@nginx.com } 4001132Smax.romanov@nginx.com cookieParts.push('Secure'); 4011132Smax.romanov@nginx.com } 4021132Smax.romanov@nginx.com 4031132Smax.romanov@nginx.com // RFC 6265, Section 4.1.1 4041132Smax.romanov@nginx.com //'HttpOnly;' 4051132Smax.romanov@nginx.com if (cookie.httponly) { 4061132Smax.romanov@nginx.com if (typeof(cookie.httponly) !== 'boolean') { 4071132Smax.romanov@nginx.com this.reject(500); 4081132Smax.romanov@nginx.com throw new Error('Value supplied for cookie "httponly" must be of type boolean'); 4091132Smax.romanov@nginx.com } 4101132Smax.romanov@nginx.com cookieParts.push('HttpOnly'); 4111132Smax.romanov@nginx.com } 4121132Smax.romanov@nginx.com 4131132Smax.romanov@nginx.com response.addHeader('Set-Cookie', cookieParts.join(';')); 4141132Smax.romanov@nginx.com }.bind(this)); 4151132Smax.romanov@nginx.com } 4161132Smax.romanov@nginx.com 4171132Smax.romanov@nginx.com // TODO: handle negotiated extensions 4181132Smax.romanov@nginx.com // if (negotiatedExtensions) { 4191132Smax.romanov@nginx.com // response += 'Sec-WebSocket-Extensions: ' + negotiatedExtensions.join(', ') + '\r\n'; 4201132Smax.romanov@nginx.com // } 421*2617Szelenkov@nginx.com 4221132Smax.romanov@nginx.com // Mark the request resolved now so that the user can't call accept or 4231132Smax.romanov@nginx.com // reject a second time. 4241132Smax.romanov@nginx.com this._resolved = true; 4251132Smax.romanov@nginx.com this.emit('requestResolved', this); 4261132Smax.romanov@nginx.com 4271132Smax.romanov@nginx.com var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig); 4281132Smax.romanov@nginx.com connection.webSocketVersion = this.webSocketVersion; 4291132Smax.romanov@nginx.com connection.remoteAddress = this.remoteAddress; 4301132Smax.romanov@nginx.com connection.remoteAddresses = this.remoteAddresses; 4311132Smax.romanov@nginx.com 4321132Smax.romanov@nginx.com var self = this; 4331132Smax.romanov@nginx.com 4341132Smax.romanov@nginx.com if (this._socketIsClosing) { 4351132Smax.romanov@nginx.com // Handle case when the client hangs up before we get a chance to 4361132Smax.romanov@nginx.com // accept the connection and send our side of the opening handshake. 4371132Smax.romanov@nginx.com cleanupFailedConnection(connection); 4381132Smax.romanov@nginx.com 4391132Smax.romanov@nginx.com } else { 4401132Smax.romanov@nginx.com response._sendHeaders(); 4411132Smax.romanov@nginx.com connection._addSocketEventListeners(); 4421132Smax.romanov@nginx.com } 4431132Smax.romanov@nginx.com 4441132Smax.romanov@nginx.com this.emit('requestAccepted', connection); 4451132Smax.romanov@nginx.com return connection; 4461132Smax.romanov@nginx.com}; 4471132Smax.romanov@nginx.com 4481132Smax.romanov@nginx.comWebSocketRequest.prototype.reject = function(status, reason, extraHeaders) { 4491132Smax.romanov@nginx.com this._verifyResolution(); 450*2617Szelenkov@nginx.com 4511132Smax.romanov@nginx.com // Mark the request resolved now so that the user can't call accept or 4521132Smax.romanov@nginx.com // reject a second time. 4531132Smax.romanov@nginx.com this._resolved = true; 4541132Smax.romanov@nginx.com this.emit('requestResolved', this); 455*2617Szelenkov@nginx.com 4561132Smax.romanov@nginx.com if (typeof(status) !== 'number') { 4571132Smax.romanov@nginx.com status = 403; 4581132Smax.romanov@nginx.com } 4591132Smax.romanov@nginx.com 4601132Smax.romanov@nginx.com var response = this.httpRequest._response; 4611132Smax.romanov@nginx.com 4621132Smax.romanov@nginx.com response.statusCode = status; 4631132Smax.romanov@nginx.com 4641132Smax.romanov@nginx.com if (reason) { 4651132Smax.romanov@nginx.com reason = reason.replace(headerSanitizeRegExp, ''); 4661132Smax.romanov@nginx.com response.addHeader('X-WebSocket-Reject-Reason', reason); 4671132Smax.romanov@nginx.com } 4681132Smax.romanov@nginx.com 4691132Smax.romanov@nginx.com if (extraHeaders) { 4701132Smax.romanov@nginx.com for (var key in extraHeaders) { 4711132Smax.romanov@nginx.com var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, ''); 4721132Smax.romanov@nginx.com var sanitizedKey = key.replace(headerSanitizeRegExp, ''); 4731132Smax.romanov@nginx.com response += (sanitizedKey + ': ' + sanitizedValue + '\r\n'); 4741132Smax.romanov@nginx.com } 4751132Smax.romanov@nginx.com } 4761132Smax.romanov@nginx.com 4771132Smax.romanov@nginx.com response.end(); 4781132Smax.romanov@nginx.com 4791132Smax.romanov@nginx.com this.emit('requestRejected', this); 4801132Smax.romanov@nginx.com}; 4811132Smax.romanov@nginx.com 4821132Smax.romanov@nginx.comWebSocketRequest.prototype._handleSocketCloseBeforeAccept = function() { 4831132Smax.romanov@nginx.com this._socketIsClosing = true; 4841132Smax.romanov@nginx.com this._removeSocketCloseListeners(); 4851132Smax.romanov@nginx.com}; 4861132Smax.romanov@nginx.com 4871132Smax.romanov@nginx.comWebSocketRequest.prototype._removeSocketCloseListeners = function() { 4881132Smax.romanov@nginx.com this.socket.removeListener('end', this._socketCloseHandler); 4891132Smax.romanov@nginx.com this.socket.removeListener('close', this._socketCloseHandler); 4901132Smax.romanov@nginx.com}; 4911132Smax.romanov@nginx.com 4921132Smax.romanov@nginx.comWebSocketRequest.prototype._verifyResolution = function() { 4931132Smax.romanov@nginx.com if (this._resolved) { 4941132Smax.romanov@nginx.com throw new Error('WebSocketRequest may only be accepted or rejected one time.'); 4951132Smax.romanov@nginx.com } 4961132Smax.romanov@nginx.com}; 4971132Smax.romanov@nginx.com 4981132Smax.romanov@nginx.comfunction cleanupFailedConnection(connection) { 4991132Smax.romanov@nginx.com // Since we have to return a connection object even if the socket is 5001132Smax.romanov@nginx.com // already dead in order not to break the API, we schedule a 'close' 5011132Smax.romanov@nginx.com // event on the connection object to occur immediately. 5021132Smax.romanov@nginx.com process.nextTick(function() { 5031132Smax.romanov@nginx.com // WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006 5041132Smax.romanov@nginx.com // Third param: Skip sending the close frame to a dead socket 5051132Smax.romanov@nginx.com connection.drop(1006, 'TCP connection lost before handshake completed.', true); 5061132Smax.romanov@nginx.com }); 5071132Smax.romanov@nginx.com} 5081132Smax.romanov@nginx.com 5091132Smax.romanov@nginx.commodule.exports = WebSocketRequest; 510