mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 14:03:15 +02:00
Websocket and whois restructured and added sections
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||||
//
|
//
|
||||||
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch)
|
||||||
//
|
//
|
||||||
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||||
// You may not use this file except in compliance with the License.
|
// You may not use this file except in compliance with the License.
|
||||||
@@ -28,14 +28,27 @@ import (
|
|||||||
"github.com/swissmakers/fail2ban-ui/internal/storage"
|
"github.com/swissmakers/fail2ban-ui/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Types and Constants
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
hub *Hub
|
||||||
|
conn *websocket.Conn
|
||||||
|
send chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hub struct {
|
||||||
|
clients map[*Client]bool
|
||||||
|
broadcast chan []byte
|
||||||
|
register chan *Client
|
||||||
|
unregister chan *Client
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Time allowed to write a message to the peer
|
writeWait = 10 * time.Second
|
||||||
writeWait = 10 * time.Second
|
pongWait = 60 * time.Second
|
||||||
|
|
||||||
// Time allowed to read the next pong message from the peer
|
|
||||||
pongWait = 60 * time.Second
|
|
||||||
|
|
||||||
// Send pings to peer with this period (must be less than pongWait)
|
|
||||||
pingPeriod = (pongWait * 9) / 10
|
pingPeriod = (pongWait * 9) / 10
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,37 +56,15 @@ var upgrader = websocket.Upgrader{
|
|||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 1024,
|
WriteBufferSize: 1024,
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
// Allow all origins for now - can be restricted in production
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client represents a WebSocket connection
|
// =========================================================================
|
||||||
type Client struct {
|
// Fail2ban-UI WebSocket Hub
|
||||||
hub *Hub
|
// =========================================================================
|
||||||
conn *websocket.Conn
|
|
||||||
send chan []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hub maintains the set of active clients and broadcasts messages to them
|
// Broadcasts the console log message to all connected clients.
|
||||||
type Hub struct {
|
|
||||||
// Registered clients
|
|
||||||
clients map[*Client]bool
|
|
||||||
|
|
||||||
// Inbound messages from clients
|
|
||||||
broadcast chan []byte
|
|
||||||
|
|
||||||
// Register requests from clients
|
|
||||||
register chan *Client
|
|
||||||
|
|
||||||
// Unregister requests from clients
|
|
||||||
unregister chan *Client
|
|
||||||
|
|
||||||
// Mutex for thread-safe operations
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// BroadcastConsoleLog broadcasts a console log message to all connected clients
|
|
||||||
func (h *Hub) BroadcastConsoleLog(message string) {
|
func (h *Hub) BroadcastConsoleLog(message string) {
|
||||||
logMsg := map[string]interface{}{
|
logMsg := map[string]interface{}{
|
||||||
"type": "console_log",
|
"type": "console_log",
|
||||||
@@ -89,11 +80,11 @@ func (h *Hub) BroadcastConsoleLog(message string) {
|
|||||||
select {
|
select {
|
||||||
case h.broadcast <- data:
|
case h.broadcast <- data:
|
||||||
default:
|
default:
|
||||||
// Channel full, drop message
|
log.Printf("Broadcast channel full, dropping console log")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHub creates a new WebSocket hub
|
// Creates new Hub instance.
|
||||||
func NewHub() *Hub {
|
func NewHub() *Hub {
|
||||||
return &Hub{
|
return &Hub{
|
||||||
clients: make(map[*Client]bool),
|
clients: make(map[*Client]bool),
|
||||||
@@ -103,9 +94,8 @@ func NewHub() *Hub {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the hub's main loop
|
// Runs the Hub.
|
||||||
func (h *Hub) Run() {
|
func (h *Hub) Run() {
|
||||||
// Start heartbeat ticker
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -139,13 +129,12 @@ func (h *Hub) Run() {
|
|||||||
h.mu.RUnlock()
|
h.mu.RUnlock()
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Send heartbeat to all clients
|
|
||||||
h.sendHeartbeat()
|
h.sendHeartbeat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendHeartbeat sends a heartbeat message to all connected clients
|
// Sends heartbeat message to all connected clients.
|
||||||
func (h *Hub) sendHeartbeat() {
|
func (h *Hub) sendHeartbeat() {
|
||||||
message := map[string]interface{}{
|
message := map[string]interface{}{
|
||||||
"type": "heartbeat",
|
"type": "heartbeat",
|
||||||
@@ -170,7 +159,10 @@ func (h *Hub) sendHeartbeat() {
|
|||||||
h.mu.RUnlock()
|
h.mu.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BroadcastBanEvent broadcasts a ban event to all connected clients
|
// =========================================================================
|
||||||
|
// Broadcast Ban Event
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
func (h *Hub) BroadcastBanEvent(event storage.BanEventRecord) {
|
func (h *Hub) BroadcastBanEvent(event storage.BanEventRecord) {
|
||||||
message := map[string]interface{}{
|
message := map[string]interface{}{
|
||||||
"type": "ban_event",
|
"type": "ban_event",
|
||||||
@@ -189,7 +181,10 @@ func (h *Hub) BroadcastBanEvent(event storage.BanEventRecord) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BroadcastUnbanEvent broadcasts an unban event to all connected clients
|
// =========================================================================
|
||||||
|
// Broadcast Unban Event
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
func (h *Hub) BroadcastUnbanEvent(event storage.BanEventRecord) {
|
func (h *Hub) BroadcastUnbanEvent(event storage.BanEventRecord) {
|
||||||
message := map[string]interface{}{
|
message := map[string]interface{}{
|
||||||
"type": "unban_event",
|
"type": "unban_event",
|
||||||
@@ -208,7 +203,11 @@ func (h *Hub) BroadcastUnbanEvent(event storage.BanEventRecord) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPump pumps messages from the WebSocket connection to the hub
|
// =========================================================================
|
||||||
|
// WebSocket Helper Functions
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Reads messages from the WebSocket connection.
|
||||||
func (c *Client) readPump() {
|
func (c *Client) readPump() {
|
||||||
defer func() {
|
defer func() {
|
||||||
c.hub.unregister <- c
|
c.hub.unregister <- c
|
||||||
@@ -232,7 +231,7 @@ func (c *Client) readPump() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePump pumps messages from the hub to the WebSocket connection
|
// Writes messages to the WebSocket connection.
|
||||||
func (c *Client) writePump() {
|
func (c *Client) writePump() {
|
||||||
ticker := time.NewTicker(pingPeriod)
|
ticker := time.NewTicker(pingPeriod)
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -255,7 +254,6 @@ func (c *Client) writePump() {
|
|||||||
}
|
}
|
||||||
w.Write(message)
|
w.Write(message)
|
||||||
|
|
||||||
// Add queued messages to the current websocket message
|
|
||||||
n := len(c.send)
|
n := len(c.send)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
w.Write([]byte{'\n'})
|
w.Write([]byte{'\n'})
|
||||||
@@ -275,7 +273,7 @@ func (c *Client) writePump() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveWS handles WebSocket requests from clients
|
// Serves the WebSocket connection.
|
||||||
func serveWS(hub *Hub, c *gin.Context) {
|
func serveWS(hub *Hub, c *gin.Context) {
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -291,12 +289,11 @@ func serveWS(hub *Hub, c *gin.Context) {
|
|||||||
|
|
||||||
client.hub.register <- client
|
client.hub.register <- client
|
||||||
|
|
||||||
// Allow collection of memory referenced by the caller by doing all work in new goroutines
|
|
||||||
go client.writePump()
|
go client.writePump()
|
||||||
go client.readPump()
|
go client.readPump()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocketHandler is the Gin handler for WebSocket connections
|
// This is called from routes.go and returns the Gin handler for WebSocket connections.
|
||||||
func WebSocketHandler(hub *Hub) gin.HandlerFunc {
|
func WebSocketHandler(hub *Hub) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
serveWS(hub, c)
|
serveWS(hub, c)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||||
//
|
//
|
||||||
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch)
|
||||||
//
|
//
|
||||||
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||||
// You may not use this file except in compliance with the License.
|
// You may not use this file except in compliance with the License.
|
||||||
@@ -25,21 +25,26 @@ import (
|
|||||||
"github.com/likexian/whois"
|
"github.com/likexian/whois"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// =========================================================================
|
||||||
whoisCache = make(map[string]cachedWhois)
|
// Types and Constants
|
||||||
whoisCacheMutex sync.RWMutex
|
// =========================================================================
|
||||||
cacheExpiry = 24 * time.Hour
|
|
||||||
)
|
|
||||||
|
|
||||||
type cachedWhois struct {
|
type cachedWhois struct {
|
||||||
data string
|
data string
|
||||||
timestamp time.Time
|
timestamp time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookupWhois performs a whois lookup for the given IP address.
|
var (
|
||||||
// It uses caching to avoid repeated queries for the same IP.
|
whoisCache = make(map[string]cachedWhois)
|
||||||
|
whoisCacheMutex sync.RWMutex
|
||||||
|
cacheExpiry = 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Lookup Whois Data
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
func lookupWhois(ip string) (string, error) {
|
func lookupWhois(ip string) (string, error) {
|
||||||
// Check cache first
|
|
||||||
whoisCacheMutex.RLock()
|
whoisCacheMutex.RLock()
|
||||||
if cached, ok := whoisCache[ip]; ok {
|
if cached, ok := whoisCache[ip]; ok {
|
||||||
if time.Since(cached.timestamp) < cacheExpiry {
|
if time.Since(cached.timestamp) < cacheExpiry {
|
||||||
@@ -49,7 +54,6 @@ func lookupWhois(ip string) (string, error) {
|
|||||||
}
|
}
|
||||||
whoisCacheMutex.RUnlock()
|
whoisCacheMutex.RUnlock()
|
||||||
|
|
||||||
// Perform whois lookup with timeout
|
|
||||||
done := make(chan string, 1)
|
done := make(chan string, 1)
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
@@ -65,20 +69,17 @@ func lookupWhois(ip string) (string, error) {
|
|||||||
var whoisData string
|
var whoisData string
|
||||||
select {
|
select {
|
||||||
case whoisData = <-done:
|
case whoisData = <-done:
|
||||||
// Success - cache will be updated below
|
|
||||||
case err := <-errChan:
|
case err := <-errChan:
|
||||||
return "", fmt.Errorf("whois lookup failed: %w", err)
|
return "", fmt.Errorf("whois lookup failed: %w", err)
|
||||||
case <-time.After(10 * time.Second):
|
case <-time.After(10 * time.Second):
|
||||||
return "", fmt.Errorf("whois lookup timeout after 10 seconds")
|
return "", fmt.Errorf("whois lookup timeout after 10 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
whoisCacheMutex.Lock()
|
whoisCacheMutex.Lock()
|
||||||
whoisCache[ip] = cachedWhois{
|
whoisCache[ip] = cachedWhois{
|
||||||
data: whoisData,
|
data: whoisData,
|
||||||
timestamp: time.Now(),
|
timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
// Clean old cache entries if cache is getting large
|
|
||||||
if len(whoisCache) > 1000 {
|
if len(whoisCache) > 1000 {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for k, v := range whoisCache {
|
for k, v := range whoisCache {
|
||||||
@@ -92,15 +93,16 @@ func lookupWhois(ip string) (string, error) {
|
|||||||
return whoisData, nil
|
return whoisData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractCountryFromWhois attempts to extract country code from whois data.
|
// =========================================================================
|
||||||
// This is a fallback if GeoIP lookup fails.
|
// Extract Country from Whois Data
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
func extractCountryFromWhois(whoisData string) string {
|
func extractCountryFromWhois(whoisData string) string {
|
||||||
lines := strings.Split(whoisData, "\n")
|
lines := strings.Split(whoisData, "\n")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
lineLower := strings.ToLower(line)
|
lineLower := strings.ToLower(line)
|
||||||
|
|
||||||
// Look for country field
|
|
||||||
if strings.HasPrefix(lineLower, "country:") {
|
if strings.HasPrefix(lineLower, "country:") {
|
||||||
parts := strings.SplitN(line, ":", 2)
|
parts := strings.SplitN(line, ":", 2)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
@@ -110,7 +112,6 @@ func extractCountryFromWhois(whoisData string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Alternative format
|
|
||||||
if strings.HasPrefix(lineLower, "country code:") {
|
if strings.HasPrefix(lineLower, "country code:") {
|
||||||
parts := strings.SplitN(line, ":", 2)
|
parts := strings.SplitN(line, ":", 2)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
|
|||||||
Reference in New Issue
Block a user