fix: Replace socket.io with native WS adapter — fixes WebSocket 1006
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Frontend uses native WebSocket API, backend was using socket.io which speaks an incompatible protocol. Switched to @nestjs/platform-ws so both sides speak native WebSocket. Also fixed JWT TTL override in docker-compose.yml (was hardcoded to 900s, now 14400s/4h). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,43 +7,47 @@ import {
|
||||
MessageBody,
|
||||
ConnectedSocket,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import WebSocket, { Server } from 'ws';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
|
||||
interface ClientMeta {
|
||||
licenseId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Console Gateway
|
||||
*
|
||||
* Provides real-time WebSocket connectivity for server console I/O.
|
||||
* Clients connect with JWT token in query params, join a room by license_id,
|
||||
* and can send/receive console commands and output.
|
||||
* NOTE: This gateway is NOT currently loaded (ConsoleModule not imported in AppModule).
|
||||
* Console I/O is handled by NatsBridgeGateway instead.
|
||||
* Kept for potential future use as a dedicated console-only WebSocket endpoint.
|
||||
*/
|
||||
@WebSocketGateway({ namespace: '/ws', cors: true })
|
||||
@WebSocketGateway({ path: '/api/console-ws' })
|
||||
export class ConsoleGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
private readonly logger = new Logger(ConsoleGateway.name);
|
||||
private clientMeta = new Map<WebSocket, ClientMeta>();
|
||||
private licenseClients = new Map<string, Set<WebSocket>>();
|
||||
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly natsService: NatsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handle client connection
|
||||
* Extract JWT from query param, validate, and join room by license_id
|
||||
*/
|
||||
async handleConnection(client: Socket) {
|
||||
async handleConnection(client: WebSocket, request: IncomingMessage) {
|
||||
try {
|
||||
const token = client.handshake.query.token as string;
|
||||
const url = new URL(request.url || '/', `http://${request.headers.host}`);
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('No token provided');
|
||||
}
|
||||
|
||||
// Verify JWT
|
||||
const payload = this.jwtService.verify(token);
|
||||
const licenseId = payload.license_id;
|
||||
|
||||
@@ -51,65 +55,65 @@ export class ConsoleGateway implements OnGatewayConnection, OnGatewayDisconnect
|
||||
throw new UnauthorizedException('Invalid token: no license_id');
|
||||
}
|
||||
|
||||
// Store license_id on socket for later use
|
||||
client.data.licenseId = licenseId;
|
||||
client.data.userId = payload.sub;
|
||||
this.clientMeta.set(client, { licenseId, userId: payload.sub });
|
||||
|
||||
// Join room specific to this license
|
||||
await client.join(licenseId);
|
||||
if (!this.licenseClients.has(licenseId)) {
|
||||
this.licenseClients.set(licenseId, new Set());
|
||||
}
|
||||
this.licenseClients.get(licenseId)!.add(client);
|
||||
|
||||
this.logger.log(`Client ${client.id} connected to license ${licenseId}`);
|
||||
this.logger.log(`Client connected to license ${licenseId}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.logger.error(`Connection failed: ${message}`);
|
||||
client.disconnect();
|
||||
client.close(4001, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle client disconnection
|
||||
*/
|
||||
handleDisconnect(client: Socket) {
|
||||
const licenseId = client.data.licenseId;
|
||||
this.logger.log(`Client ${client.id} disconnected from license ${licenseId}`);
|
||||
handleDisconnect(client: WebSocket) {
|
||||
const meta = this.clientMeta.get(client);
|
||||
if (meta?.licenseId) {
|
||||
this.licenseClients.get(meta.licenseId)?.delete(client);
|
||||
if (this.licenseClients.get(meta.licenseId)?.size === 0) {
|
||||
this.licenseClients.delete(meta.licenseId);
|
||||
}
|
||||
}
|
||||
this.clientMeta.delete(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle console input from client
|
||||
* Forward the command to NATS for execution on the game server
|
||||
*/
|
||||
@SubscribeMessage('console_input')
|
||||
async handleConsoleInput(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@ConnectedSocket() client: WebSocket,
|
||||
@MessageBody() data: { command: string },
|
||||
) {
|
||||
const licenseId = client.data.licenseId;
|
||||
const meta = this.clientMeta.get(client);
|
||||
if (!meta?.licenseId) return;
|
||||
|
||||
if (!data.command) {
|
||||
return { error: 'Command is required' };
|
||||
}
|
||||
|
||||
this.logger.debug(`Console input from ${licenseId}: ${data.command}`);
|
||||
this.logger.debug(`Console input from ${meta.licenseId}: ${data.command}`);
|
||||
|
||||
// Forward to NATS
|
||||
await this.natsService.sendServerCommand(licenseId, 'command', {
|
||||
await this.natsService.sendServerCommand(meta.licenseId, 'command', {
|
||||
command: data.command,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Send console output or event to all clients in a license room
|
||||
*/
|
||||
sendToLicense(licenseId: string, event: string, data: any) {
|
||||
this.server.to(licenseId).emit(event, data);
|
||||
const clients = this.licenseClients.get(licenseId);
|
||||
if (!clients) return;
|
||||
|
||||
const message = JSON.stringify({ event, data });
|
||||
for (const client of clients) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast console output to a specific license
|
||||
* This method would be called by a NATS subscriber when output is received
|
||||
*/
|
||||
broadcastConsoleOutput(licenseId: string, output: string) {
|
||||
this.sendToLicense(licenseId, 'console_output', { output });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user