239 lines
6.8 KiB
Go
239 lines
6.8 KiB
Go
package nakama
|
|
|
|
import (
|
|
"net/http"
|
|
"seanime/internal/constants"
|
|
"seanime/internal/events"
|
|
"seanime/internal/util"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
var upgrader = websocket.Upgrader{
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true // Allow connections from any origin
|
|
},
|
|
}
|
|
|
|
// startHostServices initializes the host services
|
|
func (m *Manager) startHostServices() {
|
|
if m.settings == nil || !m.settings.IsHost || !m.settings.Enabled {
|
|
return
|
|
}
|
|
|
|
m.logger.Info().Msg("nakama: Starting host services")
|
|
|
|
// Clean up any existing watch party session
|
|
m.watchPartyManager.Cleanup()
|
|
|
|
// Start ping routine for connected peers
|
|
go m.hostPingRoutine()
|
|
|
|
// Start stale connection cleanup routine
|
|
go m.staleConnectionCleanupRoutine()
|
|
|
|
// Send event to client about host mode being enabled
|
|
m.wsEventManager.SendEvent(events.NakamaHostStarted, map[string]interface{}{
|
|
"enabled": true,
|
|
})
|
|
}
|
|
|
|
// stopHostServices stops the host services
|
|
func (m *Manager) stopHostServices() {
|
|
m.logger.Info().Msg("nakama: Stopping host services")
|
|
|
|
// Disconnect all peers
|
|
m.peerConnections.Range(func(id string, conn *PeerConnection) bool {
|
|
conn.Close()
|
|
return true
|
|
})
|
|
m.peerConnections.Clear()
|
|
|
|
// Send event to client about host mode being disabled
|
|
m.wsEventManager.SendEvent(events.NakamaHostStopped, map[string]interface{}{
|
|
"enabled": false,
|
|
})
|
|
}
|
|
|
|
// HandlePeerConnection handles incoming WebSocket connections from peers
|
|
func (m *Manager) HandlePeerConnection(w http.ResponseWriter, r *http.Request) {
|
|
if m.settings == nil || !m.settings.IsHost || !m.settings.Enabled {
|
|
http.Error(w, "Host mode not enabled", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
m.logger.Error().Err(err).Msg("nakama: Failed to upgrade WebSocket connection")
|
|
return
|
|
}
|
|
|
|
username := r.Header.Get("X-Seanime-Nakama-Username")
|
|
// Generate a random username if username is not set (this shouldn't be the case because the peer will generate its own username)
|
|
if username == "" {
|
|
username = "Peer_" + util.RandomStringWithAlphabet(8, "bcdefhijklmnopqrstuvwxyz0123456789")
|
|
}
|
|
|
|
peerID := r.Header.Get("X-Seanime-Nakama-Peer-Id")
|
|
if peerID == "" {
|
|
m.logger.Error().Msg("nakama: Peer connection missing PeerID header")
|
|
http.Error(w, "Missing PeerID header", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
serverVersion := r.Header.Get("X-Seanime-Nakama-Server-Version")
|
|
if serverVersion != constants.Version {
|
|
http.Error(w, "Server version mismatch", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check for existing connection with the same PeerID (reconnection scenario)
|
|
var existingConnID string
|
|
m.peerConnections.Range(func(id string, existingConn *PeerConnection) bool {
|
|
if existingConn.PeerId == peerID {
|
|
existingConnID = id
|
|
return false // Stop iteration
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Remove existing connection for this PeerID to handle reconnection
|
|
if existingConnID != "" {
|
|
if oldConn, exists := m.peerConnections.Get(existingConnID); exists {
|
|
m.logger.Info().Str("peerID", peerID).Str("oldConnID", existingConnID).Msg("nakama: Removing old connection for reconnecting peer")
|
|
m.peerConnections.Delete(existingConnID)
|
|
oldConn.Close()
|
|
}
|
|
}
|
|
|
|
// Generate new internal connection ID
|
|
internalConnID := generateConnectionID()
|
|
peerConn := &PeerConnection{
|
|
ID: internalConnID,
|
|
PeerId: peerID,
|
|
Username: username,
|
|
Conn: conn,
|
|
ConnectionType: ConnectionTypePeer,
|
|
Authenticated: false,
|
|
LastPing: time.Now(),
|
|
}
|
|
|
|
m.logger.Info().Str("internalConnID", internalConnID).Str("peerID", peerID).Str("username", username).Msg("nakama: New peer connection")
|
|
|
|
// Add to connections using internal connection ID as key
|
|
m.peerConnections.Set(internalConnID, peerConn)
|
|
|
|
// Handle the connection in a goroutine
|
|
go m.handlePeerConnection(peerConn)
|
|
}
|
|
|
|
// handlePeerConnection handles messages from a specific peer
|
|
func (m *Manager) handlePeerConnection(peerConn *PeerConnection) {
|
|
defer func() {
|
|
m.logger.Info().Str("peerId", peerConn.PeerId).Str("internalConnID", peerConn.ID).Msg("nakama: Peer disconnected")
|
|
|
|
// Remove from connections (safe to call multiple times)
|
|
if _, exists := m.peerConnections.Get(peerConn.ID); exists {
|
|
m.peerConnections.Delete(peerConn.ID)
|
|
|
|
// Remove peer from watch party if they were participating
|
|
m.watchPartyManager.HandlePeerDisconnected(peerConn.PeerId)
|
|
|
|
// Send event to client about peer disconnection (only if we actually removed it)
|
|
m.wsEventManager.SendEvent(events.NakamaPeerDisconnected, map[string]interface{}{
|
|
"peerId": peerConn.PeerId,
|
|
})
|
|
}
|
|
|
|
// Close connection (safe to call multiple times)
|
|
peerConn.Close()
|
|
}()
|
|
|
|
// Set up ping/pong handler
|
|
peerConn.Conn.SetPongHandler(func(appData string) error {
|
|
peerConn.LastPing = time.Now()
|
|
return nil
|
|
})
|
|
|
|
// Set read deadline
|
|
peerConn.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
|
|
|
for {
|
|
select {
|
|
case <-m.ctx.Done():
|
|
return
|
|
default:
|
|
var message Message
|
|
err := peerConn.Conn.ReadJSON(&message)
|
|
if err != nil {
|
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
m.logger.Error().Err(err).Str("peerId", peerConn.PeerId).Msg("nakama: Unexpected close error")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Handle the message using internal connection ID for message routing
|
|
if err := m.handleMessage(&message, peerConn.ID); err != nil {
|
|
m.logger.Error().Err(err).Str("peerId", peerConn.PeerId).Str("messageType", string(message.Type)).Msg("nakama: Failed to handle message")
|
|
|
|
// Send error response
|
|
errorMsg := &Message{
|
|
Type: MessageTypeError,
|
|
Payload: ErrorPayload{
|
|
Message: err.Error(),
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
peerConn.SendMessage(errorMsg)
|
|
}
|
|
|
|
// Reset read deadline
|
|
peerConn.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
|
}
|
|
}
|
|
}
|
|
|
|
// hostPingRoutine sends ping messages to all connected peers
|
|
func (m *Manager) hostPingRoutine() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-m.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
m.peerConnections.Range(func(id string, conn *PeerConnection) bool {
|
|
// Send ping
|
|
message := &Message{
|
|
Type: MessageTypePing,
|
|
Payload: nil,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := conn.SendMessage(message); err != nil {
|
|
m.logger.Error().Err(err).Str("peerId", conn.PeerId).Msg("nakama: Failed to send ping")
|
|
// Don't close here, let the stale connection cleanup handle it
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// staleConnectionCleanupRoutine periodically removes stale connections
|
|
func (m *Manager) staleConnectionCleanupRoutine() {
|
|
ticker := time.NewTicker(120 * time.Second) // Run every 2 minutes
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-m.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
m.RemoveStaleConnections()
|
|
}
|
|
}
|
|
}
|