wip provider server command endpoints
This commit is contained in:
@@ -15,6 +15,7 @@ console.error = vi.fn();
|
||||
|
||||
import { COLLABORATION_SERVER_ORIGIN as origin } from '@/env';
|
||||
import { hocuspocusServer, initApp } from '@/servers';
|
||||
import { handleRelayServerConnection, getRelayRoom } from '@/servers/relayServer';
|
||||
|
||||
const apiEndpoint = '/collaboration/api/get-connections/';
|
||||
|
||||
@@ -205,6 +206,42 @@ describe('Server Tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /collaboration/api/get-connections?room=[ROOM_ID] returns connection info for encrypted relay room', async () => {
|
||||
const roomId = 'relay-test-room';
|
||||
|
||||
// Create a mock WebSocket that the relay server can register
|
||||
const mockWs = {
|
||||
binaryType: 'nodebuffer',
|
||||
readyState: 1, // OPEN
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
ping: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
};
|
||||
|
||||
// Register the mock connection in the relay server
|
||||
await handleRelayServerConnection(mockWs as any, roomId);
|
||||
|
||||
const room = getRelayRoom(roomId);
|
||||
expect(room).toBeDefined();
|
||||
expect(room!.size).toBe(1);
|
||||
|
||||
const app = initApp();
|
||||
|
||||
const response = await request(app)
|
||||
.get(`${apiEndpoint}?room=${roomId}&sessionKey=any-session-key`)
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', 'test-secret-api-key');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
count: 1,
|
||||
exists: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key not existing, read only connection', async () => {
|
||||
const document = await hocuspocusServer.hocuspocus.createDocument(
|
||||
'test-room',
|
||||
|
||||
+27
-30
@@ -1,6 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { hocuspocusServer } from '@/servers';
|
||||
import { closeRelayConnections } from '@/servers/relayServer';
|
||||
import { logger } from '@/utils';
|
||||
|
||||
type ResetConnectionsRequestQuery = {
|
||||
@@ -18,46 +19,42 @@ export const collaborationResetConnectionsHandler = (
|
||||
|
||||
if (!room) {
|
||||
res.status(400).json({ error: 'Room name not provided' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'not implemented yet' });
|
||||
|
||||
hocuspocusServer;
|
||||
|
||||
/**
|
||||
* If no user ID is provided, close all connections in the room
|
||||
*
|
||||
* Below we avoid database call to check if the room if for encrypted server or not (could be switching to the other), so looking in both lists
|
||||
*/
|
||||
if (!userId) {
|
||||
// hocuspocusServer.hocuspocus.closeConnections(room);
|
||||
// const doc = getYDoc(room);
|
||||
// if (doc) {
|
||||
// doc.conns.forEach((_, conn) => closeConn(doc, conn));
|
||||
// }
|
||||
hocuspocusServer.hocuspocus.closeConnections(room);
|
||||
closeRelayConnections(room);
|
||||
} else {
|
||||
const targetUserId = Array.isArray(userId) ? userId[0] : userId;
|
||||
|
||||
/**
|
||||
* Close connections for the user in the room
|
||||
* Close connections for the user in the room (hocuspocus)
|
||||
*/
|
||||
// hocuspocusServer.hocuspocus.documents.forEach((doc) => {
|
||||
// if (doc.name !== room) {
|
||||
// return;
|
||||
// }
|
||||
// doc.getConnections().forEach((connection) => {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
// if (connection.context.userId === userId) {
|
||||
// connection.close();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// const doc = getYDoc(room);
|
||||
// if (doc) {
|
||||
// doc.conns.forEach((clientIds, conn) => {
|
||||
// // TODO: with this current implementation there is no logic about user ID but only also "clientID"
|
||||
// // ... it should be adapted first as for hocuspocus before having this metadata
|
||||
// // closeConn(doc, conn)
|
||||
// });
|
||||
// }
|
||||
hocuspocusServer.hocuspocus.documents.forEach((doc) => {
|
||||
if (doc.name !== room) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.getConnections().forEach((connection) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (connection.context.userId === targetUserId) {
|
||||
connection.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Close connections for the user in the room (relay)
|
||||
*/
|
||||
closeRelayConnections(room, targetUserId);
|
||||
}
|
||||
|
||||
// res.status(200).json({ message: 'Connections reset' });
|
||||
res.status(200).json({ message: 'Connections reset' });
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@ export const collaborationWSHandler = async (
|
||||
// mimick the Hocuspocus protocol to properly hide the frontend loader
|
||||
ws.send('system:authenticated');
|
||||
|
||||
await handleRelayServerConnection(ws, roomId);
|
||||
await handleRelayServerConnection(ws, roomId, userId);
|
||||
} else {
|
||||
hocuspocusServer.hocuspocus.handleConnection(ws, req, {
|
||||
roomId: roomId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { hocuspocusServer } from '@/servers';
|
||||
import { getRelayRoom } from '@/servers/relayServer';
|
||||
import { logger } from '@/utils';
|
||||
|
||||
type getDocumentConnectionInfoRequestQuery = {
|
||||
@@ -17,37 +18,52 @@ export const getDocumentConnectionInfoHandler = (
|
||||
|
||||
if (!room) {
|
||||
res.status(400).json({ error: 'Room name not provided' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.query.sessionKey) {
|
||||
res.status(400).json({ error: 'Session key not provided' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger('Getting document connection info for room:', room);
|
||||
|
||||
res.status(500).json({ error: 'not implemented yet' });
|
||||
// we avoid database call since a document could be being encrypted or not, so looking in both lists
|
||||
// so check hocuspocus first (non-encrypted documents)
|
||||
const hocuspocusRoom = hocuspocusServer.hocuspocus.documents.get(room);
|
||||
|
||||
hocuspocusServer;
|
||||
sessionKey;
|
||||
if (hocuspocusRoom) {
|
||||
const connections = hocuspocusRoom
|
||||
.getConnections()
|
||||
.filter((connection) => connection.readOnly === false);
|
||||
|
||||
// const roomInfo = hocuspocusServer.hocuspocus.documents.get(room);
|
||||
res.status(200).json({
|
||||
count: connections.length,
|
||||
exists: connections.some(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(connection) => connection.context.sessionKey === sessionKey,
|
||||
),
|
||||
});
|
||||
|
||||
// if (!roomInfo) {
|
||||
// logger('Room not found:', room);
|
||||
// res.status(404).json({ error: 'Room not found' });
|
||||
// return;
|
||||
// }
|
||||
// const connections = roomInfo
|
||||
// .getConnections()
|
||||
// .filter((connection) => connection.readOnly === false);
|
||||
return;
|
||||
}
|
||||
|
||||
// res.status(200).json({
|
||||
// count: connections.length,
|
||||
// exists: connections.some(
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
// (connection) => connection.context.sessionKey === sessionKey,
|
||||
// ),
|
||||
// });
|
||||
const relayRoom = getRelayRoom(room);
|
||||
|
||||
if (relayRoom) {
|
||||
// the relay server is a blind passthrough, there is no readOnly distinction
|
||||
// or session key tracking, so we report all connections and cannot confirm session existence
|
||||
res.status(200).json({
|
||||
count: relayRoom.size,
|
||||
exists: false,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger('Room not found:', room);
|
||||
|
||||
res.status(404).json({ error: 'Room not found' });
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as ws from 'ws';
|
||||
|
||||
const rooms = new Map<string, Set<ws.WebSocket>>();
|
||||
const roomsMutex = new Mutex();
|
||||
const connectionMeta = new Map<ws.WebSocket, { userId: string | null }>();
|
||||
|
||||
function sendMessage(ws: ws.WebSocket, data: ws.RawData) {
|
||||
if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) {
|
||||
@@ -16,9 +17,39 @@ function sendMessage(ws: ws.WebSocket, data: ws.RawData) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getRelayRoom(roomId: string): Set<ws.WebSocket> | undefined {
|
||||
return rooms.get(roomId);
|
||||
}
|
||||
|
||||
export function closeRelayConnections(
|
||||
roomId: string,
|
||||
userId?: string,
|
||||
): void {
|
||||
const room = rooms.get(roomId);
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
for (const peer of Array.from(room)) {
|
||||
peer.close();
|
||||
}
|
||||
} else {
|
||||
for (const peer of Array.from(room)) {
|
||||
const meta = connectionMeta.get(peer);
|
||||
|
||||
if (meta?.userId === userId) {
|
||||
peer.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleRelayServerConnection(
|
||||
ws: ws.WebSocket,
|
||||
roomId: string,
|
||||
userId?: string | null,
|
||||
) {
|
||||
ws.binaryType = 'arraybuffer'; // Same configuration in the client provider
|
||||
|
||||
@@ -38,6 +69,8 @@ export async function handleRelayServerConnection(
|
||||
roomsMutexRelease();
|
||||
}
|
||||
|
||||
connectionMeta.set(ws, { userId: userId ?? null });
|
||||
|
||||
ws.on('error', () => {
|
||||
ws.close();
|
||||
});
|
||||
@@ -80,6 +113,7 @@ export async function handleRelayServerConnection(
|
||||
ws.on('close', async () => {
|
||||
clearInterval(pingInterval);
|
||||
ws.removeAllListeners();
|
||||
connectionMeta.delete(ws);
|
||||
|
||||
const roomsMutexRelease = await roomsMutex.acquire();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user