Skip to content
This repository was archived by the owner on Dec 27, 2022. It is now read-only.
Draft
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
2 changes: 1 addition & 1 deletion store/store.go → gsistore/store.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package store
package gsistore

import (
"reflect"
Expand Down
3 changes: 1 addition & 2 deletions store/store_test.go → gsistore/store_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package store
package gsistore

import (
"testing"
Expand Down Expand Up @@ -66,7 +66,6 @@ func TestChannelStoreClose(t *testing.T) {

func assertChannel(t *testing.T, channel chan *model.GameState, hasElement, hasMore bool) {
element, more := <-channel

if hasElement {
assert.NotNil(t, element)
} else {
Expand Down
17 changes: 12 additions & 5 deletions model/gamestate.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package model

type GameState struct {
Auth *AuthState `json:"auth"`
Map *MapState `json:"map"`
Player *PlayerState `json:"player"`
Provider *ProviderState `json:"provider"`
Auth *AuthState `json:"auth"`
Map *MapState `json:"map"`
Player *PlayerState `json:"player"`
Provider *ProviderState `json:"provider"`
PreviousState *GameState `json:"previously"`
}

type AuthState struct {
Expand All @@ -20,7 +21,9 @@ type ProviderState struct {
}

type MapState struct {
Name string `json:"name"`
Name string `json:"name"`
TeamCT *TeamState `json:"team_ct"`
TeamT *TeamState `json:"team_t"`
}

type PlayerState struct {
Expand All @@ -37,3 +40,7 @@ type MatchStats struct {
Mvps int `json:"mvps"`
Score int `json:"score"`
}

type TeamState struct {
Timeouts *int `json:"timeouts_remaining"`
}
73 changes: 73 additions & 0 deletions model/serverstate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package model

// Data structure sent by server, it is structured this way to economize bandwidth

type ServerState struct {
ServerInfo ServerInfo `json:"serverInfo"`
PlayerInfo []PlayerInfo `json:"playerInfo"`
}

type ServerInfo struct {
TimeStamp int `json:"timestamp"`
ServerName string `json:"servername"`
MapName string `json:"mapname"`
TimeoutsCTPrev int `json:"timeoutsCTprev"`
TimeoutsTPrev int `json:"timeoutsTprev"`
TimeoutsCT int `json:"timeoutsCT"`
TimeoutsT int `json:"timeoutsT"`
Global int `json:"global"`
}

type PlayerInfo struct {
AuthKey string `json:"authkey"`
SteamId int64 `json:"steamid,string"`
Clan string `json:"clan"` // SteamID, clan and name already exists in the gsi data, do we need to send this again?
Name string `json:"name"`
TimeInServer float64 `json:"timeinserver"` // Need a better name
KZData KZData `json:"KZData"`
}

type KZData struct {
Global bool `json:"global"`
Course int `json:"course"`
Time float64 `json:"time"`
Checkpoints int `json:"checkpoints"`
Teleports int `json:"teleports"`
}

// Stored data structure that will be communicated to bot
type FullPlayerInfo struct {
TimeStamp int `json:"timestamp"`
AuthKey string `json:"authkey"`
TimeoutsCTPrev int `json:"timeoutsCTprev"`
TimeoutsTPrev int `json:"timeoutsTprev"`
TimeoutsCT int `json:"timeoutsCT"`
TimeoutsT int `json:"timeoutsT"`
ServerName string `json:"servername"`
MapName string `json:"mapname"`
ServerGlobal int `json:"serverglobal"`
SteamId int64 `json:"steamid,string"`
Clan string `json:"clan"`
Name string `json:"name"`
TimeInServer float64 `json:"timeinserver"` // Need a better name
KZData KZData `json:"KZData"`
}

func New(sInfo *ServerInfo, pInfo *PlayerInfo) *FullPlayerInfo {
return &FullPlayerInfo{
sInfo.TimeStamp,
pInfo.AuthKey,
sInfo.TimeoutsCTPrev,
sInfo.TimeoutsTPrev,
sInfo.TimeoutsCT,
sInfo.TimeoutsT,
sInfo.ServerName,
sInfo.MapName,
sInfo.Global,
pInfo.SteamId,
pInfo.Clan,
pInfo.Name,
pInfo.TimeInServer,
pInfo.KZData,
}
}
109 changes: 94 additions & 15 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (

"github.com/gorilla/mux"
"github.com/gorilla/websocket"

"gitlab.com/prestrafe/prestrafe-gsi/gsistore"
"gitlab.com/prestrafe/prestrafe-gsi/model"
"gitlab.com/prestrafe/prestrafe-gsi/store"
"gitlab.com/prestrafe/prestrafe-gsi/smstore"
)

// Defines the public API for the Game State Integration server. The server acts as a rely between the CSGO GSI API,
Expand All @@ -34,7 +34,8 @@ type server struct {
port int
filter TokenFilter
logger *log.Logger
store store.Store
gsiStore gsistore.Store
smStore smstore.Store
httpServer *http.Server
upgrader *websocket.Upgrader
}
Expand All @@ -47,7 +48,8 @@ func New(addr string, port, ttl int, filter TokenFilter) Server {
port,
filter,
log.New(os.Stdout, "GSI-Server > ", log.LstdFlags),
store.New(time.Duration(ttl) * time.Second),
gsistore.New(time.Duration(ttl) * time.Second),
smstore.New(time.Duration(ttl) * time.Second),
nil,
nil,
}
Expand All @@ -61,9 +63,15 @@ func (s *server) Start() error {
// router.Path("/").Methods("GET").HandlerFunc(s.handleGet)
// router.Path("/").Methods("POST").HandlerFunc(s.handlePost)

router.Path("/get").Methods("GET").HandlerFunc(s.handleGet)
router.Path("/update").Methods("POST").HandlerFunc(s.handlePost)
// GSI Handlers
router.Path("/gsi/get").Methods("GET").HandlerFunc(s.handleGSIGet)
router.Path("/gsi/update").Methods("POST").HandlerFunc(s.handleGSIPost)

router.Path("/websocket").Methods("GET").HandlerFunc(s.handleWebsocket)

// SM Handlers
router.Path("/sm/update").Methods("POST").HandlerFunc(s.handleServerPost)
router.Path("/sm/get").Methods("GET").HandlerFunc(s.handleServerGet)
router.NotFoundHandler = http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
s.logger.Printf("Unmatched request: %s %s\n", request.Method, request.URL)
writer.WriteHeader(http.StatusNotFound)
Expand Down Expand Up @@ -91,11 +99,12 @@ func (s *server) Start() error {
func (s *server) Stop() error {
s.logger.Printf("Stopping GSI server on %s:%d\n", s.addr, s.port)

s.store.Close()
s.gsiStore.Close()
s.smStore.Close()
return s.httpServer.Shutdown(context.Background())
}

func (s *server) handleGet(writer http.ResponseWriter, request *http.Request) {
func (s *server) handleGSIGet(writer http.ResponseWriter, request *http.Request) {
if !strings.HasPrefix(request.Header.Get("Authorization"), "GSI ") {
s.logger.Printf("%s - Unauthorized GSI read (no token)\n", request.RemoteAddr)
writer.WriteHeader(http.StatusUnauthorized)
Expand All @@ -109,7 +118,7 @@ func (s *server) handleGet(writer http.ResponseWriter, request *http.Request) {
return
}

gameState, hasGameState := s.store.Get(authToken)
gameState, hasGameState := s.gsiStore.Get(authToken)
if !hasGameState {
s.logger.Printf("%s - Unknown GSI read to %s\n", request.RemoteAddr, authToken)
writer.WriteHeader(http.StatusNotFound)
Expand All @@ -133,7 +142,7 @@ func (s *server) handleGet(writer http.ResponseWriter, request *http.Request) {
}
}

func (s *server) handlePost(writer http.ResponseWriter, request *http.Request) {
func (s *server) handleGSIPost(writer http.ResponseWriter, request *http.Request) {
body, ioError := ioutil.ReadAll(request.Body)
if ioError != nil || body == nil || len(body) <= 0 {
s.logger.Printf("%s - Empty GSI update received: %s\n", request.RemoteAddr, ioError)
Expand All @@ -143,7 +152,11 @@ func (s *server) handlePost(writer http.ResponseWriter, request *http.Request) {

gameState := new(model.GameState)
if jsonError := json.Unmarshal(body, gameState); jsonError != nil {
s.logger.Printf("%s - Could not de-serialize game state: %s\n", request.RemoteAddr, jsonError)
if jsonError.Error() != "json: cannot unmarshal bool into Go struct field GameState.previously.map of type model.MapState" {
// Upon map change, instead of returning a map object the GSI client return a bool.
// It's not necessary to log this error; we send 400 anyway to mark that the game state is not updated.
s.logger.Printf("%s - Could not de-serialize game state: %s\n", request.RemoteAddr, jsonError)
}
writer.WriteHeader(http.StatusBadRequest)
return
}
Expand All @@ -164,9 +177,74 @@ func (s *server) handlePost(writer http.ResponseWriter, request *http.Request) {
}

if gameState.Provider != nil {
s.store.Put(authToken, gameState)
s.gsiStore.Put(authToken, gameState)
} else {
s.store.Remove(authToken)
s.gsiStore.Remove(authToken)
}

writer.WriteHeader(http.StatusOK)
}

func (s *server) handleServerGet(writer http.ResponseWriter, request *http.Request) {
if !strings.HasPrefix(request.Header.Get("Authorization"), "SM ") {
s.logger.Printf("%s - Unauthorized SM read (no token)\n", request.RemoteAddr)
writer.WriteHeader(http.StatusUnauthorized)
return
}

authToken := request.Header.Get("Authorization")[3:]
if !s.filter.Accept(authToken) {
s.logger.Printf("%s - Unauthorized SM read (rejected token)\n", request.RemoteAddr)
writer.WriteHeader(http.StatusUnauthorized)
return
}

fullPlayerState, hasFullPlayerState := s.smStore.Get(authToken)
if !hasFullPlayerState {
s.logger.Printf("%s - Unknown SM read to %s\n", request.RemoteAddr, authToken)
writer.WriteHeader(http.StatusNotFound)
return
}

response, jsonError := json.Marshal(fullPlayerState)
if jsonError != nil {
s.logger.Printf("%s - Could not serialize game state %s: %s\n", request.RemoteAddr, authToken, jsonError)
writer.WriteHeader(http.StatusInternalServerError)
return
}

writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)

if _, ioError := writer.Write(response); ioError != nil {
s.logger.Printf("%s - Could not write game state %s: %s\n", request.RemoteAddr, authToken, ioError)
writer.WriteHeader(http.StatusInternalServerError)
return
}
}

func (s *server) handleServerPost(writer http.ResponseWriter, request *http.Request) {
body, ioError := ioutil.ReadAll(request.Body)
if ioError != nil || body == nil || len(body) <= 0 {
s.logger.Printf("%s - Empty SM update received: %s\n", request.RemoteAddr, ioError)
writer.WriteHeader(http.StatusBadRequest)
return
}

serverState := new(model.ServerState)
if jsonError := json.Unmarshal(body, serverState); jsonError != nil {
s.logger.Printf("%s - Could not de-serialize server state: %s\n", request.RemoteAddr, jsonError)
writer.WriteHeader(http.StatusBadRequest)
return
}
serverInfo := serverState.ServerInfo

playerInfos := serverState.PlayerInfo

for _, player := range playerInfos {
if player.AuthKey != "" {
s.smStore.Put(&serverInfo, &player)
}
}

writer.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -195,7 +273,7 @@ func (s *server) handleWebsocket(writer http.ResponseWriter, request *http.Reque
return
}

channel := s.store.GetChannel(authToken)
channel := s.gsiStore.GetChannel(authToken)

for {
gameState, more := <-channel
Expand All @@ -204,8 +282,9 @@ func (s *server) handleWebsocket(writer http.ResponseWriter, request *http.Reque
s.logger.Printf("%s - Could not serialize game state %s: %s\n", request.RemoteAddr, authToken, ioError)
}
_ = conn.Close()
s.store.ReleaseChannel(authToken)
s.gsiStore.ReleaseChannel(authToken)
return
}

}
}
Loading