Files
websocket-qris/internal/services/websocket/client.go
2025-10-24 12:33:10 +00:00

729 lines
18 KiB
Go

package websocket
import (
"api-service/internal/config"
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
// Client mewakili koneksi klien WebSocket
type Client struct {
// Identifikasi
ID string
StaticID string
UserID string
Room string
// Koneksi
IPAddress string
Conn *websocket.Conn
Send chan WebSocketMessage
// Hub
Hub *Hub
// Context
ctx context.Context
cancel context.CancelFunc
// Status
lastPing time.Time
lastPong time.Time
connectedAt time.Time
isActive bool
// Sinkronisasi
mu sync.RWMutex
}
// NewClient membuat klien baru
func NewClient(
id, staticID, userID, room, ipAddress string,
conn *websocket.Conn,
hub *Hub,
config config.WebSocketConfig,
) *Client {
ctx, cancel := context.WithCancel(hub.ctx)
return &Client{
ID: id,
StaticID: staticID,
UserID: userID,
Room: room,
IPAddress: ipAddress,
Conn: conn,
Send: make(chan WebSocketMessage, config.ChannelBufferSize),
Hub: hub,
ctx: ctx,
cancel: cancel,
lastPing: time.Now(),
connectedAt: time.Now(),
isActive: true,
}
}
// readPup menangani pembacaan pesan dari klien
func (c *Client) readPump() {
defer func() {
c.Hub.unregister <- c
c.Conn.Close()
}()
// Konfigurasi koneksi
c.Conn.SetReadLimit(int64(c.Hub.config.MaxMessageSize))
c.resetReadDeadline()
// Setup ping/pong handlers
c.Conn.SetPingHandler(func(message string) error {
c.resetReadDeadline()
return c.Conn.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(c.Hub.config.WriteTimeout))
})
c.Conn.SetPongHandler(func(message string) error {
c.mu.Lock()
c.lastPong = time.Now()
c.isActive = true
c.mu.Unlock()
c.resetReadDeadline()
return nil
})
for {
select {
case <-c.ctx.Done():
return
default:
_, message, err := c.Conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure,
websocket.CloseNormalClosure) {
c.Hub.errorCount++
}
return
}
// Reset deadline setiap kali ada pesan masuk
c.resetReadDeadline()
c.updateLastActivity()
// Parse pesan
var msg WebSocketMessage
if err := json.Unmarshal(message, &msg); err != nil {
c.sendErrorResponse("Invalid message format", err.Error())
continue
}
// Set metadata pesan
msg.Timestamp = time.Now()
msg.ClientID = c.ID
if msg.MessageID == "" {
msg.MessageID = uuid.New().String()
}
// Proses pesan menggunakan registry
c.handleMessage(msg)
}
}
}
// writePump menangani pengiriman pesan ke klien
func (c *Client) writePump() {
ticker := time.NewTicker(c.Hub.config.PingInterval)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case message, ok := <-c.Send:
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.Conn.WriteJSON(message); err != nil {
c.Hub.errorCount++
return
}
case <-ticker.C:
// Kirim ping dan periksa respons
if err := c.sendPing(); err != nil {
c.Hub.errorCount++
return
}
// Periksa timeout pong
if c.isPongTimeout() {
return
}
case <-c.ctx.Done():
return
}
}
}
// handleMessage memproses pesan masuk
func (c *Client) handleMessage(msg WebSocketMessage) {
// Dapatkan handler dari registry
handler, exists := c.Hub.messageRegistry.GetHandler(msg.Type)
if !exists {
// Handler tidak ditemukan, gunakan handler default
c.handleDefaultMessage(msg)
return
}
// Jalankan handler
if err := handler.HandleMessage(c, msg); err != nil {
c.sendErrorResponse("Error handling message", err.Error())
c.Hub.errorCount++
}
}
// handleDefaultMessage menangani pesan tanpa handler khusus
func (c *Client) handleDefaultMessage(msg WebSocketMessage) {
switch msg.Type {
case "broadcast":
c.Hub.broadcast <- msg
c.sendDirectResponse("broadcast_sent", "Message broadcasted to all clients")
case "direct_message":
c.handleDirectMessage(msg)
case "ping":
c.handlePing(msg)
case "pong":
// Pong sudah ditangani di level koneksi
break
case "heartbeat":
c.handleHeartbeat(msg)
case "connection_test":
c.handleConnectionTest(msg)
case "get_online_users":
c.sendOnlineUsers()
case "join_room":
c.handleJoinRoom(msg)
case "leave_room":
c.handleLeaveRoom(msg)
case "get_room_info":
c.handleGetRoomInfo(msg)
case "database_query", "db_query":
c.handleDatabaseQuery(msg)
case "db_insert":
c.handleDatabaseInsert(msg)
case "db_custom_query":
c.handleDatabaseCustomQuery(msg)
case "get_stats":
c.handleGetStats(msg)
case "get_server_stats":
c.handleGetServerStats(msg)
case "get_system_health":
c.handleGetSystemHealth(msg)
case "admin_kick_client":
c.handleAdminKickClient(msg)
case "admin_kill_server":
c.handleAdminKillServer(msg)
case "admin_clear_logs":
c.handleAdminClearLogs(msg)
default:
c.sendDirectResponse("message_received", fmt.Sprintf("Message received: %v", msg.Data))
c.Hub.broadcast <- msg
}
}
// Metode helper lainnya...
func (c *Client) resetReadDeadline() {
c.Conn.SetReadDeadline(time.Now().Add(c.Hub.config.ReadTimeout))
}
func (c *Client) updateLastActivity() {
c.mu.Lock()
defer c.mu.Unlock()
c.lastPing = time.Now()
c.isActive = true
}
func (c *Client) sendPing() error {
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return err
}
c.mu.Lock()
c.lastPing = time.Now()
c.mu.Unlock()
return nil
}
func (c *Client) isPongTimeout() bool {
c.mu.RLock()
defer c.mu.RUnlock()
lastActivity := c.lastPong
if lastActivity.IsZero() {
lastActivity = c.lastPing
}
return time.Since(lastActivity) > c.Hub.config.PongTimeout
}
func (c *Client) isClientActive() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.isActive && time.Since(c.lastPing) < c.Hub.config.PongTimeout
}
func (c *Client) sendDirectResponse(messageType string, data interface{}) {
response := NewWebSocketMessage(MessageType(messageType), data, c.ID, c.Room)
select {
case c.Send <- response:
default:
// Channel penuh, abaikan pesan
}
}
func (c *Client) sendErrorResponse(error, details string) {
c.sendDirectResponse("error", map[string]interface{}{
"error": error,
"details": details,
})
}
func (c *Client) gracefulClose() {
c.mu.Lock()
c.isActive = false
c.mu.Unlock()
// Kirim pesan close
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
c.Conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
// Batalkan context
c.cancel()
}
// ReadPump mengekspos metode readPump untuk handler
func (c *Client) ReadPump() {
c.readPump()
}
// WritePump mengekspos metode writePump untuk handler
func (c *Client) WritePump() {
c.writePump()
}
// handleDirectMessage menangani pesan direct message
func (c *Client) handleDirectMessage(msg WebSocketMessage) {
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid direct message format", "Data must be an object")
return
}
targetClientID, ok := data["target_client_id"].(string)
if !ok || targetClientID == "" {
c.sendErrorResponse("Invalid target client ID", "target_client_id is required")
return
}
message, ok := data["message"].(string)
if !ok || message == "" {
c.sendErrorResponse("Invalid message content", "message is required")
return
}
// Buat pesan direct message
directMsg := NewWebSocketMessage(DirectMessage, map[string]interface{}{
"from_client_id": c.ID,
"from_user_id": c.UserID,
"message": message,
"timestamp": msg.Timestamp,
}, targetClientID, "")
// Kirim ke target client
select {
case c.Hub.broadcast <- directMsg:
c.sendDirectResponse("direct_message_sent", map[string]interface{}{
"target_client_id": targetClientID,
"message": message,
})
default:
c.sendErrorResponse("Failed to send direct message", "Message queue is full")
}
}
// handlePing menangani ping message
func (c *Client) handlePing(msg WebSocketMessage) {
// Kirim pong response
pongMsg := NewWebSocketMessage(PongMessage, map[string]interface{}{
"timestamp": msg.Timestamp.Unix(),
"client_id": c.ID,
}, c.ID, "")
select {
case c.Send <- pongMsg:
default:
// Channel penuh, abaikan
}
}
// handleHeartbeat menangani heartbeat message
func (c *Client) handleHeartbeat(msg WebSocketMessage) {
// Kirim heartbeat acknowledgment
heartbeatAck := NewWebSocketMessage(MessageType("heartbeat_ack"), map[string]interface{}{
"timestamp": time.Now().Unix(),
"client_uptime": time.Since(c.connectedAt).Seconds(),
"server_uptime": time.Since(c.Hub.startTime).Seconds(),
"client_id": c.ID,
}, c.ID, "")
select {
case c.Send <- heartbeatAck:
default:
// Channel penuh, abaikan
}
}
// handleConnectionTest menangani connection test
func (c *Client) handleConnectionTest(msg WebSocketMessage) {
// Kirim connection test result
testResult := NewWebSocketMessage(MessageType("connection_test_result"), map[string]interface{}{
"timestamp": time.Now().Unix(),
"client_id": c.ID,
"connection_status": "healthy",
"latency_ms": 0, // Could be calculated if ping timestamp is provided
"uptime_seconds": time.Since(c.connectedAt).Seconds(),
}, c.ID, "")
select {
case c.Send <- testResult:
default:
// Channel penuh, abaikan
}
}
// handleJoinRoom menangani join room
func (c *Client) handleJoinRoom(msg WebSocketMessage) {
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid join room format", "Data must be an object")
return
}
roomName, ok := data["room"].(string)
if !ok || roomName == "" {
c.sendErrorResponse("Invalid room name", "room is required")
return
}
// Update client room
oldRoom := c.Room
c.Room = roomName
// Update hub room mapping
c.Hub.mu.Lock()
if oldRoom != "" {
if roomClients, exists := c.Hub.rooms[oldRoom]; exists {
delete(roomClients, c)
if len(roomClients) == 0 {
delete(c.Hub.rooms, oldRoom)
}
}
}
if c.Hub.rooms[roomName] == nil {
c.Hub.rooms[roomName] = make(map[*Client]bool)
}
c.Hub.rooms[roomName][c] = true
c.Hub.mu.Unlock()
// Log activity
c.Hub.logActivity("client_join_room", c.ID, fmt.Sprintf("Room: %s", roomName))
// Kirim response
c.sendDirectResponse("room_joined", map[string]interface{}{
"room": roomName,
"previous_room": oldRoom,
})
}
// handleLeaveRoom menangani leave room
func (c *Client) handleLeaveRoom(msg WebSocketMessage) {
oldRoom := c.Room
if oldRoom == "" {
c.sendErrorResponse("Not in any room", "Client is not currently in a room")
return
}
// Update hub room mapping
c.Hub.mu.Lock()
if roomClients, exists := c.Hub.rooms[oldRoom]; exists {
delete(roomClients, c)
if len(roomClients) == 0 {
delete(c.Hub.rooms, oldRoom)
}
}
c.Hub.mu.Unlock()
// Clear client room
c.Room = ""
// Log activity
c.Hub.logActivity("client_leave_room", c.ID, fmt.Sprintf("Room: %s", oldRoom))
// Kirim response
c.sendDirectResponse("room_left", map[string]interface{}{
"room": oldRoom,
})
}
// handleGetRoomInfo menangani get room info
func (c *Client) handleGetRoomInfo(msg WebSocketMessage) {
c.Hub.mu.RLock()
defer c.Hub.mu.RUnlock()
roomInfo := make(map[string]interface{})
// Info ruangan saat ini
if c.Room != "" {
if roomClients, exists := c.Hub.rooms[c.Room]; exists {
clientIDs := make([]string, 0, len(roomClients))
for client := range roomClients {
clientIDs = append(clientIDs, client.ID)
}
roomInfo["current_room"] = map[string]interface{}{
"name": c.Room,
"client_count": len(roomClients),
"clients": clientIDs,
}
}
}
// Info semua ruangan
allRooms := make(map[string]int)
for roomName, clients := range c.Hub.rooms {
allRooms[roomName] = len(clients)
}
roomInfo["all_rooms"] = allRooms
roomInfo["total_rooms"] = len(c.Hub.rooms)
c.sendDirectResponse("room_info", roomInfo)
}
// handleDatabaseInsert menangani database insert
func (c *Client) handleDatabaseInsert(msg WebSocketMessage) {
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid database insert format", "Data must be an object")
return
}
table, ok := data["table"].(string)
if !ok || table == "" {
c.sendErrorResponse("Invalid table name", "table is required")
return
}
insertData, ok := data["data"].(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid insert data", "data must be an object")
return
}
// For now, just acknowledge the insert request
// In a real implementation, you would perform the actual database insert
c.sendDirectResponse("db_insert_result", map[string]interface{}{
"table": table,
"status": "acknowledged",
"message": "Insert request received (not implemented)",
"data": insertData, // Use the variable to avoid unused error
})
}
// handleDatabaseCustomQuery menangani custom database query
func (c *Client) handleDatabaseCustomQuery(msg WebSocketMessage) {
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid database query format", "Data must be an object")
return
}
database, ok := data["database"].(string)
if !ok || database == "" {
database = "default"
}
query, ok := data["query"].(string)
if !ok || query == "" {
c.sendErrorResponse("Invalid query", "query is required")
return
}
// For now, just acknowledge the query request
// In a real implementation, you would execute the query
c.sendDirectResponse("db_query_result", map[string]interface{}{
"database": database,
"query": query,
"status": "acknowledged",
"message": "Query request received (not implemented)",
})
}
// handleGetStats menangani get stats
func (c *Client) handleGetStats(msg WebSocketMessage) {
stats := c.Hub.GetStats()
c.sendDirectResponse("stats", stats)
}
// handleGetServerStats menangani get server stats
func (c *Client) handleGetServerStats(msg WebSocketMessage) {
// Create monitoring manager instance
monitoringManager := NewMonitoringManager(c.Hub)
detailedStats := monitoringManager.GetDetailedStats()
c.sendDirectResponse("server_stats", detailedStats)
}
// handleGetSystemHealth menangani get system health
func (c *Client) handleGetSystemHealth(msg WebSocketMessage) {
systemHealth := make(map[string]interface{})
if c.Hub.dbService != nil {
systemHealth["databases"] = c.Hub.dbService.Health()
systemHealth["available_dbs"] = c.Hub.dbService.ListDBs()
}
systemHealth["websocket_status"] = "healthy"
systemHealth["uptime_seconds"] = time.Since(c.Hub.startTime).Seconds()
c.sendDirectResponse("system_health", systemHealth)
}
// handleAdminKickClient menangani admin kick client
func (c *Client) handleAdminKickClient(msg WebSocketMessage) {
// This should be protected by authentication, but for now just acknowledge
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid admin command format", "Data must be an object")
return
}
targetClientID, ok := data["client_id"].(string)
if !ok || targetClientID == "" {
c.sendErrorResponse("Invalid target client ID", "client_id is required")
return
}
// For now, just acknowledge the kick request
// In a real implementation, you would check admin permissions and kick the client
c.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "kick_client",
"target_client_id": targetClientID,
"status": "acknowledged",
"message": "Kick request received (not implemented)",
})
}
// handleAdminKillServer menangani admin kill server
func (c *Client) handleAdminKillServer(msg WebSocketMessage) {
// This should be protected by authentication, but for now just acknowledge
c.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "kill_server",
"status": "acknowledged",
"message": "Kill server request received (not implemented - would require admin auth)",
})
}
// handleAdminClearLogs menangani admin clear logs
func (c *Client) handleAdminClearLogs(msg WebSocketMessage) {
// This should be protected by authentication, but for now just acknowledge
c.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "clear_logs",
"status": "acknowledged",
"message": "Clear logs request received (not implemented - would require admin auth)",
})
}
// Implementasi metode lainnya...
func (c *Client) handleDatabaseQuery(msg WebSocketMessage) {
// Implementasi yang sama seperti sebelumnya
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid database query format", "Data must be an object")
return
}
table, ok := data["table"].(string)
if !ok || table == "" {
c.sendErrorResponse("Invalid table name", "table is required")
return
}
// For now, just acknowledge the query request
c.sendDirectResponse("db_query_result", map[string]interface{}{
"table": table,
"status": "acknowledged",
"message": "Query request received (not implemented)",
})
}
func (c *Client) sendOnlineUsers() {
c.Hub.mu.RLock()
defer c.Hub.mu.RUnlock()
users := make([]map[string]interface{}, 0, len(c.Hub.clients))
for client := range c.Hub.clients {
user := map[string]interface{}{
"id": client.ID,
"static_id": client.StaticID,
"user_id": client.UserID,
"room": client.Room,
"ip_address": client.IPAddress,
"connected_at": client.connectedAt,
"last_ping": client.lastPing,
"last_pong": client.lastPong,
"is_active": client.isClientActive(),
}
users = append(users, user)
}
response := NewWebSocketMessage(OnlineUsersMessage, map[string]interface{}{
"users": users,
"count": len(users),
}, c.ID, "")
select {
case c.Send <- response:
default:
// Channel penuh, abaikan
}
}