2026-01-05 19:46:33 -08:00
|
|
|
// Desktop WebSocket Handler
|
|
|
|
|
|
|
|
|
|
import WebSocket from 'ws';
|
|
|
|
|
import { ConnectionManager } from '../services/ConnectionManager';
|
|
|
|
|
import { SessionManager } from '../services/SessionManager';
|
|
|
|
|
import { logger } from '../utils/logger';
|
2026-07-17 10:15:23 -07:00
|
|
|
import { config } from '../config';
|
|
|
|
|
import { getLockoutRemaining, recordFailure, recordSuccess } from '../utils/authThrottle';
|
|
|
|
|
|
|
|
|
|
// Close code 1008 = policy violation (used for auth abuse).
|
|
|
|
|
const WS_POLICY_VIOLATION = 1008;
|
2026-01-05 19:46:33 -08:00
|
|
|
|
|
|
|
|
interface AuthMessage {
|
|
|
|
|
type: 'auth';
|
|
|
|
|
sessionId: string | null;
|
|
|
|
|
password: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ApiResponseMessage {
|
|
|
|
|
type: 'api_response';
|
|
|
|
|
requestId: string;
|
|
|
|
|
status: number;
|
|
|
|
|
body: any;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface WsBroadcastMessage {
|
|
|
|
|
type: 'ws_broadcast';
|
|
|
|
|
data: any;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PongMessage {
|
|
|
|
|
type: 'pong';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DesktopMessage = AuthMessage | ApiResponseMessage | WsBroadcastMessage | PongMessage;
|
|
|
|
|
|
|
|
|
|
export function handleDesktopConnection(
|
|
|
|
|
socket: WebSocket,
|
|
|
|
|
connectionManager: ConnectionManager,
|
|
|
|
|
sessionManager: SessionManager
|
|
|
|
|
): void {
|
|
|
|
|
let authenticatedSessionId: string | null = null;
|
2026-07-17 10:15:23 -07:00
|
|
|
// Track failed auth attempts on this individual socket.
|
|
|
|
|
let socketAuthFailures = 0;
|
2026-01-05 19:46:33 -08:00
|
|
|
|
|
|
|
|
socket.on('message', async (data) => {
|
|
|
|
|
try {
|
|
|
|
|
const message: DesktopMessage = JSON.parse(data.toString());
|
|
|
|
|
|
|
|
|
|
switch (message.type) {
|
2026-07-17 10:15:23 -07:00
|
|
|
case 'auth': {
|
|
|
|
|
const failed = await handleAuth(socket, message, sessionManager, connectionManager, (sessionId) => {
|
2026-01-05 19:46:33 -08:00
|
|
|
authenticatedSessionId = sessionId;
|
|
|
|
|
});
|
2026-07-17 10:15:23 -07:00
|
|
|
if (failed) {
|
|
|
|
|
socketAuthFailures++;
|
|
|
|
|
if (socketAuthFailures >= config.authMaxSocketFailures) {
|
|
|
|
|
logger.warn('Desktop exceeded auth attempts, closing socket');
|
|
|
|
|
socket.close(WS_POLICY_VIOLATION, 'Too many failed attempts');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
socketAuthFailures = 0;
|
|
|
|
|
}
|
2026-01-05 19:46:33 -08:00
|
|
|
break;
|
2026-07-17 10:15:23 -07:00
|
|
|
}
|
2026-01-05 19:46:33 -08:00
|
|
|
|
|
|
|
|
case 'api_response':
|
|
|
|
|
if (authenticatedSessionId) {
|
|
|
|
|
handleApiResponse(message, authenticatedSessionId, connectionManager);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'ws_broadcast':
|
|
|
|
|
if (authenticatedSessionId) {
|
|
|
|
|
handleWsBroadcast(message, authenticatedSessionId, connectionManager);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'pong':
|
|
|
|
|
if (authenticatedSessionId) {
|
|
|
|
|
connectionManager.updateDesktopPing(authenticatedSessionId);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
logger.warn('Unknown message type from desktop:', (message as any).type);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error handling desktop message:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('close', () => {
|
|
|
|
|
if (authenticatedSessionId) {
|
|
|
|
|
connectionManager.disconnectDesktop(authenticatedSessionId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('error', (error) => {
|
|
|
|
|
logger.error('Desktop WebSocket error:', error);
|
|
|
|
|
if (authenticatedSessionId) {
|
|
|
|
|
connectionManager.disconnectDesktop(authenticatedSessionId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 10:15:23 -07:00
|
|
|
// Returns true if the auth attempt failed (so the caller can count it).
|
2026-01-05 19:46:33 -08:00
|
|
|
async function handleAuth(
|
|
|
|
|
socket: WebSocket,
|
|
|
|
|
message: AuthMessage,
|
|
|
|
|
sessionManager: SessionManager,
|
|
|
|
|
connectionManager: ConnectionManager,
|
|
|
|
|
setSessionId: (id: string) => void
|
2026-07-17 10:15:23 -07:00
|
|
|
): Promise<boolean> {
|
2026-01-05 19:46:33 -08:00
|
|
|
try {
|
|
|
|
|
let sessionId = message.sessionId;
|
|
|
|
|
let session;
|
|
|
|
|
|
|
|
|
|
if (sessionId) {
|
2026-07-17 10:15:23 -07:00
|
|
|
// Reject early if this session id is currently locked out.
|
|
|
|
|
const lockRemaining = getLockoutRemaining(sessionId);
|
|
|
|
|
if (lockRemaining > 0) {
|
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
type: 'auth_response',
|
|
|
|
|
success: false,
|
|
|
|
|
error: 'Too many failed attempts, try again later',
|
|
|
|
|
retryAfterMs: lockRemaining
|
|
|
|
|
}));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 19:46:33 -08:00
|
|
|
// Validate existing session
|
|
|
|
|
const valid = await sessionManager.validatePassword(sessionId, message.password);
|
|
|
|
|
if (!valid) {
|
2026-07-17 10:15:23 -07:00
|
|
|
recordFailure(sessionId);
|
2026-01-05 19:46:33 -08:00
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
type: 'auth_response',
|
|
|
|
|
success: false,
|
|
|
|
|
error: 'Invalid session ID or password'
|
|
|
|
|
}));
|
2026-07-17 10:15:23 -07:00
|
|
|
return true;
|
2026-01-05 19:46:33 -08:00
|
|
|
}
|
2026-07-17 10:15:23 -07:00
|
|
|
recordSuccess(sessionId);
|
2026-01-05 19:46:33 -08:00
|
|
|
session = sessionManager.getSession(sessionId);
|
|
|
|
|
} else {
|
2026-07-17 10:15:23 -07:00
|
|
|
// Create new session (may throw on limit / short password)
|
2026-01-05 19:46:33 -08:00
|
|
|
session = await sessionManager.createSession(message.password);
|
|
|
|
|
sessionId = session.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!session || !sessionId) {
|
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
type: 'auth_response',
|
|
|
|
|
success: false,
|
|
|
|
|
error: 'Failed to create session'
|
|
|
|
|
}));
|
2026-07-17 10:15:23 -07:00
|
|
|
return true;
|
2026-01-05 19:46:33 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add to connection manager
|
|
|
|
|
connectionManager.addDesktopConnection(sessionId, socket);
|
|
|
|
|
setSessionId(sessionId);
|
|
|
|
|
|
|
|
|
|
// Send success response
|
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
type: 'auth_response',
|
|
|
|
|
success: true,
|
|
|
|
|
sessionId: sessionId
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
logger.info(`Desktop authenticated: ${sessionId}`);
|
2026-07-17 10:15:23 -07:00
|
|
|
return false;
|
|
|
|
|
} catch (error: any) {
|
2026-01-05 19:46:33 -08:00
|
|
|
logger.error('Desktop auth error:', error);
|
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
type: 'auth_response',
|
|
|
|
|
success: false,
|
2026-07-17 10:15:23 -07:00
|
|
|
error: error?.message || 'Authentication failed'
|
2026-01-05 19:46:33 -08:00
|
|
|
}));
|
2026-07-17 10:15:23 -07:00
|
|
|
return true;
|
2026-01-05 19:46:33 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleApiResponse(
|
|
|
|
|
message: ApiResponseMessage,
|
|
|
|
|
sessionId: string,
|
|
|
|
|
connectionManager: ConnectionManager
|
|
|
|
|
): void {
|
|
|
|
|
connectionManager.handleApiResponse(sessionId, message.requestId, message.status, message.body);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleWsBroadcast(
|
|
|
|
|
message: WsBroadcastMessage,
|
|
|
|
|
sessionId: string,
|
|
|
|
|
connectionManager: ConnectionManager
|
|
|
|
|
): void {
|
|
|
|
|
// Forward the broadcast to all web clients for this session
|
|
|
|
|
connectionManager.broadcastToWebClients(sessionId, message.data);
|
|
|
|
|
}
|