UNPKG

@freemework/hosting

Version:

Hosting library of the Freemework Project.

983 lines 44.7 kB
import { FModuleVersionGuard, FExecutionContext, FCancellationExecutionContext, FCancellationTokenSourceManual, FException, FExceptionInvalidOperation, FExceptionAggregate, FInitableBase, FLogger, } from "@freemework/common"; import express from 'express'; import fs from "fs"; import http from "http"; import https from "https"; // import { unescape as urlDecode } from "querystring"; import WebSocket from "ws"; // import { pki } from "node-forge"; import { FHttpRequestCancellationToken } from "./FHttpRequestCancellationToken.js"; import { FHostingSettings } from "./f_hosting_settings.js"; import { packageInfo } from "./package_info.js"; FModuleVersionGuard(packageInfo); export { FHostingSettings, FHostingConfiguration } from "./f_hosting_settings.js"; export { FHttpRequestCancellationToken } from "./FHttpRequestCancellationToken.js"; export * from "./configuration/index.js"; export * from "./launcher/index.js"; export class FAbstractWebServer extends 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(request); } constructor(opts) { super(); this._name = opts.name; this._listenHost = opts.listenHost; this._listenPort = opts.listenPort; this._log = opts.log !== undefined ? opts.log : 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 === 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 = express(); 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 WebSocket.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.", 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; } } export class UnsecuredWebServer extends FAbstractWebServer { _httpServer; constructor(opts) { super(opts); // Make HTTP server instance const serverOpts = {}; this._httpServer = http.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 = 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(); }); }); } } export class SecuredWebServer extends FAbstractWebServer { _httpsServer; constructor(opts) { super(opts); // Make HTTPS server instance const serverOpts = { cert: opts.serverCertificate instanceof Buffer ? opts.serverCertificate : fs.readFileSync(opts.serverCertificate), key: opts.serverKey instanceof Buffer ? opts.serverKey : fs.readFileSync(opts.serverKey) }; if (opts.caCertificates !== null) { if (typeof opts.caCertificates === "string") { serverOpts.ca = fs.readFileSync(opts.caCertificates); } //serverOpts.ca = this.caCertificatesAsBuffer; } if (opts.serverKeyPassword !== null) { serverOpts.passphrase = opts.serverKeyPassword; } switch (opts.clientCertificateMode) { case FHostingSettings.ClientCertificateMode.NONE: case FHostingSettings.ClientCertificateMode.XFCC: // XFCC handled by AbstractWebServer serverOpts.requestCert = false; serverOpts.rejectUnauthorized = false; break; case 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.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 = 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(); }); }); } } export class FBindEndpoint extends FInitableBase { _bindPath; constructor(opts) { super(); this._bindPath = opts.bindPath; } onInit() { // NOP } onDispose() { // NOP } } export class FServersBindEndpoint extends FBindEndpoint { _servers; constructor(servers, opts) { super(opts); this._servers = servers; } } /** * 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 */ export class FWebSocketChannelSupplyEndpoint extends FServersBindEndpoint { _webSocketServers; _connections; _log; _defaultProtocol; _allowedProtocols; _connectionCounter; constructor(servers, opts) { super(servers, opts); this._log = 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.", 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 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 = 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 = 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 = 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 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 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 FExceptionInvalidOperation(`Text messages are not supported by the sub-protocol: ${subProtocol}`); } } /** * 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 */ export class FWebSocketChannelFactoryEndpoint extends FServersBindEndpoint { _webSocketServers; _connections; _autoCreateChannelBinary; _autoCreateChannelText; _log; _defaultProtocol; _allowedProtocols; _connectionCounter; constructor(servers, opts, autoCreateChannels) { super(servers, opts); this._log = 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 FCancellationTokenSourceManual(); const executionContext = new FCancellationExecutionContext(FExecutionContext.Empty, cancellationTokenSource.token); webSocket.binaryType = "nodebuffer"; const channels = new Map(); const handler = async (_executionContext, event) => { if (event instanceof 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 = 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 = 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 = 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(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 = 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(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 = 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 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 FExceptionInvalidOperation(`Text messages are not supported by the sub-protocol: ${subProtocol}`); } } export 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; } export 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}'`); } } } export 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(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(FExecutionContext.Empty, { data /*, FCancellationToken*/ }); } catch (e) { errors.push(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 FExceptionAggregate(errors); await Promise.all(this._callbacks.map(async (callback) => { try { // Notify that channel broken await callback(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 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