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