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