websocket.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. var Client = require('../client')
  2. , Stream = require('net').Stream
  3. , EventEmitter = require('events').EventEmitter
  4. , url = require('url')
  5. , util = require(process.binding('natives').util ? 'util' : 'sys')
  6. , crypto = require('crypto');
  7. WebSocket = module.exports = function(){
  8. Client.apply(this, arguments);
  9. };
  10. util.inherits(WebSocket, Client);
  11. WebSocket.prototype._onConnect = function(req, socket){
  12. var self = this
  13. , headers = [];
  14. if (!req.connection.setTimeout){
  15. req.connection.end();
  16. return false;
  17. }
  18. this.parser = new Parser();
  19. this.parser.on('data', self._onMessage.bind(this));
  20. this.parser.on('error', self._onClose.bind(this));
  21. Client.prototype._onConnect.call(this, req);
  22. if (this.request.headers.upgrade !== 'WebSocket' || !this._verifyOrigin(this.request.headers.origin)){
  23. this.listener.options.log('WebSocket connection invalid or Origin not verified');
  24. this._onClose();
  25. return false;
  26. }
  27. var origin = this.request.headers.origin,
  28. location = (origin && origin.substr(0, 5) == 'https' ? 'wss' : 'ws')
  29. + '://' + this.request.headers.host + this.request.url;
  30. this.waitingForNonce = false;
  31. if ('sec-websocket-key1' in this.request.headers){
  32. /* We need to send the 101 response immediately when using Draft 76 with
  33. a load balancing proxy, such as HAProxy. In order to protect an
  34. unsuspecting non-websocket HTTP server, HAProxy will not send the
  35. 8-byte nonce through the connection until the Upgrade: WebSocket
  36. request has been confirmed by the WebSocket server by a 101 response
  37. indicating that the server can handle the upgraded protocol. We
  38. therefore must send the 101 response immediately, and then wait for
  39. the nonce to be forwarded to us afterward in order to finish the
  40. Draft 76 handshake.
  41. */
  42. // If we don't have the nonce yet, wait for it.
  43. if (!(this.upgradeHead && this.upgradeHead.length >= 8)) {
  44. this.waitingForNonce = true;
  45. }
  46. headers = [
  47. 'HTTP/1.1 101 WebSocket Protocol Handshake',
  48. 'Upgrade: WebSocket',
  49. 'Connection: Upgrade',
  50. 'Sec-WebSocket-Origin: ' + origin,
  51. 'Sec-WebSocket-Location: ' + location
  52. ];
  53. if ('sec-websocket-protocol' in this.request.headers){
  54. headers.push('Sec-WebSocket-Protocol: ' + this.request.headers['sec-websocket-protocol']);
  55. }
  56. } else {
  57. headers = [
  58. 'HTTP/1.1 101 Web Socket Protocol Handshake',
  59. 'Upgrade: WebSocket',
  60. 'Connection: Upgrade',
  61. 'WebSocket-Origin: ' + origin,
  62. 'WebSocket-Location: ' + location
  63. ];
  64. }
  65. try {
  66. this.connection.write(headers.concat('', '').join('\r\n'));
  67. this.connection.setTimeout(0);
  68. this.connection.setNoDelay(true);
  69. this.connection.setEncoding('utf-8');
  70. } catch(e){
  71. this._onClose();
  72. return;
  73. }
  74. if (this.waitingForNonce) {
  75. // Since we will be receiving the binary nonce through the normal HTTP
  76. // data event, set the connection to 'binary' temporarily
  77. this.connection.setEncoding('binary');
  78. this._headers = headers;
  79. }
  80. else {
  81. if (this._proveReception(headers)) this._payload();
  82. }
  83. this.buffer = "";
  84. this.connection.addListener('data', function(data){
  85. if (self.waitingForNonce) {
  86. self.buffer += data;
  87. if (self.buffer.length < 8) { return; }
  88. // Restore the connection to utf8 encoding after receiving the nonce
  89. self.connection.setEncoding('utf8');
  90. self.waitingForNonce = false;
  91. // Stuff the nonce into the location where it's expected to be
  92. self.upgradeHead = self.buffer.substr(0,8);
  93. self.buffer = '';
  94. if (self._proveReception(self._headers)) { self._payload(); }
  95. return;
  96. }
  97. self.parser.add(data);
  98. });
  99. };
  100. // http://www.whatwg.org/specs/web-apps/current-work/complete/network.html#opening-handshake
  101. WebSocket.prototype._proveReception = function(headers){
  102. var self = this
  103. , k1 = this.request.headers['sec-websocket-key1']
  104. , k2 = this.request.headers['sec-websocket-key2'];
  105. if (k1 && k2){
  106. var md5 = crypto.createHash('md5');
  107. [k1, k2].forEach(function(k){
  108. var n = parseInt(k.replace(/[^\d]/g, '')),
  109. spaces = k.replace(/[^ ]/g, '').length;
  110. if (spaces === 0 || n % spaces !== 0){
  111. self.listener.options.log('Invalid WebSocket key: "' + k + '". Dropping connection');
  112. self._onClose();
  113. return false;
  114. }
  115. n /= spaces;
  116. md5.update(String.fromCharCode(
  117. n >> 24 & 0xFF,
  118. n >> 16 & 0xFF,
  119. n >> 8 & 0xFF,
  120. n & 0xFF));
  121. });
  122. md5.update(this.upgradeHead.toString('binary'));
  123. try {
  124. this.connection.write(md5.digest('binary'), 'binary');
  125. } catch(e){
  126. this._onClose();
  127. }
  128. }
  129. return true;
  130. };
  131. WebSocket.prototype._write = function(message){
  132. try {
  133. this.connection.write('\u0000', 'binary');
  134. this.connection.write(message, 'utf8');
  135. this.connection.write('\uffff', 'binary');
  136. } catch(e){
  137. this._onClose();
  138. }
  139. };
  140. WebSocket.httpUpgrade = true;
  141. function Parser(){
  142. this.buffer = '';
  143. this.i = 0;
  144. };
  145. Parser.prototype.__proto__ = EventEmitter.prototype;
  146. Parser.prototype.add = function(data){
  147. this.buffer += data;
  148. this.parse();
  149. };
  150. Parser.prototype.parse = function(){
  151. for (var i = this.i, chr, l = this.buffer.length; i < l; i++){
  152. chr = this.buffer[i];
  153. if (i === 0){
  154. if (chr != '\u0000')
  155. this.error('Bad framing. Expected null byte as first frame');
  156. else
  157. continue;
  158. }
  159. if (chr == '\ufffd'){
  160. this.emit('data', this.buffer.substr(1, this.buffer.length - 2));
  161. this.buffer = this.buffer.substr(i + 1);
  162. this.i = 0;
  163. return this.parse();
  164. }
  165. }
  166. };
  167. Parser.prototype.error = function(reason){
  168. this.buffer = '';
  169. this.i = 0;
  170. this.emit('error', reason);
  171. return this;
  172. };