All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 8m20s
184 lines
5.6 KiB
Go
184 lines
5.6 KiB
Go
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)
|
|
}
|