Allow world.* methods through the relay, validate their payloads, and track per-session world revisions in the session store.
68 lines
3.3 KiB
TypeScript
68 lines
3.3 KiB
TypeScript
import type {Namespace, Socket} from 'socket.io';
|
|
import type {RelaySessionStore} from './session_store.ts';
|
|
import {RelayError, error_ack, success} from '../utils/relay_validation.ts';
|
|
|
|
export function install_relay_routes(namespace: Namespace, store: RelaySessionStore): () => void {
|
|
const sockets = new Map<Socket, () => void>();
|
|
const connect = (socket: Socket) => {
|
|
if (!store.attach(socket)) return;
|
|
const handlers = new Map<string, (...args: unknown[]) => void>();
|
|
const route = (name: string, operation: (value: unknown) => unknown | Promise<unknown>, wrapped = true, ack_required = true) => {
|
|
const handler = (...args: unknown[]) => {
|
|
const callback = args[args.length - 1];
|
|
if (ack_required && typeof callback !== 'function') return;
|
|
let replied = false;
|
|
const ack = (value: unknown) => {
|
|
if (replied || typeof callback !== 'function' || !store.can_reply(socket)) return;
|
|
replied = true;
|
|
try {callback(value);} catch { /* A peer callback cannot disrupt route cleanup. */ }
|
|
};
|
|
if (args.length !== (typeof callback === 'function' ? 2 : 1)) {
|
|
ack(error_ack(new RelayError('INVALID_REQUEST', 'Expected one request payload')));
|
|
return;
|
|
}
|
|
void Promise.resolve().then(() => operation(args[0])).then(
|
|
value => ack(wrapped ? success(value) : value), error => ack(error_ack(error)));
|
|
};
|
|
handlers.set(name, handler);
|
|
socket.on(name, handler);
|
|
};
|
|
route('join_room', value => store.join(socket, value));
|
|
route('reauth', value => store.join(socket, value, true));
|
|
route('forward_node_ready', value => store.node_ready(socket, value));
|
|
route('relay_request', value => store.request(socket, value), false);
|
|
route('bot_log', value => store.log(socket, value), true, false);
|
|
route('bot_event', value => store.event(socket, value), true, false);
|
|
route('info_update', value => store.info(socket, value), true, false);
|
|
const unknown = (name: string, ...args: unknown[]) => {
|
|
if (handlers.has(name)) return;
|
|
const callback = args[args.length - 1];
|
|
try {
|
|
store.unknown_event(socket);
|
|
if (typeof callback === 'function' && store.can_reply(socket))
|
|
callback(error_ack(new RelayError('FORBIDDEN', 'Event is not allowed on the relay data channel')));
|
|
} catch { /* The store disconnects unknown-event floods. */ }
|
|
};
|
|
const cleanup = () => {
|
|
socket.offAny(unknown);
|
|
socket.off('disconnect', cleanup);
|
|
for (const [name, handler] of handlers) socket.off(name, handler);
|
|
sockets.delete(socket);
|
|
};
|
|
socket.onAny(unknown);
|
|
socket.on('disconnect', cleanup);
|
|
sockets.set(socket, cleanup);
|
|
};
|
|
namespace.on('connection', connect);
|
|
let disposed = false;
|
|
return () => {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
namespace.off('connection', connect);
|
|
for (const [socket, cleanup] of [...sockets]) {
|
|
cleanup();
|
|
store.disconnect(socket);
|
|
}
|
|
};
|
|
}
|