GameStats: Add standard command server

This commit is contained in:
mkwcat 2024-02-06 01:43:21 -05:00
parent 496f918f7f
commit 7ddcd982bc
No known key found for this signature in database
GPG Key ID: 7A505679CE9E7AA9
7 changed files with 329 additions and 5 deletions

View File

@ -127,3 +127,28 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
return user, nil
}
func LoginUserToGameStats(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string) (User, error) {
user := User{
UserId: userId,
GsbrCode: gsbrcd,
}
var expectedNgId *uint32
var firstName *string
var lastName *string
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &expectedNgId, &user.Email, &user.UniqueNick, &firstName, &lastName)
if err != nil {
return User{}, err
}
if firstName != nil {
user.FirstName = *firstName
}
if lastName != nil {
user.LastName = *lastName
}
return user, nil
}

99
gamestats/auth.go Normal file
View File

@ -0,0 +1,99 @@
package gamestats
import (
"math/rand"
"strconv"
"time"
"wwfc/common"
"wwfc/database"
"wwfc/gpcm"
"wwfc/logging"
"github.com/logrusorgru/aurora/v3"
)
func (g *GameStatsSession) auth(command common.GameSpyCommand) {
game := common.GetGameInfoByName(command.OtherValues["gamename"])
if game == nil {
g.replyError(gpcm.ErrDatabase)
return
}
// TODO: Validate "response"
g.SessionKey = rand.Int31n(290000000) + 10000000
g.GameInfo = game
g.Write(common.GameSpyCommand{
Command: "lc",
CommandValue: "2",
OtherValues: map[string]string{
"sesskey": strconv.FormatInt(int64(g.SessionKey), 10),
"proof": "0",
"id": "1",
},
})
}
func (g *GameStatsSession) authp(command common.GameSpyCommand) {
lid := command.OtherValues["lid"]
errorCmd := common.GameSpyCommand{
Command: "pauthr",
CommandValue: "-3",
OtherValues: map[string]string{
"lid": lid,
"errmsg": "Invalid Validation",
},
}
if lid != "" {
var err error
g.LoginID, err = strconv.Atoi(lid)
if err != nil {
logging.Error(g.ModuleName, "Error parsing login ID:", err.Error())
g.Write(errorCmd)
return
}
}
authToken := command.OtherValues["authtoken"]
if authToken == "" {
logging.Error(g.ModuleName, "No authtoken provided")
g.Write(errorCmd)
return
}
err, _, issueTime, userId, gsbrcd, _, _, _, _, _, _, _ := common.UnmarshalNASAuthToken(authToken)
if err != nil {
logging.Error(g.ModuleName, "Error unmarshalling authtoken:", err.Error())
g.Write(errorCmd)
return
}
currentTime := time.Now()
if issueTime.Before(currentTime.Add(-10*time.Minute)) || issueTime.After(currentTime) {
logging.Error(g.ModuleName, "Authtoken has expired")
g.Write(errorCmd)
return
}
g.User, err = database.LoginUserToGameStats(pool, ctx, userId, gsbrcd)
if err != nil {
logging.Error(g.ModuleName, "Error logging in user:", err.Error())
g.Write(errorCmd)
return
}
g.ModuleName = "GSTATS:" + strconv.FormatInt(int64(g.User.ProfileId), 10)
g.ModuleName += "/" + common.CalcFriendCodeString(g.User.ProfileId, "RMCJ")
g.Authenticated = true
logging.Notice(g.ModuleName, "Authenticated, game name:", aurora.Cyan(g.GameInfo.Name))
g.Write(common.GameSpyCommand{
Command: "pauthr",
CommandValue: strconv.FormatUint(uint64(g.User.ProfileId), 10),
OtherValues: map[string]string{
"lid": lid,
},
})
}

11
gamestats/error.go Normal file
View File

@ -0,0 +1,11 @@
package gamestats
import (
"wwfc/gpcm"
"wwfc/logging"
)
func (g *GameStatsSession) replyError(err gpcm.GPError) {
logging.Error(g.ModuleName, "Reply error:", err.ErrorString)
g.Conn.Write([]byte(err.GetMessage()))
}

View File

@ -1,13 +1,35 @@
package gamestats
import (
"bufio"
"bytes"
"context"
"fmt"
"net"
"wwfc/common"
"wwfc/database"
"wwfc/gpcm"
"wwfc/logging"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
)
type GameStatsSession struct {
Conn net.Conn
ModuleName string
Challenge string
SessionKey int32
GameInfo *common.GameInfo
Authenticated bool
LoginID int
User database.User
WriteBuffer []byte
}
var (
ctx = context.Background()
pool *pgxpool.Pool
@ -38,4 +60,148 @@ func StartServer() {
if err != nil {
panic(err)
}
address := *config.GameSpyAddress + ":29920"
l, err := net.Listen("tcp", address)
if err != nil {
panic(err)
}
// Close the listener when the application closes.
defer l.Close()
logging.Notice("GSTATS", "Listening on", address)
for {
// Listen for an incoming connection.
conn, err := l.Accept()
if err != nil {
panic(err)
}
// Handle connections in a new goroutine.
go handleRequest(conn)
}
}
// Handles incoming requests.
func handleRequest(conn net.Conn) {
session := GameStatsSession{
Conn: conn,
ModuleName: "GSTATS:" + conn.RemoteAddr().String(),
Challenge: common.RandomString(10),
WriteBuffer: []byte{},
}
defer conn.Close()
moduleName := "GSTATS"
err := conn.(*net.TCPConn).SetKeepAlive(true)
if err != nil {
logging.Notice(moduleName, "Unable to set keepalive:", err.Error())
}
// Send challenge
session.Write(common.GameSpyCommand{
Command: "lc",
CommandValue: "1",
OtherValues: map[string]string{
"challenge": session.Challenge,
"id": "1",
},
})
conn.Write(session.WriteBuffer)
session.WriteBuffer = []byte{}
// Here we go into the listening loop
for {
// TODO: Handle split packets
buffer := make([]byte, 1024)
n, err := bufio.NewReader(conn).Read(buffer)
if err != nil {
return
}
// Decrypt the data
for i := 0; i < n; i++ {
if i+7 <= n && bytes.Equal(buffer[i:i+7], []byte(`\final\`)) {
i += 6
continue
}
buffer[i] ^= "GameSpy3D"[i%9]
}
commands, err := common.ParseGameSpyMessage(string(buffer[:n]))
if err != nil {
logging.Error(moduleName, "Error parsing message:", err.Error())
logging.Error(moduleName, "Raw data:", string(buffer[:n]))
session.replyError(gpcm.ErrParse)
return
}
commands = session.handleCommand("ka", commands, func(command common.GameSpyCommand) {
session.Conn.Write([]byte(`\ka\\final\`))
})
commands = session.handleCommand("auth", commands, session.auth)
commands = session.handleCommand("authp", commands, session.authp)
if len(commands) != 0 && session.Authenticated == false {
logging.Error(session.ModuleName, "Attempt to run command before authentication:", aurora.Cyan(commands[0]))
session.replyError(gpcm.ErrNotLoggedIn)
return
}
commands = session.handleCommand("setpd", commands, session.setpd)
for _, command := range commands {
logging.Error(session.ModuleName, "Unknown command:", aurora.Cyan(command))
}
if len(session.WriteBuffer) > 0 {
conn.Write(session.WriteBuffer)
session.WriteBuffer = []byte{}
}
}
}
func (g *GameStatsSession) handleCommand(name string, commands []common.GameSpyCommand, handler func(command common.GameSpyCommand)) []common.GameSpyCommand {
var unhandled []common.GameSpyCommand
for _, command := range commands {
if command.Command != name {
unhandled = append(unhandled, command)
continue
}
logging.Info(g.ModuleName, "Command:", aurora.Yellow(command.Command))
handler(command)
}
return unhandled
}
func (g *GameStatsSession) ignoreCommand(name string, commands []common.GameSpyCommand) []common.GameSpyCommand {
var unhandled []common.GameSpyCommand
for _, command := range commands {
if command.Command != name {
unhandled = append(unhandled, command)
}
}
return unhandled
}
func (g *GameStatsSession) Write(command common.GameSpyCommand) {
// Encrypt the data and append it to be sent
payload := []byte(common.CreateGameSpyMessage(command))
// Exclude trailing \final\
for i := 0; i < len(payload)-7; i++ {
payload[i] ^= "GameSpy3D"[i%9]
}
g.WriteBuffer = append(g.WriteBuffer, payload...)
}

21
gamestats/setpd.go Normal file
View File

@ -0,0 +1,21 @@
package gamestats
import (
"strconv"
"time"
"wwfc/common"
)
func (g *GameStatsSession) setpd(command common.GameSpyCommand) {
g.Write(common.GameSpyCommand{
Command: "setpdr",
CommandValue: "1",
OtherValues: map[string]string{
"lid": strconv.Itoa(g.LoginID),
"pid": command.OtherValues["pid"],
"mod": strconv.Itoa(int(time.Now().Unix())),
"length": "0",
"data": `\\`,
},
})
}

View File

@ -45,6 +45,8 @@ func MakeGPError(errorCode int, errorString string, fatal bool) GPError {
}
var (
ErrNone = MakeGPError(0xFFFF, "No error.", false)
// General errors
ErrGeneral = MakeGPError(0x0000, "There was an unknown error.", true)
ErrParse = MakeGPError(0x0001, "There was an error parsing an incoming request.", true)

View File

@ -166,7 +166,7 @@ func handleRequest(conn net.Conn) {
for {
// TODO: Handle split packets
buffer := make([]byte, 1024)
_, err := bufio.NewReader(conn).Read(buffer)
n, err := bufio.NewReader(conn).Read(buffer)
if err != nil {
if errors.Is(err, io.EOF) {
// Client closed connection, terminate.
@ -178,10 +178,10 @@ func handleRequest(conn net.Conn) {
return
}
commands, err := common.ParseGameSpyMessage(string(buffer))
commands, err := common.ParseGameSpyMessage(string(buffer[:n]))
if err != nil {
logging.Error(session.ModuleName, "Error parsing message:", err.Error())
logging.Error(session.ModuleName, "Raw data:", string(buffer))
logging.Error(session.ModuleName, "Raw data:", string(buffer[:n]))
session.replyError(ErrParse)
return
}
@ -196,7 +196,7 @@ func handleRequest(conn net.Conn) {
commands = session.ignoreCommand("logout", commands)
if len(commands) != 0 && session.LoggedIn == false {
logging.Error(session.ModuleName, "Attempt to run command before login!")
logging.Error(session.ModuleName, "Attempt to run command before login:", aurora.Cyan(commands[0]))
session.replyError(ErrNotLoggedIn)
return
}
@ -211,7 +211,7 @@ func handleRequest(conn net.Conn) {
commands = session.handleCommand("getprofile", commands, session.getProfile)
for _, command := range commands {
logging.Error(session.ModuleName, "Unknown command:", aurora.Cyan(command.Command))
logging.Error(session.ModuleName, "Unknown command:", aurora.Cyan(command))
}
if session.WriteBuffer != "" {