151 lines
6.1 KiB
TypeScript
151 lines
6.1 KiB
TypeScript
import { onShutdown } from "@/utils/shutdown";
|
|
import { Fastify } from "./types";
|
|
import { buildMachineActivityEphemeral, ClientConnection, EventRouter } from "@/app/events/eventRouter";
|
|
import { Server, Socket } from "socket.io";
|
|
import { log } from "@/utils/log";
|
|
import { auth } from "@/app/auth/auth";
|
|
import { decrementWebSocketConnection, incrementWebSocketConnection, websocketEventsCounter } from "../monitoring/metrics2";
|
|
import { usageHandler } from "./socket/usageHandler";
|
|
import { rpcHandler } from "./socket/rpcHandler";
|
|
import { pingHandler } from "./socket/pingHandler";
|
|
import { sessionUpdateHandler } from "./socket/sessionUpdateHandler";
|
|
import { machineUpdateHandler } from "./socket/machineUpdateHandler";
|
|
|
|
export function startSocket(app: Fastify, eventRouter: EventRouter) {
|
|
const io = new Server(app.server, {
|
|
cors: {
|
|
origin: "*",
|
|
methods: ["GET", "POST", "OPTIONS"],
|
|
credentials: true,
|
|
allowedHeaders: ["*"]
|
|
},
|
|
transports: ['websocket', 'polling'],
|
|
pingTimeout: 45000,
|
|
pingInterval: 15000,
|
|
path: '/v1/updates',
|
|
allowUpgrades: true,
|
|
upgradeTimeout: 10000,
|
|
connectTimeout: 20000,
|
|
serveClient: false // Don't serve the client files
|
|
});
|
|
|
|
let rpcListeners = new Map<string, Map<string, Socket>>();
|
|
io.on("connection", async (socket) => {
|
|
log({ module: 'websocket' }, `New connection attempt from socket: ${socket.id}`);
|
|
const token = socket.handshake.auth.token as string;
|
|
const clientType = socket.handshake.auth.clientType as 'session-scoped' | 'user-scoped' | 'machine-scoped' | undefined;
|
|
const sessionId = socket.handshake.auth.sessionId as string | undefined;
|
|
const machineId = socket.handshake.auth.machineId as string | undefined;
|
|
|
|
if (!token) {
|
|
log({ module: 'websocket' }, `No token provided`);
|
|
socket.emit('error', { message: 'Missing authentication token' });
|
|
socket.disconnect();
|
|
return;
|
|
}
|
|
|
|
// Validate session-scoped clients have sessionId
|
|
if (clientType === 'session-scoped' && !sessionId) {
|
|
log({ module: 'websocket' }, `Session-scoped client missing sessionId`);
|
|
socket.emit('error', { message: 'Session ID required for session-scoped clients' });
|
|
socket.disconnect();
|
|
return;
|
|
}
|
|
|
|
// Validate machine-scoped clients have machineId
|
|
if (clientType === 'machine-scoped' && !machineId) {
|
|
log({ module: 'websocket' }, `Machine-scoped client missing machineId`);
|
|
socket.emit('error', { message: 'Machine ID required for machine-scoped clients' });
|
|
socket.disconnect();
|
|
return;
|
|
}
|
|
|
|
const verified = await auth.verifyToken(token);
|
|
if (!verified) {
|
|
log({ module: 'websocket' }, `Invalid token provided`);
|
|
socket.emit('error', { message: 'Invalid authentication token' });
|
|
socket.disconnect();
|
|
return;
|
|
}
|
|
|
|
const userId = verified.userId;
|
|
log({ module: 'websocket' }, `Token verified: ${userId}, clientType: ${clientType || 'user-scoped'}, sessionId: ${sessionId || 'none'}, machineId: ${machineId || 'none'}, socketId: ${socket.id}`);
|
|
|
|
// Store connection based on type
|
|
const metadata = { clientType: clientType || 'user-scoped', sessionId, machineId };
|
|
let connection: ClientConnection;
|
|
if (metadata.clientType === 'session-scoped' && sessionId) {
|
|
connection = {
|
|
connectionType: 'session-scoped',
|
|
socket,
|
|
userId,
|
|
sessionId
|
|
};
|
|
} else if (metadata.clientType === 'machine-scoped' && machineId) {
|
|
connection = {
|
|
connectionType: 'machine-scoped',
|
|
socket,
|
|
userId,
|
|
machineId
|
|
};
|
|
} else {
|
|
connection = {
|
|
connectionType: 'user-scoped',
|
|
socket,
|
|
userId
|
|
};
|
|
}
|
|
eventRouter.addConnection(userId, connection);
|
|
incrementWebSocketConnection(connection.connectionType);
|
|
|
|
// Broadcast daemon online status
|
|
if (connection.connectionType === 'machine-scoped') {
|
|
// Broadcast daemon online
|
|
const machineActivity = buildMachineActivityEphemeral(machineId!, true, Date.now());
|
|
eventRouter.emitEphemeral({
|
|
userId,
|
|
payload: machineActivity,
|
|
recipientFilter: { type: 'user-scoped-only' }
|
|
});
|
|
}
|
|
|
|
socket.on('disconnect', () => {
|
|
websocketEventsCounter.inc({ event_type: 'disconnect' });
|
|
|
|
// Cleanup connections
|
|
eventRouter.removeConnection(userId, connection);
|
|
decrementWebSocketConnection(connection.connectionType);
|
|
|
|
log({ module: 'websocket' }, `User disconnected: ${userId}`);
|
|
|
|
// Broadcast daemon offline status
|
|
if (connection.connectionType === 'machine-scoped') {
|
|
const machineActivity = buildMachineActivityEphemeral(connection.machineId, false, Date.now());
|
|
eventRouter.emitEphemeral({
|
|
userId,
|
|
payload: machineActivity,
|
|
recipientFilter: { type: 'user-scoped-only' }
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handlers
|
|
let userRpcListeners = rpcListeners.get(userId);
|
|
if (!userRpcListeners) {
|
|
userRpcListeners = new Map<string, Socket>();
|
|
rpcListeners.set(userId, userRpcListeners);
|
|
}
|
|
rpcHandler(userId, socket, eventRouter, userRpcListeners);
|
|
usageHandler(userId, socket, eventRouter);
|
|
sessionUpdateHandler(userId, socket, connection, eventRouter);
|
|
pingHandler(socket);
|
|
machineUpdateHandler(userId, socket, eventRouter);
|
|
|
|
// Ready
|
|
log({ module: 'websocket' }, `User connected: ${userId}`);
|
|
});
|
|
|
|
onShutdown('api', async () => {
|
|
await io.close();
|
|
});
|
|
} |