xref: /unit/src/nodejs/unit-http/websocket_request.js (revision 2617:18a10bb7346d)
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