import httpProxy from "http-proxy" import defaults from "linebridge/dist/server/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) }) 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(` { "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(` { "error": "404 Not found" } `) return } 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]}` 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() } }