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