@interopio/gateway-server 0.4.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/changelog.md +94 -0
  2. package/dist/gateway-ent.cjs +305 -0
  3. package/dist/gateway-ent.cjs.map +7 -0
  4. package/dist/gateway-ent.js +277 -0
  5. package/dist/gateway-ent.js.map +7 -0
  6. package/dist/index.cjs +1713 -0
  7. package/dist/index.cjs.map +7 -0
  8. package/dist/index.js +1682 -0
  9. package/dist/index.js.map +7 -0
  10. package/dist/metrics-rest.cjs +21440 -0
  11. package/dist/metrics-rest.cjs.map +7 -0
  12. package/dist/metrics-rest.js +21430 -0
  13. package/dist/metrics-rest.js.map +7 -0
  14. package/gateway-server.d.ts +69 -0
  15. package/package.json +66 -0
  16. package/readme.md +9 -0
  17. package/src/common/compose.ts +40 -0
  18. package/src/gateway/ent/config.ts +174 -0
  19. package/src/gateway/ent/index.ts +18 -0
  20. package/src/gateway/ent/logging.ts +89 -0
  21. package/src/gateway/ent/server.ts +34 -0
  22. package/src/gateway/metrics/rest.ts +20 -0
  23. package/src/gateway/ws/core.ts +90 -0
  24. package/src/index.ts +3 -0
  25. package/src/logger.ts +6 -0
  26. package/src/mesh/connections.ts +101 -0
  27. package/src/mesh/rest-directory/routes.ts +38 -0
  28. package/src/mesh/ws/broker/core.ts +163 -0
  29. package/src/mesh/ws/cluster/core.ts +107 -0
  30. package/src/mesh/ws/relays/core.ts +159 -0
  31. package/src/metrics/routes.ts +86 -0
  32. package/src/server/address.ts +47 -0
  33. package/src/server/cors.ts +311 -0
  34. package/src/server/exchange.ts +379 -0
  35. package/src/server/monitoring.ts +167 -0
  36. package/src/server/types.ts +69 -0
  37. package/src/server/ws-client-verify.ts +79 -0
  38. package/src/server.ts +316 -0
  39. package/src/utils.ts +10 -0
  40. package/types/gateway-ent.d.ts +212 -0
@@ -0,0 +1,101 @@
1
+ import getLogger from '../logger.js';
2
+
3
+ const logger = getLogger('mesh.connections');
4
+
5
+ export type Node = { node: string, endpoint: string, users?: string[] };
6
+ export type NodeConnection = { node: string, connect: Node[] }
7
+
8
+ type NodeValue = Omit<Node, 'users'> & {
9
+ users: Set<string>, memberId: number, lastAccess: number
10
+ }
11
+
12
+ export interface NodeConnections {
13
+ announce(nodes: Node[]): NodeConnection[];
14
+ remove(node: string): boolean;
15
+ }
16
+
17
+ export default class InMemoryNodeConnections implements NodeConnections {
18
+ private readonly nodes = new Map<string, NodeValue>();
19
+ private readonly nodesByEndpoint = new Map<string, string>();
20
+ private memberIds: number = 0;
21
+
22
+ constructor(private readonly timeout: number = 60000) {
23
+ }
24
+
25
+ announce(nodes: Node[]): NodeConnection[] {
26
+ for (const node of nodes) {
27
+ const {node: nodeId, users, endpoint} = node;
28
+ const foundId = this.nodesByEndpoint.get(endpoint);
29
+ if (foundId) {
30
+ if (foundId !== nodeId) {
31
+ logger.warn(`endpoint ${endpoint} clash. replacing node ${foundId} with ${nodeId}`);
32
+ this.nodesByEndpoint.set(endpoint, nodeId);
33
+ this.nodes.delete(foundId);
34
+ }
35
+ } else {
36
+ logger.info(`endpoint ${endpoint} announced for ${nodeId}`);
37
+ this.nodesByEndpoint.set(endpoint, nodeId);
38
+ }
39
+ this.nodes.set(nodeId, this.updateNode(node, new Set<string>(users ?? []), nodeId, this.nodes.get(nodeId)));
40
+ }
41
+ this.cleanupOldNodes();
42
+ const sortedNodes = Array.from(this.nodes.values()).sort((a, b) => a.memberId - b.memberId);
43
+ return nodes.map((e) => {
44
+ const node = e.node;
45
+ const connect = this.findConnections(sortedNodes, this.nodes.get(node));
46
+ return {node, connect};
47
+ });
48
+ }
49
+
50
+ remove(nodeId: string) {
51
+ const removed = this.nodes.get(nodeId);
52
+ if (removed) {
53
+ this.nodes.delete(nodeId);
54
+ const endpoint = removed.endpoint;
55
+ this.nodesByEndpoint.delete(endpoint);
56
+ logger.info(`endpoint ${endpoint} removed for ${nodeId}`);
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+
62
+ private updateNode(newNode: Node, users: Set<string>, _key: string, oldNode?: NodeValue): NodeValue {
63
+ const node: Omit<NodeValue, 'lastAccess' | 'users'> = !oldNode ? {...newNode, memberId: this.memberIds++} : oldNode;
64
+ return {...node, users, lastAccess: Date.now()};
65
+ }
66
+
67
+ private cleanupOldNodes() {
68
+ const threshold = Date.now() - this.timeout;
69
+ for (const [nodeId,v] of this.nodes) {
70
+ if (v.lastAccess < threshold) {
71
+ if (logger.enabledFor('debug')) {
72
+ logger.debug(`${nodeId} expired - no announcement since ${new Date(v.lastAccess).toISOString()}, timeout is ${this.timeout} ms.`);
73
+ }
74
+ this.nodes.delete(nodeId);
75
+ }
76
+ }
77
+ }
78
+
79
+ private findConnections(sortedNodes: NodeValue[], node?: NodeValue): Node[] {
80
+ return sortedNodes.reduce((l, c) => {
81
+ if (node !== undefined && c.memberId < node.memberId) {
82
+ const intersection = new Set(c.users);
83
+ node.users.forEach(user => {
84
+ if (!c.users.has(user)) {
85
+ intersection.delete(user);
86
+ }
87
+ });
88
+ c.users.forEach(user => {
89
+ if (!node.users.has(user)) {
90
+ intersection.delete(user);
91
+ }
92
+ });
93
+ if (intersection.size > 0) {
94
+ const e: Node = {node: c.node, endpoint: c.endpoint};
95
+ return l.concat(e);
96
+ }
97
+ }
98
+ return l;
99
+ }, new Array<Node>());
100
+ }
101
+ }
@@ -0,0 +1,38 @@
1
+ import {Middleware} from '../../server/types.js';
2
+ import {NodeConnections, Node} from '../connections.js';
3
+ import {HttpServerRequest, HttpServerResponse} from '../../server/exchange.js';
4
+
5
+ function routes(connections: NodeConnections): Middleware<HttpServerRequest, HttpServerResponse> {
6
+ return [
7
+ async (ctx, next: () => Promise<void>) => {
8
+ if (ctx.method === 'POST' && ctx.path === '/api/nodes') {
9
+ const json = await ctx.request.json;
10
+ if (!Array.isArray(json)) {
11
+ ctx.response.statusCode = 400
12
+ ctx.response._res.end();
13
+ } else {
14
+ const nodes = json as Node[];
15
+ const result = connections.announce(nodes);
16
+ const body = JSON.stringify(result);
17
+ ctx.response.headers.set('content-type', 'application/json');
18
+ ctx.response.statusCode = 200;
19
+ ctx.response._res
20
+ .end(body);
21
+ }
22
+ } else {
23
+ await next();
24
+ }
25
+ },
26
+ async ({method, path, response}, next: () => Promise<void>) => {
27
+ if (method === 'DELETE' && path?.startsWith('/api/nodes/')) {
28
+ const nodeId = path?.substring('/api/nodes/'.length);
29
+ connections.remove(nodeId);
30
+ response.statusCode = 200;
31
+ response._res.end();
32
+ } else {
33
+ await next();
34
+ }
35
+ },
36
+ ];
37
+ }
38
+ export default routes;
@@ -0,0 +1,163 @@
1
+ import * as ws from 'ws';
2
+ import getLogger from '../../../logger.js';
3
+ import {socketKey} from '../../../utils.ts';
4
+ import {IOGateway} from '@interopio/gateway';
5
+ import GatewayEncoders = IOGateway.Encoding;
6
+
7
+ const logger = getLogger('mesh.ws.broker');
8
+
9
+ type Command =
10
+ // client to broker
11
+ { type: 'hello', 'node-id': string }
12
+ | { type: 'bye', 'node-id': string }
13
+ | { type: 'data', from: string, to: 'all' | string}
14
+ // broker to client
15
+ | { type: 'node-added', 'node-id': string, 'new-node': string }
16
+ | { type: 'node-removed', 'node-id': string, 'removed-node': string }
17
+
18
+ ;
19
+
20
+
21
+ function broadcastNodeAdded(nodes: NodeSockets, newSocket: ws.WebSocket, newNodeId: string) {
22
+ Object.entries(nodes.nodes).forEach(([nodeId, socket]) => {
23
+ if (nodeId !== newNodeId) {
24
+ newSocket.send(codec.encode({type: 'node-added', 'node-id': newNodeId, "new-node": nodeId}));
25
+ socket.send(codec.encode({type: 'node-added', 'node-id': nodeId, "new-node": newNodeId}));
26
+ }
27
+ });
28
+ }
29
+
30
+ function broadcastNodeRemoved(nodes: NodeSockets, removedNodeId: string) {
31
+ Object.entries(nodes.nodes).forEach(([nodeId, socket]) => {
32
+ if (nodeId !== removedNodeId) {
33
+ socket.send(codec.encode({type: 'node-removed', 'node-id': nodeId, "removed-node": removedNodeId}));
34
+ }
35
+ });
36
+ }
37
+
38
+ function onOpen(connectedNodes: NodeSockets, key: string) {
39
+ logger.info(`[${key}] connection accepted`);
40
+ }
41
+
42
+ function onClose(connectedNodes: NodeSockets, key: string, code: number, reason: string): void {
43
+ logger.info(`[${key}] connected closed [${code}](${reason})`);
44
+ const nodeIds = connectedNodes.sockets[key];
45
+ if (nodeIds) {
46
+ delete connectedNodes.sockets[key];
47
+ for (const nodeId of nodeIds) {
48
+ delete connectedNodes.nodes[nodeId];
49
+ }
50
+ for (const nodeId of nodeIds) {
51
+ broadcastNodeRemoved(connectedNodes, nodeId);
52
+ }
53
+ }
54
+ }
55
+
56
+ function processMessage(connectedNodes: NodeSockets, socket: ws.WebSocket, key: string, msg: Command) {
57
+ switch (msg.type) {
58
+ case 'hello': {
59
+ const nodeId = msg['node-id'];
60
+ connectedNodes.nodes[nodeId] = socket;
61
+ connectedNodes.sockets[key] = connectedNodes.sockets[key] ?? [];
62
+ connectedNodes.sockets[key].push(nodeId);
63
+ logger.info(`[${key}] node ${nodeId} added.`);
64
+ broadcastNodeAdded(connectedNodes, socket, nodeId);
65
+ break;
66
+ }
67
+ case 'bye': {
68
+ const nodeId = msg["node-id"];
69
+ delete connectedNodes[nodeId];
70
+ logger.info(`[${key}] node ${nodeId} removed.`);
71
+ broadcastNodeRemoved(connectedNodes, nodeId);
72
+ break;
73
+ }
74
+ case 'data': {
75
+ const sourceNodeId = msg.from;
76
+ const targetNodeId = msg.to;
77
+ if ('all' === targetNodeId) {
78
+ Object.entries(connectedNodes.nodes).forEach(([nodeId, socket]) => {
79
+ if (nodeId !== sourceNodeId) {
80
+ socket.send(codec.encode(msg));
81
+ }
82
+ });
83
+ }
84
+ else {
85
+ const socket = connectedNodes.nodes[targetNodeId];
86
+ if (socket) {
87
+ socket.send(codec.encode(msg));
88
+ }
89
+ else {
90
+ logger.warn(`unable to send to node ${targetNodeId} message ${JSON.stringify(msg)}`);
91
+ }
92
+ }
93
+ break;
94
+ }
95
+ default: {
96
+ logger.warn(`[${key}] ignoring unknown message ${JSON.stringify(msg)}`);
97
+ break;
98
+ }
99
+ }
100
+ }
101
+
102
+ const codec = GatewayEncoders.transit<Command>({
103
+ keywordize: new Map<string, GatewayEncoders.KeywordizeCommand>([
104
+ ['/type', '*'],
105
+ ['/message/body/type', '*'],
106
+ ['/message/origin', '*'],
107
+ ['/message/receiver/type', '*'],
108
+ ['/message/source/type', '*'],
109
+ ['/message/body/type', '*'],
110
+ ])
111
+ });
112
+
113
+ function onMessage(connectedNodes: NodeSockets, socket: ws.WebSocket, key: string, msg: string): void {
114
+ try {
115
+ const decoded = codec.decode(msg);
116
+ if (logger.enabledFor('debug')) {
117
+ logger.debug(`[${key}] processing msg ${JSON.stringify(decoded)}`);
118
+ }
119
+ processMessage(connectedNodes, socket, key, decoded);
120
+ } catch (ex) {
121
+ logger.error(`[${key}] unable to process message`, ex);
122
+ }
123
+ }
124
+
125
+ class WebsocketBroker {
126
+ constructor(private readonly server: ws.WebSocketServer) {
127
+ }
128
+
129
+ async close() {
130
+ this.server.close();
131
+ }
132
+ }
133
+ type NodeSockets = {nodes: {[nodeId: string]: ws.WebSocket}, sockets: {[socket: string]: string[]}};
134
+
135
+ async function create(server: {wss: ws.WebSocketServer}): Promise<{close: () => Promise<void>}> {
136
+ const connectedNodes: NodeSockets = {nodes: {}, sockets: {}};
137
+ logger.info(`mesh server is listening`);
138
+
139
+ server.wss
140
+ .on('error', () => {
141
+ logger.error(`error starting mesh server`);
142
+ })
143
+ .on('connection', (socket, request) => {
144
+ const key = socketKey(request.socket);
145
+ onOpen(connectedNodes, key);
146
+ socket.on('error', (err: Error) => {
147
+ logger.error(`[${key}] websocket error: ${err}`, err);
148
+ });
149
+ socket.on('message', (data, isBinary) => {
150
+ if (Array.isArray(data)) {
151
+ data = Buffer.concat(data);
152
+ }
153
+ onMessage(connectedNodes, socket, key, data as unknown as string);
154
+ });
155
+ socket.on('close', (code, reason) => {
156
+ onClose(connectedNodes, key, code, reason as unknown as string);
157
+ });
158
+ });
159
+
160
+ return new WebsocketBroker(server.wss);
161
+ }
162
+
163
+ export default create;
@@ -0,0 +1,107 @@
1
+ import * as ws from 'ws';
2
+ import {socketKey} from '../../../utils.js';
3
+ import {relays} from '../relays/core.js';
4
+ import getLogger from '../../../logger.js';
5
+ import {HttpServerRequest} from '../../../server/exchange.ts';
6
+
7
+ const logger = getLogger('mesh.ws.cluster');
8
+
9
+
10
+ function onMessage(key: string, node: string | undefined, socketsByNodeId: Map<string, Map<string, ws.WebSocket>>, msg: string) {
11
+ try {
12
+ relays.send(key, node, msg, (k, err?: Error) => {
13
+ if (err) {
14
+ logger.warn(`${k} error writing msg ${msg}: ${err}`);
15
+ return;
16
+ }
17
+ if (logger.enabledFor('debug')) {
18
+ logger.debug(`${k} sent msg ${msg}`);
19
+ }
20
+ });
21
+ } catch (ex) {
22
+ logger.error(`${key} unable to process message`, ex);
23
+ if (node) {
24
+ const socket = socketsByNodeId.get(node)?.get(key);
25
+ socket?.terminate();
26
+ }
27
+ }
28
+ }
29
+
30
+ async function create(server: {wss: ws.WebSocketServer}): Promise<{ close?: () => Promise<void> }> {
31
+ const socketsByNodeId = new Map<string, Map<string, ws.WebSocket>>();
32
+ relays.onMsg = (k, nodeId, msg) => {
33
+ try {
34
+ const sockets = socketsByNodeId.get(nodeId);
35
+ if (sockets && sockets.size > 0) {
36
+ for (const [key, socket] of sockets) {
37
+ socket.send(msg, {binary: false}, (err?: Error) => {
38
+ if (err) {
39
+ logger.warn(`${key} error writing from ${k} msg ${msg}: ${err}`);
40
+ return;
41
+ }
42
+ if (logger.enabledFor('debug')) {
43
+ logger.debug(`${key} sent from ${k} msg ${msg}`);
44
+ }
45
+ });
46
+ }
47
+ } else {
48
+ logger.warn(`${k} dropped msg ${msg}.`);
49
+ }
50
+ } catch (ex) {
51
+ logger.error(`${k} unable to process message`, ex);
52
+ }
53
+ };
54
+ server.wss
55
+ .on('error', () => {
56
+ logger.error(`error starting mesh server`);
57
+ })
58
+ .on('listening', () => {
59
+ logger.info(`mesh server is listening`);
60
+ })
61
+ .on('connection', (socket, req) => {
62
+ const request = new HttpServerRequest(req);
63
+ const key = socketKey(request.socket);
64
+ const query = new URLSearchParams(request.query ?? undefined);
65
+ logger.info(`${key} connected on cluster with ${query}`);
66
+ const node = query.get('node');
67
+ if (node) {
68
+ let sockets = socketsByNodeId.get(node);
69
+ if (!sockets) {
70
+ sockets = new Map<string, ws.WebSocket>();
71
+ socketsByNodeId.set(node, sockets);
72
+ }
73
+ sockets.set(key, socket);
74
+ }
75
+ else {
76
+ socket.terminate();
77
+ return;
78
+ }
79
+ //onOpen(connectedNodes, key);
80
+ socket.on('error', (err: Error) => {
81
+ logger.error(`${key} websocket error: ${err}`, err);
82
+ });
83
+ socket.on('message', (data, _isBinary) => {
84
+ if (Array.isArray(data)) {
85
+ data = Buffer.concat(data);
86
+ }
87
+ onMessage(key, node, socketsByNodeId, data as unknown as string);
88
+ });
89
+ socket.on('close', (_code, _reason) => {
90
+ logger.info(`${key} disconnected from cluster`);
91
+ const sockets = socketsByNodeId.get(node);
92
+ if (sockets) {
93
+ sockets.delete(key);
94
+ if (sockets.size === 0) {
95
+ socketsByNodeId.delete(node);
96
+ }
97
+ }
98
+ });
99
+ });
100
+ return {
101
+ close: async () => {
102
+ server.wss.close();
103
+ }
104
+ };
105
+ }
106
+
107
+ export default create;
@@ -0,0 +1,159 @@
1
+ import * as ws from 'ws';
2
+ import {socketKey} from '../../../utils.js';
3
+ import getLogger from '../../../logger.js';
4
+
5
+ import {IOGateway} from '@interopio/gateway';
6
+ import GatewayEncoders = IOGateway.Encoding;
7
+
8
+ const logger = getLogger('mesh.ws.relay');
9
+
10
+ type Command
11
+ = { type: 'hello', 'from': string, to: string }
12
+ | { type: 'bye', 'from': string, to: 'all' | string }
13
+ | { type: 'data', from: string, to: 'all' | string, data: {body: {type: string}}}
14
+
15
+ ;
16
+
17
+ const codec = GatewayEncoders.transit<Command>({
18
+ keywordize: new Map<string, GatewayEncoders.KeywordizeCommand>([
19
+ ['/type', '*'],
20
+ ['/message/body/type', '*'],
21
+ ['/message/origin', '*'],
22
+ ['/message/receiver/type', '*'],
23
+ ['/message/source/type', '*'],
24
+ ['/message/body/type', '*'],
25
+ ])
26
+ });
27
+
28
+ export interface Relays {
29
+ onMsg?: ((key: string, node: string, msg: string) => void);
30
+ onErr?: ((key: string, err: Error) => void)
31
+ send(key: string, node: string | undefined, msg: string, cb: (key: string, err?: Error) => void): void;
32
+ }
33
+
34
+ class InternalRelays implements Relays {
35
+ // key -> socket
36
+ private readonly clients = new Map<string, ws.WebSocket>();
37
+ // node -> key
38
+ private readonly links = new Map<string, string>();
39
+ public onMsg?: (key: string, node: string, msg: string) => void
40
+ public onErr?: (key: string, err: Error) => void
41
+
42
+ add(key: string, soc: ws.WebSocket) {
43
+ this.clients.set(key, soc);
44
+ }
45
+
46
+ remove(key: string) {
47
+ this.clients.delete(key);
48
+ for (const [node, k] of this.links) {
49
+ if (k === key) {
50
+ this.links.delete(node);
51
+ }
52
+ }
53
+ }
54
+
55
+ receive(key: string, msg: string) {
56
+ const node = this.link(key, msg);
57
+
58
+ if (node && this.onMsg) {
59
+ this.onMsg(key, node, msg);
60
+ }
61
+ }
62
+
63
+ private link(key: string, msg: string): string | undefined {
64
+ try {
65
+ const decoded = codec.decode(msg);
66
+ const {type, from, to} = decoded;
67
+ if (to === 'all') {
68
+ switch (type) {
69
+ case 'hello': {
70
+ if (logger.enabledFor('debug')) {
71
+ logger.debug(`${key} registers node ${from}`);
72
+ }
73
+ this.links.set(from, key);
74
+ break;
75
+ }
76
+ case 'bye': {
77
+ if (logger.enabledFor('debug')) {
78
+ logger.debug(`${key} unregisters node ${from}`);
79
+ }
80
+ this.links.delete(from);
81
+ break;
82
+ }
83
+ }
84
+ return;
85
+ }
86
+ return from;
87
+ } catch (e) {
88
+ if (this.onErr) {
89
+ // probably a decode error or a bug
90
+ this.onErr(key, e instanceof Error ? e : new Error(`link failed :${e}`));
91
+ }
92
+ else {
93
+ logger.warn(`${key} unable to process ${msg}`, e);
94
+ }
95
+ }
96
+ }
97
+
98
+ send(key: string, node: string, msg: string, cb: (k: string, err?: Error) => void): void {
99
+ const decoded = codec.decode(msg);
100
+ if (logger.enabledFor('debug')) {
101
+ logger.debug(`${key} sending msg to ${node} ${JSON.stringify(decoded)}`);
102
+ }
103
+
104
+ const clientKey = this.links.get(node);
105
+ if (clientKey) {
106
+ const client = this.clients.get(clientKey);
107
+ if (client) {
108
+ client.send(msg, {binary: false}, (err?: Error) => {
109
+ cb(clientKey, err)
110
+ });
111
+ return;
112
+ }
113
+ }
114
+ throw new Error(`${key} no active link for ${decoded.to}`);
115
+ }
116
+ }
117
+ const internal = new InternalRelays();
118
+ export const relays: Relays = internal;
119
+
120
+
121
+ async function create(server: {wss:ws.WebSocketServer}): Promise<{ close?: () => Promise<void> }> {
122
+
123
+ logger.info(`relays server is listening`);
124
+ server.wss
125
+ .on('error', () => {
126
+ logger.error(`error starting relays server`);
127
+ })
128
+ .on('connection', (socket, request) => {
129
+ const key = socketKey(request.socket);
130
+ logger.info(`${key} connected on relays`);
131
+ internal.add(key, socket);
132
+ //onOpen(connectedNodes, key);
133
+ socket.on('error', (err: Error) => {
134
+ logger.error(`[${key}] websocket error: ${err}`, err);
135
+ });
136
+ socket.on('message', (data, _isBinary) => {
137
+ if (Array.isArray(data)) {
138
+ data = Buffer.concat(data);
139
+ }
140
+ try {
141
+ internal.receive(key, data as unknown as string);
142
+ } catch (e) {
143
+ logger.warn(`[${key}] error processing received data '${data}'`, e);
144
+ }
145
+ });
146
+ socket.on('close', (code, reason) => {
147
+ internal.remove(key);
148
+
149
+ logger.info(`${key} disconnected from relays`);
150
+ });
151
+ });
152
+ return {
153
+ close: async () => {
154
+ server.wss.close();
155
+ }
156
+ };
157
+ }
158
+
159
+ export default create;
@@ -0,0 +1,86 @@
1
+ import {Metrics} from '@interopio/gateway-metrics-api';
2
+ import {Middleware, WebExchange} from '../server/types.js';
3
+ import getLogger from '../logger.js';
4
+ import {FilePublisherConfig} from '@interopio/gateway/metrics/publisher/file';
5
+ import {HttpServerRequest, HttpServerResponse} from '../server/exchange.ts';
6
+
7
+ const logger = getLogger('metrics');
8
+
9
+ const COOKIE_NAME = 'GW_LOGIN';
10
+
11
+ function loggedIn(auth, ctx: WebExchange) {
12
+ if (auth) {
13
+ const cookieHeaderValue = ctx.request.headers.list('cookie');
14
+ const cookie = cookieHeaderValue?.join('; ').split('; ').find(value => value.startsWith(`${COOKIE_NAME}=`));
15
+ return cookie && (parseInt(cookie?.substring(COOKIE_NAME.length + 1)) > Date.now());
16
+ }
17
+ return true;
18
+ }
19
+
20
+ async function routes(config: {file?: FilePublisherConfig, auth?}): Promise<Middleware<HttpServerRequest, HttpServerResponse>> {
21
+ const {jsonFileAppender} = await import('@interopio/gateway/metrics/publisher/file');
22
+ const appender = jsonFileAppender(logger);
23
+ await appender.open(config.file?.location ?? 'metrics.ndjson', config.file?.append ?? true);
24
+ return [
25
+ async (ctx, next: () => Promise<void>) => {
26
+ if (ctx.method === 'GET' && ctx.path === '/api/metrics') {
27
+ if (loggedIn(config.auth, ctx)) {
28
+ ctx.response.statusCode = 200;
29
+ } else {
30
+ ctx.response.statusCode = 302;
31
+ ctx.response.headers.set('location', '/api/login?redirectTo=/api/metrics');
32
+ }
33
+ ctx.response._res.end();
34
+ }
35
+ else {
36
+ await next();
37
+ }
38
+ },
39
+ async (ctx, next: () => Promise<void>) => {
40
+ if (ctx.method === 'GET' && ctx.path === '/api/login') {
41
+ const redirectTo = new URLSearchParams(ctx.request.query ?? undefined).get('redirectTo');
42
+ const expires = Date.now() + 180 * 1000;
43
+ ctx.response.headers.add('set-cookie', `${COOKIE_NAME}=${expires}; Path=/api; SameSite=strict`)
44
+ if (redirectTo) {
45
+ ctx.response.statusCode = 302;
46
+ ctx.response.headers.set('location', redirectTo);
47
+ }
48
+ else {
49
+ ctx.response.statusCode = 200;
50
+ }
51
+ ctx.response._res.end();
52
+ } else {
53
+ await next();
54
+ }
55
+ },
56
+ async (ctx, next: () => Promise<void>) => {
57
+ if (ctx.method === 'POST' && ctx.path === '/api/metrics') {
58
+ if (loggedIn(config.auth, ctx)) {
59
+ ctx.response.statusCode = 202;
60
+ ctx.response._res.end();
61
+ try {
62
+ const json = await ctx.request.json;
63
+ const update = json as Metrics.Update;
64
+ if (logger.enabledFor('debug')) {
65
+ logger.debug(`${JSON.stringify(update)}`);
66
+ }
67
+ if ((config.file?.status ?? false) || (update.status === undefined)) {
68
+ await appender.write(update);
69
+ }
70
+ } catch (e) {
71
+ logger.error(`error processing metrics`, e);
72
+ }
73
+ }
74
+ else {
75
+ ctx.response.statusCode = 401;
76
+ ctx.response._res.end();
77
+ }
78
+ }
79
+ else {
80
+ await next();
81
+ }
82
+ },
83
+ ];
84
+ }
85
+
86
+ export default routes;