package server import ( "encoding/json" "fmt" "log" "math/rand" "net/http" "time" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" "github.com/gorilla/websocket" "github.com/nats-io/nats.go" ) var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }} type Gateway struct { NC *nats.Conn World *game.World // Lokale Referenz auf Räume, die DIESER Server verwaltet // In einer echten Microservice Welt wäre das separat, // aber hier hostet der Gateway auch Räume. LocalRooms map[string]*Room // Security: Tracking welcher Spieler in welchem Raum ist // PlayerID -> RoomID Mapping PlayerRooms map[string]string } func NewGateway(nc *nats.Conn, w *game.World) *Gateway { return &Gateway{ NC: nc, World: w, LocalRooms: make(map[string]*Room), PlayerRooms: make(map[string]string), } } func (gw *Gateway) HandleWS(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() // 1. HANDSHAKE (Login warten) // Der Client muss als allererstes JSON senden: {action: "CREATE"|"JOIN", name: "Hans"} var login game.LoginPayload _, msg, err := conn.ReadMessage() if err != nil { return } if err := json.Unmarshal(msg, &login); err != nil { log.Println("Ungültiger Login:", err) return } // IDs generieren playerID := fmt.Sprintf("p_%d", time.Now().UnixNano()) roomID := login.RoomID // 🔒 SECURITY: Prüfe ob dieser Spieler bereits in einem Raum ist if existingRoom, exists := gw.PlayerRooms[playerID]; exists { log.Printf("🚫 SECURITY: Player %s already in room %s, rejecting new connection", playerID, existingRoom) conn.WriteMessage(websocket.TextMessage, []byte(`{"error":"Already connected to a room"}`)) return } // 2. RAUM LOGIK if login.Action == "CREATE" { // Raum ID generieren (4 Zeichen Random) roomID = GenerateRoomCode() // Neuen Raum starten (auf diesem Server) newRoom := NewRoom(roomID, gw.NC, gw.World) gw.LocalRooms[roomID] = newRoom go newRoom.RunLoop() // Spieler lokal hinzufügen (Hack für Demo, sauberer wäre via NATS Event) newRoom.AddPlayer(playerID, login.Name) // 🔒 SECURITY: Spieler in Raum registrieren gw.PlayerRooms[playerID] = roomID } else if login.Action == "JOIN" { // Wir müssen dem Raum (egal wo er läuft) sagen: Hier ist ein Neuer! // Da wir hier keine verteilte DB haben, tricksen wir: // Wir gehen davon aus, dass wir den Raum "finden" müssen. // Für dieses Tutorial: Wir prüfen ob er lokal ist. // Wenn er auf einem anderen Server wäre, bräuchten wir ein "PlayerJoin" Subject. if room, ok := gw.LocalRooms[roomID]; ok { room.AddPlayer(playerID, login.Name) // 🔒 SECURITY: Spieler in Raum registrieren gw.PlayerRooms[playerID] = roomID } else { // Falls Raum nicht lokal: Senden wir ein "JOIN REQUEST" über NATS? // Für jetzt: Wir lassen es simpel. Wenn Raum nicht auf diesem Server -> Error. // (Für echtes Scaling bräuchten wir Redis oder NATS Request/Reply zur Raumsuche) log.Println("❌ Raum nicht gefunden (oder auf anderem Node):", roomID) conn.WriteMessage(websocket.TextMessage, []byte(`{"error":"Room not found"}`)) return } } log.Printf("Player %s (%s) joined Room %s", playerID, login.Name, roomID) // 3. PROXY LOOP // A) NATS -> WebSocket (State Updates empfangen) sub, _ := gw.NC.Subscribe(fmt.Sprintf("game.room.%s.state", roomID), func(m *nats.Msg) { conn.WriteMessage(websocket.TextMessage, m.Data) }) defer sub.Unsubscribe() // B) WebSocket -> NATS (Input senden) for { _, data, err := conn.ReadMessage() if err != nil { break } // Wir parsen kurz, um den Typ zu prüfen, oder leiten blind weiter? // Besser: Wir wrappen es in ClientInput struct var raw map[string]interface{} if err := json.Unmarshal(data, &raw); err != nil { log.Printf("⚠️ Invalid JSON from player %s: %v", playerID, err) continue } inputType, _ := raw["type"].(string) // 🔒 SECURITY: Prüfe ob der Spieler versucht für jemand anderen zu sprechen claimedPlayerID, hasPlayerID := raw["player_id"].(string) claimedRoomID, hasRoomID := raw["room_id"].(string) if hasPlayerID && claimedPlayerID != playerID { log.Printf("🚫 SECURITY BREACH: Player %s tried to send input as %s", playerID, claimedPlayerID) continue // Ignoriere böswilligen Input } if hasRoomID && claimedRoomID != roomID { log.Printf("🚫 SECURITY BREACH: Player %s tried to send input to room %s (is in %s)", playerID, claimedRoomID, roomID) continue // Ignoriere böswilligen Input } // 🔒 SECURITY: Alle Input-Felder übernehmen, aber IDs immer vom Server setzen // Remarshal des raw-Objekts in ClientInput um alle Felder (inkl. STATE-Felder) zu übernehmen inputBytes, _ := json.Marshal(raw) var input game.ClientInput json.Unmarshal(inputBytes, &input) // Security-kritische Felder vom Server überschreiben (nie Client-Werten vertrauen) input.Type = inputType input.RoomID = roomID // Server setzt den Raum input.PlayerID = playerID // Server setzt die Player-ID bytes, _ := json.Marshal(input) gw.NC.Publish(fmt.Sprintf("game.room.%s.input", roomID), bytes) } // Cleanup beim Disconnect log.Printf("✋ Player %s (%s) disconnected from Room %s", playerID, login.Name, roomID) // 🔒 SECURITY: Entferne Spieler aus PlayerRooms Tracking delete(gw.PlayerRooms, playerID) if room, ok := gw.LocalRooms[roomID]; ok { room.RemovePlayer(playerID) // Wenn leer -> Raum löschen? } } func GenerateRoomCode() string { chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" b := make([]byte, 4) for i := range b { b[i] = chars[rand.Intn(len(chars))] } return string(b) }