mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-16 19:43:06 +00:00
fix: do not serve data to an unauthenticated websocket client
This commit is contained in:
@@ -62,11 +62,22 @@ class SocketServer implements IAdapter {
|
|||||||
this.wss = new WebSocketServer({ path: `${prefix}/ws`, server, maxPayload: this.MAX_PAYLOAD });
|
this.wss = new WebSocketServer({ path: `${prefix}/ws`, server, maxPayload: this.MAX_PAYLOAD });
|
||||||
|
|
||||||
this.wss.on('connection', (ws, req) => {
|
this.wss.on('connection', (ws, req) => {
|
||||||
|
// Rejected sockets can emit an error while their close handshake is in progress.
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
let isAuthenticated = false;
|
||||||
authenticateSocket(ws, req, (error) => {
|
authenticateSocket(ws, req, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
ws.close(1008, 'Unauthorized');
|
ws.close(1008, 'Unauthorized');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
isAuthenticated = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const clientId = generateId();
|
const clientId = generateId();
|
||||||
const clientName = getRandomName();
|
const clientName = getRandomName();
|
||||||
function sendPacket<T extends MessageTag>(
|
function sendPacket<T extends MessageTag>(
|
||||||
@@ -94,8 +105,6 @@ class SocketServer implements IAdapter {
|
|||||||
// send store payload on connect
|
// send store payload on connect
|
||||||
sendPacket(MessageTag.RuntimeData, eventStore.poll());
|
sendPacket(MessageTag.RuntimeData, eventStore.poll());
|
||||||
|
|
||||||
ws.on('error', console.error);
|
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
this.clients.delete(clientId);
|
this.clients.delete(clientId);
|
||||||
logger.info(LogOrigin.Client, `${this.clients.size} Connections with disconnected: ${clientName}`);
|
logger.info(LogOrigin.Client, `${this.clients.size} Connections with disconnected: ${clientName}`);
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import type { Server } from 'node:http';
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const websocketMocks = vi.hoisted(() => {
|
||||||
|
let connectionHandler: ((socket: FakeWebSocket, request: unknown) => void) | undefined;
|
||||||
|
|
||||||
|
class FakeWebSocket {
|
||||||
|
static readonly OPEN = 1;
|
||||||
|
|
||||||
|
readyState = FakeWebSocket.OPEN;
|
||||||
|
close = vi.fn();
|
||||||
|
handlers = new Map<string, Array<(...args: unknown[]) => void>>();
|
||||||
|
|
||||||
|
on(event: string, handler: (...args: unknown[]) => void) {
|
||||||
|
this.handlers.set(event, [...(this.handlers.get(event) ?? []), handler]);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, ...args: unknown[]) {
|
||||||
|
const handlers = this.handlers.get(event) ?? [];
|
||||||
|
if (event === 'error' && handlers.length === 0) {
|
||||||
|
throw args[0];
|
||||||
|
}
|
||||||
|
for (const handler of handlers) {
|
||||||
|
handler(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeWebSocketServer {
|
||||||
|
clients = new Set<FakeWebSocket>();
|
||||||
|
|
||||||
|
on(event: string, handler: (socket: FakeWebSocket, request: unknown) => void) {
|
||||||
|
if (event === 'connection') {
|
||||||
|
connectionHandler = handler;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(callback: () => void) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
FakeWebSocket,
|
||||||
|
FakeWebSocketServer,
|
||||||
|
getConnectionHandler: () => connectionHandler,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('ws', () => ({
|
||||||
|
WebSocket: websocketMocks.FakeWebSocket,
|
||||||
|
WebSocketServer: websocketMocks.FakeWebSocketServer,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../middleware/authenticate.js', () => ({
|
||||||
|
authenticateSocket: (_socket: unknown, _request: unknown, next: (error?: Error) => void) => {
|
||||||
|
next(new Error('Unauthorized'));
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { socket } from '../WebsocketAdapter.js';
|
||||||
|
|
||||||
|
describe('WebsocketAdapter authentication', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await socket.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles an error emitted while rejecting an unauthenticated socket', () => {
|
||||||
|
socket.init({} as Server, false);
|
||||||
|
const rejectedSocket = new websocketMocks.FakeWebSocket();
|
||||||
|
const connectionHandler = websocketMocks.getConnectionHandler();
|
||||||
|
|
||||||
|
expect(connectionHandler).toBeDefined();
|
||||||
|
connectionHandler?.(rejectedSocket, {});
|
||||||
|
|
||||||
|
expect(rejectedSocket.close).toHaveBeenCalledWith(1008, 'Unauthorized');
|
||||||
|
expect(() => rejectedSocket.emit('error', new Error('socket closed'))).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user