Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Admin struct {
players *Players
sockets *Sockets
game *Game
lifecycle *Lifecycle
connections map[adminSocketConnection]struct{}

password string
Expand All @@ -39,9 +40,10 @@ type Admin struct {
socketMutex sync.Mutex
}

func (a *Admin) Init(players *Players, sockets *Sockets, password string, games ...*Game) {
func (a *Admin) Init(players *Players, sockets *Sockets, password string, lifecycle *Lifecycle, games ...*Game) {
a.players = players
a.sockets = sockets
a.lifecycle = lifecycle
a.password = password
a.cookieValue = base64.RawURLEncoding.EncodeToString([]byte(password))
a.registered = false
Expand Down
21 changes: 21 additions & 0 deletions api/admin_socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"encoding/json"
"net/http"
"time"

ws "github.com/gorilla/websocket"
)
Expand Down Expand Up @@ -44,6 +45,13 @@ func (a *Admin) ServeSocket(w http.ResponseWriter, r *http.Request) {
if err != nil {
return
}
if a.lifecycle != nil {
if !a.lifecycle.Track(connection) {
_ = connection.Close()
return
}
defer a.lifecycle.Untrack(connection)
}
if !a.addConnection(connection) {
return
}
Expand Down Expand Up @@ -127,6 +135,7 @@ func (a *Admin) broadcastSocketMessage(message AdminSocketMessage) {
a.socketMutex.Lock()
defer a.socketMutex.Unlock()
for connection := range a.connections {
setAdminWriteDeadline(connection)
if err := connection.WriteMessage(ws.TextMessage, JSON); err != nil {
delete(a.connections, connection)
_ = connection.Close()
Expand All @@ -139,5 +148,17 @@ func writeAdminSocketMessage(connection adminSocketConnection, message AdminSock
if err != nil {
return false
}
setAdminWriteDeadline(connection)
return connection.WriteMessage(ws.TextMessage, JSON) == nil
}

// setAdminWriteDeadline applies the normal write timeout without holding the
// registry mutex during I/O. Connections without a deadline API keep the
// previous behavior for test doubles.
func setAdminWriteDeadline(connection adminSocketConnection) {
if deadlineWriter, ok := connection.(interface {
SetWriteDeadline(time.Time) error
}); ok {
_ = deadlineWriter.SetWriteDeadline(time.Now().Add(socketWriteTimeout))
}
}
72 changes: 64 additions & 8 deletions api/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ func newAdminTestState(t *testing.T, password string) (*Players, *Admin) {
sockets := new(Sockets)
admin := new(Admin)
players.Init()
sockets.Init(players)
admin.Init(players, sockets, password)
sockets.Init(players, nil)
admin.Init(players, sockets, password, nil)
return players, admin
}

Expand Down Expand Up @@ -145,9 +145,9 @@ func TestAdminResetPreservesLeadersAndClearsFlag(t *testing.T) {
players.Init()
game := new(Game)
sockets := new(Sockets)
sockets.Init(players, game)
sockets.Init(players, nil, game)
admin := new(Admin)
admin.Init(players, sockets, "top-secret", game)
admin.Init(players, sockets, "top-secret", nil, game)
cookie := registerTestAdmin(t, admin, "top-secret")
for _, playerType := range []PlayerType{TypeLeader, TypeAntiPacLeader, TypeFlagLeader} {
players.New(playerType, TypeString(playerType), StatusDisc)
Expand All @@ -174,14 +174,70 @@ func TestAdminResetPreservesLeadersAndClearsFlag(t *testing.T) {
}
}

func TestAdminResetPreservesConnectedSessionsAndLeaderAuthorization(t *testing.T) {
players := new(Players)
players.Init()
game := new(Game)
sockets := new(Sockets)
sockets.Init(players, nil, game)
admin := new(Admin)
admin.Init(players, sockets, "top-secret", nil, game)
cookie := registerTestAdmin(t, admin, "top-secret")

leaderID := players.New(TypeAntiPacLeader, "Leader", StatusDisc)
activeID := players.New(TypePacman, "Active", StatusDisc)
leaderConnection := newTestConnection(leaderID)
activeConnection := newTestConnection(activeID)
sockets.hub.registerConnection(leaderConnection)
sockets.hub.registerConnection(activeConnection)
drainTestMessages(leaderConnection)
drainTestMessages(activeConnection)

request := httptest.NewRequest(http.MethodPost, "/api/admin/reset", nil)
request.AddCookie(cookie)
response := httptest.NewRecorder()
admin.ServeHTTP(response, request)
if response.Code != http.StatusNoContent {
t.Fatalf("reset status = %d, want 204", response.Code)
}

if len(players.players) != 2 {
t.Errorf("player count after reset = %d, want 2", len(players.players))
}
if leader := players.Get(leaderID); leader == nil || leader.Type != TypeAntiPacLeader {
t.Errorf("leader after reset = %#v, want AntiPac Leader", leader)
}
if active := players.Get(activeID); active == nil || active.Type != TypeGhost {
t.Errorf("active player after reset = %#v, want Ghost", active)
}
if !sockets.hub.hasConnectionForID(leaderID) || !sockets.hub.hasConnectionForID(activeID) {
t.Error("admin reset replaced a connected player session")
}
if leader, _, authorized := players.LeaderState(leaderID); !authorized || leader.ID != leaderID || leader.Type != TypeAntiPacLeader {
t.Errorf("leader authorization after reset = %#v, authorized %v", leader, authorized)
}

leaderUpdate := informPlayer(t, receiveTestMessage(t, leaderConnection))
if leaderUpdate.ID != activeID || leaderUpdate.Type != TypeGhost {
t.Errorf("leader's active-player reset update = %#v, want Ghost for %q", leaderUpdate, activeID)
}
updated := informPlayer(t, receiveTestMessage(t, activeConnection))
if updated.ID != activeID || updated.Type != TypeGhost {
t.Errorf("active reset update = %#v, want Ghost for %q", updated, activeID)
}
if len(leaderConnection.send) != 0 || len(activeConnection.send) != 0 {
t.Errorf("unexpected extra reset messages: leader=%d active=%d", len(leaderConnection.send), len(activeConnection.send))
}
}

func TestAdminFlagUpdatesSharedStateAndSocketClients(t *testing.T) {
players := new(Players)
players.Init()
game := new(Game)
sockets := new(Sockets)
sockets.Init(players, game)
sockets.Init(players, nil, game)
admin := new(Admin)
admin.Init(players, sockets, "top-secret", game)
admin.Init(players, sockets, "top-secret", nil, game)
cookie := registerTestAdmin(t, admin, "top-secret")
connection := new(recordingAdminConnection)
if !admin.addConnection(connection) {
Expand Down Expand Up @@ -242,9 +298,9 @@ func TestAdminResetClearsOfflineLocationsButPreservesActiveCoordinates(t *testin
players.Init()
game := new(Game)
sockets := new(Sockets)
sockets.Init(players, game)
sockets.Init(players, nil, game)
admin := new(Admin)
admin.Init(players, sockets, "top-secret", game)
admin.Init(players, sockets, "top-secret", nil, game)
cookie := registerTestAdmin(t, admin, "top-secret")

activeID := players.New(TypeLeader, "Active", StatusDisc)
Expand Down
4 changes: 4 additions & 0 deletions api/etc.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const (
CMD_INFORM = "inform" // inform another player change/connection
CMD_REMOVE = "remove" // remove a player marker without disclosing a location
CMD_STATE = "state" // inform clients of shared game state
// CMD_SHUTDOWN is the legacy JSON shutdown command. New servers notify
// shutdown with a 1001 Going Away close frame; clients still accept this
// command for compatibility.
CMD_SHUTDOWN = "shutdown" // inform clients of server shutdown

// player type
TypeHidden PlayerType = 0
Expand Down
3 changes: 3 additions & 0 deletions api/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ func (h *Hub) enqueue(connection *Conn, message []byte) bool {
case connection.send <- message:
return true
default:
// Channel buffer is full. Client cannot keep up with real-time updates.
return false
}
}
Expand Down Expand Up @@ -420,3 +421,5 @@ func (h *Hub) clearOfflineLocations() {
h.broadcastRemove(playerID, onlyViewers, nil)
}
}


17 changes: 13 additions & 4 deletions api/leader.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,20 @@ type leaderSocketConnection interface {
}

type Leader struct {
players *Players
game *Game
sockets *Sockets
players *Players
game *Game
sockets *Sockets
lifecycle *Lifecycle

connections map[leaderSocketConnection]PlayerID
socketMutex sync.Mutex
}

func (l *Leader) Init(players *Players, game *Game, sockets *Sockets) {
func (l *Leader) Init(players *Players, game *Game, sockets *Sockets, lifecycle *Lifecycle) {
l.players = players
l.game = game
l.sockets = sockets
l.lifecycle = lifecycle
l.connections = make(map[leaderSocketConnection]PlayerID)
players.AddObserver(l.BroadcastPlayer)
players.AddRemovalObserver(l.BroadcastRemoval)
Expand Down Expand Up @@ -230,6 +232,13 @@ func (l *Leader) ServeSocket(w http.ResponseWriter, r *http.Request) {
if err != nil {
return
}
if l.lifecycle != nil {
if !l.lifecycle.Track(connection) {
_ = connection.Close()
return
}
defer l.lifecycle.Untrack(connection)
}
if !l.addConnection(connection, state.Leader.ID) {
return
}
Expand Down
4 changes: 2 additions & 2 deletions api/leader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ func newLeaderTestState() (*Players, *Game, *Sockets, *Leader) {
players.Init()
game := new(Game)
sockets := new(Sockets)
sockets.Init(players, game)
sockets.Init(players, nil, game)
leader := new(Leader)
leader.Init(players, game, sockets)
leader.Init(players, game, sockets, nil)
return players, game, sockets, leader
}

Expand Down
Loading