forked from Open-audit-foundation/Open-Audit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.ts
More file actions
96 lines (83 loc) · 3.63 KB
/
Copy pathserver.ts
File metadata and controls
96 lines (83 loc) · 3.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
* Custom Next.js server with an attached WebSocket server.
* Broadcasts newly translated Soroban events to all connected clients.
*
* Run with: npx ts-node --project tsconfig.server.json server.ts
* (or via the `dev:ws` npm script)
*/
import { createServer, IncomingMessage } from "http";
import { parse } from "url";
import next from "next";
import { WebSocketServer, WebSocket } from "ws";
import { MOCK_RAW_EVENTS } from "./lib/mock-data";
import { translateEvent } from "./lib/translator/registry";
import { startHorizonStreamingIndexer } from "./lib/stellar/indexer";
import { getNetworkConfig } from "./lib/stellar/client";
import { captureExceptionSync } from "./lib/telemetry";
const dev = process.env.NODE_ENV !== "production";
const port = parseInt(process.env.PORT ?? "3000", 10);
const MAX_WS_CONNECTIONS_PER_IP = parseInt(process.env.MAX_WS_CONNECTIONS_PER_IP ?? "5", 10);
const connectionsByIp = new Map<string, number>();
function getClientIp(req: IncomingMessage): string {
const forwardedFor = req.headers["x-forwarded-for"];
if (typeof forwardedFor === "string" && forwardedFor.length > 0) {
return forwardedFor.split(",")[0].trim();
}
return req.socket.remoteAddress ?? "unknown";
}
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const httpServer = createServer((req, res) => {
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss://* https://horizon-testnet.stellar.org https://soroban-testnet.stellar.org https://horizon.stellar.org https://mainnet.stellar.validationcloud.io; img-src 'self' data:; font-src 'self' data:;");
const parsedUrl = parse(req.url ?? "/", true);
handle(req, res, parsedUrl);
});
const wss = new WebSocketServer({ server: httpServer, path: "/ws/events" });
wss.on("connection", (socket, request) => {
const clientIp = request ? getClientIp(request) : "unknown";
const activeConnections = (connectionsByIp.get(clientIp) ?? 0) + 1;
if (activeConnections > MAX_WS_CONNECTIONS_PER_IP) {
console.warn(
`[WS] Rejecting connection from ${clientIp}: too many connections (${activeConnections})`
);
socket.close(1008, "Too many connections from this IP");
return;
}
connectionsByIp.set(clientIp, activeConnections);
console.log(`[WS] Client connected from ${clientIp} (${activeConnections} active)`);
socket.on("close", () => {
const remaining = (connectionsByIp.get(clientIp) ?? 1) - 1;
if (remaining <= 0) {
connectionsByIp.delete(clientIp);
} else {
connectionsByIp.set(clientIp, remaining);
}
console.log(`[WS] Client disconnected from ${clientIp} (${Math.max(remaining, 0)} remaining)`);
});
});
/** Broadcast a JSON payload to every connected client. */
function broadcast(data: unknown): void {
const message = JSON.stringify(data);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Start the real-time streaming indexer
const indexer = startHorizonStreamingIndexer({
networkConfig: getNetworkConfig(),
onEvent: (rawEvent) => {
console.log(`[Indexer] New event: ${rawEvent.id} from contract ${rawEvent.contractId}`);
const translated = translateEvent(rawEvent);
broadcast(translated);
},
onError: (err) => {
captureExceptionSync(err, { context: { operation: "horizonStreamingIndexer" } });
},
});
httpServer.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});