@freemework/hosting
Version:
Hosting library of the Freemework Project.
1,016 lines • 47.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FWebSocketChannelFactoryEndpoint = exports.FWebSocketChannelSupplyEndpoint = exports.FServersBindEndpoint = exports.FBindEndpoint = exports.SecuredWebServer = exports.UnsecuredWebServer = exports.FAbstractWebServer = exports.FHttpRequestCancellationToken = exports.FHostingConfiguration = exports.FHostingSettings = void 0;
exports.instanceofWebServer = instanceofWebServer;
exports.createWebServer = createWebServer;
exports.createWebServers = createWebServers;
const common_1 = require("@freemework/common");
const express_1 = __importDefault(require("express"));
const fs_1 = __importDefault(require("fs"));
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
// import { unescape as urlDecode } from "querystring";
const ws_1 = __importDefault(require("ws"));
// import { pki } from "node-forge";
const FHttpRequestCancellationToken_js_1 = require("./FHttpRequestCancellationToken.js");
const f_hosting_settings_js_1 = require("./f_hosting_settings.js");
const package_info_js_1 = require("./package_info.js");
(0, common_1.FModuleVersionGuard)(package_info_js_1.packageInfo);
var f_hosting_settings_js_2 = require("./f_hosting_settings.js");
Object.defineProperty(exports, "FHostingSettings", { enumerable: true, get: function () { return f_hosting_settings_js_2.FHostingSettings; } });
Object.defineProperty(exports, "FHostingConfiguration", { enumerable: true, get: function () { return f_hosting_settings_js_2.FHostingConfiguration; } });
var FHttpRequestCancellationToken_js_2 = require("./FHttpRequestCancellationToken.js");
Object.defineProperty(exports, "FHttpRequestCancellationToken", { enumerable: true, get: function () { return FHttpRequestCancellationToken_js_2.FHttpRequestCancellationToken; } });
__exportStar(require("./configuration/index.js"), exports);
__exportStar(require("./launcher/index.js"), exports);
class FAbstractWebServer extends common_1.FInitableBase {
_trustProxy;
_name;
_listenHost;
_listenPort;
_webSockets;
_log;
_handleUpgrade;
_onUpgrade;
_onRequestImpl;
_handlers;
// private readonly _caCertificates: ReadonlyArray<[pki.Certificate, Buffer]>;
_rootExpressApplication;
static createFCancellationToken(request) {
return new FHttpRequestCancellationToken_js_1.FHttpRequestCancellationToken(request);
}
constructor(opts) {
super();
this._name = opts.name;
this._listenHost = opts.listenHost;
this._listenPort = opts.listenPort;
this._log = opts.log !== undefined ? opts.log : common_1.FLogger.create(this.constructor.name);
this._trustProxy = opts.trustProxy !== undefined ? opts.trustProxy : false;
this._handleUpgrade = opts.handleUpgrade !== undefined ? opts.handleUpgrade : true;
this._webSockets = {};
this._handlers = new Map();
this._rootExpressApplication = null;
let onXfccRequest = null;
let onXfccUpgrade = null;
if ("type" in opts) {
const friendlyOpts = opts;
if ("clientCertificateMode" in friendlyOpts &&
friendlyOpts.clientCertificateMode === f_hosting_settings_js_1.FHostingSettings.ClientCertificateMode.XFCC) {
if (friendlyOpts.caCertificates === undefined ||
!(typeof friendlyOpts.caCertificates === "string"
|| friendlyOpts.caCertificates instanceof Buffer
|| Array.isArray(friendlyOpts.caCertificates))) {
throw new Error("ClientCertificateMode.XFCC required at least one CA certificate");
}
// this._caCertificates = parseCertificates(friendlyOpts.caCertificates);
onXfccRequest = this.onRequestXFCC.bind(this);
onXfccUpgrade = this.onUpgradeXFCC.bind(this);
}
else {
// if (
// friendlyOpts.type === "https" &&
// friendlyOpts.caCertificates !== undefined &&
// (
// typeof friendlyOpts.caCertificates === "string"
// || friendlyOpts.caCertificates instanceof Buffer
// || Array.isArray(friendlyOpts.caCertificates)
// )
// ) {
// this._caCertificates = parseCertificates(friendlyOpts.caCertificates);
// } else {
// this._caCertificates = [];
// }
}
}
else {
// this._caCertificates = [];
}
this._onRequestImpl = onXfccRequest !== null ? onXfccRequest : this.onRequestCommon.bind(this);
this._onUpgrade = this._handleUpgrade
? (onXfccUpgrade !== null
? onXfccUpgrade
: this.onUpgradeCommon.bind(this))
: null;
}
/**
* Lazy create for Express Application
*/
get rootExpressApplication() {
if (this._rootExpressApplication === null) {
this._rootExpressApplication = (0, express_1.default)();
const trustProxy = this._trustProxy;
if (trustProxy !== undefined) {
this._rootExpressApplication.set("trust proxy", trustProxy);
}
}
return this._rootExpressApplication;
}
set rootExpressApplication(value) {
if (this._rootExpressApplication !== null) {
throw new Error("Wrong operation at current state. Express application already set. Override is not allowed.");
}
this._rootExpressApplication = value;
}
get name() { return this._name; }
bindRequestHandler(bindPath, value) {
if (this._handlers.has(bindPath)) {
throw new Error(`Wrong operation. Path '${bindPath}' already bound`);
}
this._handlers.set(bindPath, value);
}
createWebSocketServer(bindPath) {
const websocketServer = new ws_1.default.Server({ noServer: true });
this._webSockets[bindPath] = websocketServer;
return websocketServer;
}
async destroyWebSocketServer(bindPath) {
const logger = this._log;
const webSocketServer = this._webSockets[bindPath];
if (webSocketServer !== undefined) {
delete this._webSockets[bindPath];
await new Promise((resolve) => {
webSocketServer.close((err) => {
if (err !== undefined) {
if (logger.isWarnEnabled) {
logger.warn(this.initExecutionContext, () => `Web Socket Server was closed with error. Inner message: ${err.message} `);
}
logger.trace(this.initExecutionContext, () => "Web Socket Server was closed with error.", common_1.FException.wrapIfNeeded(err));
}
// dispose never raise any errors
resolve();
});
});
}
}
async onInit() {
if (this._onUpgrade !== null) {
this.underlyingServer.on("upgrade", this._onUpgrade);
}
await this.onListen();
}
onRequest(req, res) {
this._onRequestImpl(req, res);
}
onRequestCommon(req, res) {
const logger = this._log;
if (this._handlers.size > 0 && req.url !== undefined) {
const { pathname } = new URL(req.url);
if (pathname !== undefined) {
for (const bindPath of this._handlers.keys()) {
if (pathname.startsWith(bindPath)) {
const handler = this._handlers.get(bindPath);
handler(req, res);
return;
}
}
}
}
// A proper handler was not found, fallback to rootExpressApplication
if (this._rootExpressApplication !== null) {
this._rootExpressApplication(req, res);
return;
}
logger.warn(this.initExecutionContext, "Request was handled but no listener.");
res.writeHead(503);
res.statusMessage = "Service Unavailable";
res.end();
}
onRequestXFCC(req, res) {
if (this.validateXFCC(req)) {
this.onRequestCommon(req, res);
return;
}
res.statusMessage = "Client certificate is required.";
res.writeHead(401);
res.end();
}
onUpgradeCommon(req, socket, head) {
const logger = this._log;
const urlPath = req.url;
if (urlPath !== undefined) {
const wss = this._webSockets[urlPath];
if (wss !== undefined) {
logger.debug(this.initExecutionContext, () => `Upgrade the server on url path '${urlPath}' for WebSocket server.`);
wss.handleUpgrade(req, socket, head, function (ws) {
wss.emit("connection", ws, req);
});
}
else {
// Proposal of https://github.com/freemework/freemework/issues/8
// socket.destroy();
}
}
else {
// Proposal of https://github.com/freemework/freemework/issues/8
// socket.destroy();
}
}
onUpgradeXFCC(req, socket, head) {
if (this.validateXFCC(req)) {
this.onUpgradeCommon(req, socket, head);
return;
}
socket.write("HTTP/1.1 401 Client certificate is required.\r\n\r\n");
socket.end();
}
validateXFCC(req) {
const logger = this._log;
const xfccHeaderData = req && req.headers && req.headers["x-forwarded-client-cert"];
if (typeof xfccHeaderData === "string") {
logger.trace(this.initExecutionContext, () => `X-Forwarded-Client-Cert header: ${xfccHeaderData}`);
// const clientCertPem = urlDecode(xfccHeaderData);
// const clientCert = pki.certificateFromPem(clientCertPem);
// for (const caCert of this.caCertificatesAsPki) {
// try {
// if (caCert.verify(clientCert)) {
// return true;
// }
// } catch (e) {
// logger.trace(this.initExecutionContext, "Verify failed.", FException.wrapIfNeeded(e));
// }
// }
}
else {
logger.debug(this.initExecutionContext, "Request with no X-Forwarded-Client-Cert header.");
}
return false;
}
}
exports.FAbstractWebServer = FAbstractWebServer;
class UnsecuredWebServer extends FAbstractWebServer {
_httpServer;
constructor(opts) {
super(opts);
// Make HTTP server instance
const serverOpts = {};
this._httpServer = http_1.default.createServer(serverOpts, this.onRequest.bind(this));
}
get underlyingServer() { return this._httpServer; }
onListen() {
const logger = this._log;
logger.debug(this.initExecutionContext, "UnsecuredWebServer#listen()");
const server = this._httpServer;
return new Promise((resolve, reject) => {
logger.info(this.initExecutionContext, () => `Starting Web Server '${this._name}' ...`);
server
.on("listening", () => {
const address = server.address();
if (address !== null) {
if (typeof address === "string") {
logger.info(this.initExecutionContext, () => `Web Server '${this._name}' was started on ${address}`);
}
else {
logger.info(this.initExecutionContext, () => `${address.family} Web Server '${this._name}' was started on http://${address.address}:${address.port}`);
}
}
resolve();
})
.on("error", reject)
.listen(this._listenPort, this._listenHost);
});
}
async onDispose() {
const logger = this._log;
logger.debug(this.initExecutionContext, "UnsecuredWebServer#onDispose()");
const server = this._httpServer;
const address = server.address();
if (address !== null) {
if (typeof address === "string") {
logger.info(this.initExecutionContext, () => `Stopping Web Server '${this._name}' http://${address}...`);
}
else {
logger.info(this.initExecutionContext, () => `Stopping ${address.family} Web Server '${this._name}' http://${address.address}:${address.port}...`);
}
}
else {
logger.info(this.initExecutionContext, "Stopping Web Server...");
}
await new Promise((destroyResolve) => {
server.close((e) => {
if (e) {
const ex = common_1.FException.wrapIfNeeded(e);
logger.warn(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped with error: ${ex.message}`);
logger.debug(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped with error`, ex);
}
else {
logger.info(this.initExecutionContext, "The Web Server was stopped");
}
destroyResolve();
});
});
}
}
exports.UnsecuredWebServer = UnsecuredWebServer;
class SecuredWebServer extends FAbstractWebServer {
_httpsServer;
constructor(opts) {
super(opts);
// Make HTTPS server instance
const serverOpts = {
cert: opts.serverCertificate instanceof Buffer ? opts.serverCertificate : fs_1.default.readFileSync(opts.serverCertificate),
key: opts.serverKey instanceof Buffer ? opts.serverKey : fs_1.default.readFileSync(opts.serverKey)
};
if (opts.caCertificates !== null) {
if (typeof opts.caCertificates === "string") {
serverOpts.ca = fs_1.default.readFileSync(opts.caCertificates);
}
//serverOpts.ca = this.caCertificatesAsBuffer;
}
if (opts.serverKeyPassword !== null) {
serverOpts.passphrase = opts.serverKeyPassword;
}
switch (opts.clientCertificateMode) {
case f_hosting_settings_js_1.FHostingSettings.ClientCertificateMode.NONE:
case f_hosting_settings_js_1.FHostingSettings.ClientCertificateMode.XFCC: // XFCC handled by AbstractWebServer
serverOpts.requestCert = false;
serverOpts.rejectUnauthorized = false;
break;
case f_hosting_settings_js_1.FHostingSettings.ClientCertificateMode.REQUEST:
serverOpts.requestCert = true;
serverOpts.rejectUnauthorized = false;
break;
default:
// By default use FHostingConfiguration.SecuredWebServer.ClientCertMode.TRUST mode
serverOpts.requestCert = true;
serverOpts.rejectUnauthorized = true;
break;
}
this._httpsServer = https_1.default.createServer(serverOpts, this.onRequest.bind(this));
}
get underlyingServer() { return this._httpsServer; }
onListen() {
const logger = this._log;
logger.debug(this.initExecutionContext, "SecuredWebServer#listen()");
const server = this._httpsServer;
return new Promise((resolve, reject) => {
logger.info(this.initExecutionContext, () => `Starting Web Server '${this._name}' ...`);
server
.on("listening", () => {
const address = server.address();
if (address !== null) {
if (typeof address === "string") {
logger.info(this.initExecutionContext, () => `Web Server '${this._name}' was started on ${address}`);
}
else {
logger.info(this.initExecutionContext, () => `${address.family} Web Server '${this._name}' was started on https://${address.address}:${address.port}`);
}
}
resolve();
})
.on("error", reject)
.listen(this._listenPort, this._listenHost);
});
}
async onDispose() {
const logger = this._log;
logger.debug(this.initExecutionContext, "SecuredWebServer#onDispose()");
const server = this._httpsServer;
logger.info(this.initExecutionContext, () => `Stopping Web Server '${this._name}' ...`);
await new Promise((destroyResolve) => {
server.close((e) => {
if (e) {
const ex = common_1.FException.wrapIfNeeded(e);
logger.warn(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped with error: ${ex.message}`);
logger.debug(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped with error`, ex);
}
else {
logger.info(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped`);
}
destroyResolve();
});
});
}
}
exports.SecuredWebServer = SecuredWebServer;
class FBindEndpoint extends common_1.FInitableBase {
_bindPath;
constructor(opts) {
super();
this._bindPath = opts.bindPath;
}
onInit() {
// NOP
}
onDispose() {
// NOP
}
}
exports.FBindEndpoint = FBindEndpoint;
class FServersBindEndpoint extends FBindEndpoint {
_servers;
constructor(servers, opts) {
super(opts);
this._servers = servers;
}
}
exports.FServersBindEndpoint = FServersBindEndpoint;
/**
* This endpoint supplies communication channels for each client connection.
*
* WebSocket Client is hosting the channel:
* - Client's messages delivered via SubscriberChannel
* - To deliver message to client use PublisherChannel
*
* If you need opposite behavior take a look for `WebSocketChannelFactoryEndpoint`
*
* You need to override onOpenBinaryChannel and/or onOpenTextChannel to obtain necessary channel
*/
class FWebSocketChannelSupplyEndpoint extends FServersBindEndpoint {
_webSocketServers;
_connections;
_log;
_defaultProtocol;
_allowedProtocols;
_connectionCounter;
constructor(servers, opts) {
super(servers, opts);
this._log = common_1.FLogger.create(this.constructor.name);
this._webSocketServers = [];
this._connections = new Set();
this._defaultProtocol = opts.defaultProtocol;
this._allowedProtocols = new Set([this._defaultProtocol]);
if (opts.allowedProtocols !== undefined) {
opts.allowedProtocols.forEach((allowedProtocol) => {
this._allowedProtocols.add(allowedProtocol);
});
}
this._connectionCounter = 0;
}
onInit() {
super.onInit();
for (const server of this._servers) {
const webSocketServer = server.createWebSocketServer(this._bindPath); // new WebSocket.Server({ noServer: true });
this._webSocketServers.push(webSocketServer);
webSocketServer.on("connection", this.onConnection.bind(this));
}
}
async onDispose() {
const logger = this._log;
const connections = [...this._connections.values()];
this._connections.clear();
for (const webSocket of connections) {
webSocket.close(1001, "going away");
webSocket.terminate();
}
const webSocketServers = this._webSocketServers.splice(0).reverse();
for (const webSocketServer of webSocketServers) {
await new Promise((resolve) => {
webSocketServer.close((err) => {
if (err !== undefined) {
logger.warn(this.initExecutionContext, () => `Web Socket Server was closed with error. Inner message: ${err.message} `);
logger.trace(this.initExecutionContext, "Web Socket Server was closed with error.", common_1.FException.wrapIfNeeded(err));
}
// dispose never raise any errors
resolve();
});
});
}
}
onConnection(webSocket, request) {
if (this.disposing) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
webSocket.close(1001, "going away");
webSocket.terminate();
return;
}
const logger = this._log;
if (this._connectionCounter === Number.MAX_SAFE_INTEGER) {
this._connectionCounter = 0;
}
const connectionNumber = this._connectionCounter++;
const ipAddress = request.socket.remoteAddress;
if (ipAddress !== undefined) {
logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was established from ${ipAddress} `);
}
if (logger.isInfoEnabled) {
logger.info(this.initExecutionContext, `Connection #${connectionNumber} was established`);
}
const subProtocol = webSocket.protocol || this._defaultProtocol;
if (webSocket.protocol !== undefined) {
if (!this._allowedProtocols.has(subProtocol)) {
logger.warn(this.initExecutionContext, () => `Connection #${connectionNumber} dropped. Not supported sub-protocol: ${subProtocol}`);
// https://tools.ietf.org/html/rfc6455#section-7.4.1
webSocket.close(1007, `Wrong sub-protocol: ${subProtocol}`);
webSocket.terminate();
return;
}
}
const FCancellationTokenSource = new common_1.FCancellationTokenSourceManual();
webSocket.binaryType = "nodebuffer";
const channels = new Map();
webSocket.onmessage = async ({ data }) => {
try {
let channelsTuple = channels.get(subProtocol);
if (channelsTuple === undefined) {
channelsTuple = {};
channels.set(subProtocol, channelsTuple);
}
if (data instanceof Buffer) {
if (channelsTuple.binaryChannel === undefined) {
const binaryChannel = new _FWebSocketChannelSupplyEndpointHelpers.WebSocketBinaryChannelImpl(webSocket);
try {
this.onOpenBinaryChannel(webSocket, subProtocol, binaryChannel);
}
catch (e) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
const friendlyError = common_1.FException.wrapIfNeeded(e);
webSocket.close(1011, friendlyError.message);
webSocket.terminate();
return;
}
channelsTuple.binaryChannel = binaryChannel;
}
await channelsTuple.binaryChannel.onMessage(FCancellationTokenSource.token, data);
}
else if (typeof data === "string") {
if (channelsTuple.textChannel === undefined) {
const textChannel = new _FWebSocketChannelSupplyEndpointHelpers.WebSocketTextChannelImpl(webSocket);
try {
this.onOpenTextChannel(webSocket, subProtocol, textChannel);
}
catch (e) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
const friendlyError = common_1.FException.wrapIfNeeded(e);
webSocket.close(1011, friendlyError.message);
webSocket.terminate();
return;
}
channelsTuple.textChannel = textChannel;
}
await channelsTuple.textChannel.onMessage(FCancellationTokenSource.token, data);
}
else {
logger.debug(this.initExecutionContext, () => `Connection #${connectionNumber} cannot handle a message due not supported type. Terminate socket...`);
// https://tools.ietf.org/html/rfc6455#section-7.4.1
webSocket.close(1003, `Not supported message type`);
webSocket.terminate();
return;
}
}
catch (e) {
if (logger.isInfoEnabled || logger.isTraceEnabled) {
const ex = common_1.FException.wrapIfNeeded(e);
logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} onMessage failed: ${ex.message}`);
logger.trace(this.initExecutionContext, () => `Connection #${connectionNumber} onMessage failed:`, ex);
}
}
};
webSocket.onclose = ({ code, reason }) => {
logger.trace(this.initExecutionContext, () => `Connection #${connectionNumber} was closed: ${JSON.stringify({ code, reason })} `);
logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was closed`);
FCancellationTokenSource.cancel();
this._connections.delete(webSocket);
const closedError = new common_1.FException(`WebSocket was closed: ${code} ${reason}`);
for (const channelsTuple of channels.values()) {
if (channelsTuple.binaryChannel !== undefined) {
channelsTuple.binaryChannel.onClose(closedError).catch(console.error);
}
if (channelsTuple.textChannel !== undefined) {
channelsTuple.textChannel.onClose(closedError).catch(console.error);
}
}
channels.clear();
};
this._connections.add(webSocket);
}
/**
* The method should be overridden. The method called by the endpoint,
* when WSClient sent first binary message for specified sub-protocol.
* @param webSocket WebSocket instance
* @param subProtocol These strings are used to indicate sub-protocols,
* so that a single server can implement multiple WebSocket sub-protocols (for example,
* you might want one server to be able to handle different types of interactions
* depending on the specified protocol).
* @param channel Binary channel instance to be user in inherited class
*/
onOpenBinaryChannel(_webSocket, subProtocol, _channel) {
throw new common_1.FExceptionInvalidOperation(`Binary messages are not supported by the sub-protocol: ${subProtocol}`);
}
/**
* The method should be overridden. The method called by the endpoint,
* when WSClient sent first text message for specified sub-protocol.
* @param webSocket WebSocket instance
* @param subProtocol These strings are used to indicate sub-protocols,
* so that a single server can implement multiple WebSocket sub-protocols (for example,
* you might want one server to be able to handle different types of interactions
* depending on the specified protocol).
* @param channel Text channel instance to be user in inherited class
*/
onOpenTextChannel(_webSocket, subProtocol, _channel) {
throw new common_1.FExceptionInvalidOperation(`Text messages are not supported by the sub-protocol: ${subProtocol}`);
}
}
exports.FWebSocketChannelSupplyEndpoint = FWebSocketChannelSupplyEndpoint;
/**
* This endpoint requests you for communication channels for each client connection.
*
* WebSocket Client is used the channel:
* - Client's messages delivered via PublisherChannel
* - To deliver message to client use SubscriberChannel
*
* If you need opposite behavior take a look for `WebSocketChannelSupplyEndpoint`
*
* You need to override createBinaryChannel and/or createTextChannel to provide necessary channel
*/
class FWebSocketChannelFactoryEndpoint extends FServersBindEndpoint {
_webSocketServers;
_connections;
_autoCreateChannelBinary;
_autoCreateChannelText;
_log;
_defaultProtocol;
_allowedProtocols;
_connectionCounter;
constructor(servers, opts, autoCreateChannels) {
super(servers, opts);
this._log = common_1.FLogger.create(this.constructor.name);
this._webSocketServers = [];
this._connections = new Set();
this._defaultProtocol = opts.defaultProtocol;
this._allowedProtocols = new Set([this._defaultProtocol]);
if (opts.allowedProtocols !== undefined) {
opts.allowedProtocols.forEach((allowedProtocol) => {
this._allowedProtocols.add(allowedProtocol);
});
}
this._connectionCounter = 0;
this._autoCreateChannelBinary = false;
this._autoCreateChannelText = false;
if (autoCreateChannels !== undefined) {
if (autoCreateChannels.binary === true) {
this._autoCreateChannelBinary = true;
}
if (autoCreateChannels.text === true) {
this._autoCreateChannelText = true;
}
}
}
onInit() {
super.onInit();
for (const server of this._servers) {
const webSocketServer = server.createWebSocketServer(this._bindPath); // new WebSocket.Server({ noServer: true });
this._webSocketServers.push(webSocketServer);
webSocketServer.on("connection", this.onConnection.bind(this));
}
}
async onDispose() {
// Prevent open new connection
for (const server of this._servers) {
await server.destroyWebSocketServer(this._bindPath);
}
const connections = [...this._connections.values()];
this._connections.clear();
for (const webSocket of connections) {
webSocket.close(1001, "going away");
webSocket.terminate();
}
this._webSocketServers.splice(0);
}
async onConnection(webSocket, request) {
if (this.disposing) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
webSocket.close(1001, "going away");
webSocket.terminate();
return;
}
const logger = this._log;
if (this._connectionCounter === Number.MAX_SAFE_INTEGER) {
this._connectionCounter = 0;
}
const connectionNumber = this._connectionCounter++;
const ipAddress = request.connection.remoteAddress;
if (ipAddress !== undefined) {
logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was established from ${ipAddress} `);
}
else {
logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was established`);
}
const subProtocol = webSocket.protocol || this._defaultProtocol;
if (webSocket.protocol !== undefined) {
if (!this._allowedProtocols.has(subProtocol)) {
logger.warn(this.initExecutionContext, `Connection #${connectionNumber} dropped. Not supported sub-protocol: ${subProtocol}`);
// https://tools.ietf.org/html/rfc6455#section-7.4.1
webSocket.close(1007, `Wrong sub-protocol: ${subProtocol}`);
webSocket.terminate();
return;
}
}
const cancellationTokenSource = new common_1.FCancellationTokenSourceManual();
const executionContext = new common_1.FCancellationExecutionContext(common_1.FExecutionContext.Empty, cancellationTokenSource.token);
webSocket.binaryType = "nodebuffer";
const channels = new Map();
const handler = async (_executionContext, event) => {
if (event instanceof common_1.FException) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
logger.debug(this.initExecutionContext, "Channel emits error. " + event.message);
logger.trace(this.initExecutionContext, "Channel emits error.", event);
webSocket.close(1011, event.message);
webSocket.terminate();
}
else {
const { data } = event;
await new Promise(sendResolve => webSocket.send(data, () => sendResolve()));
}
};
if (this._autoCreateChannelBinary === true) {
let channelsTuple = channels.get(subProtocol);
if (channelsTuple === undefined) {
channelsTuple = {};
channels.set(subProtocol, channelsTuple);
}
try {
const binaryChannel = await this.createBinaryChannel(executionContext, webSocket, subProtocol);
channelsTuple.binaryChannel = binaryChannel;
binaryChannel.addHandler(handler);
}
catch (e) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
const friendlyError = common_1.FException.wrapIfNeeded(e);
logger.debug(this.initExecutionContext, () => `Could not create binary channel. ${friendlyError.message}`);
logger.trace(this.initExecutionContext, "Could not create binary channel.", friendlyError);
webSocket.close(1011, friendlyError.message);
webSocket.terminate();
return;
}
}
if (this._autoCreateChannelText === true) {
let channelsTuple = channels.get(subProtocol);
if (channelsTuple === undefined) {
channelsTuple = {};
channels.set(subProtocol, channelsTuple);
}
try {
const textChannel = await this.createTextChannel(executionContext, webSocket, subProtocol);
channelsTuple.textChannel = textChannel;
textChannel.addHandler(handler);
}
catch (e) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
const friendlyError = common_1.FException.wrapIfNeeded(e);
logger.debug(this.initExecutionContext, () => `Could not create text channel. ${friendlyError.message}`);
logger.trace(this.initExecutionContext, "Could not create text channel.", friendlyError);
webSocket.close(1011, friendlyError.message);
webSocket.terminate();
return;
}
}
webSocket.onmessage = async ({ data }) => {
try {
let channelsTuple = channels.get(subProtocol);
if (channelsTuple === undefined) {
channelsTuple = {};
channels.set(subProtocol, channelsTuple);
}
if (data instanceof Buffer) {
if (channelsTuple.binaryChannel === undefined) {
try {
const binaryChannel = await this.createBinaryChannel(executionContext, webSocket, subProtocol);
channelsTuple.binaryChannel = binaryChannel;
binaryChannel.addHandler(handler);
}
catch (e) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
const friendlyError = common_1.FException.wrapIfNeeded(e);
logger.debug(this.initExecutionContext, () => `Could not create binary channel. ${friendlyError.message}`);
logger.trace(this.initExecutionContext, "Could not create binary channel.", friendlyError);
webSocket.close(1011, friendlyError.message);
webSocket.terminate();
return;
}
}
await channelsTuple.binaryChannel.send(common_1.FExecutionContext.Empty, data);
}
else if (typeof data === "string") {
if (channelsTuple.textChannel === undefined) {
try {
const textChannel = await this.createTextChannel(executionContext, webSocket, subProtocol);
channelsTuple.textChannel = textChannel;
textChannel.addHandler(handler);
}
catch (e) {
// https://tools.ietf.org/html/rfc6455#section-7.4.1
const friendlyError = common_1.FException.wrapIfNeeded(e);
logger.debug(this.initExecutionContext, () => `Could not create text channel. ${friendlyError.message}`);
logger.trace(this.initExecutionContext, "Could not create text channel.", friendlyError);
webSocket.close(1011, friendlyError.message);
webSocket.terminate();
return;
}
}
await channelsTuple.textChannel.send(common_1.FExecutionContext.Empty, data);
}
else {
logger.debug(this.initExecutionContext, () => `Connection #${connectionNumber} cannot handle a message due not supported type. Terminate socket...`);
// https://tools.ietf.org/html/rfc6455#section-7.4.1
webSocket.close(1003, `Not supported message type`);
webSocket.terminate();
return;
}
}
catch (e) {
const ex = common_1.FException.wrapIfNeeded(e);
logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} onMessage failed: ${ex.message}`);
logger.trace(this.initExecutionContext, () => `Connection #${connectionNumber} onMessage failed:`, ex);
}
};
webSocket.onclose = ({ code, reason }) => {
logger.trace(this.initExecutionContext, () => `Connection #${connectionNumber} was closed: ${JSON.stringify({ code, reason })} `);
logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was closed`);
cancellationTokenSource.cancel();
this._connections.delete(webSocket);
for (const channelsTuple of channels.values()) {
if (channelsTuple.binaryChannel !== undefined) {
channelsTuple.binaryChannel.removeHandler(handler);
channelsTuple.binaryChannel.dispose().catch(console.error);
}
if (channelsTuple.textChannel !== undefined) {
channelsTuple.textChannel.removeHandler(handler);
channelsTuple.textChannel.dispose().catch(console.error);
}
}
channels.clear();
};
this._connections.add(webSocket);
}
/**
* The method should be overridden. The method called by the endpoint,
* when WSClient sent first binary message for specified sub-protocol.
* @param webSocket WebSocket instance
* @param subProtocol These strings are used to indicate sub-protocols,
* so that a single server can implement multiple WebSocket sub-protocols (for example,
* you might want one server to be able to handle different types of interactions
* depending on the specified protocol).
*/
createBinaryChannel(_executionContext, _webSocket, subProtocol) {
throw new common_1.FExceptionInvalidOperation(`Binary messages are not supported by the sub-protocol: ${subProtocol}`);
}
/**
* The method should be overridden. The method called by the endpoint,
* when WSClient sent first text message for specified sub-protocol.
* @param webSocket WebSocket instance
* @param subProtocol These strings are used to indicate sub-protocols,
* so that a single server can implement multiple WebSocket sub-protocols (for example,
* you might want one server to be able to handle different types of interactions
* depending on the specified protocol).
*/
createTextChannel(_executionContext, _webSocket, subProtocol) {
throw new common_1.FExceptionInvalidOperation(`Text messages are not supported by the sub-protocol: ${subProtocol}`);
}
}
exports.FWebSocketChannelFactoryEndpoint = FWebSocketChannelFactoryEndpoint;
function instanceofWebServer(server) {
if (server instanceof UnsecuredWebServer) {
return true;
}
if (server instanceof SecuredWebServer) {
return true;
}
if (process.env["NODE_ENV"] === "development" &&
"name" in server &&
"underlyingServer" in server &&
"rootExpressApplication" in server &&
"bindRequestHandler" in server &&
"createWebSocketServer" in server &&
"listen" in server) {
// Look like the server is WebServer like. Allow it only in development
return true;
}
return false;
}
function createWebServer(serverOpts) {
switch (serverOpts.type) {
case "http":
return new UnsecuredWebServer(serverOpts);
case "https":
return new SecuredWebServer(serverOpts);
default: {
const { type } = serverOpts;
throw new Error(`Not supported server type '${type}'`);
}
}
}
function createWebServers(serversOpts) {
return serversOpts.map(serverOpts => createWebServer(serverOpts));
}
// function parseCertificate(certificate: Buffer | string): [pki.Certificate, Buffer] {
// let cert: pki.Certificate;
// let data: Buffer;
// if (typeof certificate === "string") {
// data = fs.readFileSync(certificate);
// cert = pki.certificateFromPem(data.toString("ascii"));
// } else {
// data = certificate;
// cert = pki.certificateFromPem(certificate.toString("ascii"));
// }
// return [cert, data];
// }
// function parseCertificates(certificates: Buffer | string | Array<string | Buffer>): Array<[pki.Certificate, Buffer]> {
// if (certificates instanceof Buffer || typeof certificates === "string") {
// return [parseCertificate(certificates)];
// } else {
// return certificates.map(parseCertificate);
// }
// }
var _FWebSocketChannelSupplyEndpointHelpers;
(function (_FWebSocketChannelSupplyEndpointHelpers) {
class WebSocketChannelBase {
_webSocket;
_callbacks;
_isBroken;
constructor(webSocket) {
this._webSocket = webSocket;
this._callbacks = [];
this._isBroken = false;
}
async onClose(ex) {
if (this._isBroken) {
// Already sent error-based callback, nothing to do
return;
}
await Promise.all(this._callbacks.map(async (callback) => {
try {
// Notify that channel broken
await callback(common_1.FExecutionContext.Empty, ex);
}
catch (e) {
// Nothing to do any more with fucking client's callback. Just log STDERR.
console.error(e);
}
}));
}
async onMessage(_cancellationToken, data) {
if (this._isBroken) {
console.error("Skip received messages due channel is broken");
return;
}
const errors = [];
const safePromises = this._callbacks.map(async (callback) => {
try {
await callback(common_1.FExecutionContext.Empty, { data /*, FCancellationToken*/ });
}
catch (e) {
errors.push(common_1.FException.wrapIfNeeded(e));
}
});
await Promise.all(safePromises);
if (errors.length > 0) {
// The callback supplier is shit coder. Closing channel and socket to prevent flowing shit...
this._isBroken = true;
// https://tools.ietf.org/html/rfc6455#section-7.4.1
this._webSocket.close(1011, "A server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.");
this._webSocket.terminate();
const aggregatedError = new common_1.FExceptionAggregate(errors);
await Promise.all(this._callbacks.map(async (callback) => {
try {
// Notify that channel broken
await callback(common_1.FExecutionContext.Empty, aggregatedError);
}
catch (e) {
// Nothing to do any more with fucking client's callback. Just log STDERR.
console.error(e);
}
}));
}
}
addHandler(cb) {
this._callbacks.push(cb);
}
removeHandler(cb) {
const index = this._callbacks.indexOf(cb);
if (index !== -1) {
this._callbacks.splice(index, 1);
}
}
send(_executionContext, data) {
if (this._isBroken) {
throw new common_1.FExceptionInvalidOperation("Cannot send message on broken channel");
}
return new Promise(sendResolve => this._webSocket.send(data, () => sendResolve()));
}
}
_FWebSocketChannelSupplyEndpointHelpers.WebSocketChannelBase = WebSocketChannelBase;
class WebSocketBinaryChannelImpl extends WebSocketChannelBase {
}
_FWebSocketChannelSupplyEndpointHelpers.WebSocketBinaryChannelImpl = WebSocketBinaryChannelImpl;
class WebSocketTextChannelImpl extends WebSocketChannelBase {
}
_FWebSocketChannelSupplyEndpointHelpers.WebSocketTextChannelImpl = WebSocketTextChannelImpl;
})(_FWebSocketChannelSupplyEndpointHelpers || (_FWebSocketChannelSupplyEndpointHelpers = {}));
//# sourceMappingURL=index.js.map