mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
support for multiple gateways & improve gateway codebase
This commit is contained in:
parent
e6cb90e270
commit
a7a4073348
@ -20,6 +20,8 @@
|
||||
/**/**/d_data
|
||||
/**/**/redis_data
|
||||
/**/**/*.env
|
||||
/**/**/.nginx
|
||||
/**/**/nginx-bin
|
||||
|
||||
# Locks
|
||||
/**/**/package-lock.json
|
||||
|
2
packages/server/.gitignore
vendored
2
packages/server/.gitignore
vendored
@ -20,6 +20,8 @@
|
||||
/**/**/d_data
|
||||
/**/**/redis_data
|
||||
/**/**/*.env
|
||||
/**/**/.nginx
|
||||
/**/**/nginx-bin
|
||||
|
||||
# Locks
|
||||
/**/**/package-lock.json
|
||||
|
157
packages/server/gateway/index.js
Normal file → Executable file
157
packages/server/gateway/index.js
Normal file → Executable file
@ -8,24 +8,26 @@ import { onExit } from "signal-exit"
|
||||
import chalk from "chalk"
|
||||
import treeKill from "tree-kill"
|
||||
|
||||
import getIgnoredFiles from "./utils/getIgnoredFiles"
|
||||
import scanServices from "./utils/scanServices"
|
||||
import spawnService from "./utils/spawnService"
|
||||
import Proxy from "./proxy"
|
||||
import RELP from "./repl"
|
||||
import comtyAscii from "./ascii"
|
||||
import pkg from "../package.json"
|
||||
|
||||
import ServiceManager from "./services/manager"
|
||||
import Service from "./services/service"
|
||||
import * as Managers from "./managers"
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production"
|
||||
const sslKey = path.resolve(process.cwd(), ".ssl", "privkey.pem")
|
||||
const sslCert = path.resolve(process.cwd(), ".ssl", "cert.pem")
|
||||
|
||||
/**
|
||||
* Gateway class - Main entry point for the service orchestrator
|
||||
* Manages service discovery, spawning, and communication
|
||||
*/
|
||||
export default class Gateway {
|
||||
static gatewayMode = process.env.GATEWAY_MODE ?? "http_proxy"
|
||||
|
||||
eventBus = new EventEmitter()
|
||||
|
||||
state = {
|
||||
@ -39,7 +41,8 @@ export default class Gateway {
|
||||
services = []
|
||||
serviceRegistry = Observable.from({})
|
||||
|
||||
proxy = null
|
||||
gateway = null
|
||||
|
||||
ipcRouter = null
|
||||
|
||||
/**
|
||||
@ -134,12 +137,8 @@ export default class Gateway {
|
||||
this.onServiceReady(service)
|
||||
}
|
||||
|
||||
if (data.type === "router:register") {
|
||||
await this.handleRouterRegistration(service, data)
|
||||
}
|
||||
|
||||
if (data.type === "router:ws:register") {
|
||||
await this.handleWebsocketRegistration(service, data)
|
||||
if (data.type === "service:register") {
|
||||
await this.handleServiceRegistration(service, data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,65 +160,49 @@ export default class Gateway {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
this.proxy.unregisterAllFromService(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle router registration requests from services
|
||||
* @param {Service} service - Service registering a route
|
||||
* @param {object} msg - Registration message
|
||||
*/
|
||||
async handleRouterRegistration(service, msg) {
|
||||
const id = service.id
|
||||
|
||||
if (msg.data.path_overrides) {
|
||||
for await (const pathOverride of msg.data.path_overrides) {
|
||||
await this.proxy.register({
|
||||
serviceId: id,
|
||||
path: `/${pathOverride}`,
|
||||
target: `http://${this.state.internalIp}:${msg.data.listen.port}/${pathOverride}`,
|
||||
pathRewrite: {
|
||||
[`^/${pathOverride}`]: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
await this.proxy.register({
|
||||
serviceId: id,
|
||||
path: `/${id}`,
|
||||
target: `http://${msg.data.listen.ip}:${msg.data.listen.port}`,
|
||||
})
|
||||
if (typeof this.gateway.unregisterAllFromService === "function") {
|
||||
this.gateway.unregisterAllFromService(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle websocket registration requests from services
|
||||
* @param {Service} service - Service registering a websocket
|
||||
* Handle both router and websocket registration requests from services
|
||||
* @param {Service} service - Service registering a route or websocket
|
||||
* @param {object} msg - Registration message
|
||||
* @param {boolean} isWebsocket - Whether this is a websocket registration
|
||||
*/
|
||||
async handleWebsocketRegistration(service, msg) {
|
||||
const id = service.id
|
||||
const listenPort = msg.data.listen_port ?? msg.data.listen?.port
|
||||
let target = `http://${this.state.internalIp}:${listenPort}`
|
||||
async handleServiceRegistration(service, data) {
|
||||
const { id } = service
|
||||
const { namespace, http, websocket, listen } = data.register
|
||||
|
||||
if (!msg.data.ws_path && msg.data.namespace) {
|
||||
target += `/${msg.data.namespace}`
|
||||
}
|
||||
|
||||
if (msg.data.ws_path && msg.data.ws_path !== "/") {
|
||||
target += `/${msg.data.ws_path}`
|
||||
}
|
||||
|
||||
await this.proxy.register({
|
||||
if (http && http.enabled === true && Array.isArray(http.paths)) {
|
||||
for (const path of http.paths) {
|
||||
await this.gateway.register({
|
||||
serviceId: id,
|
||||
path: `/${msg.data.namespace}`,
|
||||
target: target,
|
||||
pathRewrite: {
|
||||
[`^/${msg.data.namespace}`]: "",
|
||||
},
|
||||
ws: true,
|
||||
path: path,
|
||||
target: `${http.proto}://${listen.ip}:${listen.port}${path}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (websocket && websocket.enabled === true) {
|
||||
await this.gateway.register({
|
||||
serviceId: id,
|
||||
websocket: true,
|
||||
path: websocket.path,
|
||||
target: `${http.proto}://${listen.ip}:${listen.port}${websocket.path}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.state.allReady) {
|
||||
if (typeof this.gateway.applyConfiguration === "function") {
|
||||
await this.gateway.applyConfiguration()
|
||||
}
|
||||
if (typeof this.gateway.reload === "function") {
|
||||
await this.gateway.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all services are ready and trigger the ready event
|
||||
@ -232,6 +215,7 @@ export default class Gateway {
|
||||
).every((service) => service.initialized)
|
||||
|
||||
if (allServicesInitialized) {
|
||||
this.state.allReady = true
|
||||
this.onAllServicesReady()
|
||||
}
|
||||
}
|
||||
@ -247,29 +231,34 @@ export default class Gateway {
|
||||
* Handle when all services are ready
|
||||
*/
|
||||
onAllServicesReady = async () => {
|
||||
if (this.state.allReady) {
|
||||
return false
|
||||
}
|
||||
//console.clear()
|
||||
//console.log(comtyAscii)
|
||||
|
||||
console.clear()
|
||||
this.state.allReady = true
|
||||
|
||||
console.log(comtyAscii)
|
||||
console.log("\n\n\n")
|
||||
console.log(`🎉 All services[${this.services.length}] ready!\n`)
|
||||
console.log(`USE: select <service>, reload, exit`)
|
||||
|
||||
await this.proxy.listen(this.state.proxyPort, this.state.internalIp)
|
||||
if (typeof this.gateway.applyConfiguration === "function") {
|
||||
await this.gateway.applyConfiguration()
|
||||
}
|
||||
|
||||
if (typeof this.gateway.start === "function") {
|
||||
await this.gateway.start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources on gateway exit
|
||||
*/
|
||||
onGatewayExit = () => {
|
||||
console.clear()
|
||||
//console.clear()
|
||||
console.log(`\n🛑 Preparing to exit...`)
|
||||
console.log(`Stopping proxy...`)
|
||||
|
||||
this.proxy.close()
|
||||
if (typeof this.gateway.stop === "function") {
|
||||
console.log(`Stopping gateway...`)
|
||||
this.gateway.stop()
|
||||
}
|
||||
|
||||
console.log(`Stopping all services...`)
|
||||
this.serviceManager.stopAllServices()
|
||||
|
||||
@ -280,6 +269,13 @@ export default class Gateway {
|
||||
* Initialize the gateway and start all services
|
||||
*/
|
||||
async initialize() {
|
||||
if (!Managers[this.constructor.gatewayMode]) {
|
||||
console.error(
|
||||
`❌ Gateway mode [${this.constructor.gatewayMode}] not supported`,
|
||||
)
|
||||
return 0
|
||||
}
|
||||
|
||||
onExit(this.onGatewayExit)
|
||||
|
||||
// Increase limits to handle many services
|
||||
@ -287,7 +283,6 @@ export default class Gateway {
|
||||
process.stderr.setMaxListeners(150)
|
||||
|
||||
this.services = await scanServices()
|
||||
this.proxy = new Proxy()
|
||||
this.ipcRouter = new IPCRouter()
|
||||
|
||||
if (this.services.length === 0) {
|
||||
@ -295,12 +290,24 @@ export default class Gateway {
|
||||
return process.exit(1)
|
||||
}
|
||||
|
||||
// Initialize gateway
|
||||
this.gateway = new Managers[this.constructor.gatewayMode]({
|
||||
port: this.state.proxyPort,
|
||||
internalIp: this.state.internalIp,
|
||||
cert_file_name: sslCert,
|
||||
key_file_name: sslKey,
|
||||
})
|
||||
|
||||
if (typeof this.gateway.initialize === "function") {
|
||||
await this.gateway.initialize()
|
||||
}
|
||||
|
||||
// Make key components available globally
|
||||
global.eventBus = this.eventBus
|
||||
global.ipcRouter = this.ipcRouter
|
||||
global.proxy = this.proxy
|
||||
|
||||
console.clear()
|
||||
//console.clear()
|
||||
console.log(comtyAscii)
|
||||
console.log(
|
||||
`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${this.state.internalIp} | ${isProduction ? "production" : "development"} \n\n\n`,
|
||||
@ -316,6 +323,12 @@ export default class Gateway {
|
||||
await this.createServicesRegistry()
|
||||
await this.createServiceInstances()
|
||||
|
||||
// WARNING: Starting relp makes uwebsockets unable to work properly, surging some bugs from nodejs (domain.enter)
|
||||
// use another alternative to parse commands, like stdin reading or something...
|
||||
//this.startRELP()
|
||||
}
|
||||
|
||||
startRELP() {
|
||||
// Initialize REPL interface
|
||||
new RELP({
|
||||
attachAllServicesSTD: () =>
|
||||
|
310
packages/server/gateway/managers/http-proxy/index.js
Executable file
310
packages/server/gateway/managers/http-proxy/index.js
Executable file
@ -0,0 +1,310 @@
|
||||
import httpProxy from "http-proxy"
|
||||
import defaults from "linebridge/dist/defaults"
|
||||
import pkg from "../../../package.json" // Ajustado la ruta para que coincida con tu estructura
|
||||
|
||||
import http from "node:http"
|
||||
import https from "node:https"
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
function getHttpServerEngine(extraOptions = {}, handler = () => {}) {
|
||||
const sslKey = path.resolve(process.cwd(), ".ssl", "privkey.pem")
|
||||
const sslCert = path.resolve(process.cwd(), ".ssl", "cert.pem")
|
||||
|
||||
if (fs.existsSync(sslKey) && fs.existsSync(sslCert)) {
|
||||
console.log("Using HTTPS server")
|
||||
return https.createServer(
|
||||
{
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert),
|
||||
...extraOptions,
|
||||
},
|
||||
handler,
|
||||
)
|
||||
} else {
|
||||
console.log("Using HTTP server")
|
||||
return http.createServer(extraOptions, handler)
|
||||
}
|
||||
}
|
||||
|
||||
export default class Proxy {
|
||||
constructor() {
|
||||
this.routes = new Map()
|
||||
this.config = {}
|
||||
|
||||
// Crear servidor HTTP
|
||||
this.server = getHttpServerEngine({}, this.handleRequest.bind(this))
|
||||
|
||||
// Manejar upgrades de WebSocket
|
||||
this.server.on("upgrade", this.handleUpgrade.bind(this))
|
||||
|
||||
// Crear una única instancia de proxy que se reutilizará
|
||||
this.proxyServer = httpProxy.createProxyServer({
|
||||
changeOrigin: true,
|
||||
xfwd: true,
|
||||
})
|
||||
|
||||
// Manejar errores del proxy
|
||||
this.proxyServer.on("error", (err, req, res) => {
|
||||
console.error("Proxy error:", err)
|
||||
if (res && !res.headersSent) {
|
||||
res.writeHead(502, { "Content-Type": "application/json" })
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: "Bad Gateway",
|
||||
message: err.message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
register = ({ serviceId, path, target, pathRewrite, websocket } = {}) => {
|
||||
if (!path || !target) {
|
||||
throw new Error("Path and target are required")
|
||||
}
|
||||
|
||||
if (this.routes.has(path)) {
|
||||
console.warn(`Route already registered [${path}], skipping...`)
|
||||
return false
|
||||
}
|
||||
|
||||
const routeObj = {
|
||||
serviceId: serviceId ?? "default_service",
|
||||
path,
|
||||
target,
|
||||
pathRewrite,
|
||||
isWebSocket: !!websocket,
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🔗 Registering ${websocket ? "websocket" : "http"} route [${path}] -> [${target}]`,
|
||||
)
|
||||
this.routes.set(path, routeObj)
|
||||
return true
|
||||
}
|
||||
|
||||
unregister = (path) => {
|
||||
if (!this.routes.has(path)) {
|
||||
console.warn(`Route not registered [${path}], skipping...`)
|
||||
return false
|
||||
}
|
||||
|
||||
console.log(`🔗 Unregistering route [${path}]`)
|
||||
this.routes.delete(path)
|
||||
return true
|
||||
}
|
||||
|
||||
unregisterAllFromService = (serviceId) => {
|
||||
const pathsToRemove = []
|
||||
|
||||
this.routes.forEach((route, path) => {
|
||||
if (route.serviceId === serviceId) {
|
||||
pathsToRemove.push(path)
|
||||
}
|
||||
})
|
||||
|
||||
pathsToRemove.forEach(this.unregister)
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
// No es necesario inicializar nada, el servidor ya está configurado
|
||||
return true
|
||||
}
|
||||
|
||||
start = async (port = 9000, host = "0.0.0.0") => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.listen(port, host, (err) => {
|
||||
if (err) {
|
||||
console.error("Failed to start server:", err)
|
||||
return reject(err)
|
||||
}
|
||||
console.log(`🚀 Server listening on ${host}:${port}`)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
stop = async () => {
|
||||
return new Promise((resolve) => {
|
||||
this.server.close(() => {
|
||||
console.log("Server stopped")
|
||||
this.proxyServer.close()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
applyConfiguration = async () => {
|
||||
// No se necesita aplicar configuración específica
|
||||
return true
|
||||
}
|
||||
|
||||
reload = async () => {
|
||||
// No es necesario recargar nada
|
||||
return true
|
||||
}
|
||||
|
||||
rewritePath = (rewriteConfig, path) => {
|
||||
let result = path
|
||||
|
||||
if (!rewriteConfig) return result
|
||||
|
||||
for (const [pattern, replacement] of Object.entries(rewriteConfig)) {
|
||||
const regex = new RegExp(pattern)
|
||||
if (regex.test(path)) {
|
||||
result = result.replace(regex, replacement)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
setCorsHeaders = (res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET,HEAD,PUT,PATCH,POST,DELETE",
|
||||
)
|
||||
res.setHeader("Access-Control-Allow-Headers", "*")
|
||||
return res
|
||||
}
|
||||
|
||||
findRouteForPath(url) {
|
||||
const urlPath = url.split("?")[0]
|
||||
const segments = urlPath.split("/").filter(Boolean)
|
||||
|
||||
if (segments.length === 0) return null
|
||||
|
||||
const namespace = `/${segments[0]}`
|
||||
return this.routes.get(namespace)
|
||||
}
|
||||
|
||||
handleRequest = (req, res) => {
|
||||
this.setCorsHeaders(res)
|
||||
|
||||
// Si es una solicitud OPTIONS, responder inmediatamente
|
||||
if (req.method === "OPTIONS") {
|
||||
res.statusCode = 204
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// Responder a solicitudes raíz
|
||||
if (req.url === "/") {
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
lb_version: defaults?.version || "unknown",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Encontrar la ruta para esta solicitud
|
||||
const route = this.findRouteForPath(req.url)
|
||||
|
||||
if (!route) {
|
||||
res.statusCode = 404
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: "Gateway route not found",
|
||||
details:
|
||||
"The requested route does not exist or the service is down",
|
||||
path: req.url,
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Preparar opciones de proxy
|
||||
const proxyOptions = {
|
||||
target: route.target,
|
||||
changeOrigin: true,
|
||||
xfwd: true,
|
||||
}
|
||||
|
||||
// save original url
|
||||
req.originalUrl = req.url
|
||||
|
||||
// Aplicar reescritura de ruta si está configurada
|
||||
if (route.pathRewrite) {
|
||||
req.url = this.rewritePath(route.pathRewrite, req.url)
|
||||
} else {
|
||||
req.url = this.rewritePath({ [`^${route.path}`]: "" }, req.url)
|
||||
}
|
||||
|
||||
// Agregar encabezados personalizados
|
||||
this.proxyServer.on("proxyReq", (proxyReq, req, res, options) => {
|
||||
proxyReq.setHeader("x-linebridge-version", pkg.version)
|
||||
proxyReq.setHeader(
|
||||
"x-forwarded-for",
|
||||
req.socket.remoteAddress || req.ip,
|
||||
)
|
||||
proxyReq.setHeader("x-service-id", route.serviceId)
|
||||
proxyReq.setHeader(
|
||||
"X-Forwarded-Proto",
|
||||
req.socket.encrypted ? "https" : "http",
|
||||
)
|
||||
})
|
||||
|
||||
// Proxy la solicitud
|
||||
this.proxyServer.web(req, res, proxyOptions, (err) => {
|
||||
if (err) {
|
||||
console.error("Proxy error:", err)
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 502
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: "Bad Gateway",
|
||||
message: err.message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleUpgrade = (req, socket, head) => {
|
||||
// Encontrar la ruta para esta conexión WebSocket
|
||||
const route = this.findRouteForPath(req.url)
|
||||
|
||||
if (!route) {
|
||||
console.error(`WebSocket route not found for ${req.url}`)
|
||||
socket.write("HTTP/1.1 404 Not Found\r\n\r\n")
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
// save original url
|
||||
req.originalUrl = req.url
|
||||
|
||||
// Aplicar reescritura de ruta si está configurada
|
||||
if (route.pathRewrite) {
|
||||
req.url = this.rewritePath(route.pathRewrite, req.url)
|
||||
} else {
|
||||
req.url = this.rewritePath({ [`^${route.path}`]: "" }, req.url)
|
||||
}
|
||||
|
||||
// Crear un objeto de opciones de proxy específico para WebSocket
|
||||
const wsProxyOptions = {
|
||||
target: route.target,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
}
|
||||
|
||||
// Proxy la conexión WebSocket
|
||||
this.proxyServer.ws(req, socket, head, wsProxyOptions, (err) => {
|
||||
if (err) {
|
||||
console.error("WebSocket proxy error:", err)
|
||||
socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
||||
socket.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
2
packages/server/gateway/managers/index.js
Normal file
2
packages/server/gateway/managers/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as nginx } from "./nginx"
|
||||
export { default as http_proxy } from "./http-proxy"
|
599
packages/server/gateway/managers/nginx/index.js
Executable file
599
packages/server/gateway/managers/nginx/index.js
Executable file
@ -0,0 +1,599 @@
|
||||
import fs from "node:fs/promises"
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import { execSync, spawn } from "node:child_process"
|
||||
import { platform } from "node:os"
|
||||
|
||||
const localNginxBinary = path.resolve(process.cwd(), "nginx")
|
||||
|
||||
/**
|
||||
* NginxManager - Optimized version that batches configurations
|
||||
* Waits for all services to register before applying configuration
|
||||
*/
|
||||
export default class NginxManager {
|
||||
constructor(options = {}) {
|
||||
this.options = options
|
||||
|
||||
this.ssl = {
|
||||
on: false,
|
||||
cert_file_name: null,
|
||||
key_file_name: null,
|
||||
}
|
||||
this.port = options.port || 9000
|
||||
this.internalIp = options.internalIp || "0.0.0.0"
|
||||
|
||||
// Set binary path
|
||||
this.nginxBinary = existsSync(localNginxBinary)
|
||||
? localNginxBinary
|
||||
: "nginx"
|
||||
|
||||
// Directory structure
|
||||
this.nginxWorkDir =
|
||||
options.nginxWorkDir || path.join(process.cwd(), ".nginx")
|
||||
this.configDir = path.join(this.nginxWorkDir, "conf")
|
||||
this.tempDir = path.join(this.nginxWorkDir, "temp")
|
||||
this.logsDir = path.join(this.tempDir, "logs")
|
||||
this.cacheDir = path.join(this.tempDir, "cache")
|
||||
|
||||
// Configuration files
|
||||
this.mainConfigPath = path.join(this.configDir, "nginx.conf")
|
||||
this.servicesConfigPath = path.join(this.configDir, "services.conf")
|
||||
|
||||
// Process reference
|
||||
this.nginxProcess = null
|
||||
this.isNginxRunning = false
|
||||
|
||||
// Debug mode
|
||||
this.debug = options.debug || false
|
||||
|
||||
if (
|
||||
existsSync(this.options.cert_file_name) &&
|
||||
existsSync(this.options.key_file_name)
|
||||
) {
|
||||
console.log("[nginx] Setting SSL listen mode")
|
||||
this.ssl.on = true
|
||||
this.ssl.cert_file_name = this.options.cert_file_name
|
||||
this.ssl.key_file_name = this.options.key_file_name
|
||||
}
|
||||
}
|
||||
|
||||
routes = new Map() // key: path, value: { serviceId, target, pathRewrite, ws }
|
||||
|
||||
/**
|
||||
* Initialize the directory structure and configuration files
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
// Create directories
|
||||
this._ensureDirectories()
|
||||
|
||||
// Create mime.types file
|
||||
await this.writeMimeTypes()
|
||||
|
||||
// Generate main config file
|
||||
await this.generateMainConfig()
|
||||
|
||||
console.log(`🔧 Using Nginx binary: ${this.nginxBinary}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to initialize Nginx configuration:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure all required directories exist
|
||||
*/
|
||||
_ensureDirectories() {
|
||||
const dirs = [
|
||||
this.configDir,
|
||||
this.tempDir,
|
||||
this.logsDir,
|
||||
this.cacheDir,
|
||||
path.join(this.cacheDir, "client_body"),
|
||||
path.join(this.cacheDir, "proxy"),
|
||||
]
|
||||
|
||||
// Create all directories
|
||||
for (const dir of dirs) {
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Create empty log files
|
||||
const logFiles = [
|
||||
path.join(this.logsDir, "access.log"),
|
||||
path.join(this.logsDir, "error.log"),
|
||||
]
|
||||
|
||||
for (const file of logFiles) {
|
||||
if (!existsSync(file)) {
|
||||
writeFileSync(file, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the main Nginx configuration file
|
||||
*/
|
||||
async generateMainConfig() {
|
||||
// Normalize paths for Nginx
|
||||
const normalizedConfigDir = this.configDir.replace(/\\/g, "/")
|
||||
const normalizedTempDir = this.tempDir.replace(/\\/g, "/")
|
||||
const normalizedLogsDir = path
|
||||
.join(this.tempDir, "logs")
|
||||
.replace(/\\/g, "/")
|
||||
const normalizedCacheDir = path
|
||||
.join(this.tempDir, "cache")
|
||||
.replace(/\\/g, "/")
|
||||
|
||||
const config = `
|
||||
# Nginx configuration for Comty API Gateway
|
||||
# Auto-generated - Do not edit manually
|
||||
|
||||
worker_processes auto;
|
||||
error_log ${normalizedLogsDir}/error.log ${this.debug ? "debug" : "error"};
|
||||
pid ${normalizedTempDir}/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include ${normalizedConfigDir}/mime.types;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
log_format debug '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'upstream_addr: $upstream_addr '
|
||||
'upstream_status: $upstream_status '
|
||||
'request_time: $request_time '
|
||||
'http_version: $server_protocol';
|
||||
|
||||
access_log ${normalizedLogsDir}/access.log ${this.debug ? "debug" : "main"};
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
|
||||
tcp_nodelay on;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
# WebSocket support
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# Temp directories
|
||||
client_body_temp_path ${normalizedCacheDir}/client_body;
|
||||
proxy_temp_path ${normalizedCacheDir}/proxy;
|
||||
|
||||
# Set proxy timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
|
||||
server {
|
||||
${this.ssl.on ? `listen ${this.port} ssl;` : `listen ${this.port};`}
|
||||
server_name _;
|
||||
|
||||
${this.ssl.cert_file_name ? `ssl_certificate ${this.ssl.cert_file_name};` : ""}
|
||||
${this.ssl.key_file_name ? `ssl_certificate_key ${this.ssl.key_file_name};` : ""}
|
||||
|
||||
# Default route
|
||||
location / {
|
||||
add_header Content-Type application/json;
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Headers' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET,HEAD,PUT,PATCH,POST,DELETE' always;
|
||||
|
||||
return 200 '{"ok":1}';
|
||||
}
|
||||
|
||||
# Include service-specific configurations
|
||||
include ${normalizedConfigDir}/services.conf;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
console.log(`📝 Nginx configuration initialized at ${this.configDir}`)
|
||||
|
||||
await fs.writeFile(this.mainConfigPath, config)
|
||||
}
|
||||
|
||||
// Create mime.types file if it doesn't exist
|
||||
async writeMimeTypes() {
|
||||
const mimeTypesPath = path.join(this.configDir, "mime.types")
|
||||
|
||||
if (!existsSync(mimeTypesPath)) {
|
||||
// Basic MIME types
|
||||
const mimeTypes = `types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
text/xml xml;
|
||||
image/gif gif;
|
||||
image/jpeg jpeg jpg;
|
||||
application/javascript js;
|
||||
text/plain txt;
|
||||
image/png png;
|
||||
image/svg+xml svg svgz;
|
||||
application/json json;
|
||||
}`
|
||||
|
||||
await fs.writeFile(mimeTypesPath, mimeTypes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new service route in Nginx - queues for batch processing
|
||||
* @param {Object} routeConfig - Route configuration
|
||||
* @returns {Boolean} - Success status
|
||||
*/
|
||||
async register(routeConfig) {
|
||||
try {
|
||||
const {
|
||||
serviceId,
|
||||
path: routePath,
|
||||
target,
|
||||
pathRewrite,
|
||||
websocket,
|
||||
} = routeConfig
|
||||
|
||||
// Normalize path
|
||||
let normalizedPath = routePath.startsWith("/")
|
||||
? routePath
|
||||
: `/${routePath}`
|
||||
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
`🔍 Registering route for [${serviceId}]: ${normalizedPath} -> ${target} (${websocket ? "WebSocket" : "HTTP"})`,
|
||||
)
|
||||
}
|
||||
|
||||
// Store the route with improved handling of path rewrites
|
||||
const effectivePathRewrite = pathRewrite || {}
|
||||
|
||||
this.routes.set(normalizedPath, {
|
||||
serviceId,
|
||||
target,
|
||||
pathRewrite: effectivePathRewrite,
|
||||
websocket: !!websocket,
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ Failed to register route for [${routeConfig.serviceId}]:`,
|
||||
error,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the current configuration (generate config and reload/start Nginx)
|
||||
*/
|
||||
async applyConfiguration() {
|
||||
try {
|
||||
console.log(
|
||||
`🔄 Applying configuration with ${this.routes.size} routes...`,
|
||||
)
|
||||
|
||||
// Generate services configuration
|
||||
await this.regenerateServicesConfig()
|
||||
|
||||
// Verify configuration is valid
|
||||
const configTest = this.execNginxCommand(
|
||||
["-t", "-c", this.mainConfigPath],
|
||||
true,
|
||||
)
|
||||
if (!configTest.success) {
|
||||
throw new Error(
|
||||
`Configuration validation failed: ${configTest.error}`,
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`✅ Configuration applied successfully`)
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to apply configuration:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all routes for a specific service
|
||||
* @param {String} serviceId - Service ID to unregister
|
||||
* @returns {Boolean} - Success status
|
||||
*/
|
||||
async unregisterAllFromService(serviceId) {
|
||||
try {
|
||||
// Find and remove all routes for this service
|
||||
for (const [path, route] of this.routes.entries()) {
|
||||
if (route.serviceId === serviceId) {
|
||||
this.routes.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📝 Removed routes for service [${serviceId}]`)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ Failed to unregister routes for service [${serviceId}]:`,
|
||||
error,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the services configuration file
|
||||
*/
|
||||
async regenerateServicesConfig() {
|
||||
let config = `# Service routes\n# Last updated: ${new Date().toISOString()}\n# Total routes: ${this.routes.size}\n\n`
|
||||
|
||||
// Special case - no routes yet
|
||||
if (this.routes.size === 0) {
|
||||
config += "# No services registered yet\n"
|
||||
await fs.writeFile(this.servicesConfigPath, config)
|
||||
return
|
||||
}
|
||||
|
||||
// Add all routes
|
||||
for (const [path, route] of this.routes.entries()) {
|
||||
config += this.generateLocationBlock(path, route)
|
||||
}
|
||||
|
||||
// Write the config
|
||||
await fs.writeFile(this.servicesConfigPath, config)
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`📄 Writted [${this.routes.size}] service routes`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a location block for a route
|
||||
* @param {String} path - Route path
|
||||
* @param {Object} route - Route configuration
|
||||
* @returns {String} - Nginx location block
|
||||
*/
|
||||
generateLocationBlock(path, route) {
|
||||
// Create rewrite configuration if needed
|
||||
let rewriteConfig = ""
|
||||
|
||||
if (route.pathRewrite && Object.keys(route.pathRewrite).length > 0) {
|
||||
rewriteConfig += "# Path rewrite rules\n"
|
||||
for (const [pattern, replacement] of Object.entries(
|
||||
route.pathRewrite,
|
||||
)) {
|
||||
// Improved rewrite pattern that preserves query parameters
|
||||
rewriteConfig += `\trewrite ${pattern} ${replacement}$is_args$args break;`
|
||||
}
|
||||
} else {
|
||||
// If no explicit rewrite is defined, but we need to strip the path prefix,
|
||||
// Generate a default rewrite that preserves the URL structure
|
||||
if (path !== "/") {
|
||||
rewriteConfig += "# Default path rewrite to strip prefix\n"
|
||||
rewriteConfig += `\trewrite ^${path}(/.*)$ $1$is_args$args break;\n`
|
||||
rewriteConfig += `\trewrite ^${path}$ / break;`
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this is a root location or a more specific path
|
||||
const locationDirective =
|
||||
path === "/" ? "location /" : `location ${path}`
|
||||
|
||||
// Build the full location block with proper indentation
|
||||
return `
|
||||
${locationDirective} {
|
||||
if ($request_method = OPTIONS) {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Headers' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET,HEAD,PUT,PATCH,POST,DELETE';
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
# Set proxy configuration
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass_request_headers on;
|
||||
|
||||
# Standard proxy headers
|
||||
proxy_set_header Host $host:$server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Set headers for WebSocket support
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# Proxy pass to service
|
||||
proxy_pass ${route.target};
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Nginx server
|
||||
* @returns {Boolean} - Success status
|
||||
*/
|
||||
async start() {
|
||||
try {
|
||||
// Start Nginx
|
||||
this.nginxProcess = spawn(
|
||||
this.nginxBinary,
|
||||
[
|
||||
"-c",
|
||||
this.mainConfigPath,
|
||||
"-g",
|
||||
"daemon off;",
|
||||
"-p",
|
||||
this.tempDir,
|
||||
],
|
||||
{
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
)
|
||||
|
||||
this.nginxProcess.stdout.on("data", (data) => {
|
||||
console.log(`[Nginx] ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
this.nginxProcess.stderr.on("data", (data) => {
|
||||
console.error(`[Nginx] ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
this.nginxProcess.on("close", (code) => {
|
||||
this.isNginxRunning = false
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(`Nginx process exited with code ${code}`)
|
||||
}
|
||||
this.nginxProcess = null
|
||||
})
|
||||
|
||||
// Wait briefly to check for immediate startup errors
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
if (this.nginxProcess.exitCode !== null) {
|
||||
throw new Error(
|
||||
`Nginx failed to start (exit code: ${this.nginxProcess.exitCode})`,
|
||||
)
|
||||
}
|
||||
|
||||
this.isNginxRunning = true
|
||||
console.log(`🚀 Nginx started on port ${this.port}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
this.isNginxRunning = false
|
||||
console.error("❌ Failed to start Nginx:", error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an Nginx command
|
||||
* @param {Array} args - Command arguments
|
||||
* @param {Boolean} returnOutput - Whether to return command output
|
||||
* @returns {Object} - Success status and output/error
|
||||
*/
|
||||
execNginxCommand(args, returnOutput = false) {
|
||||
try {
|
||||
// Always include prefix to set the temp directory
|
||||
const allArgs = [...args, "-p", this.tempDir]
|
||||
|
||||
const cmdString = `"${this.nginxBinary}" ${allArgs.join(" ")}`
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`🔍 Executing: ${cmdString}`)
|
||||
}
|
||||
|
||||
const output = execSync(cmdString, {
|
||||
encoding: "utf8",
|
||||
stdio: returnOutput ? "pipe" : "inherit",
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: returnOutput ? output : null,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
output: error.stdout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the Nginx configuration
|
||||
* @returns {Boolean} - Success status
|
||||
*/
|
||||
async reload() {
|
||||
try {
|
||||
// Test configuration validity
|
||||
const configTest = this.execNginxCommand(
|
||||
["-t", "-c", this.mainConfigPath],
|
||||
true,
|
||||
)
|
||||
if (!configTest.success) {
|
||||
throw new Error(
|
||||
`Configuration test failed: ${configTest.error}`,
|
||||
)
|
||||
}
|
||||
|
||||
// If Nginx isn't running, start it
|
||||
if (
|
||||
!this.isNginxRunning ||
|
||||
!this.nginxProcess ||
|
||||
this.nginxProcess.exitCode !== null
|
||||
) {
|
||||
return await this.start()
|
||||
}
|
||||
|
||||
// Send reload signal
|
||||
const result = this.execNginxCommand([
|
||||
"-s",
|
||||
"reload",
|
||||
"-c",
|
||||
this.mainConfigPath,
|
||||
])
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Failed to reload Nginx: ${result.error}`)
|
||||
}
|
||||
|
||||
console.log("🔄 Nginx configuration reloaded")
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to reload Nginx:", error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the Nginx server
|
||||
* @returns {Boolean} - Success status
|
||||
*/
|
||||
async close() {
|
||||
try {
|
||||
if (this.nginxProcess) {
|
||||
// Try graceful shutdown first
|
||||
this.execNginxCommand(["-s", "quit", "-c", this.mainConfigPath])
|
||||
|
||||
// Give Nginx time to shut down
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// If still running, force kill
|
||||
if (this.nginxProcess && this.nginxProcess.exitCode === null) {
|
||||
this.nginxProcess.kill("SIGTERM")
|
||||
|
||||
// If STILL running after another second, use SIGKILL
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
if (
|
||||
this.nginxProcess &&
|
||||
this.nginxProcess.exitCode === null
|
||||
) {
|
||||
this.nginxProcess.kill("SIGKILL")
|
||||
}
|
||||
}
|
||||
|
||||
this.nginxProcess = null
|
||||
}
|
||||
|
||||
this.isNginxRunning = false
|
||||
console.log("🛑 Nginx stopped")
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to stop Nginx:", error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,227 +0,0 @@
|
||||
import httpProxy from "http-proxy"
|
||||
import defaults from "linebridge/dist/defaults"
|
||||
|
||||
import pkg from "../package.json"
|
||||
|
||||
import http from "node:http"
|
||||
import https from "node:https"
|
||||
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
function getHttpServerEngine(extraOptions = {}, handler = () => {}) {
|
||||
const sslKey = path.resolve(process.cwd(), ".ssl", "privkey.pem")
|
||||
const sslCert = path.resolve(process.cwd(), ".ssl", "cert.pem")
|
||||
|
||||
if (fs.existsSync(sslKey) && fs.existsSync(sslCert)) {
|
||||
return https.createServer(
|
||||
{
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert),
|
||||
...extraOptions,
|
||||
},
|
||||
handler,
|
||||
)
|
||||
} else {
|
||||
return http.createServer(extraOptions, handler)
|
||||
}
|
||||
}
|
||||
|
||||
export default class Proxy {
|
||||
constructor() {
|
||||
this.proxys = new Map()
|
||||
this.wsProxys = new Map()
|
||||
|
||||
this.http = getHttpServerEngine({}, this.handleHttpRequest)
|
||||
this.http.on("upgrade", this.handleHttpUpgrade)
|
||||
}
|
||||
|
||||
http = null
|
||||
|
||||
register = ({ serviceId, path, target, pathRewrite, ws } = {}) => {
|
||||
if (!path) {
|
||||
throw new Error("Path is required")
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
throw new Error("Target is required")
|
||||
}
|
||||
|
||||
if (this.proxys.has(path)) {
|
||||
console.warn(`Proxy already registered [${path}], skipping...`)
|
||||
return false
|
||||
}
|
||||
|
||||
const proxy = httpProxy.createProxyServer({
|
||||
target: target,
|
||||
})
|
||||
|
||||
proxy.on("proxyReq", (proxyReq, req, res, options) => {
|
||||
proxyReq.setHeader("x-linebridge-version", pkg.version)
|
||||
proxyReq.setHeader("x-forwarded-for", req.socket.remoteAddress)
|
||||
proxyReq.setHeader("x-service-id", serviceId)
|
||||
proxyReq.setHeader(
|
||||
"X-Forwarded-Proto",
|
||||
req.socket.encrypted ? "https" : "http",
|
||||
)
|
||||
})
|
||||
|
||||
proxy.on("error", (e) => {
|
||||
console.error(e)
|
||||
})
|
||||
|
||||
const proxyObj = {
|
||||
serviceId: serviceId ?? "default_service",
|
||||
path: path,
|
||||
target: target,
|
||||
pathRewrite: pathRewrite,
|
||||
proxy: proxy,
|
||||
}
|
||||
|
||||
if (ws) {
|
||||
console.log(
|
||||
`🔗 Registering websocket proxy [${path}] -> [${target}]`,
|
||||
)
|
||||
this.wsProxys.set(path, proxyObj)
|
||||
} else {
|
||||
console.log(`🔗 Registering path proxy [${path}] -> [${target}]`)
|
||||
this.proxys.set(path, proxyObj)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
unregister = (path) => {
|
||||
if (!this.proxys.has(path)) {
|
||||
console.warn(`Proxy not registered [${path}], skipping...`)
|
||||
return false
|
||||
}
|
||||
|
||||
console.log(`🔗 Unregistering path proxy [${path}]`)
|
||||
|
||||
this.proxys.get(path).proxy.close()
|
||||
this.proxys.delete(path)
|
||||
}
|
||||
|
||||
unregisterAllFromService = (serviceId) => {
|
||||
this.proxys.forEach((value, key) => {
|
||||
if (value.serviceId === serviceId) {
|
||||
this.unregister(value.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
listen = async (port = 9000, host = "0.0.0.0", cb) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
this.http.listen(port, host, () => {
|
||||
console.log(`🔗 Proxy listening on ${host}:${port}`)
|
||||
|
||||
if (cb) {
|
||||
cb(this)
|
||||
}
|
||||
|
||||
resolve(this)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
rewritePath = (rewriteConfig, path) => {
|
||||
let result = path
|
||||
const rules = []
|
||||
|
||||
for (const [key, value] of Object.entries(rewriteConfig)) {
|
||||
rules.push({
|
||||
regex: new RegExp(key),
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
|
||||
for (const rule of rules) {
|
||||
if (rule.regex.test(path)) {
|
||||
result = result.replace(rule.regex, rule.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
setCorsHeaders = (res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET,HEAD,PUT,PATCH,POST,DELETE",
|
||||
)
|
||||
res.setHeader("Access-Control-Allow-Headers", "*")
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
handleHttpRequest = (req, res) => {
|
||||
res = this.setCorsHeaders(res)
|
||||
|
||||
const sanitizedUrl = req.url.split("?")[0]
|
||||
|
||||
// preflight continue with code 204
|
||||
if (req.method === "OPTIONS") {
|
||||
res.statusCode = 204
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
if (sanitizedUrl === "/") {
|
||||
return res.end(
|
||||
JSON.stringify({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
lb_version: defaults.version,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const namespace = `/${sanitizedUrl.split("/")[1]}`
|
||||
const route = this.proxys.get(namespace)
|
||||
|
||||
if (!route) {
|
||||
res.statusCode = 404
|
||||
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: "Gateway route not found",
|
||||
details:
|
||||
"The gateway route you are trying to access does not exist, maybe the service is down...",
|
||||
namespace: namespace,
|
||||
}),
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (route.pathRewrite) {
|
||||
req.url = this.rewritePath(route.pathRewrite, req.url)
|
||||
}
|
||||
|
||||
route.proxy.web(req, res)
|
||||
}
|
||||
|
||||
handleHttpUpgrade = (req, socket, head) => {
|
||||
const namespace = `/${req.url.split("/")[1].split("?")[0]}`
|
||||
const route = this.wsProxys.get(namespace)
|
||||
|
||||
if (!route) {
|
||||
// destroy socket
|
||||
socket.destroy()
|
||||
return false
|
||||
}
|
||||
|
||||
if (route.pathRewrite) {
|
||||
req.url = this.rewritePath(route.pathRewrite, req.url)
|
||||
}
|
||||
|
||||
route.proxy.ws(req, socket, head)
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.http.close()
|
||||
}
|
||||
}
|
0
packages/server/gateway/repl.js
Normal file → Executable file
0
packages/server/gateway/repl.js
Normal file → Executable file
0
packages/server/gateway/services/manager.js
Normal file → Executable file
0
packages/server/gateway/services/manager.js
Normal file → Executable file
0
packages/server/gateway/services/service.js
Normal file → Executable file
0
packages/server/gateway/services/service.js
Normal file → Executable file
0
packages/server/gateway/utils/createServiceLogTransformer.js
Normal file → Executable file
0
packages/server/gateway/utils/createServiceLogTransformer.js
Normal file → Executable file
0
packages/server/gateway/utils/getIgnoredFiles.js
Normal file → Executable file
0
packages/server/gateway/utils/getIgnoredFiles.js
Normal file → Executable file
0
packages/server/gateway/utils/scanServices.js
Normal file → Executable file
0
packages/server/gateway/utils/scanServices.js
Normal file → Executable file
0
packages/server/gateway/utils/spawnService.js
Normal file → Executable file
0
packages/server/gateway/utils/spawnService.js
Normal file → Executable file
0
packages/server/gateway/vars.js
Normal file → Executable file
0
packages/server/gateway/vars.js
Normal file → Executable file
@ -11,6 +11,8 @@
|
||||
"dev": "cross-env NODE_ENV=development hermes-node ./start.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.2",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@infisical/sdk": "^2.1.8",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.56.1",
|
||||
@ -25,7 +27,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"http-proxy": "^1.18.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"linebridge": "^0.24.1",
|
||||
"linebridge": "^0.25.2",
|
||||
"minimatch": "^10.0.1",
|
||||
"minio": "^8.0.1",
|
||||
"module-alias": "^2.2.3",
|
||||
|
Loading…
x
Reference in New Issue
Block a user