syndilights/displayclienthtml/websocket_demo/node_modules/websocket/lib/WebSocketRequest.js

479 lines
17 KiB
JavaScript

/************************************************************************
* Copyright 2010-2011 Worlize Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***********************************************************************/
var deprecation = require('./Deprecation');
var crypto = require('crypto');
var util = require('util');
var url = require('url');
var EventEmitter = require('events').EventEmitter;
var WebSocketConnection = require('./WebSocketConnection');
var Constants = require('./Constants');
var headerValueSplitRegExp = /,\s*/;
var headerParamSplitRegExp = /;\s*/;
var headerSanitizeRegExp = /[\r\n]/g;
var separators = [
"(", ")", "<", ">", "@",
",", ";", ":", "\\", "\"",
"/", "[", "]", "?", "=",
"{", "}", " ", String.fromCharCode(9)
];
var controlChars = [String.fromCharCode(127) /* DEL */];
for (var i=0; i < 31; i ++) {
/* US-ASCII Control Characters */
controlChars.push(String.fromCharCode(i));
}
var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;
var cookieValueDQuoteValidateRegEx = /^"[^"]*"$/;
var controlCharsAndSemicolonRegEx = /[\x00-\x20\x3b]/g;
var cookieSeparatorRegEx = /[;,] */;
var httpStatusDescriptions = {
100: "Continue",
101: "Switching Protocols",
200: "OK",
201: "Created",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
406: "Not Acceptable",
407: "Proxy Authorization Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Long",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
426: "Upgrade Required",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported"
};
function WebSocketRequest(socket, httpRequest, serverConfig) {
this.socket = socket;
this.httpRequest = httpRequest;
this.resource = httpRequest.url;
this.remoteAddress = socket.remoteAddress;
this.serverConfig = serverConfig;
}
util.inherits(WebSocketRequest, EventEmitter);
WebSocketRequest.prototype.readHandshake = function() {
var self = this;
var request = this.httpRequest;
// Decode URL
this.resourceURL = url.parse(this.resource, true);
this.host = request.headers['host'];
if (!this.host) {
throw new Error("Client must provide a Host header.");
}
this.key = request.headers['sec-websocket-key'];
if (!this.key) {
throw new Error("Client must provide a value for Sec-WebSocket-Key.");
}
this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10);
// Deprecated websocketVersion (proper casing...)
Object.defineProperty(this, "websocketVersion", {
get: function() {
deprecation.warn('websocketVersion');
return this.webSocketVersion;
}
});
if (!this.webSocketVersion || isNaN(this.webSocketVersion)) {
throw new Error("Client must provide a value for Sec-WebSocket-Version.");
}
switch (this.webSocketVersion) {
case 8:
case 13:
break;
default:
var e = new Error("Unsupported websocket client version: " + this.webSocketVersion +
"Only versions 8 and 13 are supported.");
e.httpCode = 426;
e.headers = {
"Sec-WebSocket-Version": "13"
};
throw e;
}
if (this.webSocketVersion === 13) {
this.origin = request.headers['origin'];
}
else if (this.webSocketVersion === 8) {
this.origin = request.headers['sec-websocket-origin'];
}
// Protocol is optional.
var protocolString = request.headers['sec-websocket-protocol'];
this.protocolFullCaseMap = {};
this.requestedProtocols = [];
if (protocolString) {
var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp);
requestedProtocolsFullCase.forEach(function(protocol) {
var lcProtocol = protocol.toLocaleLowerCase();
self.requestedProtocols.push(lcProtocol);
self.protocolFullCaseMap[lcProtocol] = protocol;
});
}
if (request.headers['x-forwarded-for']) {
this.remoteAddress = request.headers['x-forwarded-for'].split(', ')[0];
}
// Extensions are optional.
var extensionsString = request.headers['sec-websocket-extensions'];
this.requestedExtensions = this.parseExtensions(extensionsString);
// Cookies are optional
var cookieString = request.headers['cookie'];
this.cookies = this.parseCookies(cookieString);
};
WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
if (!extensionsString || extensionsString.length === 0) {
return [];
}
extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
extensions.forEach(function(extension, index, array) {
var params = extension.split(headerParamSplitRegExp);
var extensionName = params[0];
var extensionParams = params.slice(1);
extensionParams.forEach(function(rawParam, index, array) {
var arr = rawParam.split('=');
var obj = {
name: arr[0],
value: arr[1]
};
array.splice(index, 1, obj);
});
var obj = {
name: extensionName,
params: extensionParams
};
array.splice(index, 1, obj);
});
return extensions;
};
// This function adapted from node-cookie
// https://github.com/shtylman/node-cookie
WebSocketRequest.prototype.parseCookies = function(str) {
// Sanity Check
if (!str || typeof(str) !== 'string') {
return [];
}
var cookies = [];
var pairs = str.split(cookieSeparatorRegEx);
pairs.forEach(function(pair) {
var eq_idx = pair.indexOf('=');
if (eq_idx === -1) {
cookies.push({
name: pair,
value: null
});
return;
}
var key = pair.substr(0, eq_idx).trim();
var val = pair.substr(++eq_idx, pair.length).trim();
// quoted values
if ('"' == val[0]) {
val = val.slice(1, -1);
}
cookies.push({
name: key,
value: decodeURIComponent(val)
});
});
return cookies;
};
WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
// TODO: Handle extensions
if (acceptedProtocol) {
var protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()];
if (typeof(protocolFullCase) === 'undefined') {
protocolFullCase = acceptedProtocol;
}
}
else {
protocolFullCase = acceptedProtocol;
}
this.protocolFullCaseMap = null;
var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);
connection.webSocketVersion = this.webSocketVersion;
// Deprecated websocketVersion (proper casing...)
Object.defineProperty(connection, "websocketVersion", {
get: function() {
deprecation.warn('websocketVersion');
return this.webSocketVersion;
}
});
connection.remoteAddress = this.remoteAddress;
// Create key validation hash
var sha1 = crypto.createHash('sha1');
sha1.update(this.key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
var acceptKey = sha1.digest('base64');
var response = "HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: " + acceptKey + "\r\n";
if (protocolFullCase) {
// validate protocol
for (var i=0; i < protocolFullCase.length; i++) {
var charCode = protocolFullCase.charCodeAt(i);
var character = protocolFullCase.charAt(i);
if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
this.reject(500);
throw new Error("Illegal character '" + String.fromCharCode(character) + "' in subprotocol.");
}
}
if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
this.reject(500);
throw new Error("Specified protocol was not requested by the client.");
}
protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, '');
response += "Sec-WebSocket-Protocol: " + protocolFullCase + "\r\n";
}
this.requestedProtocols = null;
if (allowedOrigin) {
allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
if (this.webSocketVersion === 13) {
response += "Origin: " + allowedOrigin + "\r\n";
}
else if (this.webSocketVersion === 8) {
response += "Sec-WebSocket-Origin: " + allowedOrigin + "\r\n";
}
}
if (cookies) {
if (!Array.isArray(cookies)) {
this.reject(500);
throw new Error("Value supplied for 'cookies' argument must be an array.");
}
var seenCookies = {};
cookies.forEach(function(cookie) {
if (!cookie.name || !cookie.value) {
this.reject(500);
throw new Error("Each cookie to set must at least provide a 'name' and 'value'");
}
// Make sure there are no \r\n sequences inserted
cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, '');
cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, '');
if (seenCookies[cookie.name]) {
this.reject(500);
throw new Error("You may not specify the same cookie name twice.");
}
seenCookies[cookie.name] = true;
// token (RFC 2616, Section 2.2)
var invalidChar = cookie.name.match(cookieNameValidateRegEx);
if (invalidChar) {
this.reject(500);
throw new Error("Illegal character " + invalidChar[0] + " in cookie name");
}
// RFC 6265, Section 4.1.1
// *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
if (cookie.value.match(cookieValueDQuoteValidateRegEx)) {
invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx);
} else {
invalidChar = cookie.value.match(cookieValueValidateRegEx);
}
if (invalidChar) {
this.reject(500);
throw new Error("Illegal character " + invalidChar[0] + " in cookie value");
}
var cookieParts = [cookie.name + "=" + cookie.value];
// RFC 6265, Section 4.1.1
// "Path=" path-value | <any CHAR except CTLs or ";">
if(cookie.path){
invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx);
if (invalidChar) {
this.reject(500);
throw new Error("Illegal character " + invalidChar[0] + " in cookie path");
}
cookieParts.push("Path=" + cookie.path);
}
// RFC 6265, Section 4.1.2.3
// "Domain=" subdomain
if (cookie.domain) {
if (typeof(cookie.domain) !== 'string') {
this.reject(500);
throw new Error("Domain must be specified and must be a string.");
}
var domain = cookie.domain.toLowerCase();
invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx);
if (invalidChar) {
this.reject(500);
throw new Error("Illegal character " + invalidChar[0] + " in cookie domain");
}
cookieParts.push("Domain=" + cookie.domain.toLowerCase());
}
// RFC 6265, Section 4.1.1
//"Expires=" sane-cookie-date | Force Date object requirement by using only epoch
if (cookie.expires) {
if (!(cookie.expires instanceof Date)){
this.reject(500);
throw new Error("Value supplied for cookie 'expires' must be a vaild date object");
}
cookieParts.push("Expires=" + cookie.expires.toGMTString());
}
// RFC 6265, Section 4.1.1
//"Max-Age=" non-zero-digit *DIGIT
if (cookie.maxage) {
var maxage = cookie.maxage;
if (typeof(maxage) === 'string') {
maxage = parseInt(maxage, 10);
}
if (isNaN(maxage) || maxage <= 0 ) {
this.reject(500);
throw new Error("Value supplied for cookie 'maxage' must be a non-zero number");
}
maxage = Math.round(maxage);
cookieParts.push("Max-Age=" + maxage.toString(10));
}
// RFC 6265, Section 4.1.1
//"Secure;"
if (cookie.secure) {
if (typeof(cookie.secure) !== "boolean") {
this.reject(500);
throw new Error("Value supplied for cookie 'secure' must be of type boolean");
}
cookieParts.push("Secure");
}
// RFC 6265, Section 4.1.1
//"HttpOnly;"
if (cookie.httponly) {
if (typeof(cookie.httponly) !== "boolean") {
this.reject(500);
throw new Error("Value supplied for cookie 'httponly' must be of type boolean");
}
cookieParts.push("HttpOnly");
}
response += ("Set-Cookie: " + cookieParts.join(';') + "\r\n");
}.bind(this));
}
// TODO: handle negotiated extensions
// if (negotiatedExtensions) {
// response += "Sec-WebSocket-Extensions: " + negotiatedExtensions.join(", ") + "\r\n";
// }
response += "\r\n";
try {
this.socket.write(response, 'ascii');
}
catch(e) {
if (Constants.DEBUG) {
console.log("Error Writing to Socket: " + e.toString());
}
// Since we have to return a connection object even if the socket is
// already dead in order not to break the API, we schedule a 'close'
// event on the connection object to occur immediately.
process.nextTick(function() {
// WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
// Third param: Skip sending the close frame to a dead socket
connection.drop(1006, "TCP connection lost before handshake completed.", true);
});
}
this.emit('requestAccepted', connection);
return connection;
};
WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
if (typeof(status) !== 'number') {
status = 403;
}
var response = "HTTP/1.1 " + status + " " + httpStatusDescriptions[status] + "\r\n" +
"Connection: close\r\n";
if (reason) {
reason = reason.replace(headerSanitizeRegExp, '');
response += "X-WebSocket-Reject-Reason: " + reason + "\r\n";
}
if (extraHeaders) {
for (var key in extraHeaders) {
var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
var sanitizedKey = key.replace(headerSanitizeRegExp, '');
response += (sanitizedKey + ": " + sanitizedValue + "\r\n");
}
}
response += "\r\n";
this.socket.end(response, 'ascii');
this.emit('requestRejected', this);
};
module.exports = WebSocketRequest;