UNPKG

@interopio/gateway-server

Version:

## `@glue42/gateway-ent` compatibility

1,634 lines (1,614 loc) 52.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { GatewayServer: () => server_exports, default: () => index_default }); module.exports = __toCommonJS(index_exports); // src/server.ts var server_exports = {}; __export(server_exports, { Factory: () => Factory }); var import_ws = require("ws"); var import_node_http = __toESM(require("node:http"), 1); var import_node_https = __toESM(require("node:https"), 1); var import_node_fs = require("node:fs"); var import_node_async_hooks = require("node:async_hooks"); var import_gateway7 = require("@interopio/gateway"); // src/logger.ts var import_gateway = require("@interopio/gateway"); function getLogger(name) { return import_gateway.IOGateway.Logging.getLogger(`gateway.server.${name}`); } // src/utils.ts function socketKey(socket) { const remoteIp = socket.remoteAddress; if (!remoteIp) { throw new Error("Socket has no remote address"); } return `${remoteIp}:${socket.remotePort}`; } // src/server/types.ts var WebExchange = class { get method() { return this.request.method; } get path() { return this.request.path; } }; // src/server/exchange.ts function requestToProtocol(request, defaultProtocol) { let proto = request.headers.get("x-forwarded-proto"); if (Array.isArray(proto)) { proto = proto[0]; } if (proto !== void 0) { return proto.split(",", 1)[0].trim(); } return defaultProtocol; } function requestToHost(request, defaultHost) { let host = request.headers.get("x-forwarded-for"); if (host === void 0) { host = request.headers.get("x-forwarded-host"); if (Array.isArray(host)) { host = host[0]; } if (host) { const port = request.headers.one("x-forwarded-port"); if (port) { host = `${host}:${port}`; } } if (host === void 0) { host = request.headers.one("host"); } } if (Array.isArray(host)) { host = host[0]; } if (host) { return host.split(",", 1)[0].trim(); } return defaultHost; } var HttpServerRequest = class { constructor(_req) { this._req = _req; this._headers = new IncomingMessageHeaders(_req); } _body; _url; _headers; get http2() { return this._req.httpVersionMajor >= 2; } get headers() { return this._headers; } get path() { return this.URL?.pathname; } get URL() { this._url ??= new URL(this._req.url, `${this.protocol}://${this.host}`); return this._url; } get query() { return this.URL?.search; } get method() { return this._req.method; } get host() { let dh = void 0; if (this._req.httpVersionMajor >= 2) { dh = (this._req?.headers)[":authority"]; } if (dh === void 0) { dh = this._req?.socket.remoteAddress; } return requestToHost(this, dh); } get protocol() { let dp = void 0; if (this._req.httpVersionMajor > 2) { dp = this._req.headers[":scheme"]; } if (dp === void 0) { dp = this._req?.socket["encrypted"] ? "https" : "http"; } return requestToProtocol(this, dp); } get socket() { return this._req.socket; } get body() { this._body ??= new Promise((resolve, reject) => { const chunks = []; this._req.on("error", (err) => reject(err)).on("data", (chunk) => chunks.push(chunk)).on("end", () => { resolve(new Blob(chunks)); }); }); return this._body; } get text() { return this.body.then(async (blob) => await blob.text()); } get json() { return this.body.then(async (blob) => { const json = JSON.parse(await blob.text()); return json; }); } }; var IncomingMessageHeaders = class { constructor(_msg) { this._msg = _msg; } has(name) { return this._msg.headers[name] !== void 0; } get(name) { return this._msg.headers[name]; } list(name) { return toList(this._msg.headers[name]); } one(name) { const value = this._msg.headers[name]; if (Array.isArray(value)) { return value[0]; } return value; } keys() { return Object.keys(this._msg.headers).values(); } }; var OutgoingMessageHeaders = class { constructor(_msg) { this._msg = _msg; } has(name) { return this._msg.hasHeader(name); } keys() { return this._msg.getHeaderNames().values(); } get(name) { return this._msg.getHeader(name); } one(name) { const value = this._msg.getHeader(name); if (Array.isArray(value)) { return value[0]; } return value; } set(name, value) { if (!this._msg.headersSent) { if (Array.isArray(value)) { value = value.map((v) => typeof v === "number" ? String(v) : v); } else if (typeof value === "number") { value = String(value); } if (value) { this._msg.setHeader(name, value); } else { this._msg.removeHeader(name); } } return this; } add(name, value) { if (!this._msg.headersSent) { this._msg.appendHeader(name, value); } return this; } list(name) { const values = this.get(name); return toList(values); } }; var HttpServerResponse = class { constructor(_res) { this._res = _res; this._headers = new OutgoingMessageHeaders(_res); } _headers; get statusCode() { return this._res.statusCode; } set statusCode(value) { if (this._res.headersSent) { return; } this._res.statusCode = value; } get headers() { return this._headers; } }; var DefaultWebExchange = class extends WebExchange { constructor(request, response) { super(); this.request = request; this.response = response; } }; function toList(values) { if (typeof values === "string") { values = [values]; } if (typeof values === "number") { values = [String(values)]; } const list = []; if (values) { for (const value of values) { if (value) { list.push(...parseHeader(value)); } } } return list; } function parseHeader(value) { const list = []; { let start2 = 0; let end = 0; for (let i = 0; i < value.length; i++) { switch (value.charCodeAt(i)) { case 32: if (start2 === end) { start2 = end = i + 1; } break; case 44: list.push(value.slice(start2, end)); start2 = end = i + 1; break; default: end = end + 1; break; } } list.push(value.slice(start2, end)); } return list; } // src/gateway/ws/core.ts var import_gateway2 = require("@interopio/gateway"); var GatewayEncoders = import_gateway2.IOGateway.Encoding; var log = getLogger("ws"); var codec = GatewayEncoders.json(); function initClient(key, socket, host) { const opts = { key, host, codec, onPing: () => { socket.ping((err) => { if (err) { log.warn(`failed to ping ${key}`, err); } else { log.info(`ping sent to ${key}`); } }); }, onDisconnect: (reason) => { switch (reason) { case "inactive": { log.warn(`no heartbeat (ping) received from ${key}, closing socket`); socket.close(4001, "ping expected"); break; } case "shutdown": { socket.close(1001, "shutdown"); break; } } } }; try { return this.client((data) => socket.send(data), opts); } catch (err) { log.warn(`${key} failed to create client`, err); } } async function create(server) { log.info("start gateway"); await this.start({ endpoint: server.endpoint }); server.wss.on("error", (err) => { log.error("error starting the gateway websocket server", err); }).on("connection", (socket, req) => { const request = new HttpServerRequest(req); const key = socketKey(request.socket); const host = request.host; log.info(`${key} connected on gw from ${host}`); const client = initClient.call(this, key, socket); if (!client) { log.error(`${key} gw client init failed`); socket.terminate(); return; } socket.on("error", (err) => { log.error(`${key} websocket error: ${err}`, err); }); socket.on("message", (data, _isBinary) => { if (Array.isArray(data)) { data = Buffer.concat(data); } client.send(data); }); socket.on("close", (code) => { log.info(`${key} disconnected from gw. code: ${code}`); client.close(); }); }); return { close: async () => { server.wss.close(); await this.stop(); } }; } var core_default = create; // src/mesh/connections.ts var logger = getLogger("mesh.connections"); var InMemoryNodeConnections = class { constructor(timeout = 6e4) { this.timeout = timeout; } nodes = /* @__PURE__ */ new Map(); nodesByEndpoint = /* @__PURE__ */ new Map(); memberIds = 0; announce(nodes) { for (const node of nodes) { const { node: nodeId, users, endpoint } = node; const foundId = this.nodesByEndpoint.get(endpoint); if (foundId) { if (foundId !== nodeId) { logger.warn(`endpoint ${endpoint} clash. replacing node ${foundId} with ${nodeId}`); this.nodesByEndpoint.set(endpoint, nodeId); this.nodes.delete(foundId); } } else { logger.info(`endpoint ${endpoint} announced for ${nodeId}`); this.nodesByEndpoint.set(endpoint, nodeId); } this.nodes.set(nodeId, this.updateNode(node, new Set(users ?? []), nodeId, this.nodes.get(nodeId))); } this.cleanupOldNodes(); const sortedNodes = Array.from(this.nodes.values()).sort((a, b) => a.memberId - b.memberId); return nodes.map((e) => { const node = e.node; const connect = this.findConnections(sortedNodes, this.nodes.get(node)); return { node, connect }; }); } remove(nodeId) { const removed = this.nodes.get(nodeId); if (removed) { this.nodes.delete(nodeId); const endpoint = removed.endpoint; this.nodesByEndpoint.delete(endpoint); logger.info(`endpoint ${endpoint} removed for ${nodeId}`); return true; } return false; } updateNode(newNode, users, _key, oldNode) { const node = !oldNode ? { ...newNode, memberId: this.memberIds++ } : oldNode; return { ...node, users, lastAccess: Date.now() }; } cleanupOldNodes() { const threshold = Date.now() - this.timeout; for (const [nodeId, v] of this.nodes) { if (v.lastAccess < threshold) { if (logger.enabledFor("debug")) { logger.debug(`${nodeId} expired - no announcement since ${new Date(v.lastAccess).toISOString()}, timeout is ${this.timeout} ms.`); } this.nodes.delete(nodeId); } } } findConnections(sortedNodes, node) { return sortedNodes.reduce((l, c) => { if (node !== void 0 && c.memberId < node.memberId) { const intersection = new Set(c.users); node.users.forEach((user) => { if (!c.users.has(user)) { intersection.delete(user); } }); c.users.forEach((user) => { if (!node.users.has(user)) { intersection.delete(user); } }); if (intersection.size > 0) { const e = { node: c.node, endpoint: c.endpoint }; return l.concat(e); } } return l; }, new Array()); } }; // src/mesh/rest-directory/routes.ts function routes(connections) { return [ async (ctx, next) => { if (ctx.method === "POST" && ctx.path === "/api/nodes") { const json = await ctx.request.json; if (!Array.isArray(json)) { ctx.response.statusCode = 400; ctx.response._res.end(); } else { const nodes = json; const result = connections.announce(nodes); const body = JSON.stringify(result); ctx.response.headers.set("content-type", "application/json"); ctx.response.statusCode = 200; ctx.response._res.end(body); } } else { await next(); } }, async ({ method, path, response }, next) => { if (method === "DELETE" && path?.startsWith("/api/nodes/")) { const nodeId = path?.substring("/api/nodes/".length); connections.remove(nodeId); response.statusCode = 200; response._res.end(); } else { await next(); } } ]; } var routes_default = routes; // src/mesh/ws/broker/core.ts var import_gateway3 = require("@interopio/gateway"); var GatewayEncoders2 = import_gateway3.IOGateway.Encoding; var logger2 = getLogger("mesh.ws.broker"); function broadcastNodeAdded(nodes, newSocket, newNodeId) { Object.entries(nodes.nodes).forEach(([nodeId, socket]) => { if (nodeId !== newNodeId) { newSocket.send(codec2.encode({ type: "node-added", "node-id": newNodeId, "new-node": nodeId })); socket.send(codec2.encode({ type: "node-added", "node-id": nodeId, "new-node": newNodeId })); } }); } function broadcastNodeRemoved(nodes, removedNodeId) { Object.entries(nodes.nodes).forEach(([nodeId, socket]) => { if (nodeId !== removedNodeId) { socket.send(codec2.encode({ type: "node-removed", "node-id": nodeId, "removed-node": removedNodeId })); } }); } function onOpen(connectedNodes, key) { logger2.info(`[${key}] connection accepted`); } function onClose(connectedNodes, key, code, reason) { logger2.info(`[${key}] connected closed [${code}](${reason})`); const nodeIds = connectedNodes.sockets[key]; if (nodeIds) { delete connectedNodes.sockets[key]; for (const nodeId of nodeIds) { delete connectedNodes.nodes[nodeId]; } for (const nodeId of nodeIds) { broadcastNodeRemoved(connectedNodes, nodeId); } } } function processMessage(connectedNodes, socket, key, msg) { switch (msg.type) { case "hello": { const nodeId = msg["node-id"]; connectedNodes.nodes[nodeId] = socket; connectedNodes.sockets[key] = connectedNodes.sockets[key] ?? []; connectedNodes.sockets[key].push(nodeId); logger2.info(`[${key}] node ${nodeId} added.`); broadcastNodeAdded(connectedNodes, socket, nodeId); break; } case "bye": { const nodeId = msg["node-id"]; delete connectedNodes[nodeId]; logger2.info(`[${key}] node ${nodeId} removed.`); broadcastNodeRemoved(connectedNodes, nodeId); break; } case "data": { const sourceNodeId = msg.from; const targetNodeId = msg.to; if ("all" === targetNodeId) { Object.entries(connectedNodes.nodes).forEach(([nodeId, socket2]) => { if (nodeId !== sourceNodeId) { socket2.send(codec2.encode(msg)); } }); } else { const socket2 = connectedNodes.nodes[targetNodeId]; if (socket2) { socket2.send(codec2.encode(msg)); } else { logger2.warn(`unable to send to node ${targetNodeId} message ${JSON.stringify(msg)}`); } } break; } default: { logger2.warn(`[${key}] ignoring unknown message ${JSON.stringify(msg)}`); break; } } } var codec2 = GatewayEncoders2.transit({ keywordize: /* @__PURE__ */ new Map([ ["/type", "*"], ["/message/body/type", "*"], ["/message/origin", "*"], ["/message/receiver/type", "*"], ["/message/source/type", "*"], ["/message/body/type", "*"] ]) }); function onMessage(connectedNodes, socket, key, msg) { try { const decoded = codec2.decode(msg); if (logger2.enabledFor("debug")) { logger2.debug(`[${key}] processing msg ${JSON.stringify(decoded)}`); } processMessage(connectedNodes, socket, key, decoded); } catch (ex) { logger2.error(`[${key}] unable to process message`, ex); } } var WebsocketBroker = class { constructor(server) { this.server = server; } async close() { this.server.close(); } }; async function create2(server) { const connectedNodes = { nodes: {}, sockets: {} }; logger2.info(`mesh server is listening`); server.wss.on("error", () => { logger2.error(`error starting mesh server`); }).on("connection", (socket, request) => { const key = socketKey(request.socket); onOpen(connectedNodes, key); socket.on("error", (err) => { logger2.error(`[${key}] websocket error: ${err}`, err); }); socket.on("message", (data, isBinary) => { if (Array.isArray(data)) { data = Buffer.concat(data); } onMessage(connectedNodes, socket, key, data); }); socket.on("close", (code, reason) => { onClose(connectedNodes, key, code, reason); }); }); return new WebsocketBroker(server.wss); } var core_default2 = create2; // src/mesh/ws/relays/core.ts var import_gateway4 = require("@interopio/gateway"); var GatewayEncoders3 = import_gateway4.IOGateway.Encoding; var logger3 = getLogger("mesh.ws.relay"); var codec3 = GatewayEncoders3.transit({ keywordize: /* @__PURE__ */ new Map([ ["/type", "*"], ["/message/body/type", "*"], ["/message/origin", "*"], ["/message/receiver/type", "*"], ["/message/source/type", "*"], ["/message/body/type", "*"] ]) }); var InternalRelays = class { // key -> socket clients = /* @__PURE__ */ new Map(); // node -> key links = /* @__PURE__ */ new Map(); onMsg; onErr; add(key, soc) { this.clients.set(key, soc); } remove(key) { this.clients.delete(key); for (const [node, k] of this.links) { if (k === key) { this.links.delete(node); } } } receive(key, msg) { const node = this.link(key, msg); if (node && this.onMsg) { this.onMsg(key, node, msg); } } link(key, msg) { try { const decoded = codec3.decode(msg); const { type, from, to } = decoded; if (to === "all") { switch (type) { case "hello": { if (logger3.enabledFor("debug")) { logger3.debug(`${key} registers node ${from}`); } this.links.set(from, key); break; } case "bye": { if (logger3.enabledFor("debug")) { logger3.debug(`${key} unregisters node ${from}`); } this.links.delete(from); break; } } return; } return from; } catch (e) { if (this.onErr) { this.onErr(key, e instanceof Error ? e : new Error(`link failed :${e}`)); } else { logger3.warn(`${key} unable to process ${msg}`, e); } } } send(key, node, msg, cb) { const decoded = codec3.decode(msg); if (logger3.enabledFor("debug")) { logger3.debug(`${key} sending msg to ${node} ${JSON.stringify(decoded)}`); } const clientKey = this.links.get(node); if (clientKey) { const client = this.clients.get(clientKey); if (client) { client.send(msg, { binary: false }, (err) => { cb(clientKey, err); }); return; } } throw new Error(`${key} no active link for ${decoded.to}`); } }; var internal = new InternalRelays(); var relays = internal; async function create3(server) { logger3.info(`relays server is listening`); server.wss.on("error", () => { logger3.error(`error starting relays server`); }).on("connection", (socket, request) => { const key = socketKey(request.socket); logger3.info(`${key} connected on relays`); internal.add(key, socket); socket.on("error", (err) => { logger3.error(`[${key}] websocket error: ${err}`, err); }); socket.on("message", (data, _isBinary) => { if (Array.isArray(data)) { data = Buffer.concat(data); } try { internal.receive(key, data); } catch (e) { logger3.warn(`[${key}] error processing received data '${data}'`, e); } }); socket.on("close", (code, reason) => { internal.remove(key); logger3.info(`${key} disconnected from relays`); }); }); return { close: async () => { server.wss.close(); } }; } var core_default3 = create3; // src/mesh/ws/cluster/core.ts var logger4 = getLogger("mesh.ws.cluster"); function onMessage2(key, node, socketsByNodeId, msg) { try { relays.send(key, node, msg, (k, err) => { if (err) { logger4.warn(`${k} error writing msg ${msg}: ${err}`); return; } if (logger4.enabledFor("debug")) { logger4.debug(`${k} sent msg ${msg}`); } }); } catch (ex) { logger4.error(`${key} unable to process message`, ex); if (node) { const socket = socketsByNodeId.get(node)?.get(key); socket?.terminate(); } } } async function create4(server) { const socketsByNodeId = /* @__PURE__ */ new Map(); relays.onMsg = (k, nodeId, msg) => { try { const sockets = socketsByNodeId.get(nodeId); if (sockets && sockets.size > 0) { for (const [key, socket] of sockets) { socket.send(msg, { binary: false }, (err) => { if (err) { logger4.warn(`${key} error writing from ${k} msg ${msg}: ${err}`); return; } if (logger4.enabledFor("debug")) { logger4.debug(`${key} sent from ${k} msg ${msg}`); } }); } } else { logger4.warn(`${k} dropped msg ${msg}.`); } } catch (ex) { logger4.error(`${k} unable to process message`, ex); } }; server.wss.on("error", () => { logger4.error(`error starting mesh server`); }).on("listening", () => { logger4.info(`mesh server is listening`); }).on("connection", (socket, req) => { const request = new HttpServerRequest(req); const key = socketKey(request.socket); const query = new URLSearchParams(request.query ?? void 0); logger4.info(`${key} connected on cluster with ${query}`); const node = query.get("node"); if (node) { let sockets = socketsByNodeId.get(node); if (!sockets) { sockets = /* @__PURE__ */ new Map(); socketsByNodeId.set(node, sockets); } sockets.set(key, socket); } else { socket.terminate(); return; } socket.on("error", (err) => { logger4.error(`${key} websocket error: ${err}`, err); }); socket.on("message", (data, _isBinary) => { if (Array.isArray(data)) { data = Buffer.concat(data); } onMessage2(key, node, socketsByNodeId, data); }); socket.on("close", (_code, _reason) => { logger4.info(`${key} disconnected from cluster`); const sockets = socketsByNodeId.get(node); if (sockets) { sockets.delete(key); if (sockets.size === 0) { socketsByNodeId.delete(node); } } }); }); return { close: async () => { server.wss.close(); } }; } var core_default4 = create4; // src/metrics/routes.ts var logger5 = getLogger("metrics"); var COOKIE_NAME = "GW_LOGIN"; function loggedIn(auth, ctx) { if (auth) { const cookieHeaderValue = ctx.request.headers.list("cookie"); const cookie = cookieHeaderValue?.join("; ").split("; ").find((value) => value.startsWith(`${COOKIE_NAME}=`)); return cookie && parseInt(cookie?.substring(COOKIE_NAME.length + 1)) > Date.now(); } return true; } async function routes2(config) { const { jsonFileAppender } = await import("@interopio/gateway/metrics/publisher/file"); const appender = jsonFileAppender(logger5); await appender.open(config.file?.location ?? "metrics.ndjson", config.file?.append ?? true); return [ async (ctx, next) => { if (ctx.method === "GET" && ctx.path === "/api/metrics") { if (loggedIn(config.auth, ctx)) { ctx.response.statusCode = 200; } else { ctx.response.statusCode = 302; ctx.response.headers.set("location", "/api/login?redirectTo=/api/metrics"); } ctx.response._res.end(); } else { await next(); } }, async (ctx, next) => { if (ctx.method === "GET" && ctx.path === "/api/login") { const redirectTo = new URLSearchParams(ctx.request.query ?? void 0).get("redirectTo"); const expires = Date.now() + 180 * 1e3; ctx.response.headers.add("set-cookie", `${COOKIE_NAME}=${expires}; Path=/api; SameSite=strict`); if (redirectTo) { ctx.response.statusCode = 302; ctx.response.headers.set("location", redirectTo); } else { ctx.response.statusCode = 200; } ctx.response._res.end(); } else { await next(); } }, async (ctx, next) => { if (ctx.method === "POST" && ctx.path === "/api/metrics") { if (loggedIn(config.auth, ctx)) { ctx.response.statusCode = 202; ctx.response._res.end(); try { const json = await ctx.request.json; const update = json; if (logger5.enabledFor("debug")) { logger5.debug(`${JSON.stringify(update)}`); } if ((config.file?.status ?? false) || update.status === void 0) { await appender.write(update); } } catch (e) { logger5.error(`error processing metrics`, e); } } else { ctx.response.statusCode = 401; ctx.response._res.end(); } } else { await next(); } } ]; } var routes_default2 = routes2; // src/common/compose.ts function compose(...middleware) { if (!Array.isArray(middleware)) { throw new Error("middleware must be array!"); } for (const fn of middleware) { if (typeof fn !== "function") { throw new Error("middleware must be compose of functions!"); } } return async function(ctx, next) { let index = -1; return await dispatch(0); async function dispatch(i) { if (i < index) { throw new Error("next() called multiple times"); } index = i; let fn; if (i === middleware.length) { fn = next; } else { fn = middleware[i]; } if (!fn) { return; } await fn(ctx, dispatch.bind(null, i + 1)); } }; } // src/server/address.ts var import_node_os = require("node:os"); var PORT_RANGE_MATCHER = /^(\d+|(0x[\da-f]+))(-(\d+|(0x[\da-f]+)))?$/i; function validPort(port) { if (port > 65535) throw new Error(`bad port ${port}`); return port; } function* portRange(port) { if (typeof port === "string") { for (const portRange2 of port.split(",")) { const trimmed = portRange2.trim(); const matchResult = PORT_RANGE_MATCHER.exec(trimmed); if (matchResult) { const start2 = parseInt(matchResult[1]); const end = parseInt(matchResult[4] ?? matchResult[1]); for (let i = validPort(start2); i < validPort(end) + 1; i++) { yield i; } } else { throw new Error(`'${portRange2}' is not a valid port or range.`); } } } else { yield validPort(port); } } var localIp = (() => { function first(a) { return a.length > 0 ? a[0] : void 0; } const addresses = Object.values((0, import_node_os.networkInterfaces)()).flatMap((details) => { return (details ?? []).filter((info) => info.family === "IPv4"); }).reduce((acc, info) => { acc[info.internal ? "internal" : "external"].push(info); return acc; }, { internal: [], external: [] }); return (first(addresses.internal) ?? first(addresses.external))?.address; })(); // src/server/monitoring.ts var import_node_v8 = require("node:v8"); var import_promises = require("node:fs/promises"); var log2 = getLogger("monitoring"); var DEFAULT_OPTIONS = { memoryLimit: 1024 * 1024 * 1024, // 1GB reportInterval: 10 * 60 * 1e3, // 10 min dumpLocation: ".", // current folder maxBackups: 10, dumpPrefix: "Heap" }; function fetchStats() { return (0, import_node_v8.getHeapStatistics)(); } async function dumpHeap(opts) { const prefix = opts.dumpPrefix ?? "Heap"; const target = `${opts.dumpLocation}/${prefix}.heapsnapshot`; if (log2.enabledFor("debug")) { log2.debug(`starting heap dump in ${target}`); } await fileExists(opts.dumpLocation).catch(async (_) => { if (log2.enabledFor("debug")) { log2.debug(`dump location ${opts.dumpLocation} does not exists. Will try to create it`); } try { await (0, import_promises.mkdir)(opts.dumpLocation, { recursive: true }); log2.info(`dump location dir ${opts.dumpLocation} successfully created`); } catch (e) { log2.error(`failed to create dump location ${opts.dumpLocation}`); } }); const dumpFileName = (0, import_node_v8.writeHeapSnapshot)(target); log2.info(`heap dumped`); try { log2.debug(`rolling snapshot backups`); const lastFileName = `${opts.dumpLocation}/${prefix}.${opts.maxBackups}.heapsnapshot`; await fileExists(lastFileName).then(async () => { if (log2.enabledFor("debug")) { log2.debug(`deleting ${lastFileName}`); } try { await (0, import_promises.unlink)(lastFileName); } catch (e) { log2.warn(`failed to delete ${lastFileName}`, e); } }).catch(() => { }); for (let i = opts.maxBackups - 1; i > 0; i--) { const currentFileName = `${opts.dumpLocation}/${prefix}.${i}.heapsnapshot`; const nextFileName = `${opts.dumpLocation}/${prefix}.${i + 1}.heapsnapshot`; await fileExists(currentFileName).then(async () => { try { await (0, import_promises.rename)(currentFileName, nextFileName); } catch (e) { log2.warn(`failed to rename ${currentFileName} to ${nextFileName}`, e); } }).catch(() => { }); } const firstFileName = `${opts.dumpLocation}/${prefix}.${1}.heapsnapshot`; try { await (0, import_promises.rename)(dumpFileName, firstFileName); } catch (e) { log2.warn(`failed to rename ${dumpFileName} to ${firstFileName}`, e); } log2.debug("snapshots rolled"); } catch (e) { log2.error("error rolling backups", e); throw e; } } async function fileExists(path) { log2.trace(`checking file ${path}`); await (0, import_promises.access)(path); } async function processStats(stats, state, opts) { if (log2.enabledFor("debug")) { log2.debug(`processing heap stats ${JSON.stringify(stats)}`); } const limit = Math.min(opts.memoryLimit, 0.95 * stats.heap_size_limit); const used = stats.used_heap_size; log2.info(`heap stats ${JSON.stringify(stats)}`); if (used >= limit) { log2.warn(`used heap ${used} bytes exceeds memory limit ${limit} bytes`); if (state.memoryLimitExceeded) { delete state.snapshot; } else { state.memoryLimitExceeded = true; state.snapshot = true; } await dumpHeap(opts); } else { state.memoryLimitExceeded = false; delete state.snapshot; } } function start(opts) { const merged = { ...DEFAULT_OPTIONS, ...opts }; let stopped = false; const state = { memoryLimitExceeded: false }; const report = async () => { const stats = fetchStats(); await processStats(stats, state, merged); }; const interval = setInterval(report, merged.reportInterval); const channel = async (command) => { if (!stopped) { command ??= "run"; switch (command) { case "run": { await report(); break; } case "dump": { await dumpHeap(merged); break; } case "stop": { stopped = true; clearInterval(interval); log2.info("exit memory diagnostic"); break; } } } return stopped; }; return { ...merged, channel }; } async function run({ channel }, command) { if (!await channel(command)) { log2.warn(`cannot execute command "${command}" already closed`); } } async function stop(m) { return await run(m, "stop"); } // src/server/ws-client-verify.ts var import_gateway5 = require("@interopio/gateway"); var log3 = getLogger("gateway.ws.client-verify"); function acceptsMissing(originFilters) { switch (originFilters.missing) { case "allow": // fall-through case "whitelist": return true; case "block": // fall-through case "blacklist": return false; default: return false; } } function tryMatch(originFilters, origin) { const block = originFilters.block ?? originFilters["blacklist"]; const allow = originFilters.allow ?? originFilters["whitelist"]; if (block.length > 0 && import_gateway5.IOGateway.Filtering.valuesMatch(block, origin)) { log3.warn(`origin ${origin} matches block filter`); return false; } else if (allow.length > 0 && import_gateway5.IOGateway.Filtering.valuesMatch(allow, origin)) { if (log3.enabledFor("debug")) { log3.debug(`origin ${origin} matches allow filter`); } return true; } } function acceptsNonMatched(originFilters) { switch (originFilters.non_matched) { case "allow": // fall-through case "whitelist": return true; case "block": // fall-through case "blacklist": return false; default: return false; } } function acceptsOrigin(origin, originFilters) { if (!originFilters) { return true; } if (!origin) { return acceptsMissing(originFilters); } else { const matchResult = tryMatch(originFilters, origin); if (matchResult) { return matchResult; } else { return acceptsNonMatched(originFilters); } } } function regexifyOriginFilters(originFilters) { if (originFilters) { const block = (originFilters.block ?? originFilters.blacklist ?? []).map(import_gateway5.IOGateway.Filtering.regexify); const allow = (originFilters.allow ?? originFilters.whitelist ?? []).map(import_gateway5.IOGateway.Filtering.regexify); return { non_matched: originFilters.non_matched ?? "allow", missing: originFilters.missing ?? "allow", allow, block }; } } // src/server/cors.ts var import_gateway6 = require("@interopio/gateway"); function isSameOrigin(request) { const origin = request.headers.one("origin"); if (origin === void 0) { return true; } const url = request.URL; const actualProtocol = url.protocol; const actualHost = url.host; const originUrl = new URL(origin); const originHost = originUrl.host; const originProtocol = originUrl.protocol; return actualProtocol === originProtocol && actualHost === originHost; } function isCorsRequest(request) { return request.headers.has("origin") && !isSameOrigin(request); } function isPreFlightRequest(request) { return request.method === "OPTIONS" && request.headers.has("origin") && request.headers.has("access-control-request-method"); } var VARY_HEADERS = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]; function processRequest(exchange, config) { const { request, response } = exchange; const responseHeaders = response.headers; if (!responseHeaders.has("Vary")) { responseHeaders.set("Vary", VARY_HEADERS.join(", ")); } else { const varyHeaders = responseHeaders.list("Vary"); for (const header of VARY_HEADERS) { if (!varyHeaders.find((h) => h === header)) { varyHeaders.push(header); } } responseHeaders.set("Vary", varyHeaders.join(", ")); } try { if (!isCorsRequest(request)) { return true; } } catch (e) { if (logger6.enabledFor("debug")) { logger6.debug(`reject: origin is malformed`); } rejectRequest(response); return false; } if (responseHeaders.has("access-control-allow-origin")) { logger6.trace(`skip: already contains "Access-Control-Allow-Origin"`); return true; } const preFlightRequest = isPreFlightRequest(request); if (config) { return handleInternal(exchange, config, preFlightRequest); } if (preFlightRequest) { rejectRequest(response); return false; } return true; } function validateConfig(config) { if (config) { const headers = config.headers; if (headers?.allow && headers.allow !== ALL) { headers.allow = headers.allow.map((header) => header.toLowerCase()); } const origins = config.origins; if (origins?.allow && origins.allow !== ALL) { origins.allow = origins.allow.map((origin) => { if (typeof origin === "string") { return origin.toLowerCase(); } return origin; }); } return config; } } var handler = (config) => { validateConfig(config); return async (ctx, next) => { const isValid = processRequest(ctx, config); if (!isValid || isPreFlightRequest(ctx.request)) { ctx.response._res.end(); } else { await next(); } }; }; var cors_default = (config) => [handler(config)]; var logger6 = getLogger("cors"); function rejectRequest(response) { response.statusCode = 403; } function handleInternal(exchange, config, preFlightRequest) { const { request, response } = exchange; const responseHeaders = response.headers; const requestOrigin = request.headers.one("origin"); const allowOrigin = checkOrigin(config, requestOrigin); if (allowOrigin === void 0) { if (logger6.enabledFor("debug")) { logger6.debug(`reject: '${requestOrigin}' origin is not allowed`); } rejectRequest(response); return false; } const requestMethod = getMethodToUse(request, preFlightRequest); const allowMethods = checkMethods(config, requestMethod); if (allowMethods === void 0) { if (logger6.enabledFor("debug")) { logger6.debug(`reject: HTTP '${requestMethod}' is not allowed`); } rejectRequest(response); return false; } const requestHeaders = getHeadersToUse(request, preFlightRequest); const allowHeaders = checkHeaders(config, requestHeaders); if (preFlightRequest && allowHeaders === void 0) { if (logger6.enabledFor("debug")) { logger6.debug(`reject: headers '${requestHeaders}' are not allowed`); } rejectRequest(response); return false; } responseHeaders.set("access-control-allow-origin", allowOrigin); if (preFlightRequest) { responseHeaders.set("access-control-allow-methods", allowMethods.join(",")); } if (preFlightRequest && allowHeaders !== void 0 && allowHeaders.length > 0) { responseHeaders.set("access-control-allow-headers", allowHeaders.join(", ")); } const exposeHeaders = config.headers?.expose; if (exposeHeaders && exposeHeaders.length > 0) { responseHeaders.set("access-control-expose-headers", exposeHeaders.join(", ")); } if (config.credentials?.allow) { responseHeaders.set("access-control-allow-credentials", "true"); } if (config.privateNetwork?.allow && request.headers.one("access-control-request-private-network") === "true") { responseHeaders.set("access-control-allow-private-network", "true"); } return true; } var ALL = "*"; var DEFAULT_METHODS = ["GET", "HEAD"]; function validateAllowCredentials(config) { if (config.credentials?.allow === true && config.origins?.allow === ALL) { throw new Error(`when credentials.allow is true origins.allow cannot be "*"`); } } function validateAllowPrivateNetwork(config) { if (config.privateNetwork?.allow === true && config.origins?.allow === ALL) { throw new Error(`when privateNetwork.allow is true origins.allow cannot be "*"`); } } function checkOrigin(config, origin) { if (origin) { const allowedOrigins = config.origins?.allow; if (allowedOrigins) { if (allowedOrigins === ALL) { validateAllowCredentials(config); validateAllowPrivateNetwork(config); return ALL; } const originToCheck = trimTrailingSlash(origin.toLowerCase()); for (const allowedOrigin of allowedOrigins) { if (allowedOrigin === ALL || import_gateway6.IOGateway.Filtering.valueMatches(allowedOrigin, originToCheck)) { return origin; } } } } } function checkMethods(config, requestMethod) { if (requestMethod) { const allowedMethods = config.methods?.allow ?? DEFAULT_METHODS; if (allowedMethods === ALL) { return [requestMethod]; } if (import_gateway6.IOGateway.Filtering.valuesMatch(allowedMethods, requestMethod)) { return allowedMethods; } } } function checkHeaders(config, requestHeaders) { if (requestHeaders === void 0) { return; } if (requestHeaders.length == 0) { return []; } const allowedHeaders = config.headers?.allow; if (allowedHeaders === void 0) { return; } const allowAnyHeader = allowedHeaders === ALL; const result = []; for (const requestHeader of requestHeaders) { const value = requestHeader?.trim(); if (value) { if (allowAnyHeader) { result.push(value); } else { for (const allowedHeader of allowedHeaders) { if (value.toLowerCase() == allowedHeader) { result.push(value); break; } } } } } if (result.length > 0) { return result; } } function trimTrailingSlash(origin) { return origin.endsWith("/") ? origin.slice(0, -1) : origin; } function getMethodToUse(request, isPreFlight) { return isPreFlight ? request.headers.one("access-control-request-method") : request.method; } function getHeadersToUse(request, isPreFlight) { const headers = request.headers; return isPreFlight ? headers.list("access-control-request-headers") : Array.from(headers.keys()); } // src/server.ts var logger7 = getLogger("app"); function secureContextOptions(ssl) { const options = {}; if (ssl.key) options.key = (0, import_node_fs.readFileSync)(ssl.key); if (ssl.cert) options.cert = (0, import_node_fs.readFileSync)(ssl.cert); if (ssl.ca) options.ca = (0, import_node_fs.readFileSync)(ssl.ca); return options; } function createListener(middleware, routes3) { const storage = new import_node_async_hooks.AsyncLocalStorage(); const listener = compose( async ({ response }, next) => { response.headers.set("server", "gateway-server"); await next(); }, ...cors_default({ origins: { allow: [/http:\/\/localhost(:\d+)?/] }, methods: { allow: ["GET", "HEAD", "POST", "DELETE"] }, headers: { allow: "*" }, credentials: { allow: true } }), ...middleware, async ({ request, response }, next) => { if (request.method === "GET" && request.path === "/health") { response.statusCode = 200; response._res.end(import_node_http.default.STATUS_CODES[200]); } else { await next(); } }, async ({ request, response }, next) => { if (request.method === "GET" && request.path === "/") { response._res.end(`io.Gateway Server`); } else { await next(); } }, async ({ request, response }, _next) => { const route = routes3.get(request.path); if (route) { response.statusCode = 426; response._res.appendHeader("Upgrade", "websocket").appendHeader("Connection", "Upgrade").appendHeader("Content-Type", "text/plain"); response._res.end(`This service [${request.path}] requires use of the websocket protocol.`); } else { response.statusCode = 404; response._res.end(import_node_http.default.STATUS_CODES[404]); } } ); return (request, response) => { const exchange = new DefaultWebExchange(new HttpServerRequest(request), new HttpServerResponse(response)); return storage.run(exchange, async () => { if (logger7.enabledFor("debug")) { const socket = exchange.request._req.socket; if (logger7.enabledFor("debug")) { logger7.debug(`received ${exchange.method} request for ${exchange.path} from ${socket.remoteAddress}:${socket.remotePort}`); } } return await listener(exchange); }); }; } function promisify(fn) { return new Promise((resolve, reject) => { const r = fn((err) => { if (err) { reject(err); } else { resolve(r); } }); }); } function memoryMonitor(config) { if (config) { return start({ memoryLimit: config.memory_limit, dumpLocation: config.dump_location, dumpPrefix: config.dump_prefix, reportInterval: config.report_interval, maxBackups: config.max_backups }); } } function regexAwareReplacer(_key, value) { return value instanceof RegExp ? value.toString() : value; } var Factory = async (options) => { const ssl = options.ssl; const createServer = ssl ? (options2, handler2) => import_node_https.default.createServer({ ...options2, ...secureContextOptions(ssl) }, handler2) : (options2, handler2) => import_node_http.default.createServer(options2, handler2); const monitor = memoryMonitor(options.memory); const middleware = []; const routes3 = /* @__PURE__ */ new Map(); const gw = import_gateway7.IOGateway.Factory({ ...options.gateway }); if (options.gateway) { const config = options.gateway; routes3.set(config.route ?? "/", { default: config.route === void 0, ping: options.gateway.ping, factory: core_default.bind(gw), maxConnections: config.limits?.max_connections, originFilters: regexifyOriginFilters(config.origins) }); } if (options.mesh) { const connections = new InMemoryNodeConnections(options.mesh.timeout ?? 6e4); middleware.push(...routes_default(connections)); const ping = options.mesh.ping ?? 3e4; routes3.set("/broker", { factory: core_default2, ping }); routes3.set("/cluster", { factory: core_default4, ping }); routes3.set("/relays", { factory: core_default3, ping }); } if (options.metrics) { middleware.push(...await routes_default2(options.metrics)); } const ports = portRange(options.port ?? 0); const host = options.host; const serverP = new Promise((resolve, reject) => { const onSocketError = (err) => logger7.error(`socket error: ${err}`, err); const server2 = createServer( {}, createListener(middleware, routes3) ); server2.on("error", (e) => { if (e["code"] === "EADDRINUSE") { logger7.debug(`port ${e["port"]} already in use on address ${e["address"]}`); const { value: port } = ports.next(); if (port) { logger7.info(`retry starting server on port ${port} and host ${host ?? "<unspecified>"}`); server2.close(); server2.listen(port, host); } else { logger7.warn(`all configured port(s) ${options.port} are in use. closing...`); server2.close(); reject(e); } } else { logger7.error(`server error: ${e.message}`, e); reject(e); } }); server2.on("listening", async () => { const info = server2.address(); for (const [path, route] of routes3) { try { logger7.info(`creating ws server for [${path}]. max connections: ${route.maxConnections ?? "<unlimited>"}, origin filters: ${route.originFilters ? JSON.stringify(route.originFilters, regexAwareReplacer) : "<none>"}`); const wss = new import_ws.WebSocketServer({ noServer: true }); const endpoint = `${ssl ? "wss" : "ws"}://${localIp}:${info.port}${path}`; const handler2 = await route.factory({ endpoint, wss }); const pingInterval = route.ping; if (pingInterval) { const pingIntervalId = setInterval(() => { for (const client of wss.clients) { if (client["connected"] === false) { client.terminate(); } client["connected"] = false; client.ping(); } }, pingInterval); wss.on("close", () => { clearInterval(pingIntervalId); }); } route.wss = wss; route.close = handler2.close?.bind(handler2); } catch (e) { logger7.warn(`failed to init route ${path}`, e); } } logger7.info(`http server listening on ${info.address}:${info.port}`); resolve(server2); }); server2.on("upgrade", (req, socket, head) => { socket.addListener("error", onSocketError); try { const request = new HttpServerRequest(req); const path = request.path ?? "/"; const route = routes3.get(path) ?? Array.from(routes3.values()).find((route2) => { if (path === "/" && route2.default === true) { return true; }