You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

404 lines
11 KiB

4 years ago
  1. 'use strict';
  2. const EventEmitter = require('events');
  3. const { createHash } = require('crypto');
  4. const { createServer, STATUS_CODES } = require('http');
  5. const PerMessageDeflate = require('./permessage-deflate');
  6. const WebSocket = require('./websocket');
  7. const { format, parse } = require('./extension');
  8. const { GUID, kWebSocket } = require('./constants');
  9. const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
  10. /**
  11. * Class representing a WebSocket server.
  12. *
  13. * @extends EventEmitter
  14. */
  15. class WebSocketServer extends EventEmitter {
  16. /**
  17. * Create a `WebSocketServer` instance.
  18. *
  19. * @param {Object} options Configuration options
  20. * @param {Number} options.backlog The maximum length of the queue of pending
  21. * connections
  22. * @param {Boolean} options.clientTracking Specifies whether or not to track
  23. * clients
  24. * @param {Function} options.handleProtocols A hook to handle protocols
  25. * @param {String} options.host The hostname where to bind the server
  26. * @param {Number} options.maxPayload The maximum allowed message size
  27. * @param {Boolean} options.noServer Enable no server mode
  28. * @param {String} options.path Accept only connections matching this path
  29. * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable
  30. * permessage-deflate
  31. * @param {Number} options.port The port where to bind the server
  32. * @param {http.Server} options.server A pre-created HTTP/S server to use
  33. * @param {Function} options.verifyClient A hook to reject connections
  34. * @param {Function} callback A listener for the `listening` event
  35. */
  36. constructor(options, callback) {
  37. super();
  38. options = {
  39. maxPayload: 100 * 1024 * 1024,
  40. perMessageDeflate: false,
  41. handleProtocols: null,
  42. clientTracking: true,
  43. verifyClient: null,
  44. noServer: false,
  45. backlog: null, // use default (511 as implemented in net.js)
  46. server: null,
  47. host: null,
  48. path: null,
  49. port: null,
  50. ...options
  51. };
  52. if (options.port == null && !options.server && !options.noServer) {
  53. throw new TypeError(
  54. 'One of the "port", "server", or "noServer" options must be specified'
  55. );
  56. }
  57. if (options.port != null) {
  58. this._server = createServer((req, res) => {
  59. const body = STATUS_CODES[426];
  60. res.writeHead(426, {
  61. 'Content-Length': body.length,
  62. 'Content-Type': 'text/plain'
  63. });
  64. res.end(body);
  65. });
  66. this._server.listen(
  67. options.port,
  68. options.host,
  69. options.backlog,
  70. callback
  71. );
  72. } else if (options.server) {
  73. this._server = options.server;
  74. }
  75. if (this._server) {
  76. this._removeListeners = addListeners(this._server, {
  77. listening: this.emit.bind(this, 'listening'),
  78. error: this.emit.bind(this, 'error'),
  79. upgrade: (req, socket, head) => {
  80. this.handleUpgrade(req, socket, head, (ws) => {
  81. this.emit('connection', ws, req);
  82. });
  83. }
  84. });
  85. }
  86. if (options.perMessageDeflate === true) options.perMessageDeflate = {};
  87. if (options.clientTracking) this.clients = new Set();
  88. this.options = options;
  89. }
  90. /**
  91. * Returns the bound address, the address family name, and port of the server
  92. * as reported by the operating system if listening on an IP socket.
  93. * If the server is listening on a pipe or UNIX domain socket, the name is
  94. * returned as a string.
  95. *
  96. * @return {(Object|String|null)} The address of the server
  97. * @public
  98. */
  99. address() {
  100. if (this.options.noServer) {
  101. throw new Error('The server is operating in "noServer" mode');
  102. }
  103. if (!this._server) return null;
  104. return this._server.address();
  105. }
  106. /**
  107. * Close the server.
  108. *
  109. * @param {Function} cb Callback
  110. * @public
  111. */
  112. close(cb) {
  113. if (cb) this.once('close', cb);
  114. //
  115. // Terminate all associated clients.
  116. //
  117. if (this.clients) {
  118. for (const client of this.clients) client.terminate();
  119. }
  120. const server = this._server;
  121. if (server) {
  122. this._removeListeners();
  123. this._removeListeners = this._server = null;
  124. //
  125. // Close the http server if it was internally created.
  126. //
  127. if (this.options.port != null) {
  128. server.close(() => this.emit('close'));
  129. return;
  130. }
  131. }
  132. process.nextTick(emitClose, this);
  133. }
  134. /**
  135. * See if a given request should be handled by this server instance.
  136. *
  137. * @param {http.IncomingMessage} req Request object to inspect
  138. * @return {Boolean} `true` if the request is valid, else `false`
  139. * @public
  140. */
  141. shouldHandle(req) {
  142. if (this.options.path) {
  143. const index = req.url.indexOf('?');
  144. const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
  145. if (pathname !== this.options.path) return false;
  146. }
  147. return true;
  148. }
  149. /**
  150. * Handle a HTTP Upgrade request.
  151. *
  152. * @param {http.IncomingMessage} req The request object
  153. * @param {net.Socket} socket The network socket between the server and client
  154. * @param {Buffer} head The first packet of the upgraded stream
  155. * @param {Function} cb Callback
  156. * @public
  157. */
  158. handleUpgrade(req, socket, head, cb) {
  159. socket.on('error', socketOnError);
  160. const key =
  161. req.headers['sec-websocket-key'] !== undefined
  162. ? req.headers['sec-websocket-key'].trim()
  163. : false;
  164. const version = +req.headers['sec-websocket-version'];
  165. const extensions = {};
  166. if (
  167. req.method !== 'GET' ||
  168. req.headers.upgrade.toLowerCase() !== 'websocket' ||
  169. !key ||
  170. !keyRegex.test(key) ||
  171. (version !== 8 && version !== 13) ||
  172. !this.shouldHandle(req)
  173. ) {
  174. return abortHandshake(socket, 400);
  175. }
  176. if (this.options.perMessageDeflate) {
  177. const perMessageDeflate = new PerMessageDeflate(
  178. this.options.perMessageDeflate,
  179. true,
  180. this.options.maxPayload
  181. );
  182. try {
  183. const offers = parse(req.headers['sec-websocket-extensions']);
  184. if (offers[PerMessageDeflate.extensionName]) {
  185. perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
  186. extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
  187. }
  188. } catch (err) {
  189. return abortHandshake(socket, 400);
  190. }
  191. }
  192. //
  193. // Optionally call external client verification handler.
  194. //
  195. if (this.options.verifyClient) {
  196. const info = {
  197. origin:
  198. req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
  199. secure: !!(req.connection.authorized || req.connection.encrypted),
  200. req
  201. };
  202. if (this.options.verifyClient.length === 2) {
  203. this.options.verifyClient(info, (verified, code, message, headers) => {
  204. if (!verified) {
  205. return abortHandshake(socket, code || 401, message, headers);
  206. }
  207. this.completeUpgrade(key, extensions, req, socket, head, cb);
  208. });
  209. return;
  210. }
  211. if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
  212. }
  213. this.completeUpgrade(key, extensions, req, socket, head, cb);
  214. }
  215. /**
  216. * Upgrade the connection to WebSocket.
  217. *
  218. * @param {String} key The value of the `Sec-WebSocket-Key` header
  219. * @param {Object} extensions The accepted extensions
  220. * @param {http.IncomingMessage} req The request object
  221. * @param {net.Socket} socket The network socket between the server and client
  222. * @param {Buffer} head The first packet of the upgraded stream
  223. * @param {Function} cb Callback
  224. * @throws {Error} If called more than once with the same socket
  225. * @private
  226. */
  227. completeUpgrade(key, extensions, req, socket, head, cb) {
  228. //
  229. // Destroy the socket if the client has already sent a FIN packet.
  230. //
  231. if (!socket.readable || !socket.writable) return socket.destroy();
  232. if (socket[kWebSocket]) {
  233. throw new Error(
  234. 'server.handleUpgrade() was called more than once with the same ' +
  235. 'socket, possibly due to a misconfiguration'
  236. );
  237. }
  238. const digest = createHash('sha1')
  239. .update(key + GUID)
  240. .digest('base64');
  241. const headers = [
  242. 'HTTP/1.1 101 Switching Protocols',
  243. 'Upgrade: websocket',
  244. 'Connection: Upgrade',
  245. `Sec-WebSocket-Accept: ${digest}`
  246. ];
  247. const ws = new WebSocket(null);
  248. let protocol = req.headers['sec-websocket-protocol'];
  249. if (protocol) {
  250. protocol = protocol.trim().split(/ *, */);
  251. //
  252. // Optionally call external protocol selection handler.
  253. //
  254. if (this.options.handleProtocols) {
  255. protocol = this.options.handleProtocols(protocol, req);
  256. } else {
  257. protocol = protocol[0];
  258. }
  259. if (protocol) {
  260. headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
  261. ws.protocol = protocol;
  262. }
  263. }
  264. if (extensions[PerMessageDeflate.extensionName]) {
  265. const params = extensions[PerMessageDeflate.extensionName].params;
  266. const value = format({
  267. [PerMessageDeflate.extensionName]: [params]
  268. });
  269. headers.push(`Sec-WebSocket-Extensions: ${value}`);
  270. ws._extensions = extensions;
  271. }
  272. //
  273. // Allow external modification/inspection of handshake headers.
  274. //
  275. this.emit('headers', headers, req);
  276. socket.write(headers.concat('\r\n').join('\r\n'));
  277. socket.removeListener('error', socketOnError);
  278. ws.setSocket(socket, head, this.options.maxPayload);
  279. if (this.clients) {
  280. this.clients.add(ws);
  281. ws.on('close', () => this.clients.delete(ws));
  282. }
  283. cb(ws);
  284. }
  285. }
  286. module.exports = WebSocketServer;
  287. /**
  288. * Add event listeners on an `EventEmitter` using a map of <event, listener>
  289. * pairs.
  290. *
  291. * @param {EventEmitter} server The event emitter
  292. * @param {Object.<String, Function>} map The listeners to add
  293. * @return {Function} A function that will remove the added listeners when called
  294. * @private
  295. */
  296. function addListeners(server, map) {
  297. for (const event of Object.keys(map)) server.on(event, map[event]);
  298. return function removeListeners() {
  299. for (const event of Object.keys(map)) {
  300. server.removeListener(event, map[event]);
  301. }
  302. };
  303. }
  304. /**
  305. * Emit a `'close'` event on an `EventEmitter`.
  306. *
  307. * @param {EventEmitter} server The event emitter
  308. * @private
  309. */
  310. function emitClose(server) {
  311. server.emit('close');
  312. }
  313. /**
  314. * Handle premature socket errors.
  315. *
  316. * @private
  317. */
  318. function socketOnError() {
  319. this.destroy();
  320. }
  321. /**
  322. * Close the connection when preconditions are not fulfilled.
  323. *
  324. * @param {net.Socket} socket The socket of the upgrade request
  325. * @param {Number} code The HTTP response status code
  326. * @param {String} [message] The HTTP response body
  327. * @param {Object} [headers] Additional HTTP response headers
  328. * @private
  329. */
  330. function abortHandshake(socket, code, message, headers) {
  331. if (socket.writable) {
  332. message = message || STATUS_CODES[code];
  333. headers = {
  334. Connection: 'close',
  335. 'Content-Type': 'text/html',
  336. 'Content-Length': Buffer.byteLength(message),
  337. ...headers
  338. };
  339. socket.write(
  340. `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` +
  341. Object.keys(headers)
  342. .map((h) => `${h}: ${headers[h]}`)
  343. .join('\r\n') +
  344. '\r\n\r\n' +
  345. message
  346. );
  347. }
  348. socket.removeListener('error', socketOnError);
  349. socket.destroy();
  350. }