UNPKG

@duongtrungnguyen/nestro

Version:
246 lines 10.8 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 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); import { request as httpRequest } from "http"; import { Inject, Injectable } from "@nestjs/common"; import { request as httpsRequest } from "https"; import { buildInstanceWsUrl, debugError, debugLog, debugWarn } from "../../../common"; import { BaseProxyService } from "./base-proxy.service"; import { setupOutgoing, setupSocket } from "../utils"; import { CONNECTION_ERROR_CODES } from "../constants"; import { DiscoveryService } from "../../../discovery"; let WsProxyService = class extends 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 = buildInstanceWsUrl(instance); debugLog(WsProxyService.name, `Proxying WebSocket request from ${originalUrl} to ${targetUrl}`); await this.executeProxy(req, res, { ...proxyOptions, target: targetUrl }, this.handleProxy.bind(this), { onConnectFailed: (err) => { debugError(WsProxyService.name, `Connection failed to ${targetUrl}: ${err.message}`); tryAnotherInstance(); } }); }); } catch (error) { throw this.handleDiscoveryError(error); } } async proxyToTarget(originalUrl, req, res, proxyOptions, targetUrl) { 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 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 || "/"); 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) { 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 = setupOutgoing({}, options, req); setupSocket(socket); if (head?.length) socket.unshift(head); let proxyReq; try { proxyReq = (isSSL ? httpsRequest : httpRequest)(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) { 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) => { 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") { 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) { debugLog(WsProxyService.name, "WebSocket upgrade successful"); 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) => { 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([ Injectable(), __decorateParam(0, Inject(DiscoveryService)) ], WsProxyService); export { WsProxyService }; //# sourceMappingURL=ws-proxy.service.js.map