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.
326 lines
9.3 KiB
326 lines
9.3 KiB
/*! |
|
* ws: a node.js websocket client |
|
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com> |
|
* MIT Licensed |
|
*/ |
|
|
|
'use strict'; |
|
|
|
const safeBuffer = require('safe-buffer'); |
|
const EventEmitter = require('events'); |
|
const crypto = require('crypto'); |
|
const Ultron = require('ultron'); |
|
const http = require('http'); |
|
const url = require('url'); |
|
|
|
const PerMessageDeflate = require('./PerMessageDeflate'); |
|
const Extensions = require('./Extensions'); |
|
const constants = require('./Constants'); |
|
const WebSocket = require('./WebSocket'); |
|
|
|
const Buffer = safeBuffer.Buffer; |
|
|
|
/** |
|
* Class representing a WebSocket server. |
|
* |
|
* @extends EventEmitter |
|
*/ |
|
class WebSocketServer extends EventEmitter { |
|
/** |
|
* Create a `WebSocketServer` instance. |
|
* |
|
* @param {Object} options Configuration options |
|
* @param {String} options.host The hostname where to bind the server |
|
* @param {Number} options.port The port where to bind the server |
|
* @param {http.Server} options.server A pre-created HTTP/S server to use |
|
* @param {Function} options.verifyClient An hook to reject connections |
|
* @param {Function} options.handleProtocols An hook to handle protocols |
|
* @param {String} options.path Accept only connections matching this path |
|
* @param {Boolean} options.noServer Enable no server mode |
|
* @param {Boolean} options.clientTracking Specifies whether or not to track clients |
|
* @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate |
|
* @param {Number} options.maxPayload The maximum allowed message size |
|
* @param {Function} callback A listener for the `listening` event |
|
*/ |
|
constructor (options, callback) { |
|
super(); |
|
|
|
options = Object.assign({ |
|
maxPayload: 100 * 1024 * 1024, |
|
perMessageDeflate: false, |
|
handleProtocols: null, |
|
clientTracking: true, |
|
verifyClient: null, |
|
noServer: false, |
|
backlog: null, // use default (511 as implemented in net.js) |
|
server: null, |
|
host: null, |
|
path: null, |
|
port: null |
|
}, options); |
|
|
|
if (options.port == null && !options.server && !options.noServer) { |
|
throw new TypeError('missing or invalid options'); |
|
} |
|
|
|
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) { |
|
this._ultron = new Ultron(this._server); |
|
this._ultron.on('listening', () => this.emit('listening')); |
|
this._ultron.on('error', (err) => this.emit('error', err)); |
|
this._ultron.on('upgrade', (req, socket, head) => { |
|
this.handleUpgrade(req, socket, head, (client) => { |
|
this.emit('connection', client, req); |
|
}); |
|
}); |
|
} |
|
|
|
if (options.perMessageDeflate === true) options.perMessageDeflate = {}; |
|
if (options.clientTracking) this.clients = new Set(); |
|
this.options = options; |
|
} |
|
|
|
/** |
|
* Close the server. |
|
* |
|
* @param {Function} cb Callback |
|
* @public |
|
*/ |
|
close (cb) { |
|
// |
|
// Terminate all associated clients. |
|
// |
|
if (this.clients) { |
|
for (const client of this.clients) client.terminate(); |
|
} |
|
|
|
const server = this._server; |
|
|
|
if (server) { |
|
this._ultron.destroy(); |
|
this._ultron = this._server = null; |
|
|
|
// |
|
// Close the http server if it was internally created. |
|
// |
|
if (this.options.port != null) return server.close(cb); |
|
} |
|
|
|
if (cb) cb(); |
|
} |
|
|
|
/** |
|
* See if a given request should be handled by this server instance. |
|
* |
|
* @param {http.IncomingMessage} req Request object to inspect |
|
* @return {Boolean} `true` if the request is valid, else `false` |
|
* @public |
|
*/ |
|
shouldHandle (req) { |
|
if (this.options.path && url.parse(req.url).pathname !== this.options.path) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
/** |
|
* Handle a HTTP Upgrade request. |
|
* |
|
* @param {http.IncomingMessage} req The request object |
|
* @param {net.Socket} socket The network socket between the server and client |
|
* @param {Buffer} head The first packet of the upgraded stream |
|
* @param {Function} cb Callback |
|
* @public |
|
*/ |
|
handleUpgrade (req, socket, head, cb) { |
|
socket.on('error', socketError); |
|
|
|
const version = +req.headers['sec-websocket-version']; |
|
const extensions = {}; |
|
|
|
if ( |
|
req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket' || |
|
!req.headers['sec-websocket-key'] || (version !== 8 && version !== 13) || |
|
!this.shouldHandle(req) |
|
) { |
|
return abortConnection(socket, 400); |
|
} |
|
|
|
if (this.options.perMessageDeflate) { |
|
const perMessageDeflate = new PerMessageDeflate( |
|
this.options.perMessageDeflate, |
|
true, |
|
this.options.maxPayload |
|
); |
|
|
|
try { |
|
const offers = Extensions.parse( |
|
req.headers['sec-websocket-extensions'] |
|
); |
|
|
|
if (offers[PerMessageDeflate.extensionName]) { |
|
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); |
|
extensions[PerMessageDeflate.extensionName] = perMessageDeflate; |
|
} |
|
} catch (err) { |
|
return abortConnection(socket, 400); |
|
} |
|
} |
|
|
|
var protocol = (req.headers['sec-websocket-protocol'] || '').split(/, */); |
|
|
|
// |
|
// Optionally call external protocol selection handler. |
|
// |
|
if (this.options.handleProtocols) { |
|
protocol = this.options.handleProtocols(protocol, req); |
|
if (protocol === false) return abortConnection(socket, 401); |
|
} else { |
|
protocol = protocol[0]; |
|
} |
|
|
|
// |
|
// Optionally call external client verification handler. |
|
// |
|
if (this.options.verifyClient) { |
|
const info = { |
|
origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], |
|
secure: !!(req.connection.authorized || req.connection.encrypted), |
|
req |
|
}; |
|
|
|
if (this.options.verifyClient.length === 2) { |
|
this.options.verifyClient(info, (verified, code, message) => { |
|
if (!verified) return abortConnection(socket, code || 401, message); |
|
|
|
this.completeUpgrade( |
|
protocol, |
|
extensions, |
|
version, |
|
req, |
|
socket, |
|
head, |
|
cb |
|
); |
|
}); |
|
return; |
|
} |
|
|
|
if (!this.options.verifyClient(info)) return abortConnection(socket, 401); |
|
} |
|
|
|
this.completeUpgrade(protocol, extensions, version, req, socket, head, cb); |
|
} |
|
|
|
/** |
|
* Upgrade the connection to WebSocket. |
|
* |
|
* @param {String} protocol The chosen subprotocol |
|
* @param {Object} extensions The accepted extensions |
|
* @param {Number} version The WebSocket protocol version |
|
* @param {http.IncomingMessage} req The request object |
|
* @param {net.Socket} socket The network socket between the server and client |
|
* @param {Buffer} head The first packet of the upgraded stream |
|
* @param {Function} cb Callback |
|
* @private |
|
*/ |
|
completeUpgrade (protocol, extensions, version, req, socket, head, cb) { |
|
// |
|
// Destroy the socket if the client has already sent a FIN packet. |
|
// |
|
if (!socket.readable || !socket.writable) return socket.destroy(); |
|
|
|
const key = crypto.createHash('sha1') |
|
.update(req.headers['sec-websocket-key'] + constants.GUID, 'binary') |
|
.digest('base64'); |
|
|
|
const headers = [ |
|
'HTTP/1.1 101 Switching Protocols', |
|
'Upgrade: websocket', |
|
'Connection: Upgrade', |
|
`Sec-WebSocket-Accept: ${key}` |
|
]; |
|
|
|
if (protocol) headers.push(`Sec-WebSocket-Protocol: ${protocol}`); |
|
if (extensions[PerMessageDeflate.extensionName]) { |
|
const params = extensions[PerMessageDeflate.extensionName].params; |
|
const value = Extensions.format({ |
|
[PerMessageDeflate.extensionName]: [params] |
|
}); |
|
headers.push(`Sec-WebSocket-Extensions: ${value}`); |
|
} |
|
|
|
// |
|
// Allow external modification/inspection of handshake headers. |
|
// |
|
this.emit('headers', headers, req); |
|
|
|
socket.write(headers.concat('\r\n').join('\r\n')); |
|
|
|
const client = new WebSocket([socket, head], null, { |
|
maxPayload: this.options.maxPayload, |
|
protocolVersion: version, |
|
extensions, |
|
protocol |
|
}); |
|
|
|
if (this.clients) { |
|
this.clients.add(client); |
|
client.on('close', () => this.clients.delete(client)); |
|
} |
|
|
|
socket.removeListener('error', socketError); |
|
cb(client); |
|
} |
|
} |
|
|
|
module.exports = WebSocketServer; |
|
|
|
/** |
|
* Handle premature socket errors. |
|
* |
|
* @private |
|
*/ |
|
function socketError () { |
|
this.destroy(); |
|
} |
|
|
|
/** |
|
* Close the connection when preconditions are not fulfilled. |
|
* |
|
* @param {net.Socket} socket The socket of the upgrade request |
|
* @param {Number} code The HTTP response status code |
|
* @param {String} [message] The HTTP response body |
|
* @private |
|
*/ |
|
function abortConnection (socket, code, message) { |
|
if (socket.writable) { |
|
message = message || http.STATUS_CODES[code]; |
|
socket.write( |
|
`HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + |
|
'Connection: close\r\n' + |
|
'Content-type: text/html\r\n' + |
|
`Content-Length: ${Buffer.byteLength(message)}\r\n` + |
|
'\r\n' + |
|
message |
|
); |
|
} |
|
|
|
socket.removeListener('error', socketError); |
|
socket.destroy(); |
|
}
|
|
|