@interopio/gateway-server
Version:
## `@glue42/gateway-ent` compatibility
1,653 lines (1,635 loc) • 50.7 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/server.ts
var server_exports = {};
__export(server_exports, {
Factory: () => Factory
});
import { WebSocketServer } from "ws";
import http from "node:http";
import https from "node:https";
import { readFileSync } from "node:fs";
import { AsyncLocalStorage } from "node:async_hooks";
import { IOGateway as IOGateway7 } from "@interopio/gateway";
// src/logger.ts
import { IOGateway } from "@interopio/gateway";
function getLogger(name) {
return 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
import { IOGateway as IOGateway2 } from "@interopio/gateway";
var GatewayEncoders = IOGateway2.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
import { IOGateway as IOGateway3 } from "@interopio/gateway";
var GatewayEncoders2 = IOGateway3.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
import { IOGateway as IOGateway4 } from "@interopio/gateway";
var GatewayEncoders3 = IOGateway4.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
import { networkInterfaces } from "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(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
import { getHeapStatistics, writeHeapSnapshot } from "node:v8";
import { access, mkdir, rename, unlink } from "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 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 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 = 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 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 rename(currentFileName, nextFileName);
} catch (e) {
log2.warn(`failed to rename ${currentFileName} to ${nextFileName}`, e);
}
}).catch(() => {
});
}
const firstFileName = `${opts.dumpLocation}/${prefix}.${1}.heapsnapshot`;
try {
await 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 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
import { IOGateway as IOGateway5 } from "@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 && IOGateway5.Filtering.valuesMatch(block, origin)) {
log3.warn(`origin ${origin} matches block filter`);
return false;
} else if (allow.length > 0 && IOGateway5.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(IOGateway5.Filtering.regexify);
const allow = (originFilters.allow ?? originFilters.whitelist ?? []).map(IOGateway5.Filtering.regexify);
return {
non_matched: originFilters.non_matched ?? "allow",
missing: originFilters.missing ?? "allow",
allow,
block
};
}
}
// src/server/cors.ts
import { IOGateway as IOGateway6 } from "@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 || IOGateway6.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 (IOGateway6.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 = readFileSync(ssl.key);
if (ssl.cert) options.cert = readFileSync(ssl.cert);
if (ssl.ca) options.ca = readFileSync(ssl.ca);
return options;
}
function createListener(middleware, routes3) {
const storage = new 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(http.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(http.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) => https.createServer({ ...options2, ...secureContextOptions(ssl) }, handler2) : (options2, handler2) => http.createServer(options2, handler2);
const monitor = memoryMonitor(options.memory);
const middleware = [];
const routes3 = /* @__PURE__ */ new Map();
const gw = IOGateway7.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 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;
}
});
const host2 = request.host;
const info = socketKey(request.socket);
if (route?.wss) {
socket.removeListener("error", onSocketError);
const wss = route.wss;
if (route.maxConnections !== void 0 && wss.clients?.size >= route.maxConnections) {
logger7.warn(`${info} dropping ws connection request from ${host2} on ${path}. max connections exceeded.`);
socket.destroy();
return;
}
const origin = request.headers["origin"];
if (!acceptsOrigin(origin, route.originFilters)) {
logger7.info(`${info} dropping ws connection request from ${host2} on ${path}. origin ${origin ?? "<missing>"}`);
socket.destroy();
return;
}
if (logger7.enabledFor("debug")) {
logger7.debug(`${info} accepted new ws connection request from ${host2} on ${path}`);
}
wss.handleUpgrade(req, socket, head, (ws) => {
ws.on("pong", () => ws["connected"] = true);
ws.on("ping", () => {
});
wss.emit("connection", ws, req);
});
} else {
logger7.warn(`${info} rejected upgrade request from ${host2} on ${path}`);
socket.destroy();
}
} catch (err) {
logger7.error(`upgrade error: ${err}`, err);
}
}).on("close", async () => {
logger7.info(`http server closed.`);
});
try {
const { value: port } = ports.next();
server2.listen(port, host);
} catch (e) {
logger7.error(`error starting web socket server`, e);
reject(e instanceof Error ? e : new Error(`listen failed: ${e}`));
}
});
const server = await serverP;
return new class {
gateway = gw;
async close() {
for (const [path, route] of routes3) {
try {
i