UNPKG

@freemework/hosting

Version:

Hosting library of the Freemework Project.

1,016 lines 47.4 kB
"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