2025-04-01 01:00:11 +00:00

317 lines
7.1 KiB
JavaScript
Executable File

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)) {
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(config = {}) {
this.routes = new Map()
this.config = config
// Create HTTP server
this.server = getHttpServerEngine({}, this.handleRequest.bind(this))
// Handle WebSocket upgrades
this.server.on("upgrade", this.handleUpgrade.bind(this))
// Create a single proxy instance that will be reused
this.proxyServer = httpProxy.createProxyServer({
changeOrigin: true,
xfwd: true,
})
// Handle proxy errors
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 initialization needed, the server is already configured
return true
}
start = async () => {
return new Promise((resolve, reject) => {
this.server.listen(
this.config.port,
this.config.internalIp,
(err) => {
if (err) {
console.error("Failed to start server:", err)
return reject(err)
}
console.log(
`🚀 Gateway listening on ${this.config.port}:${this.config.internalIp}`,
)
resolve()
},
)
})
}
stop = async () => {
return new Promise((resolve) => {
this.server.close(() => {
console.log("Server stopped")
this.proxyServer.close()
resolve()
})
})
}
applyConfiguration = async () => {
// No specific configuration needs to be applied
return true
}
reload = async () => {
// No need to reload anything
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)
// If it's an OPTIONS request, respond immediately
if (req.method === "OPTIONS") {
res.statusCode = 204
res.end()
return
}
// Respond to root requests
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
}
// Find the route for this request
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
}
// Prepare proxy options
const proxyOptions = {
target: route.target,
changeOrigin: true,
xfwd: true,
}
// save original url
req.originalUrl = req.url
// Apply path rewriting if configured
if (route.pathRewrite) {
req.url = this.rewritePath(route.pathRewrite, req.url)
} else {
req.url = this.rewritePath({ [`^${route.path}`]: "" }, req.url)
}
// Add custom headers
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 the request
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) => {
// Find the route for this WebSocket connection
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
// Apply path rewriting if configured
if (route.pathRewrite) {
req.url = this.rewritePath(route.pathRewrite, req.url)
} else {
req.url = this.rewritePath({ [`^${route.path}`]: "" }, req.url)
}
// Create WebSocket-specific proxy options
const wsProxyOptions = {
target: route.target,
ws: true,
changeOrigin: true,
}
// Proxy the WebSocket connection
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()
}
})
}
}