WebSocketRequest.js 18 KB

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