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
|
/**/**/d_data
|
||||||
/**/**/redis_data
|
/**/**/redis_data
|
||||||
/**/**/*.env
|
/**/**/*.env
|
||||||
|
/**/**/.nginx
|
||||||
|
/**/**/nginx-bin
|
||||||
|
|
||||||
# Locks
|
# Locks
|
||||||
/**/**/package-lock.json
|
/**/**/package-lock.json
|
||||||
|
2
packages/server/.gitignore
vendored
2
packages/server/.gitignore
vendored
@ -20,6 +20,8 @@
|
|||||||
/**/**/d_data
|
/**/**/d_data
|
||||||
/**/**/redis_data
|
/**/**/redis_data
|
||||||
/**/**/*.env
|
/**/**/*.env
|
||||||
|
/**/**/.nginx
|
||||||
|
/**/**/nginx-bin
|
||||||
|
|
||||||
# Locks
|
# Locks
|
||||||
/**/**/package-lock.json
|
/**/**/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 chalk from "chalk"
|
||||||
import treeKill from "tree-kill"
|
import treeKill from "tree-kill"
|
||||||
|
|
||||||
import getIgnoredFiles from "./utils/getIgnoredFiles"
|
|
||||||
import scanServices from "./utils/scanServices"
|
import scanServices from "./utils/scanServices"
|
||||||
import spawnService from "./utils/spawnService"
|
|
||||||
import Proxy from "./proxy"
|
|
||||||
import RELP from "./repl"
|
import RELP from "./repl"
|
||||||
import comtyAscii from "./ascii"
|
import comtyAscii from "./ascii"
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
|
|
||||||
import ServiceManager from "./services/manager"
|
import ServiceManager from "./services/manager"
|
||||||
import Service from "./services/service"
|
import Service from "./services/service"
|
||||||
|
import * as Managers from "./managers"
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production"
|
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
|
* Gateway class - Main entry point for the service orchestrator
|
||||||
* Manages service discovery, spawning, and communication
|
* Manages service discovery, spawning, and communication
|
||||||
*/
|
*/
|
||||||
export default class Gateway {
|
export default class Gateway {
|
||||||
|
static gatewayMode = process.env.GATEWAY_MODE ?? "http_proxy"
|
||||||
|
|
||||||
eventBus = new EventEmitter()
|
eventBus = new EventEmitter()
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -39,7 +41,8 @@ export default class Gateway {
|
|||||||
services = []
|
services = []
|
||||||
serviceRegistry = Observable.from({})
|
serviceRegistry = Observable.from({})
|
||||||
|
|
||||||
proxy = null
|
gateway = null
|
||||||
|
|
||||||
ipcRouter = null
|
ipcRouter = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,12 +137,8 @@ export default class Gateway {
|
|||||||
this.onServiceReady(service)
|
this.onServiceReady(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === "router:register") {
|
if (data.type === "service:register") {
|
||||||
await this.handleRouterRegistration(service, data)
|
await this.handleServiceRegistration(service, data)
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "router:ws:register") {
|
|
||||||
await this.handleWebsocketRegistration(service, data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,65 +160,49 @@ export default class Gateway {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.proxy.unregisterAllFromService(id)
|
if (typeof this.gateway.unregisterAllFromService === "function") {
|
||||||
}
|
this.gateway.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}`,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle websocket registration requests from services
|
* Handle both router and websocket registration requests from services
|
||||||
* @param {Service} service - Service registering a websocket
|
* @param {Service} service - Service registering a route or websocket
|
||||||
* @param {object} msg - Registration message
|
* @param {object} msg - Registration message
|
||||||
|
* @param {boolean} isWebsocket - Whether this is a websocket registration
|
||||||
*/
|
*/
|
||||||
async handleWebsocketRegistration(service, msg) {
|
async handleServiceRegistration(service, data) {
|
||||||
const id = service.id
|
const { id } = service
|
||||||
const listenPort = msg.data.listen_port ?? msg.data.listen?.port
|
const { namespace, http, websocket, listen } = data.register
|
||||||
let target = `http://${this.state.internalIp}:${listenPort}`
|
|
||||||
|
|
||||||
if (!msg.data.ws_path && msg.data.namespace) {
|
if (http && http.enabled === true && Array.isArray(http.paths)) {
|
||||||
target += `/${msg.data.namespace}`
|
for (const path of http.paths) {
|
||||||
}
|
await this.gateway.register({
|
||||||
|
|
||||||
if (msg.data.ws_path && msg.data.ws_path !== "/") {
|
|
||||||
target += `/${msg.data.ws_path}`
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.proxy.register({
|
|
||||||
serviceId: id,
|
serviceId: id,
|
||||||
path: `/${msg.data.namespace}`,
|
path: path,
|
||||||
target: target,
|
target: `${http.proto}://${listen.ip}:${listen.port}${path}`,
|
||||||
pathRewrite: {
|
|
||||||
[`^/${msg.data.namespace}`]: "",
|
|
||||||
},
|
|
||||||
ws: true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
* Check if all services are ready and trigger the ready event
|
||||||
@ -232,6 +215,7 @@ export default class Gateway {
|
|||||||
).every((service) => service.initialized)
|
).every((service) => service.initialized)
|
||||||
|
|
||||||
if (allServicesInitialized) {
|
if (allServicesInitialized) {
|
||||||
|
this.state.allReady = true
|
||||||
this.onAllServicesReady()
|
this.onAllServicesReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,29 +231,34 @@ export default class Gateway {
|
|||||||
* Handle when all services are ready
|
* Handle when all services are ready
|
||||||
*/
|
*/
|
||||||
onAllServicesReady = async () => {
|
onAllServicesReady = async () => {
|
||||||
if (this.state.allReady) {
|
//console.clear()
|
||||||
return false
|
//console.log(comtyAscii)
|
||||||
}
|
|
||||||
|
|
||||||
console.clear()
|
console.log("\n\n\n")
|
||||||
this.state.allReady = true
|
|
||||||
|
|
||||||
console.log(comtyAscii)
|
|
||||||
console.log(`🎉 All services[${this.services.length}] ready!\n`)
|
console.log(`🎉 All services[${this.services.length}] ready!\n`)
|
||||||
console.log(`USE: select <service>, reload, exit`)
|
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
|
* Clean up resources on gateway exit
|
||||||
*/
|
*/
|
||||||
onGatewayExit = () => {
|
onGatewayExit = () => {
|
||||||
console.clear()
|
//console.clear()
|
||||||
console.log(`\n🛑 Preparing to exit...`)
|
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...`)
|
console.log(`Stopping all services...`)
|
||||||
this.serviceManager.stopAllServices()
|
this.serviceManager.stopAllServices()
|
||||||
|
|
||||||
@ -280,6 +269,13 @@ export default class Gateway {
|
|||||||
* Initialize the gateway and start all services
|
* Initialize the gateway and start all services
|
||||||
*/
|
*/
|
||||||
async initialize() {
|
async initialize() {
|
||||||
|
if (!Managers[this.constructor.gatewayMode]) {
|
||||||
|
console.error(
|
||||||
|
`❌ Gateway mode [${this.constructor.gatewayMode}] not supported`,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
onExit(this.onGatewayExit)
|
onExit(this.onGatewayExit)
|
||||||
|
|
||||||
// Increase limits to handle many services
|
// Increase limits to handle many services
|
||||||
@ -287,7 +283,6 @@ export default class Gateway {
|
|||||||
process.stderr.setMaxListeners(150)
|
process.stderr.setMaxListeners(150)
|
||||||
|
|
||||||
this.services = await scanServices()
|
this.services = await scanServices()
|
||||||
this.proxy = new Proxy()
|
|
||||||
this.ipcRouter = new IPCRouter()
|
this.ipcRouter = new IPCRouter()
|
||||||
|
|
||||||
if (this.services.length === 0) {
|
if (this.services.length === 0) {
|
||||||
@ -295,12 +290,24 @@ export default class Gateway {
|
|||||||
return process.exit(1)
|
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
|
// Make key components available globally
|
||||||
global.eventBus = this.eventBus
|
global.eventBus = this.eventBus
|
||||||
global.ipcRouter = this.ipcRouter
|
global.ipcRouter = this.ipcRouter
|
||||||
global.proxy = this.proxy
|
global.proxy = this.proxy
|
||||||
|
|
||||||
console.clear()
|
//console.clear()
|
||||||
console.log(comtyAscii)
|
console.log(comtyAscii)
|
||||||
console.log(
|
console.log(
|
||||||
`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${this.state.internalIp} | ${isProduction ? "production" : "development"} \n\n\n`,
|
`\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.createServicesRegistry()
|
||||||
await this.createServiceInstances()
|
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
|
// Initialize REPL interface
|
||||||
new RELP({
|
new RELP({
|
||||||
attachAllServicesSTD: () =>
|
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"
|
"dev": "cross-env NODE_ENV=development hermes-node ./start.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.13.2",
|
||||||
|
"@grpc/proto-loader": "^0.7.13",
|
||||||
"@infisical/sdk": "^2.1.8",
|
"@infisical/sdk": "^2.1.8",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/auto-instrumentations-node": "^0.56.1",
|
"@opentelemetry/auto-instrumentations-node": "^0.56.1",
|
||||||
@ -25,7 +27,7 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"linebridge": "^0.24.1",
|
"linebridge": "^0.25.2",
|
||||||
"minimatch": "^10.0.1",
|
"minimatch": "^10.0.1",
|
||||||
"minio": "^8.0.1",
|
"minio": "^8.0.1",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user