WebSocketRequest.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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. if (this.serverConfig.parseExtensions) {
  157. var extensionsString = request.headers['sec-websocket-extensions'];
  158. this.requestedExtensions = this.parseExtensions(extensionsString);
  159. } else {
  160. this.requestedExtensions = [];
  161. }
  162. // Cookies are optional
  163. if (this.serverConfig.parseCookies) {
  164. var cookieString = request.headers['cookie'];
  165. this.cookies = this.parseCookies(cookieString);
  166. } else {
  167. this.cookies = [];
  168. }
  169. };
  170. WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
  171. if (!extensionsString || extensionsString.length === 0) {
  172. return [];
  173. }
  174. var extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
  175. extensions.forEach(function(extension, index, array) {
  176. var params = extension.split(headerParamSplitRegExp);
  177. var extensionName = params[0];
  178. var extensionParams = params.slice(1);
  179. extensionParams.forEach(function(rawParam, index, array) {
  180. var arr = rawParam.split('=');
  181. var obj = {
  182. name: arr[0],
  183. value: arr[1]
  184. };
  185. array.splice(index, 1, obj);
  186. });
  187. var obj = {
  188. name: extensionName,
  189. params: extensionParams
  190. };
  191. array.splice(index, 1, obj);
  192. });
  193. return extensions;
  194. };
  195. // This function adapted from node-cookie
  196. // https://github.com/shtylman/node-cookie
  197. WebSocketRequest.prototype.parseCookies = function(str) {
  198. // Sanity Check
  199. if (!str || typeof(str) !== 'string') {
  200. return [];
  201. }
  202. var cookies = [];
  203. var pairs = str.split(cookieSeparatorRegEx);
  204. pairs.forEach(function(pair) {
  205. var eq_idx = pair.indexOf('=');
  206. if (eq_idx === -1) {
  207. cookies.push({
  208. name: pair,
  209. value: null
  210. });
  211. return;
  212. }
  213. var key = pair.substr(0, eq_idx).trim();
  214. var val = pair.substr(++eq_idx, pair.length).trim();
  215. // quoted values
  216. if ('"' === val[0]) {
  217. val = val.slice(1, -1);
  218. }
  219. cookies.push({
  220. name: key,
  221. value: decodeURIComponent(val)
  222. });
  223. });
  224. return cookies;
  225. };
  226. WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
  227. this._verifyResolution();
  228. // TODO: Handle extensions
  229. var protocolFullCase;
  230. if (acceptedProtocol) {
  231. protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()];
  232. if (typeof(protocolFullCase) === 'undefined') {
  233. protocolFullCase = acceptedProtocol;
  234. }
  235. }
  236. else {
  237. protocolFullCase = acceptedProtocol;
  238. }
  239. this.protocolFullCaseMap = null;
  240. // Create key validation hash
  241. var sha1 = crypto.createHash('sha1');
  242. sha1.update(this.key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
  243. var acceptKey = sha1.digest('base64');
  244. var response = 'HTTP/1.1 101 Switching Protocols\r\n' +
  245. 'Upgrade: websocket\r\n' +
  246. 'Connection: Upgrade\r\n' +
  247. 'Sec-WebSocket-Accept: ' + acceptKey + '\r\n';
  248. if (protocolFullCase) {
  249. // validate protocol
  250. for (var i=0; i < protocolFullCase.length; i++) {
  251. var charCode = protocolFullCase.charCodeAt(i);
  252. var character = protocolFullCase.charAt(i);
  253. if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
  254. this.reject(500);
  255. throw new Error('Illegal character "' + String.fromCharCode(character) + '" in subprotocol.');
  256. }
  257. }
  258. if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
  259. this.reject(500);
  260. throw new Error('Specified protocol was not requested by the client.');
  261. }
  262. protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, '');
  263. response += 'Sec-WebSocket-Protocol: ' + protocolFullCase + '\r\n';
  264. }
  265. this.requestedProtocols = null;
  266. if (allowedOrigin) {
  267. allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
  268. if (this.webSocketVersion === 13) {
  269. response += 'Origin: ' + allowedOrigin + '\r\n';
  270. }
  271. else if (this.webSocketVersion === 8) {
  272. response += 'Sec-WebSocket-Origin: ' + allowedOrigin + '\r\n';
  273. }
  274. }
  275. if (cookies) {
  276. if (!Array.isArray(cookies)) {
  277. this.reject(500);
  278. throw new Error('Value supplied for "cookies" argument must be an array.');
  279. }
  280. var seenCookies = {};
  281. cookies.forEach(function(cookie) {
  282. if (!cookie.name || !cookie.value) {
  283. this.reject(500);
  284. throw new Error('Each cookie to set must at least provide a "name" and "value"');
  285. }
  286. // Make sure there are no \r\n sequences inserted
  287. cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, '');
  288. cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, '');
  289. if (seenCookies[cookie.name]) {
  290. this.reject(500);
  291. throw new Error('You may not specify the same cookie name twice.');
  292. }
  293. seenCookies[cookie.name] = true;
  294. // token (RFC 2616, Section 2.2)
  295. var invalidChar = cookie.name.match(cookieNameValidateRegEx);
  296. if (invalidChar) {
  297. this.reject(500);
  298. throw new Error('Illegal character ' + invalidChar[0] + ' in cookie name');
  299. }
  300. // RFC 6265, Section 4.1.1
  301. // *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
  302. if (cookie.value.match(cookieValueDQuoteValidateRegEx)) {
  303. invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx);
  304. } else {
  305. invalidChar = cookie.value.match(cookieValueValidateRegEx);
  306. }
  307. if (invalidChar) {
  308. this.reject(500);
  309. throw new Error('Illegal character ' + invalidChar[0] + ' in cookie value');
  310. }
  311. var cookieParts = [cookie.name + '=' + cookie.value];
  312. // RFC 6265, Section 4.1.1
  313. // 'Path=' path-value | <any CHAR except CTLs or ';'>
  314. if(cookie.path){
  315. invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx);
  316. if (invalidChar) {
  317. this.reject(500);
  318. throw new Error('Illegal character ' + invalidChar[0] + ' in cookie path');
  319. }
  320. cookieParts.push('Path=' + cookie.path);
  321. }
  322. // RFC 6265, Section 4.1.2.3
  323. // 'Domain=' subdomain
  324. if (cookie.domain) {
  325. if (typeof(cookie.domain) !== 'string') {
  326. this.reject(500);
  327. throw new Error('Domain must be specified and must be a string.');
  328. }
  329. invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx);
  330. if (invalidChar) {
  331. this.reject(500);
  332. throw new Error('Illegal character ' + invalidChar[0] + ' in cookie domain');
  333. }
  334. cookieParts.push('Domain=' + cookie.domain.toLowerCase());
  335. }
  336. // RFC 6265, Section 4.1.1
  337. //'Expires=' sane-cookie-date | Force Date object requirement by using only epoch
  338. if (cookie.expires) {
  339. if (!(cookie.expires instanceof Date)){
  340. this.reject(500);
  341. throw new Error('Value supplied for cookie "expires" must be a vaild date object');
  342. }
  343. cookieParts.push('Expires=' + cookie.expires.toGMTString());
  344. }
  345. // RFC 6265, Section 4.1.1
  346. //'Max-Age=' non-zero-digit *DIGIT
  347. if (cookie.maxage) {
  348. var maxage = cookie.maxage;
  349. if (typeof(maxage) === 'string') {
  350. maxage = parseInt(maxage, 10);
  351. }
  352. if (isNaN(maxage) || maxage <= 0 ) {
  353. this.reject(500);
  354. throw new Error('Value supplied for cookie "maxage" must be a non-zero number');
  355. }
  356. maxage = Math.round(maxage);
  357. cookieParts.push('Max-Age=' + maxage.toString(10));
  358. }
  359. // RFC 6265, Section 4.1.1
  360. //'Secure;'
  361. if (cookie.secure) {
  362. if (typeof(cookie.secure) !== 'boolean') {
  363. this.reject(500);
  364. throw new Error('Value supplied for cookie "secure" must be of type boolean');
  365. }
  366. cookieParts.push('Secure');
  367. }
  368. // RFC 6265, Section 4.1.1
  369. //'HttpOnly;'
  370. if (cookie.httponly) {
  371. if (typeof(cookie.httponly) !== 'boolean') {
  372. this.reject(500);
  373. throw new Error('Value supplied for cookie "httponly" must be of type boolean');
  374. }
  375. cookieParts.push('HttpOnly');
  376. }
  377. response += ('Set-Cookie: ' + cookieParts.join(';') + '\r\n');
  378. }.bind(this));
  379. }
  380. // TODO: handle negotiated extensions
  381. // if (negotiatedExtensions) {
  382. // response += 'Sec-WebSocket-Extensions: ' + negotiatedExtensions.join(', ') + '\r\n';
  383. // }
  384. // Mark the request resolved now so that the user can't call accept or
  385. // reject a second time.
  386. this._resolved = true;
  387. this.emit('requestResolved', this);
  388. response += '\r\n';
  389. var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);
  390. connection.webSocketVersion = this.webSocketVersion;
  391. connection.remoteAddress = this.remoteAddress;
  392. connection.remoteAddresses = this.remoteAddresses;
  393. var self = this;
  394. if (this._socketIsClosing) {
  395. // Handle case when the client hangs up before we get a chance to
  396. // accept the connection and send our side of the opening handshake.
  397. cleanupFailedConnection(connection);
  398. }
  399. else {
  400. this.socket.write(response, 'ascii', function(error) {
  401. if (error) {
  402. cleanupFailedConnection(connection);
  403. return;
  404. }
  405. self._removeSocketCloseListeners();
  406. connection._addSocketEventListeners();
  407. });
  408. }
  409. this.emit('requestAccepted', connection);
  410. return connection;
  411. };
  412. WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
  413. this._verifyResolution();
  414. // Mark the request resolved now so that the user can't call accept or
  415. // reject a second time.
  416. this._resolved = true;
  417. this.emit('requestResolved', this);
  418. if (typeof(status) !== 'number') {
  419. status = 403;
  420. }
  421. var response = 'HTTP/1.1 ' + status + ' ' + httpStatusDescriptions[status] + '\r\n' +
  422. 'Connection: close\r\n';
  423. if (reason) {
  424. reason = reason.replace(headerSanitizeRegExp, '');
  425. response += 'X-WebSocket-Reject-Reason: ' + reason + '\r\n';
  426. }
  427. if (extraHeaders) {
  428. for (var key in extraHeaders) {
  429. var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
  430. var sanitizedKey = key.replace(headerSanitizeRegExp, '');
  431. response += (sanitizedKey + ': ' + sanitizedValue + '\r\n');
  432. }
  433. }
  434. response += '\r\n';
  435. this.socket.end(response, 'ascii');
  436. this.emit('requestRejected', this);
  437. };
  438. WebSocketRequest.prototype._handleSocketCloseBeforeAccept = function() {
  439. this._socketIsClosing = true;
  440. this._removeSocketCloseListeners();
  441. };
  442. WebSocketRequest.prototype._removeSocketCloseListeners = function() {
  443. this.socket.removeListener('end', this._socketCloseHandler);
  444. this.socket.removeListener('close', this._socketCloseHandler);
  445. };
  446. WebSocketRequest.prototype._verifyResolution = function() {
  447. if (this._resolved) {
  448. throw new Error('WebSocketRequest may only be accepted or rejected one time.');
  449. }
  450. };
  451. function cleanupFailedConnection(connection) {
  452. // Since we have to return a connection object even if the socket is
  453. // already dead in order not to break the API, we schedule a 'close'
  454. // event on the connection object to occur immediately.
  455. process.nextTick(function() {
  456. // WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
  457. // Third param: Skip sending the close frame to a dead socket
  458. connection.drop(1006, 'TCP connection lost before handshake completed.', true);
  459. });
  460. }
  461. module.exports = WebSocketRequest;