WebSocketClient.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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 utils = require('./utils');
  17. var extend = utils.extend;
  18. var util = require('util');
  19. var EventEmitter = require('events').EventEmitter;
  20. var http = require('http');
  21. var https = require('https');
  22. var url = require('url');
  23. var crypto = require('crypto');
  24. var WebSocketConnection = require('./WebSocketConnection');
  25. var bufferAllocUnsafe = utils.bufferAllocUnsafe;
  26. var protocolSeparators = [
  27. '(', ')', '<', '>', '@',
  28. ',', ';', ':', '\\', '\"',
  29. '/', '[', ']', '?', '=',
  30. '{', '}', ' ', String.fromCharCode(9)
  31. ];
  32. var excludedTlsOptions = ['hostname','port','method','path','headers'];
  33. function WebSocketClient(config) {
  34. // Superclass Constructor
  35. EventEmitter.call(this);
  36. // TODO: Implement extensions
  37. this.config = {
  38. // 1MiB max frame size.
  39. maxReceivedFrameSize: 0x100000,
  40. // 8MiB max message size, only applicable if
  41. // assembleFragments is true
  42. maxReceivedMessageSize: 0x800000,
  43. // Outgoing messages larger than fragmentationThreshold will be
  44. // split into multiple fragments.
  45. fragmentOutgoingMessages: true,
  46. // Outgoing frames are fragmented if they exceed this threshold.
  47. // Default is 16KiB
  48. fragmentationThreshold: 0x4000,
  49. // Which version of the protocol to use for this session. This
  50. // option will be removed once the protocol is finalized by the IETF
  51. // It is only available to ease the transition through the
  52. // intermediate draft protocol versions.
  53. // At present, it only affects the name of the Origin header.
  54. webSocketVersion: 13,
  55. // If true, fragmented messages will be automatically assembled
  56. // and the full message will be emitted via a 'message' event.
  57. // If false, each frame will be emitted via a 'frame' event and
  58. // the application will be responsible for aggregating multiple
  59. // fragmented frames. Single-frame messages will emit a 'message'
  60. // event in addition to the 'frame' event.
  61. // Most users will want to leave this set to 'true'
  62. assembleFragments: true,
  63. // The Nagle Algorithm makes more efficient use of network resources
  64. // by introducing a small delay before sending small packets so that
  65. // multiple messages can be batched together before going onto the
  66. // wire. This however comes at the cost of latency, so the default
  67. // is to disable it. If you don't need low latency and are streaming
  68. // lots of small messages, you can change this to 'false'
  69. disableNagleAlgorithm: true,
  70. // The number of milliseconds to wait after sending a close frame
  71. // for an acknowledgement to come back before giving up and just
  72. // closing the socket.
  73. closeTimeout: 5000,
  74. // Options to pass to https.connect if connecting via TLS
  75. tlsOptions: {}
  76. };
  77. if (config) {
  78. var tlsOptions;
  79. if (config.tlsOptions) {
  80. tlsOptions = config.tlsOptions;
  81. delete config.tlsOptions;
  82. }
  83. else {
  84. tlsOptions = {};
  85. }
  86. extend(this.config, config);
  87. extend(this.config.tlsOptions, tlsOptions);
  88. }
  89. this._req = null;
  90. switch (this.config.webSocketVersion) {
  91. case 8:
  92. case 13:
  93. break;
  94. default:
  95. throw new Error('Requested webSocketVersion is not supported. Allowed values are 8 and 13.');
  96. }
  97. }
  98. util.inherits(WebSocketClient, EventEmitter);
  99. WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, headers, extraRequestOptions) {
  100. var self = this;
  101. if (typeof(protocols) === 'string') {
  102. if (protocols.length > 0) {
  103. protocols = [protocols];
  104. }
  105. else {
  106. protocols = [];
  107. }
  108. }
  109. if (!(protocols instanceof Array)) {
  110. protocols = [];
  111. }
  112. this.protocols = protocols;
  113. this.origin = origin;
  114. if (typeof(requestUrl) === 'string') {
  115. this.url = url.parse(requestUrl);
  116. }
  117. else {
  118. this.url = requestUrl; // in case an already parsed url is passed in.
  119. }
  120. if (!this.url.protocol) {
  121. throw new Error('You must specify a full WebSocket URL, including protocol.');
  122. }
  123. if (!this.url.host) {
  124. throw new Error('You must specify a full WebSocket URL, including hostname. Relative URLs are not supported.');
  125. }
  126. this.secure = (this.url.protocol === 'wss:');
  127. // validate protocol characters:
  128. this.protocols.forEach(function(protocol) {
  129. for (var i=0; i < protocol.length; i ++) {
  130. var charCode = protocol.charCodeAt(i);
  131. var character = protocol.charAt(i);
  132. if (charCode < 0x0021 || charCode > 0x007E || protocolSeparators.indexOf(character) !== -1) {
  133. throw new Error('Protocol list contains invalid character "' + String.fromCharCode(charCode) + '"');
  134. }
  135. }
  136. });
  137. var defaultPorts = {
  138. 'ws:': '80',
  139. 'wss:': '443'
  140. };
  141. if (!this.url.port) {
  142. this.url.port = defaultPorts[this.url.protocol];
  143. }
  144. var nonce = bufferAllocUnsafe(16);
  145. for (var i=0; i < 16; i++) {
  146. nonce[i] = Math.round(Math.random()*0xFF);
  147. }
  148. this.base64nonce = nonce.toString('base64');
  149. var hostHeaderValue = this.url.hostname;
  150. if ((this.url.protocol === 'ws:' && this.url.port !== '80') ||
  151. (this.url.protocol === 'wss:' && this.url.port !== '443')) {
  152. hostHeaderValue += (':' + this.url.port);
  153. }
  154. var reqHeaders = {};
  155. if (this.secure && this.config.tlsOptions.hasOwnProperty('headers')) {
  156. // Allow for additional headers to be provided when connecting via HTTPS
  157. extend(reqHeaders, this.config.tlsOptions.headers);
  158. }
  159. if (headers) {
  160. // Explicitly provided headers take priority over any from tlsOptions
  161. extend(reqHeaders, headers);
  162. }
  163. extend(reqHeaders, {
  164. 'Upgrade': 'websocket',
  165. 'Connection': 'Upgrade',
  166. 'Sec-WebSocket-Version': this.config.webSocketVersion.toString(10),
  167. 'Sec-WebSocket-Key': this.base64nonce,
  168. 'Host': reqHeaders.Host || hostHeaderValue
  169. });
  170. if (this.protocols.length > 0) {
  171. reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', ');
  172. }
  173. if (this.origin) {
  174. if (this.config.webSocketVersion === 13) {
  175. reqHeaders['Origin'] = this.origin;
  176. }
  177. else if (this.config.webSocketVersion === 8) {
  178. reqHeaders['Sec-WebSocket-Origin'] = this.origin;
  179. }
  180. }
  181. // TODO: Implement extensions
  182. var pathAndQuery;
  183. // Ensure it begins with '/'.
  184. if (this.url.pathname) {
  185. pathAndQuery = this.url.path;
  186. }
  187. else if (this.url.path) {
  188. pathAndQuery = '/' + this.url.path;
  189. }
  190. else {
  191. pathAndQuery = '/';
  192. }
  193. function handleRequestError(error) {
  194. self._req = null;
  195. self.emit('connectFailed', error);
  196. }
  197. var requestOptions = {
  198. agent: false
  199. };
  200. if (extraRequestOptions) {
  201. extend(requestOptions, extraRequestOptions);
  202. }
  203. // These options are always overridden by the library. The user is not
  204. // allowed to specify these directly.
  205. extend(requestOptions, {
  206. hostname: this.url.hostname,
  207. port: this.url.port,
  208. method: 'GET',
  209. path: pathAndQuery,
  210. headers: reqHeaders
  211. });
  212. if (this.secure) {
  213. var tlsOptions = this.config.tlsOptions;
  214. for (var key in tlsOptions) {
  215. if (tlsOptions.hasOwnProperty(key) && excludedTlsOptions.indexOf(key) === -1) {
  216. requestOptions[key] = tlsOptions[key];
  217. }
  218. }
  219. }
  220. var req = this._req = (this.secure ? https : http).request(requestOptions);
  221. req.on('upgrade', function handleRequestUpgrade(response, socket, head) {
  222. self._req = null;
  223. req.removeListener('error', handleRequestError);
  224. self.socket = socket;
  225. self.response = response;
  226. self.firstDataChunk = head;
  227. self.validateHandshake();
  228. });
  229. req.on('error', handleRequestError);
  230. req.on('response', function(response) {
  231. self._req = null;
  232. if (utils.eventEmitterListenerCount(self, 'httpResponse') > 0) {
  233. self.emit('httpResponse', response, self);
  234. if (response.socket) {
  235. response.socket.end();
  236. }
  237. }
  238. else {
  239. var headerDumpParts = [];
  240. for (var headerName in response.headers) {
  241. headerDumpParts.push(headerName + ': ' + response.headers[headerName]);
  242. }
  243. self.failHandshake(
  244. 'Server responded with a non-101 status: ' +
  245. response.statusCode + ' ' + response.statusMessage +
  246. '\nResponse Headers Follow:\n' +
  247. headerDumpParts.join('\n') + '\n'
  248. );
  249. }
  250. });
  251. req.end();
  252. };
  253. WebSocketClient.prototype.validateHandshake = function() {
  254. var headers = this.response.headers;
  255. if (this.protocols.length > 0) {
  256. this.protocol = headers['sec-websocket-protocol'];
  257. if (this.protocol) {
  258. if (this.protocols.indexOf(this.protocol) === -1) {
  259. this.failHandshake('Server did not respond with a requested protocol.');
  260. return;
  261. }
  262. }
  263. else {
  264. this.failHandshake('Expected a Sec-WebSocket-Protocol header.');
  265. return;
  266. }
  267. }
  268. if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) {
  269. this.failHandshake('Expected a Connection: Upgrade header from the server');
  270. return;
  271. }
  272. if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) {
  273. this.failHandshake('Expected an Upgrade: websocket header from the server');
  274. return;
  275. }
  276. var sha1 = crypto.createHash('sha1');
  277. sha1.update(this.base64nonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
  278. var expectedKey = sha1.digest('base64');
  279. if (!headers['sec-websocket-accept']) {
  280. this.failHandshake('Expected Sec-WebSocket-Accept header from server');
  281. return;
  282. }
  283. if (headers['sec-websocket-accept'] !== expectedKey) {
  284. this.failHandshake('Sec-WebSocket-Accept header from server didn\'t match expected value of ' + expectedKey);
  285. return;
  286. }
  287. // TODO: Support extensions
  288. this.succeedHandshake();
  289. };
  290. WebSocketClient.prototype.failHandshake = function(errorDescription) {
  291. if (this.socket && this.socket.writable) {
  292. this.socket.end();
  293. }
  294. this.emit('connectFailed', new Error(errorDescription));
  295. };
  296. WebSocketClient.prototype.succeedHandshake = function() {
  297. var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config);
  298. connection.webSocketVersion = this.config.webSocketVersion;
  299. connection._addSocketEventListeners();
  300. this.emit('connect', connection);
  301. if (this.firstDataChunk.length > 0) {
  302. connection.handleSocketData(this.firstDataChunk);
  303. }
  304. this.firstDataChunk = null;
  305. };
  306. WebSocketClient.prototype.abort = function() {
  307. if (this._req) {
  308. this._req.abort();
  309. }
  310. };
  311. module.exports = WebSocketClient;