| |
| |
| 'use strict'; |
|
|
| const EventEmitter = require('events'); |
| const http = require('http'); |
| const { Duplex } = require('stream'); |
| const { createHash } = require('crypto'); |
|
|
| const extension = require('./extension'); |
| const PerMessageDeflate = require('./permessage-deflate'); |
| const subprotocol = require('./subprotocol'); |
| const WebSocket = require('./websocket'); |
| const { CLOSE_TIMEOUT, GUID, kWebSocket } = require('./constants'); |
|
|
| const keyRegex = /^[+/0-9A-Za-z]{22}==$/; |
|
|
| const RUNNING = 0; |
| const CLOSING = 1; |
| const CLOSED = 2; |
|
|
| |
| |
| |
| |
| |
| class WebSocketServer extends EventEmitter { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| constructor(options, callback) { |
| super(); |
|
|
| options = { |
| allowSynchronousEvents: true, |
| autoPong: true, |
| maxBufferedChunks: 1024 * 1024, |
| maxFragments: 128 * 1024, |
| maxPayload: 100 * 1024 * 1024, |
| skipUTF8Validation: false, |
| perMessageDeflate: false, |
| handleProtocols: null, |
| clientTracking: true, |
| closeTimeout: CLOSE_TIMEOUT, |
| verifyClient: null, |
| noServer: false, |
| backlog: null, |
| server: null, |
| host: null, |
| path: null, |
| port: null, |
| WebSocket, |
| ...options |
| }; |
|
|
| if ( |
| (options.port == null && !options.server && !options.noServer) || |
| (options.port != null && (options.server || options.noServer)) || |
| (options.server && options.noServer) |
| ) { |
| throw new TypeError( |
| 'One and only one of the "port", "server", or "noServer" options ' + |
| 'must be specified' |
| ); |
| } |
|
|
| if (options.port != null) { |
| this._server = http.createServer((req, res) => { |
| const body = http.STATUS_CODES[426]; |
|
|
| res.writeHead(426, { |
| 'Content-Length': body.length, |
| 'Content-Type': 'text/plain' |
| }); |
| res.end(body); |
| }); |
| this._server.listen( |
| options.port, |
| options.host, |
| options.backlog, |
| callback |
| ); |
| } else if (options.server) { |
| this._server = options.server; |
| } |
|
|
| if (this._server) { |
| const emitConnection = this.emit.bind(this, 'connection'); |
|
|
| this._removeListeners = addListeners(this._server, { |
| listening: this.emit.bind(this, 'listening'), |
| error: this.emit.bind(this, 'error'), |
| upgrade: (req, socket, head) => { |
| this.handleUpgrade(req, socket, head, emitConnection); |
| } |
| }); |
| } |
|
|
| if (options.perMessageDeflate === true) options.perMessageDeflate = {}; |
| if (options.clientTracking) { |
| this.clients = new Set(); |
| this._shouldEmitClose = false; |
| } |
|
|
| this.options = options; |
| this._state = RUNNING; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| address() { |
| if (this.options.noServer) { |
| throw new Error('The server is operating in "noServer" mode'); |
| } |
|
|
| if (!this._server) return null; |
| return this._server.address(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| close(cb) { |
| if (this._state === CLOSED) { |
| if (cb) { |
| this.once('close', () => { |
| cb(new Error('The server is not running')); |
| }); |
| } |
|
|
| process.nextTick(emitClose, this); |
| return; |
| } |
|
|
| if (cb) this.once('close', cb); |
|
|
| if (this._state === CLOSING) return; |
| this._state = CLOSING; |
|
|
| if (this.options.noServer || this.options.server) { |
| if (this._server) { |
| this._removeListeners(); |
| this._removeListeners = this._server = null; |
| } |
|
|
| if (this.clients) { |
| if (!this.clients.size) { |
| process.nextTick(emitClose, this); |
| } else { |
| this._shouldEmitClose = true; |
| } |
| } else { |
| process.nextTick(emitClose, this); |
| } |
| } else { |
| const server = this._server; |
|
|
| this._removeListeners(); |
| this._removeListeners = this._server = null; |
|
|
| |
| |
| |
| |
| server.close(() => { |
| emitClose(this); |
| }); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| shouldHandle(req) { |
| if (this.options.path) { |
| const index = req.url.indexOf('?'); |
| const pathname = index !== -1 ? req.url.slice(0, index) : req.url; |
|
|
| if (pathname !== this.options.path) return false; |
| } |
|
|
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| handleUpgrade(req, socket, head, cb) { |
| socket.on('error', socketOnError); |
|
|
| const key = req.headers['sec-websocket-key']; |
| const upgrade = req.headers.upgrade; |
| const version = +req.headers['sec-websocket-version']; |
|
|
| if (req.method !== 'GET') { |
| const message = 'Invalid HTTP method'; |
| abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); |
| return; |
| } |
|
|
| if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { |
| const message = 'Invalid Upgrade header'; |
| abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); |
| return; |
| } |
|
|
| if (key === undefined || !keyRegex.test(key)) { |
| const message = 'Missing or invalid Sec-WebSocket-Key header'; |
| abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); |
| return; |
| } |
|
|
| if (version !== 13 && version !== 8) { |
| const message = 'Missing or invalid Sec-WebSocket-Version header'; |
| abortHandshakeOrEmitwsClientError(this, req, socket, 400, message, { |
| 'Sec-WebSocket-Version': '13, 8' |
| }); |
| return; |
| } |
|
|
| if (!this.shouldHandle(req)) { |
| abortHandshake(socket, 400); |
| return; |
| } |
|
|
| const secWebSocketProtocol = req.headers['sec-websocket-protocol']; |
| let protocols = new Set(); |
|
|
| if (secWebSocketProtocol !== undefined) { |
| try { |
| protocols = subprotocol.parse(secWebSocketProtocol); |
| } catch (err) { |
| const message = 'Invalid Sec-WebSocket-Protocol header'; |
| abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); |
| return; |
| } |
| } |
|
|
| const secWebSocketExtensions = req.headers['sec-websocket-extensions']; |
| const extensions = {}; |
|
|
| if ( |
| this.options.perMessageDeflate && |
| secWebSocketExtensions !== undefined |
| ) { |
| const perMessageDeflate = new PerMessageDeflate({ |
| ...this.options.perMessageDeflate, |
| isServer: true, |
| maxPayload: this.options.maxPayload |
| }); |
|
|
| try { |
| const offers = extension.parse(secWebSocketExtensions); |
|
|
| if (offers[PerMessageDeflate.extensionName]) { |
| perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); |
| extensions[PerMessageDeflate.extensionName] = perMessageDeflate; |
| } |
| } catch (err) { |
| const message = |
| 'Invalid or unacceptable Sec-WebSocket-Extensions header'; |
| abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); |
| return; |
| } |
| } |
|
|
| |
| |
| |
| if (this.options.verifyClient) { |
| const info = { |
| origin: |
| req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], |
| secure: !!(req.socket.authorized || req.socket.encrypted), |
| req |
| }; |
|
|
| if (this.options.verifyClient.length === 2) { |
| this.options.verifyClient(info, (verified, code, message, headers) => { |
| if (!verified) { |
| return abortHandshake(socket, code || 401, message, headers); |
| } |
|
|
| this.completeUpgrade( |
| extensions, |
| key, |
| protocols, |
| req, |
| socket, |
| head, |
| cb |
| ); |
| }); |
| return; |
| } |
|
|
| if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); |
| } |
|
|
| this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| completeUpgrade(extensions, key, protocols, req, socket, head, cb) { |
| |
| |
| |
| if (!socket.readable || !socket.writable) return socket.destroy(); |
|
|
| if (socket[kWebSocket]) { |
| throw new Error( |
| 'server.handleUpgrade() was called more than once with the same ' + |
| 'socket, possibly due to a misconfiguration' |
| ); |
| } |
|
|
| if (this._state > RUNNING) return abortHandshake(socket, 503); |
|
|
| const digest = createHash('sha1') |
| .update(key + GUID) |
| .digest('base64'); |
|
|
| const headers = [ |
| 'HTTP/1.1 101 Switching Protocols', |
| 'Upgrade: websocket', |
| 'Connection: Upgrade', |
| `Sec-WebSocket-Accept: ${digest}` |
| ]; |
|
|
| const ws = new this.options.WebSocket(null, undefined, this.options); |
|
|
| if (protocols.size) { |
| |
| |
| |
| const protocol = this.options.handleProtocols |
| ? this.options.handleProtocols(protocols, req) |
| : protocols.values().next().value; |
|
|
| if (protocol) { |
| headers.push(`Sec-WebSocket-Protocol: ${protocol}`); |
| ws._protocol = protocol; |
| } |
| } |
|
|
| if (extensions[PerMessageDeflate.extensionName]) { |
| const params = extensions[PerMessageDeflate.extensionName].params; |
| const value = extension.format({ |
| [PerMessageDeflate.extensionName]: [params] |
| }); |
| headers.push(`Sec-WebSocket-Extensions: ${value}`); |
| ws._extensions = extensions; |
| } |
|
|
| |
| |
| |
| this.emit('headers', headers, req); |
|
|
| socket.write(headers.concat('\r\n').join('\r\n')); |
| socket.removeListener('error', socketOnError); |
|
|
| ws.setSocket(socket, head, { |
| allowSynchronousEvents: this.options.allowSynchronousEvents, |
| maxBufferedChunks: this.options.maxBufferedChunks, |
| maxFragments: this.options.maxFragments, |
| maxPayload: this.options.maxPayload, |
| skipUTF8Validation: this.options.skipUTF8Validation |
| }); |
|
|
| if (this.clients) { |
| this.clients.add(ws); |
| ws.on('close', () => { |
| this.clients.delete(ws); |
|
|
| if (this._shouldEmitClose && !this.clients.size) { |
| process.nextTick(emitClose, this); |
| } |
| }); |
| } |
|
|
| cb(ws, req); |
| } |
| } |
|
|
| module.exports = WebSocketServer; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function addListeners(server, map) { |
| for (const event of Object.keys(map)) server.on(event, map[event]); |
|
|
| return function removeListeners() { |
| for (const event of Object.keys(map)) { |
| server.removeListener(event, map[event]); |
| } |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function emitClose(server) { |
| server._state = CLOSED; |
| server.emit('close'); |
| } |
|
|
| |
| |
| |
| |
| |
| function socketOnError() { |
| this.destroy(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function abortHandshake(socket, code, message, headers) { |
| |
| |
| |
| |
| |
| |
| |
| |
| message = message || http.STATUS_CODES[code]; |
| headers = { |
| Connection: 'close', |
| 'Content-Type': 'text/html', |
| 'Content-Length': Buffer.byteLength(message), |
| ...headers |
| }; |
|
|
| socket.once('finish', socket.destroy); |
|
|
| socket.end( |
| `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + |
| Object.keys(headers) |
| .map((h) => `${h}: ${headers[h]}`) |
| .join('\r\n') + |
| '\r\n\r\n' + |
| message |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function abortHandshakeOrEmitwsClientError( |
| server, |
| req, |
| socket, |
| code, |
| message, |
| headers |
| ) { |
| if (server.listenerCount('wsClientError')) { |
| const err = new Error(message); |
| Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); |
|
|
| server.emit('wsClientError', err, socket, req); |
| } else { |
| abortHandshake(socket, code, message, headers); |
| } |
| } |
|
|