uws-repack
Version:
Repacked version of uws
361 lines (320 loc) • 11.4 kB
JavaScript
'use strict';
const url = require('url');
const http = require('http');
//const https = require('https'); // should we ever create an https server?
const EventEmitter = require('events');
const EE_ERROR = "Registering more than one listener to a WebSocket is not supported.";
function noop() {}
const uws = (() => {
try {
return require(`./uws_${process.platform}_${process.versions.modules}`);
} catch (e) {
throw new Error('Compilation of µWebSockets has failed and there is no pre-compiled binary ' +
'available for your system. Please install a supported C++11 compiler and reinstall the module \'uws\'.');
}
})();
exports.PERMESSAGE_DEFLATE = 1;
exports.SERVER_NO_CONTEXT_TAKEOVER = 2;
exports.CLIENT_NO_CONTEXT_TAKEOVER = 4;
exports.OPCODE_TEXT = 1;
exports.OPCODE_BINARY = 2;
exports.OPCODE_PING = 9;
class Socket {
/**
* Creates a Socket instance.
*
* @param {Object} nativeSocket Socket instance
* @param {Server} server Server instance
*/
constructor(nativeSocket, nativeServer) {
this.nativeSocket = nativeSocket;
this.nativeServer = nativeServer;
this.onmessage = noop;
this.onclose = noop;
this.upgradeReq = null;
}
/**
* Registers a callback for given eventName.
*
* @param {String} eventName Event name
* @param {Function} f Event listener
* @public
*/
on(eventName, f) {
if (eventName === 'message') {
if (this.onmessage !== noop) {
throw Error(EE_ERROR);
}
this.onmessage = f;
} else if (eventName === 'close') {
if (this.onclose !== noop) {
throw Error(EE_ERROR);
}
this.onclose = f;
}
return this;
}
/**
* Registers a callback for given eventName to be executed once.
*
* @param {String} eventName Event name
* @param {Function} f Event listener
* @public
*/
once(eventName, f) {
if (eventName === 'message') {
if (this.onmessage !== noop) {
throw Error(EE_ERROR);
}
this.onmessage = () => {
f();
this.onmessage = noop;
};
} else if (eventName === 'close') {
if (this.onclose !== noop) {
throw Error(EE_ERROR);
}
this.onclose = () => {
f();
this.onclose = noop;
};
}
return this;
}
/**
* Removes all registered callbacks for the given eventName
* or, in the case of no eventName, all registered callbacks.
*
* @param {String} [eventName] Event name
* @public
*/
removeAllListeners(eventName) {
if (!eventName || eventName === 'message') {
this.onmessage = noop;
} else if (!eventName || eventName === 'close') {
this.onclose = noop;
}
return this;
}
/**
* Removes one registered callback for the given eventName.
*
* @param {String} eventName Event name
* @param {Function} callback
* @public
*/
removeListener(eventName, cb) {
if (eventName === 'message' && this.onmessage === cb) {
this.onmessage = noop;
}
if (eventName === 'close' && this.onclose === cb) {
this.onclose = noop;
}
return this;
}
/**
* Sends a message.
*
* @param {String|Buffer} message The message to send
* @param {Object} options Send options
* @param {Function} cb optional callback
* @public
*/
send(/*message , [options,] cb*/) {
var args = [].slice.apply(arguments),
message = args.shift(),
cb = args.pop() || Function.prototype,
options = args.pop() || null;
if (!this.nativeSocket)
return cb(new Error('not opened'));
const binary = options && options.binary || typeof message !== 'string';
this.nativeServer.send(this.nativeSocket, message, binary ? exports.OPCODE_BINARY : exports.OPCODE_TEXT, cb);
}
/**
* Sends a ping.
*
* @param {String|Buffer} message The message to send
* @param {Object} options Send options
* @param {Boolean} dontFailWhenClosed optional boolean
* @public
*/
ping(message, options, dontFailWhenClosed) {
/* ignore sends on closed sockets */
if (!this.nativeSocket) {
return;
}
this.nativeServer.send(this.nativeSocket, message, exports.OPCODE_PING);
}
/**
* Phony _socket object constructed on read.
*
* @public
*/
get _socket() {
const address = this.nativeServer.getAddress(this.nativeSocket);
return {
remotePort : address[0],
remoteAddress : address[1],
remoteFamily : address[2],
localPort : address[3],
localAddress : address[4],
};
}
/**
* Closes the socket.
*
* @public
*/
close(code, data) {
/* ignore close on closed sockets */
if (!this.nativeSocket) return;
/* Engine.IO, we cannot emit 'close' from within this function call */
const nativeSocket = this.nativeSocket, nativeServer = this.nativeServer;
process.nextTick(() => {
nativeServer.close(nativeSocket, code, data);
});
this.nativeServer = this.nativeSocket = null;
}
}
class Server extends EventEmitter {
/**
* Creates a Server instance.
*
* @param {Object} options Configuration options
*/
constructor(options, callback) {
super();
var nativeOptions = exports.PERMESSAGE_DEFLATE;
if (options.perMessageDeflate !== undefined) {
if (options.perMessageDeflate === false) {
nativeOptions = 0;
} else {
if (options.perMessageDeflate.serverNoContextTakeover === true) {
nativeOptions |= exports.SERVER_NO_CONTEXT_TAKEOVER;
}
if (options.perMessageDeflate.clientNoContextTakeover === true) {
nativeOptions |= exports.CLIENT_NO_CONTEXT_TAKEOVER;
}
}
}
this.nativeServer = new uws.Server(0, nativeOptions, options.maxPayload);
// can these be made private?
this._upgradeReq = null;
this._upgradeCallback = noop;
this._upgradeListener = null;
if (!options.noServer) {
this.httpServer = options.server ? options.server : http.createServer((request, response) => {
// todo: default HTTP response
response.end();
});
if (options.path && (!options.path.length || options.path[0] !== '/')) {
options.path = '/' + options.path;
}
this.httpServer.on('upgrade', this._upgradeListener = ((request, socket, head) => {
if (options.path) {
var u = url.parse(request.url);
if (u && u.pathname !== options.path)
return;
}
if (options.verifyClient) {
const info = {
origin: request.headers.origin,
secure: request.connection.authorized !== undefined || request.connection.encrypted !== undefined,
req: request
};
if (options.verifyClient.length === 2) {
options.verifyClient(info, (result, code, name) => {
if (result) {
this.handleUpgrade(request, socket, head, (ws) => {
this.emit('connection', ws);
});
} else {
// todo: send code & message
socket.end();
}
});
} else {
if (options.verifyClient(info)) {
this.handleUpgrade(request, socket, head, (ws) => {
this.emit('connection', ws);
});
} else {
// todo: send code & message
socket.end();
}
}
} else {
this.handleUpgrade(request, socket, head, (ws) => {
this.emit('connection', ws);
});
}
}));
}
this.nativeServer.onDisconnection((nativeSocket, code, message, socket) => {
socket.nativeServer = socket.nativeSocket = null;
socket.onclose(code, message);
this.nativeServer.setData(nativeSocket);
});
this.nativeServer.onMessage((nativeSocket, message, binary, socket) => {
socket.onmessage(binary ? message : message.toString());
});
this.nativeServer.onConnection((nativeSocket) => {
const socket = new Socket(nativeSocket, this.nativeServer);
this.nativeServer.setData(nativeSocket, socket);
socket.upgradeReq = {
url: this._upgradeReq.url,
headers: this._upgradeReq.headers,
connection: socket._socket
};
this._upgradeCallback(socket);
});
if (options.port) {
this.httpServer.listen(options.port, callback);
}
}
/**
* Handles a HTTP Upgrade request.
*
* @param {http.IncomingMessage} request HTTP request
* @param {net.Socket} Socket between server and client
* @param {Buffer} upgradeHead The first packet of the upgraded stream
* @param {Function} callback Callback function
* @public
*/
handleUpgrade(request, socket, upgradeHead, callback) {
const secKey = request.headers['sec-websocket-key'];
if (secKey && secKey.length == 24) {
socket.setNoDelay(true);
const ticket = this.nativeServer.transfer(socket._handle.fd, socket.ssl ? socket.ssl._external : null);
socket.on('close', (error) => {
this._upgradeReq = request;
this._upgradeCallback = callback ? callback : noop;
this.nativeServer.upgrade(ticket, secKey, request.headers['sec-websocket-extensions']);
});
}
socket.destroy();
}
/**
* Broadcast a message to all sockets.
*
* @param {String|Buffer} message The message to broadcast
* @param {Object} options Broadcast options
* @public
*/
broadcast(message, options) {
this.nativeServer.broadcast(message, options && options.binary || false);
}
/**
* Closes the server.
*
* @public
*/
close() {
if (this._upgradeListener && this.httpServer) {
this.httpServer.removeListener('upgrade', this._upgradeListener);
}
this.nativeServer.close();
}
}
exports.Server = Server;
exports.uws = uws;