diff --git a/internal/api/v1.go b/internal/api/v1.go index 9aa97897..a3913e60 100644 --- a/internal/api/v1.go +++ b/internal/api/v1.go @@ -93,6 +93,9 @@ func (handler *V1Handler) getLobbies(writer http.ResponseWriter, _ *http.Request } func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *http.Request) { + // Limit request body size to 10MB to prevent memory exhaustion + request.Body = http.MaxBytesReader(writer, request.Body, 10*1024*1024) + var data game.LobbyRestoreData base64Decoder := base64.NewDecoder(base64.StdEncoding, request.Body) if err := json.NewDecoder(base64Decoder).Decode(&data); err != nil { @@ -102,16 +105,35 @@ func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *ht } lobby := data.Lobby - // We add the lobby, while the lobby mutex is aqcuired. This prevents us + if lobby == nil { + http.Error(writer, "invalid lobby data", http.StatusBadRequest) + return + } + + // Basic validation of lobby data + if lobby.LobbyID == "" { + http.Error(writer, "invalid lobby ID", http.StatusBadRequest) + return + } + + // We add the lobby, while the lobby mutex is acquired. This prevents us // from attempting to connect to the lobby, before the internal state has // been restored correctly. + var resurrected bool lobby.Synchronized(func() { - if state.ResurrectLobby(lobby) { + resurrected = state.ResurrectLobby(lobby) + if resurrected { lobby.WriteObject = WriteObject lobby.WritePreparedMessage = WritePreparedMessage lobby.ResurrectUnsynchronized(&data) } }) + + if resurrected { + writer.WriteHeader(http.StatusNoContent) + } else { + http.Error(writer, "lobby already exists", http.StatusConflict) + } } func (handler *V1Handler) postLobby(writer http.ResponseWriter, request *http.Request) { diff --git a/internal/frontend/lobby.js b/internal/frontend/lobby.js index 596f0954..385d57a9 100644 --- a/internal/frontend/lobby.js +++ b/internal/frontend/lobby.js @@ -57,14 +57,29 @@ function connectToWebsocket() { console.log("Socket Closed Connection: ", event); if (restoreData && event.reason === "lobby_gone") { - console.log("Resurrecting lobby ...",); - fetch('/v1/lobby/resurrect', { + console.log("Resurrecting lobby ..."); + fetch(`${rootPath}/v1/lobby/resurrect`, { method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, body: restoreData, - }).then(() => { - console.log("Attempting to reestablish socket connection after resurrection ..."); - socketIsConnecting = false; - connectToWebsocket(); + }).then(response => { + if (response.ok) { + console.log("Attempting to reestablish socket connection after resurrection ..."); + socketIsConnecting = false; + connectToWebsocket(); + } else { + console.error("Failed to resurrect lobby:", response.statusText); + showTextDialog("connection-error-dialog", + '{{.Translation.Get "error-connecting"}}', + 'Failed to restore lobby. Please try again later.'); + } + }).catch(error => { + console.error("Error resurrecting lobby:", error); + showTextDialog("connection-error-dialog", + '{{.Translation.Get "error-connecting"}}', + 'Failed to restore lobby. Please try again later.'); }); return @@ -1022,7 +1037,6 @@ function registerMessageHandler(targetSocket) { console.log("Shutdown event received"); if (parsed.data) { restoreData = parsed.data; - // FIXMe Text anpassen! showDialog("shutdown-dialog", "Server shutting down", document.createTextNode("Sorry, but the server is about to shut down. Attempting to restore lobby on restart ...")); } else { diff --git a/internal/game/data.go b/internal/game/data.go index 9a84e9aa..cc29f2c7 100644 --- a/internal/game/data.go +++ b/internal/game/data.go @@ -106,7 +106,7 @@ type LobbyRestoreData struct { func (lobby *Lobby) ResurrectUnsynchronized(restoreData *LobbyRestoreData) { lobby.lowercaser = WordlistData[lobby.Wordpack].Lowercaser() - // Since we don't know how long the restart took, we extend all timers.\ + // Since we don't know how long the restart took, we extend all timers. // We add an additional second for good measure. now := time.Now() timeDiff := now.Sub(restoreData.ShutdownTime).Milliseconds() + 1000 @@ -166,7 +166,7 @@ const ( func (lobby *Lobby) GetPlayerByID(id uuid.UUID) *Player { for _, player := range lobby.Players { - if player.ID == player.ID { + if player.ID == id { return player } } diff --git a/internal/game/lobby.go b/internal/game/lobby.go index bf0f0cc8..e87ef1dc 100644 --- a/internal/game/lobby.go +++ b/internal/game/lobby.go @@ -1157,12 +1157,15 @@ func (lobby *Lobby) Shutdown() { // Since broadcast is synchronous, we gotta use the asynchronous queue, to // make sure the message is received before closing. var waitGroup sync.WaitGroup - waitGroup.Add(len(lobby.Players)) for _, player := range lobby.Players { - player.ws.Async(func() { - defer waitGroup.Done() - player.ws.WriteClose(1012, []byte("server_restart")) - }) + if player.ws != nil { + waitGroup.Add(1) + ws := player.ws + ws.Async(func() { + defer waitGroup.Done() + ws.WriteClose(1012, []byte("server_restart")) + }) + } } waitGroup.Wait() } diff --git a/internal/state/lobbies.go b/internal/state/lobbies.go index 6c6b1e0a..ae2daa20 100644 --- a/internal/state/lobbies.go +++ b/internal/state/lobbies.go @@ -72,8 +72,8 @@ func addLobby(lobby *game.Lobby) { } func ResurrectLobby(lobby *game.Lobby) bool { - globalStateMutex.RLock() - defer globalStateMutex.RUnlock() + globalStateMutex.Lock() + defer globalStateMutex.Unlock() existingLobby := getLobby(lobby.LobbyID) if existingLobby == nil {