mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
263 lines
6.9 KiB
JavaScript
263 lines
6.9 KiB
JavaScript
// WebSocket Manager for Fail2ban UI
|
|
// Handles real-time communication with the backend
|
|
|
|
"use strict";
|
|
|
|
class WebSocketManager {
|
|
constructor() {
|
|
this.ws = null;
|
|
this.reconnectAttempts = 0;
|
|
this.maxReconnectAttempts = Infinity;
|
|
this.reconnectDelay = 1000; // Start with 1 second
|
|
this.maxReconnectDelay = 30000; // Max 30 seconds
|
|
this.isConnecting = false;
|
|
this.isConnected = false;
|
|
this.lastBanEventId = null;
|
|
this.statusCallbacks = [];
|
|
this.banEventCallbacks = [];
|
|
|
|
// Connection metrics for tooltip
|
|
this.connectedAt = null;
|
|
this.lastHeartbeatAt = null;
|
|
this.messageCount = 0;
|
|
this.totalReconnects = 0;
|
|
this.initialConnection = true;
|
|
|
|
// Get WebSocket URL
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const host = window.location.host;
|
|
this.wsUrl = `${protocol}//${host}/api/ws`;
|
|
|
|
this.connect();
|
|
}
|
|
|
|
connect() {
|
|
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
|
return;
|
|
}
|
|
|
|
this.isConnecting = true;
|
|
this.updateStatus('connecting', 'Connecting...');
|
|
|
|
try {
|
|
this.ws = new WebSocket(this.wsUrl);
|
|
|
|
this.ws.onopen = () => {
|
|
this.isConnecting = false;
|
|
this.isConnected = true;
|
|
this.connectedAt = new Date();
|
|
if (!this.initialConnection) {
|
|
this.totalReconnects++;
|
|
}
|
|
this.initialConnection = false;
|
|
this.reconnectAttempts = 0;
|
|
this.reconnectDelay = 1000;
|
|
this.updateStatus('connected', 'Connected');
|
|
console.log('WebSocket connected');
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
this.messageCount++;
|
|
this.handleMessage(message);
|
|
} catch (err) {
|
|
console.error('Error parsing WebSocket message:', err);
|
|
}
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
this.updateStatus('error', 'Connection error');
|
|
};
|
|
|
|
this.ws.onclose = () => {
|
|
this.isConnecting = false;
|
|
this.isConnected = false;
|
|
this.updateStatus('disconnected', 'Disconnected');
|
|
console.log('WebSocket disconnected');
|
|
|
|
// Attempt to reconnect
|
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
this.scheduleReconnect();
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('Error creating WebSocket connection:', error);
|
|
this.isConnecting = false;
|
|
this.updateStatus('error', 'Connection failed');
|
|
this.scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
scheduleReconnect() {
|
|
this.reconnectAttempts++;
|
|
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
|
|
|
|
this.updateStatus('reconnecting', 'Reconnecting...');
|
|
|
|
setTimeout(() => {
|
|
this.connect();
|
|
}, delay);
|
|
}
|
|
|
|
handleMessage(message) {
|
|
switch (message.type) {
|
|
case 'ban_event':
|
|
this.handleBanEvent(message.data);
|
|
break;
|
|
case 'unban_event':
|
|
this.handleBanEvent(message.data); // Use same handler for unban events
|
|
break;
|
|
case 'heartbeat':
|
|
this.handleHeartbeat(message);
|
|
break;
|
|
default:
|
|
console.log('Unknown message type:', message.type);
|
|
}
|
|
}
|
|
|
|
handleBanEvent(eventData) {
|
|
// Check if we've already processed this event (prevent duplicates)
|
|
// Only check if event has an ID and we have a lastBanEventId
|
|
if (eventData.id && this.lastBanEventId !== null && eventData.id <= this.lastBanEventId) {
|
|
console.log('Skipping duplicate ban event:', eventData.id);
|
|
return;
|
|
}
|
|
|
|
// Update lastBanEventId if event has an ID
|
|
if (eventData.id) {
|
|
if (this.lastBanEventId === null || eventData.id > this.lastBanEventId) {
|
|
this.lastBanEventId = eventData.id;
|
|
}
|
|
}
|
|
|
|
console.log('Processing ban event:', eventData);
|
|
|
|
// Notify all registered callbacks
|
|
this.banEventCallbacks.forEach(callback => {
|
|
try {
|
|
callback(eventData);
|
|
} catch (err) {
|
|
console.error('Error in ban event callback:', err);
|
|
}
|
|
});
|
|
}
|
|
|
|
handleHeartbeat(message) {
|
|
// Update status to show backend is healthy
|
|
this.lastHeartbeatAt = new Date();
|
|
if (this.isConnected) {
|
|
this.updateStatus('connected', 'Connected');
|
|
}
|
|
}
|
|
|
|
updateStatus(state, text) {
|
|
this.statusCallbacks.forEach(callback => {
|
|
try {
|
|
callback(state, text);
|
|
} catch (err) {
|
|
console.error('Error in status callback:', err);
|
|
}
|
|
});
|
|
}
|
|
|
|
onStatusChange(callback) {
|
|
this.statusCallbacks.push(callback);
|
|
}
|
|
|
|
onBanEvent(callback) {
|
|
this.banEventCallbacks.push(callback);
|
|
}
|
|
|
|
disconnect() {
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
this.isConnected = false;
|
|
this.isConnecting = false;
|
|
}
|
|
|
|
getConnectionState() {
|
|
if (!this.ws) {
|
|
return 'disconnected';
|
|
}
|
|
|
|
switch (this.ws.readyState) {
|
|
case WebSocket.CONNECTING:
|
|
return 'connecting';
|
|
case WebSocket.OPEN:
|
|
return 'connected';
|
|
case WebSocket.CLOSING:
|
|
return 'disconnecting';
|
|
case WebSocket.CLOSED:
|
|
return 'disconnected';
|
|
default:
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
isHealthy() {
|
|
return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN;
|
|
}
|
|
|
|
formatDuration(seconds) {
|
|
if (seconds < 60) return `${seconds}s`;
|
|
if (seconds < 3600) {
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${mins}m ${secs}s`;
|
|
}
|
|
const hours = Math.floor(seconds / 3600);
|
|
const mins = Math.floor((seconds % 3600) / 60);
|
|
return `${hours}h ${mins}m`;
|
|
}
|
|
|
|
getConnectionInfo() {
|
|
if (!this.isConnected || !this.connectedAt) {
|
|
return null;
|
|
}
|
|
|
|
const now = new Date();
|
|
const duration = Math.floor((now - this.connectedAt) / 1000);
|
|
const durationStr = this.formatDuration(duration);
|
|
|
|
const lastHeartbeat = this.lastHeartbeatAt
|
|
? Math.floor((now - this.lastHeartbeatAt) / 1000)
|
|
: null;
|
|
const heartbeatStr = lastHeartbeat !== null
|
|
? (lastHeartbeat < 60 ? `${lastHeartbeat}s ago` : `${Math.floor(lastHeartbeat / 60)}m ago`)
|
|
: 'Never';
|
|
|
|
const protocol = this.wsUrl.startsWith('wss:') ? 'WSS (Secure)' : 'WS';
|
|
|
|
return {
|
|
duration: durationStr,
|
|
lastHeartbeat: heartbeatStr,
|
|
url: this.wsUrl,
|
|
messages: this.messageCount,
|
|
reconnects: this.totalReconnects,
|
|
protocol: protocol
|
|
};
|
|
}
|
|
}
|
|
|
|
// Create global instance - initialize immediately
|
|
var wsManager = null;
|
|
|
|
// Initialize WebSocket manager
|
|
if (typeof window !== 'undefined') {
|
|
// Initialize immediately if DOM is already loaded, otherwise wait
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (!wsManager) {
|
|
wsManager = new WebSocketManager();
|
|
}
|
|
});
|
|
} else {
|
|
wsManager = new WebSocketManager();
|
|
}
|
|
}
|
|
|