From 58002a1b380988ff0320aedd84c21273b49fc549 Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 11 May 2024 22:57:47 +0200 Subject: [PATCH 1/4] Groundwork for handler that allows reconnecting --- api/handler/handler.go | 6 ++++++ api/websocket/connection.go | 4 ++++ main.go | 3 +++ 3 files changed, 13 insertions(+) diff --git a/api/handler/handler.go b/api/handler/handler.go index 6615f86..6c661e0 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -126,3 +126,9 @@ func JoinPrivateGame(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.IndentedJSON(http.StatusOK, info) } + +func JoinGame(c *gin.Context) { + gameID := c.Param("id") + log.Println(gameID) + c.JSON(http.StatusOK, gameID) +} diff --git a/api/websocket/connection.go b/api/websocket/connection.go index be29d8c..464303c 100644 --- a/api/websocket/connection.go +++ b/api/websocket/connection.go @@ -70,3 +70,7 @@ func waitForAndHandlePlayerID(ctx context.Context, conn *gorillaws.Conn) { lobby.Game.SetWebsocketConnectionFor(ctx, player, conn) log.Println("player after setting connection: ", player) } + +func ConnectWsForGame(c *gin.Context) { + +} diff --git a/main.go b/main.go index 9cee4de..3d5837c 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,10 @@ func main() { router.GET("/api/hostPrivate", handler.HostPrivateGameHandler) router.POST("/api/joinPrivate", handler.JoinPrivateGame) router.GET("/api/ws", websocket.RegisterWebSocketConnection) + router.GET("/api/getLobbyForPassphrase/:phrase", handler.GetLobbyForPassphraseHandler) + router.GET("/api/registerWsForGame/:id", websocket.ConnectWsForGame) + router.POST("/api/joinGame/:id", handler.JoinGame) if debugMode { log.Println("Starting service WITHOUT TLS") From fc088a04fe47c9bae7404ef2f0e03713ba2f0fc7 Mon Sep 17 00:00:00 2001 From: Marco Date: Sun, 12 May 2024 15:36:30 +0200 Subject: [PATCH 2/4] More work for introducing a new game handler, also: ratelimiter --- api/handler/handler.go | 68 +++++++++++++++++++++---------------- api/handler/handler_test.go | 2 +- api/websocket/connection.go | 2 +- go.mod | 3 ++ go.sum | 4 +++ lobby_registry/lobby.go | 58 ------------------------------- lobby_registry/registry.go | 61 --------------------------------- main.go | 5 ++- usher/usher.go | 45 ------------------------ 9 files changed, 49 insertions(+), 199 deletions(-) delete mode 100644 lobby_registry/lobby.go delete mode 100644 lobby_registry/registry.go delete mode 100644 usher/usher.go diff --git a/api/handler/handler.go b/api/handler/handler.go index 6c661e0..b7382a1 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -1,24 +1,26 @@ package handler import ( - "log" "mchess_server/api" "mchess_server/chess" - lobbies "mchess_server/lobby_registry" - "mchess_server/usher" + "mchess_server/lobbies" "mchess_server/utils" "net/http" "sync" "github.com/gin-gonic/gin" "github.com/google/uuid" + "go.uber.org/ratelimit" ) var mut sync.Mutex +var limiter = ratelimit.New(10) + +func HostGameHandler(c *gin.Context) { + limiter.Take() -func HostPrivateGameHandler(c *gin.Context) { player := chess.NewPlayer(uuid.New()) - u := usher.GetUsher() + u := lobbies.GetUsher() mut.Lock() defer mut.Unlock() @@ -36,6 +38,8 @@ func HostPrivateGameHandler(c *gin.Context) { } func GetLobbyForPassphraseHandler(c *gin.Context) { + limiter.Take() + reqPassphrase := c.Param("phrase") if reqPassphrase == "" { c.IndentedJSON(http.StatusBadRequest, reqPassphrase) @@ -57,34 +61,17 @@ func GetLobbyForPassphraseHandler(c *gin.Context) { c.IndentedJSON(http.StatusOK, lobbyInfo) } -func RegisterForRandomGame(c *gin.Context) { - player := chess.NewPlayer(uuid.New()) - usher := usher.GetUsher() - - mut.Lock() - defer mut.Unlock() - lobby := usher.WelcomeNewPlayer(player) - usher.AddPlayerToLobbyAndStartGameIfFull(player, lobby) - - info := api.PlayerInfo{ - PlayerID: &player.Uuid, - LobbyID: &lobby.Uuid, - } - log.Println("responding with info ", info) - - c.Header("Access-Control-Allow-Origin", "*") - c.IndentedJSON(http.StatusOK, info) -} - +// TODO: this will be replaced by the JoinGameHandler() func JoinPrivateGame(c *gin.Context) { + limiter.Take() + req := api.PlayerInfo{} - log.Println(c.Request.Body) err := c.ShouldBindJSON(&req) if err != nil || req.Passphrase == nil || *req.Passphrase == "" { c.IndentedJSON(http.StatusNotFound, req) } - u := usher.GetUsher() + u := lobbies.GetUsher() if req.Passphrase != nil && *req.Passphrase != "" && @@ -127,8 +114,29 @@ func JoinPrivateGame(c *gin.Context) { c.IndentedJSON(http.StatusOK, info) } -func JoinGame(c *gin.Context) { - gameID := c.Param("id") - log.Println(gameID) - c.JSON(http.StatusOK, gameID) +func JoinGameHandler(c *gin.Context) { + limiter.Take() + + id := c.Param("id") + idAsUUID, err := uuid.Parse(id) + if err != nil { + c.IndentedJSON(http.StatusBadRequest, nil) + return + } + + passphrase := api.Passphrase{} + c.ShouldBindJSON(&passphrase) + + u := lobbies.GetUsher() + lobby := u.GetLobbyByID(idAsUUID) + if lobby == nil { + c.IndentedJSON(http.StatusNotFound, nil) + return + } + + lobbyInfo := api.LobbyInfo{ + ID: &lobby.Uuid, + } + + c.IndentedJSON(http.StatusOK, lobbyInfo) } diff --git a/api/handler/handler_test.go b/api/handler/handler_test.go index 8e69445..ff5fead 100644 --- a/api/handler/handler_test.go +++ b/api/handler/handler_test.go @@ -20,7 +20,7 @@ func Test_GetLobbyFromPassphraseHandler(t *testing.T) { t.Run("host a lobby", func(t *testing.T) { r1 := httptest.NewRecorder() ctx1, e1 := gin.CreateTestContext(r1) - e1.GET("/api/hostPrivate", HostPrivateGameHandler) + e1.GET("/api/hostPrivate", HostGameHandler) hostGameRequest, _ := http.NewRequest("GET", "/api/hostPrivate", nil) ctx1.Request = hostGameRequest diff --git a/api/websocket/connection.go b/api/websocket/connection.go index 464303c..1f18929 100644 --- a/api/websocket/connection.go +++ b/api/websocket/connection.go @@ -8,7 +8,7 @@ import ( "mchess_server/api" "net/http" - lobbies "mchess_server/lobby_registry" + "mchess_server/lobbies" "github.com/gin-gonic/gin" gorillaws "github.com/gorilla/websocket" diff --git a/go.mod b/go.mod index 38fda34..3f129d6 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/stretchr/testify v1.9.0 ) +require github.com/benbjohnson/clock v1.3.0 // indirect + require ( github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect @@ -32,6 +34,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/ratelimit v0.3.1 golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect diff --git a/go.sum b/go.sum index 51dc3b8..d95f318 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -69,6 +71,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= +go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= diff --git a/lobby_registry/lobby.go b/lobby_registry/lobby.go deleted file mode 100644 index d900002..0000000 --- a/lobby_registry/lobby.go +++ /dev/null @@ -1,58 +0,0 @@ -package lobby_registry - -import ( - "mchess_server/chess" - "mchess_server/utils" - - "github.com/google/uuid" -) - -type Lobby struct { - Uuid uuid.UUID - Game *chess.Game - PlayerJoined chan bool - Passphrase utils.Passphrase -} - -func NewEmptyLobbyWithUUID(uuid uuid.UUID) *Lobby { - return &Lobby{ - Uuid: uuid, - Game: chess.NewGame(), - PlayerJoined: make(chan bool), - } -} - -func newEmptyLobbyWithPassphrase() *Lobby { - lobby := NewEmptyLobbyWithUUID(uuid.New()) - lobby.Passphrase = utils.NewPassphrase() - - return lobby -} - -func (l *Lobby) AddPlayerAndStartGameIfFull(player *chess.Player) { - l.Game.AddPlayersToGame(player) - if l.IsFull() { - l.Game.StartHandling() - } -} - -func (w *Lobby) IsFull() bool { - return len(w.Game.GetPlayers()) == 2 -} - -func (l *Lobby) GetPlayerByUUID(uuid uuid.UUID) (*chess.Player, bool) { - for _, player := range l.Game.GetPlayers() { - if player.Uuid == uuid { - return player, true - } - } - return nil, false -} - -func (l *Lobby) GetPlayer1() *chess.Player { - return l.Game.GetPlayer1() -} - -func (l *Lobby) GetPlayer2() *chess.Player { - return l.Game.GetPlayer2() -} diff --git a/lobby_registry/registry.go b/lobby_registry/registry.go deleted file mode 100644 index 1412462..0000000 --- a/lobby_registry/registry.go +++ /dev/null @@ -1,61 +0,0 @@ -package lobby_registry - -import ( - "mchess_server/utils" - - "github.com/google/uuid" -) - -type LobbyRegistry struct { - lobbies map[uuid.UUID]*Lobby -} - -var instance *LobbyRegistry - -func GetLobbyRegistry() *LobbyRegistry { - if instance == nil { - instance = newLobbyRegistry() - } - - return instance -} - -func newLobbyRegistry() *LobbyRegistry { - return &LobbyRegistry{lobbies: make(map[uuid.UUID]*Lobby)} -} - -func (r *LobbyRegistry) CreateNewPrivateLobby() *Lobby { - lobby := newEmptyLobbyWithPassphrase() - r.addNewLobby(lobby) - return lobby -} - -func (r *LobbyRegistry) GetLobbyForPlayer() *Lobby { - for _, lobby := range r.lobbies { - if !lobby.IsFull() { - return lobby - } - } - - newLobby := NewEmptyLobbyWithUUID(uuid.New()) - r.addNewLobby(newLobby) - return newLobby -} - -func (r *LobbyRegistry) GetLobbyByUUID(uuid uuid.UUID) *Lobby { - return r.lobbies[uuid] -} - -func (r *LobbyRegistry) GetLobbyByPassphrase(p utils.Passphrase) *Lobby { - for _, lobby := range r.lobbies { - if lobby.Passphrase == p { - return lobby - } - } - return nil -} - -func (r *LobbyRegistry) addNewLobby(lobby *Lobby) uuid.UUID { - r.lobbies[lobby.Uuid] = lobby - return lobby.Uuid -} diff --git a/main.go b/main.go index 3d5837c..ab54f4a 100644 --- a/main.go +++ b/main.go @@ -24,14 +24,13 @@ func main() { } router := gin.Default() - router.GET("/api/random", handler.RegisterForRandomGame) - router.GET("/api/hostPrivate", handler.HostPrivateGameHandler) + router.GET("/api/hostPrivate", handler.HostGameHandler) router.POST("/api/joinPrivate", handler.JoinPrivateGame) router.GET("/api/ws", websocket.RegisterWebSocketConnection) router.GET("/api/getLobbyForPassphrase/:phrase", handler.GetLobbyForPassphraseHandler) router.GET("/api/registerWsForGame/:id", websocket.ConnectWsForGame) - router.POST("/api/joinGame/:id", handler.JoinGame) + router.POST("/api/joinGame/:id", handler.JoinGameHandler) if debugMode { log.Println("Starting service WITHOUT TLS") diff --git a/usher/usher.go b/usher/usher.go deleted file mode 100644 index cfbbd51..0000000 --- a/usher/usher.go +++ /dev/null @@ -1,45 +0,0 @@ -package usher - -import ( - "mchess_server/chess" - lobbies "mchess_server/lobby_registry" - "mchess_server/utils" -) - -type Usher struct { -} - -var instance *Usher - -func newUsher() *Usher { - return &Usher{} -} - -func GetUsher() *Usher { - if instance == nil { - instance = newUsher() - } - return instance -} - -func (u *Usher) WelcomeNewPlayer(player *chess.Player) *lobbies.Lobby { - lobby := lobbies.GetLobbyRegistry().GetLobbyForPlayer() - return lobby -} - -func (u *Usher) CreateNewPrivateLobby(player *chess.Player) *lobbies.Lobby { - lobby := lobbies.GetLobbyRegistry().CreateNewPrivateLobby() - return lobby -} - -func (u *Usher) FindExistingPrivateLobby(p utils.Passphrase) *lobbies.Lobby { - lobby := lobbies.GetLobbyRegistry().GetLobbyByPassphrase(p) - if lobby == nil || lobby.IsFull() { - return nil - } - return lobby -} - -func (u *Usher) AddPlayerToLobbyAndStartGameIfFull(player *chess.Player, lobby *lobbies.Lobby) { - lobby.AddPlayerAndStartGameIfFull(player) -} From 4207a0ed724c13ccaf39c397d5cdf584ada08392 Mon Sep 17 00:00:00 2001 From: Marco Date: Sun, 12 May 2024 15:37:53 +0200 Subject: [PATCH 3/4] forgot files --- lobbies/lobby.go | 58 ++++++++++++++++++++++++++++++++++++++++++ lobbies/registry.go | 61 +++++++++++++++++++++++++++++++++++++++++++++ lobbies/usher.go | 50 +++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 lobbies/lobby.go create mode 100644 lobbies/registry.go create mode 100644 lobbies/usher.go diff --git a/lobbies/lobby.go b/lobbies/lobby.go new file mode 100644 index 0000000..d10285c --- /dev/null +++ b/lobbies/lobby.go @@ -0,0 +1,58 @@ +package lobbies + +import ( + "mchess_server/chess" + "mchess_server/utils" + + "github.com/google/uuid" +) + +type Lobby struct { + Uuid uuid.UUID + Game *chess.Game + PlayerJoined chan bool + Passphrase utils.Passphrase +} + +func NewEmptyLobbyWithUUID(uuid uuid.UUID) *Lobby { + return &Lobby{ + Uuid: uuid, + Game: chess.NewGame(), + PlayerJoined: make(chan bool), + } +} + +func newEmptyLobbyWithPassphrase() *Lobby { + lobby := NewEmptyLobbyWithUUID(uuid.New()) + lobby.Passphrase = utils.NewPassphrase() + + return lobby +} + +func (l *Lobby) AddPlayerAndStartGameIfFull(player *chess.Player) { + l.Game.AddPlayersToGame(player) + if l.IsFull() { + l.Game.StartHandling() + } +} + +func (w *Lobby) IsFull() bool { + return len(w.Game.GetPlayers()) == 2 +} + +func (l *Lobby) GetPlayerByUUID(uuid uuid.UUID) (*chess.Player, bool) { + for _, player := range l.Game.GetPlayers() { + if player.Uuid == uuid { + return player, true + } + } + return nil, false +} + +func (l *Lobby) GetPlayer1() *chess.Player { + return l.Game.GetPlayer1() +} + +func (l *Lobby) GetPlayer2() *chess.Player { + return l.Game.GetPlayer2() +} diff --git a/lobbies/registry.go b/lobbies/registry.go new file mode 100644 index 0000000..56d11dc --- /dev/null +++ b/lobbies/registry.go @@ -0,0 +1,61 @@ +package lobbies + +import ( + "mchess_server/utils" + + "github.com/google/uuid" +) + +type LobbyRegistry struct { + lobbies map[uuid.UUID]*Lobby +} + +var instance *LobbyRegistry + +func GetLobbyRegistry() *LobbyRegistry { + if instance == nil { + instance = newLobbyRegistry() + } + + return instance +} + +func newLobbyRegistry() *LobbyRegistry { + return &LobbyRegistry{lobbies: make(map[uuid.UUID]*Lobby)} +} + +func (r *LobbyRegistry) CreateNewPrivateLobby() *Lobby { + lobby := newEmptyLobbyWithPassphrase() + r.addNewLobby(lobby) + return lobby +} + +func (r *LobbyRegistry) GetLobbyForPlayer() *Lobby { + for _, lobby := range r.lobbies { + if !lobby.IsFull() { + return lobby + } + } + + newLobby := NewEmptyLobbyWithUUID(uuid.New()) + r.addNewLobby(newLobby) + return newLobby +} + +func (r *LobbyRegistry) GetLobbyByUUID(uuid uuid.UUID) *Lobby { + return r.lobbies[uuid] +} + +func (r *LobbyRegistry) GetLobbyByPassphrase(p utils.Passphrase) *Lobby { + for _, lobby := range r.lobbies { + if lobby.Passphrase == p { + return lobby + } + } + return nil +} + +func (r *LobbyRegistry) addNewLobby(lobby *Lobby) uuid.UUID { + r.lobbies[lobby.Uuid] = lobby + return lobby.Uuid +} diff --git a/lobbies/usher.go b/lobbies/usher.go new file mode 100644 index 0000000..38a0574 --- /dev/null +++ b/lobbies/usher.go @@ -0,0 +1,50 @@ +package lobbies + +import ( + "mchess_server/chess" + "mchess_server/utils" + + "github.com/google/uuid" +) + +type Usher struct { +} + +var usherInstance *Usher + +func newUsher() *Usher { + return &Usher{} +} + +func GetUsher() *Usher { + if usherInstance == nil { + usherInstance = newUsher() + } + return usherInstance +} + +func (u *Usher) WelcomeNewPlayer(player *chess.Player) *Lobby { + lobby := GetLobbyRegistry().GetLobbyForPlayer() + return lobby +} + +func (u *Usher) CreateNewPrivateLobby(player *chess.Player) *Lobby { + lobby := GetLobbyRegistry().CreateNewPrivateLobby() + return lobby +} + +func (u *Usher) FindExistingPrivateLobby(p utils.Passphrase) *Lobby { + lobby := GetLobbyRegistry().GetLobbyByPassphrase(p) + if lobby == nil || lobby.IsFull() { + return nil + } + return lobby +} + +func (*Usher) GetLobbyByID(id uuid.UUID) *Lobby { + return GetLobbyRegistry().GetLobbyByUUID(id) +} + +func (u *Usher) AddPlayerToLobbyAndStartGameIfFull(player *chess.Player, lobby *Lobby) { + lobby.AddPlayerAndStartGameIfFull(player) +} From 97ad45e505c696ab2bd34ba748c33efdbcabd540 Mon Sep 17 00:00:00 2001 From: Marco Date: Sun, 12 May 2024 15:42:40 +0200 Subject: [PATCH 4/4] Rate limit websocket connection --- api/{websocket/connection.go => handler/websocket.go} | 7 ++++++- main.go | 5 ++--- 2 files changed, 8 insertions(+), 4 deletions(-) rename api/{websocket/connection.go => handler/websocket.go} (96%) diff --git a/api/websocket/connection.go b/api/handler/websocket.go similarity index 96% rename from api/websocket/connection.go rename to api/handler/websocket.go index 1f18929..5ca8d46 100644 --- a/api/websocket/connection.go +++ b/api/handler/websocket.go @@ -1,4 +1,4 @@ -package websocket +package handler import ( "context" @@ -21,6 +21,8 @@ var upgrader = gorillaws.Upgrader{ } func RegisterWebSocketConnection(c *gin.Context) { + limiter.Take() + log.Println(c.Request) conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { @@ -31,6 +33,8 @@ func RegisterWebSocketConnection(c *gin.Context) { } func waitForAndHandlePlayerID(ctx context.Context, conn *gorillaws.Conn) { + limiter.Take() + msgType, msg, err := conn.ReadMessage() if err != nil { errorMessage := fmt.Sprintf("Reading from websocket connection did not work: %s", err) @@ -72,5 +76,6 @@ func waitForAndHandlePlayerID(ctx context.Context, conn *gorillaws.Conn) { } func ConnectWsForGame(c *gin.Context) { + limiter.Take() } diff --git a/main.go b/main.go index ab54f4a..87ebd93 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "flag" "log" "mchess_server/api/handler" - "mchess_server/api/websocket" "github.com/gin-gonic/gin" ) @@ -26,10 +25,10 @@ func main() { router := gin.Default() router.GET("/api/hostPrivate", handler.HostGameHandler) router.POST("/api/joinPrivate", handler.JoinPrivateGame) - router.GET("/api/ws", websocket.RegisterWebSocketConnection) + router.GET("/api/ws", handler.RegisterWebSocketConnection) router.GET("/api/getLobbyForPassphrase/:phrase", handler.GetLobbyForPassphraseHandler) - router.GET("/api/registerWsForGame/:id", websocket.ConnectWsForGame) + router.GET("/api/registerWsForGame/:id", handler.ConnectWsForGame) router.POST("/api/joinGame/:id", handler.JoinGameHandler) if debugMode {