UNPKG

@duongtrungnguyen/nestro

Version:
267 lines 12 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index); var ws_proxy_service_exports = {}; __export(ws_proxy_service_exports, { WsProxyService: () => WsProxyService }); module.exports = __toCommonJS(ws_proxy_service_exports); var import_http = require("http"); var import_common = require("@nestjs/common"); var import_https = require("https"); var import_common2 = require("../../../common"); var import_base_proxy = require("./base-proxy.service"); var import_utils = require("../utils"); var import_constants = require("../constants"); var import_discovery = require("../../../discovery"); let WsProxyService = class extends import_base_proxy.BaseProxyService { constructor(discoveryService) { super(discoveryService); } /** * Proxies a WebSocket request to the target server. * Uses load balancing if service is provided, otherwise uses direct target. * * @param req - The incoming HTTP request for WebSocket upgrade. * @param res - The outgoing HTTP response. * @param routeConfig - Configuration for the proxy route. * @returns A promise that resolves when the WebSocket connection is closed. */ async proxyRequest(req, res, routeConfig) { this.validateRouteConfig(routeConfig); const proxyOptions = this.buildProxyOptions(routeConfig); const originalUrl = req.originalUrl || req.url; try { if (routeConfig.service) { await this.proxyToService(originalUrl, req, res, proxyOptions, routeConfig.service); } else { await this.proxyToTarget(originalUrl, req, res, proxyOptions, routeConfig.target); } } catch (error) { this.handleError(error, req, res, routeConfig.target); } } async proxyToService(originalUrl, req, res, proxyOptions, service) { try { await this.discoveryService.discover(service, async (instance, tryAnotherInstance) => { const targetUrl = (0, import_common2.buildInstanceWsUrl)(instance); (0, import_common2.debugLog)(WsProxyService.name, `Proxying WebSocket request from ${originalUrl} to ${targetUrl}`); await this.executeProxy(req, res, { ...proxyOptions, target: targetUrl }, this.handleProxy.bind(this), { onConnectFailed: (err) => { (0, import_common2.debugError)(WsProxyService.name, `Connection failed to ${targetUrl}: ${err.message}`); tryAnotherInstance(); } }); }); } catch (error) { throw this.handleDiscoveryError(error); } } async proxyToTarget(originalUrl, req, res, proxyOptions, targetUrl) { (0, import_common2.debugLog)(WsProxyService.name, `Proxying WebSocket request from ${originalUrl} to ${targetUrl}`); await this.executeProxy(req, res, { ...proxyOptions, target: targetUrl }, this.handleProxy.bind(this)); } /** * Checks if an error is related to connection issues * * @param err - The error to check * @returns boolean indicating if this is a connection error */ isConnectionError(err) { return import_constants.CONNECTION_ERROR_CODES.some((code) => err.message.includes(code) || err.code === code) || /connect|connection|timeout/i.test(err.message); } /** * Proxies a WebSocket connection to the target server. * * @param req - The incoming HTTP request for WebSocket upgrade. * @param socket - The client socket. * @param head - The first packet of the upgraded stream. * @param options - Proxy configuration options. * @param callbacks - Optional callbacks for lifecycle events. */ handleProxy(req, _, options = {}, callbacks = {}) { const socket = req.socket; const head = Buffer.from(""); try { const normalizedOptions = this.normalizeOptions(options, req.url || "/"); (0, import_common2.debugLog)(WsProxyService.name, `Proxying WebSocket to: ${normalizedOptions.target}`); if (req.method !== "GET" || !req.headers.upgrade || req.headers.upgrade.toLowerCase() !== "websocket") { socket.destroy(); return; } this.addForwardedHeaders(req, normalizedOptions.xfwd, true); this.streamRequest(req, socket, head, normalizedOptions, callbacks); } catch (err) { (0, import_common2.debugError)(WsProxyService.name, `WebSocket setup error: ${err.message}`); this.handleError(err, req, socket, void 0, callbacks.onError); if (callbacks.onConnectFailed) { callbacks.onConnectFailed(err); } socket.end(); } } /** * Streams a WebSocket connection to the target server. * * @param req - The incoming HTTP request. * @param socket - The client socket. * @param head - The first packet of the upgraded stream. * @param options - Normalized proxy options. * @param callbacks - Optional callbacks for lifecycle events. */ streamRequest(req, socket, head, options, callbacks) { const target = options.target; const isSSL = target.protocol === "https:" || target.protocol === "wss:"; const proxyOptions = (0, import_utils.setupOutgoing)({}, options, req); (0, import_utils.setupSocket)(socket); if (head?.length) socket.unshift(head); let proxyReq; try { proxyReq = (isSSL ? import_https.request : import_http.request)(proxyOptions); } catch (err) { this.handleRequestCreationError(err, socket, callbacks); return; } callbacks.onProxyReqWs?.(proxyReq, req, socket, options, head); this.setupProxyRequestHandlers(req, socket, proxyReq, options, callbacks); proxyReq.end(); } /** * Handles errors that occur during the creation of a WebSocket proxy request. * Logs the error, invokes the `onConnectFailed` callback if provided, and closes the socket if it is still open. * * @param err - The error encountered during request creation. * @param socket - The socket associated with the WebSocket connection. * @param callbacks - An object containing optional proxy callback functions. */ handleRequestCreationError(err, socket, callbacks) { (0, import_common2.debugError)(WsProxyService.name, `Failed to create WebSocket proxy request: ${err.message}`); callbacks.onConnectFailed?.(err); if (!socket.destroyed) socket.end(); } /** * Sets up event handlers for a proxy WebSocket request, managing error handling, * HTTP fallback, WebSocket upgrade, and request timeout. * * @param req - The incoming HTTP request from the client. * @param socket - The network socket associated with the client connection. * @param proxyReq - The outgoing proxy request to the target server. * @param options - Proxy options including target and timeout settings. * @param callbacks - Callback functions for handling connection and error events. */ setupProxyRequestHandlers(req, socket, proxyReq, options, callbacks) { const onOutgoingError = (err) => { (0, import_common2.debugError)(WsProxyService.name, `WebSocket proxy error: ${err.message}`); if (this.isConnectionError(err)) { callbacks.onConnectFailed?.(err); } else { this.handleError(err, req, socket, options.target, callbacks.onError); } if (!socket.destroyed) socket.end(); }; proxyReq.on("error", onOutgoingError); proxyReq.on("response", (proxyRes) => { if (!proxyRes.headers.upgrade || proxyRes.headers.upgrade.toLowerCase() !== "websocket") { (0, import_common2.debugWarn)(WsProxyService.name, "WebSocket upgrade failed, falling back to HTTP"); socket.write(this.createHttpHeader(`HTTP/${proxyRes.httpVersion} ${proxyRes.statusCode} ${proxyRes.statusMessage || ""}`, proxyRes.headers)); proxyRes.pipe(socket); } }); proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => { this.handleWebSocketUpgrade(req, socket, proxyRes, proxySocket, proxyHead, options, callbacks); }); if (options.timeout) { proxyReq.setTimeout(options.timeout, () => { const timeoutError = new Error(`WebSocket proxy timeout after ${options.timeout}ms`); proxyReq.destroy(timeoutError); callbacks.onConnectFailed?.(timeoutError); }); } } /** * Handles the WebSocket upgrade process between the client and the proxy target. * * This method sets up the necessary socket piping and event listeners to proxy * WebSocket connections, including error handling and lifecycle callbacks. * * @param req - The incoming HTTP request from the client. * @param socket - The socket associated with the client connection. * @param proxyRes - The HTTP response from the proxy target. * @param proxySocket - The socket connected to the proxy target. * @param proxyHead - Any buffered data from the proxy target's upgrade response. * @param options - Proxy configuration options. * @param callbacks - Callback functions for handling proxy events (open, close, error). */ handleWebSocketUpgrade(req, socket, proxyRes, proxySocket, proxyHead, options, callbacks) { (0, import_common2.debugLog)(WsProxyService.name, "WebSocket upgrade successful"); (0, import_utils.setupSocket)(proxySocket); if (proxyHead?.length) proxySocket.unshift(proxyHead); socket.write(this.createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers)); proxySocket.pipe(socket).pipe(proxySocket); proxySocket.on("error", (err) => { (0, import_common2.debugError)(WsProxyService.name, `WebSocket proxy socket error: ${err.message}`); this.handleError(err, req, socket, options.target, callbacks.onError); }); const handleClose = () => callbacks.onClose?.(proxyRes, proxySocket, proxyHead); proxySocket.on("end", handleClose); proxySocket.on("close", handleClose); socket.on("error", () => { if (!proxySocket.destroyed) proxySocket.end(); }); callbacks.onOpen?.(proxySocket); } /** * Creates an HTTP header string from a status line and headers object * * @param line - The status line * @param headers - The headers object * @returns Formatted HTTP header string */ createHttpHeader(line, headers) { const lines = [line]; for (const [key, value] of Object.entries(headers)) { if (value !== void 0) { if (Array.isArray(value)) { for (const val of value) { lines.push(`${key}: ${val}`); } } else { lines.push(`${key}: ${value}`); } } } return lines.join("\r\n") + "\r\n\r\n"; } }; WsProxyService = __decorateClass([ (0, import_common.Injectable)(), __decorateParam(0, (0, import_common.Inject)(import_discovery.DiscoveryService)) ], WsProxyService); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { WsProxyService }); //# sourceMappingURL=ws-proxy.service.js.map