Add RPC support for GPSP, GameStats, ServerBrowser

This commit is contained in:
mkwcat 2024-05-06 18:13:14 -04:00
parent 22a3e1d98b
commit fc69fe9a61
No known key found for this signature in database
GPG Key ID: 7A505679CE9E7AA9
14 changed files with 360 additions and 408 deletions

View File

@ -1,11 +1,15 @@
package gamestats
import (
"wwfc/common"
"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()))
common.SendPacket(ServerName, g.ConnIndex, []byte(err.GetMessage()))
if err.Fatal {
common.CloseConnection(ServerName, g.ConnIndex)
}
}

View File

@ -1,13 +1,9 @@
package gamestats
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"strings"
"wwfc/common"
"wwfc/database"
"wwfc/gpcm"
@ -15,10 +11,14 @@ import (
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"github.com/sasha-s/go-deadlock"
)
var ServerName = "gamestats"
type GameStatsSession struct {
Conn net.Conn
ConnIndex uint64
RemoteAddr string
ModuleName string
Challenge string
@ -29,6 +29,7 @@ type GameStatsSession struct {
LoginID int
User database.User
ReadBuffer []byte
WriteBuffer []byte
}
@ -38,7 +39,9 @@ var (
serverName string
webSalt string
webHashPad string
sessionsByConnIndex = make(map[uint64]*GameStatsSession)
mutex = deadlock.RWMutex{}
)
func StartServer() {
@ -47,7 +50,6 @@ func StartServer() {
serverName = config.ServerName
webSalt = common.RandomString(32)
webHashPad = common.RandomString(8)
common.ReadGameList()
@ -62,51 +64,27 @@ func StartServer() {
if err != nil {
panic(err)
}
address := *config.GameSpyAddress + ":29920"
l, err := net.Listen("tcp", address)
if err != nil {
panic(err)
}
go func() {
// 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(),
func NewConnection(index uint64, address string) {
session := &GameStatsSession{
ConnIndex: index,
RemoteAddr: address,
ModuleName: "GSTATS:" + address,
Challenge: common.RandomString(10),
SessionKey: 0,
GameInfo: nil,
Authenticated: false,
LoginID: 0,
User: database.User{},
ReadBuffer: []byte{},
WriteBuffer: []byte{},
}
defer conn.Close()
err := conn.(*net.TCPConn).SetKeepAlive(true)
if err != nil {
logging.Notice(session.ModuleName, "Unable to set keepalive:", err.Error())
}
// Send challenge
session.Write(common.GameSpyCommand{
payload := common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "lc",
CommandValue: "1",
OtherValues: map[string]string{
@ -114,104 +92,115 @@ func handleRequest(conn net.Conn) {
"id": "1",
},
})
conn.Write(session.WriteBuffer)
session.WriteBuffer = []byte{}
common.SendPacket(ServerName, index, []byte(payload))
logging.Notice(session.ModuleName, "Connection established from", conn.RemoteAddr())
logging.Notice(session.ModuleName, "Connection established from", address)
// Here we go into the listening loop
for {
buffer := make([]byte, 0x4000)
bufferSize := 0
message := ""
mutex.Lock()
sessionsByConnIndex[index] = session
mutex.Unlock()
}
// Packets can be received in fragments, so this loop makes sure the packet has been fully received before continuing
for {
if bufferSize >= len(buffer) {
logging.Error(session.ModuleName, "Buffer overflow")
return
}
func CloseConnection(index uint64) {
mutex.RLock()
session := sessionsByConnIndex[index]
mutex.RUnlock()
readSize, err := bufio.NewReader(conn).Read(buffer[bufferSize:])
if err != nil {
if errors.Is(err, io.EOF) {
logging.Info(session.ModuleName, "Connection closed")
return
}
if session == nil {
logging.Error("GSTATS", "Cannot find session for this connection index:", aurora.Cyan(index))
return
}
logging.Error(session.ModuleName, "Connection error:", err.Error())
return
}
logging.Notice(session.ModuleName, "Connection closed")
bufferSize += readSize
mutex.Lock()
delete(sessionsByConnIndex, index)
mutex.Unlock()
}
if !bytes.Contains(buffer[max(0, bufferSize-readSize-6):bufferSize], []byte(`\final\`)) {
continue
}
func HandlePacket(index uint64, data []byte) {
mutex.RLock()
session := sessionsByConnIndex[index]
mutex.RUnlock()
// Decrypt the data
decrypted := ""
for i := 0; i < bufferSize; i++ {
if i+7 <= bufferSize && bytes.Equal(buffer[i:i+7], []byte(`\final\`)) {
// Append the decrypted content to the message
message += decrypted + `\final\`
decrypted = ""
if session == nil {
logging.Error("GSTATS", "Cannot find session for this connection index:", aurora.Cyan(index))
return
}
// Remove the processed data
buffer = buffer[i+7:]
bufferSize -= i + 7
i = 0
defer func() {
if r := recover(); r != nil {
logging.Error(session.ModuleName, "Panic:", r)
}
}()
if bufferSize < 7 || !bytes.Contains(buffer[:bufferSize], []byte(`\final\`)) {
break
}
continue
}
// Enforce maximum buffer size
length := len(session.ReadBuffer) + len(data)
if length > 0x4000 {
logging.Error(session.ModuleName, "Buffer overflow")
return
}
decrypted += string(rune(buffer[i] ^ "GameSpy3D"[i%9]))
}
session.ReadBuffer = append(session.ReadBuffer, data...)
// Continue to processing the message if we have a full message and another message is not expected
if len(message) > 0 && bufferSize <= 0 {
break
}
// Packets can be received in fragments, so make sure we're at the end of a packet
if string(session.ReadBuffer[max(0, length-7):length]) != `\final\` {
return
}
// Decrypt the data, can decrypt multiple packets
decrypted := strings.Builder{}
decrypted.Grow(length)
p := 0
for i := 0; i < length; i++ {
if string(session.ReadBuffer[i:i+7]) == `\final\` {
decrypted.WriteString(`\final\`)
i += 6
p = 0
continue
}
commands, err := common.ParseGameSpyMessage(message)
if err != nil {
logging.Error(session.ModuleName, "Error parsing message:", err.Error())
logging.Error(session.ModuleName, "Raw data:", message)
session.replyError(gpcm.ErrParse)
return
}
decrypted.WriteRune(rune(session.ReadBuffer[i] ^ "GameSpy3D"[p]))
p = (p + 1) % 9
}
commands = session.handleCommand("ka", commands, func(command common.GameSpyCommand) {
session.Write(common.GameSpyCommand{
Command: "ka",
})
message := decrypted.String()
commands, err := common.ParseGameSpyMessage(message)
if err != nil {
logging.Error(session.ModuleName, "Error parsing message:", err.Error())
logging.Error(session.ModuleName, "Raw data:", message)
session.replyError(gpcm.ErrParse)
return
}
commands = session.handleCommand("ka", commands, func(command common.GameSpyCommand) {
session.Write(common.GameSpyCommand{
Command: "ka",
})
})
commands = session.handleCommand("auth", commands, session.auth)
commands = session.handleCommand("authp", commands, session.authp)
commands = session.handleCommand("auth", commands, session.auth)
commands = session.handleCommand("authp", commands, session.authp)
if len(commands) != 0 && !session.Authenticated {
logging.Error(session.ModuleName, "Attempt to run command before authentication:", aurora.Cyan(commands[0]))
session.replyError(gpcm.ErrNotLoggedIn)
return
}
if len(commands) != 0 && !session.Authenticated {
logging.Error(session.ModuleName, "Attempt to run command before authentication:", aurora.Cyan(commands[0]))
session.replyError(gpcm.ErrNotLoggedIn)
return
}
commands = session.handleCommand("getpd", commands, session.getpd)
commands = session.handleCommand("setpd", commands, session.setpd)
common.UNUSED(session.ignoreCommand)
commands = session.handleCommand("getpd", commands, session.getpd)
commands = session.handleCommand("setpd", commands, session.setpd)
common.UNUSED(session.ignoreCommand)
for _, command := range commands {
logging.Error(session.ModuleName, "Unknown command:", aurora.Cyan(command))
}
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{}
}
if len(session.WriteBuffer) > 0 {
common.SendPacket(ServerName, session.ConnIndex, session.WriteBuffer)
session.WriteBuffer = []byte{}
}
}

View File

@ -409,9 +409,9 @@ func (g *GameSpySession) replyError(err GPError) {
if !g.LoginInfoSet {
msg := err.GetMessage()
// logging.Info(g.ModuleName, "Sending error message:", msg)
common.SendPacket("gpcm", g.ConnIndex, []byte(msg))
common.SendPacket(ServerName, g.ConnIndex, []byte(msg))
if err.Fatal {
common.CloseConnection("gpcm", g.ConnIndex)
common.CloseConnection(ServerName, g.ConnIndex)
}
return
}
@ -423,8 +423,8 @@ func (g *GameSpySession) replyError(err GPError) {
msg := err.GetMessageTranslate(g.GameName, g.Region, g.Language, g.ConsoleFriendCode, deviceId)
// logging.Info(g.ModuleName, "Sending error message:", msg)
common.SendPacket("gpcm", g.ConnIndex, []byte(msg))
common.SendPacket(ServerName, g.ConnIndex, []byte(msg))
if err.Fatal {
common.CloseConnection("gpcm", g.ConnIndex)
common.CloseConnection(ServerName, g.ConnIndex)
}
}

View File

@ -247,7 +247,7 @@ func sendMessageToSession(msgType string, from uint32, session *GameSpySession,
"msg": msg,
},
})
common.SendPacket("gpcm", session.ConnIndex, []byte(message))
common.SendPacket(ServerName, session.ConnIndex, []byte(message))
}
func sendMessageToSessionBuffer(msgType string, from uint32, session *GameSpySession, msg string) {

View File

@ -27,7 +27,7 @@ func kickPlayer(profileID uint32, reason string) {
case "network_error":
// No error message
common.CloseConnection("gpcm", session.ConnIndex)
common.CloseConnection(ServerName, session.ConnIndex)
return
}
@ -37,7 +37,7 @@ func kickPlayer(profileID uint32, reason string) {
Fatal: true,
WWFCMessage: errorMessage,
})
common.CloseConnection("gpcm", session.ConnIndex)
common.CloseConnection(ServerName, session.ConnIndex)
}
}

View File

@ -269,7 +269,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
otherSession, exists := sessions[g.User.ProfileId]
if exists {
otherSession.replyError(ErrForcedDisconnect)
common.CloseConnection("gpcm", otherSession.ConnIndex)
common.CloseConnection(ServerName, otherSession.ConnIndex)
for i := 0; ; i++ {
mutex.Unlock()
@ -336,7 +336,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
OtherValues: otherValues,
})
common.SendPacket("gpcm", g.ConnIndex, []byte(payload))
common.SendPacket(ServerName, g.ConnIndex, []byte(payload))
}
func (g *GameSpySession) exLogin(command common.GameSpyCommand) {

View File

@ -13,6 +13,8 @@ import (
"github.com/sasha-s/go-deadlock"
)
var ServerName = "gpcm"
type GameSpySession struct {
ConnIndex uint64
RemoteAddr string
@ -113,7 +115,6 @@ func CloseConnection(index uint64) {
}
}
// Handles incoming requests.
func NewConnection(index uint64, address string) {
session := &GameSpySession{
ConnIndex: index,
@ -121,7 +122,7 @@ func NewConnection(index uint64, address string) {
User: database.User{},
ModuleName: "GPCM:" + address,
LoggedIn: false,
Challenge: "",
Challenge: common.RandomString(10),
StatusSet: false,
Status: "",
LocString: "",
@ -129,9 +130,6 @@ func NewConnection(index uint64, address string) {
AuthFriendList: []uint32{},
}
// Set challenge
session.Challenge = common.RandomString(10)
payload := common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "lc",
CommandValue: "1",
@ -140,7 +138,7 @@ func NewConnection(index uint64, address string) {
"id": "1",
},
})
common.SendPacket("gpcm", index, []byte(payload))
common.SendPacket(ServerName, index, []byte(payload))
logging.Notice(session.ModuleName, "Connection established from", address)
@ -176,7 +174,7 @@ func HandlePacket(index uint64, data []byte) {
// Commands must be handled in a certain order, not in the order supplied by the client
commands = session.handleCommand("ka", commands, func(command common.GameSpyCommand) {
common.SendPacket("gpcm", session.ConnIndex, []byte(`\ka\\final\`))
common.SendPacket(ServerName, session.ConnIndex, []byte(`\ka\\final\`))
})
commands = session.handleCommand("login", commands, session.login)
commands = session.handleCommand("wwfc_exlogin", commands, session.exLogin)
@ -202,7 +200,7 @@ func HandlePacket(index uint64, data []byte) {
}
if session.WriteBuffer != "" {
common.SendPacket("gpcm", session.ConnIndex, []byte(session.WriteBuffer))
common.SendPacket(ServerName, session.ConnIndex, []byte(session.WriteBuffer))
session.WriteBuffer = ""
}
}

View File

@ -360,7 +360,7 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
},
})
common.SendPacket("gpcm", toSession.ConnIndex, []byte(message))
common.SendPacket(ServerName, toSession.ConnIndex, []byte(message))
// Append sender's profile ID to dest's RecvStatusFromList
toSession.RecvStatusFromList = append(toSession.RecvStatusFromList, g.User.ProfileId)

View File

@ -1,12 +1,16 @@
package gpsp
import (
"net"
"wwfc/common"
"wwfc/gpcm"
"wwfc/logging"
)
func replyError(moduleName string, conn net.Conn, err gpcm.GPError) {
func replyError(moduleName string, connIndex uint64, err gpcm.GPError) {
logging.Error(moduleName, "Reply error:", err.ErrorString)
conn.Write([]byte(err.GetMessage()))
msg := err.GetMessage()
common.SendPacket(ServerName, connIndex, []byte(msg))
if err.Fatal {
common.CloseConnection(ServerName, connIndex)
}
}

View File

@ -1,85 +1,54 @@
package gpsp
import (
"bufio"
"net"
"wwfc/common"
"wwfc/gpcm"
"wwfc/logging"
)
var ServerName = "gpsp"
func StartServer() {
// Get config
config := common.GetConfig()
address := *config.GameSpyAddress + ":29901"
l, err := net.Listen("tcp", address)
if err != nil {
panic(err)
}
go func() {
// Close the listener when the application closes.
defer l.Close()
logging.Notice("GPSP", "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) {
defer conn.Close()
func NewConnection(index uint64, address string) {
}
func CloseConnection(index uint64) {
}
func HandlePacket(index uint64, data []byte) {
moduleName := "GPSP"
err := conn.(*net.TCPConn).SetKeepAlive(true)
if err != nil {
logging.Notice(moduleName, "Unable to set keepalive:", err.Error())
// TODO: Handle split packets
message := ""
for _, b := range data {
message += string(b)
}
// Here we go into the listening loop
for {
// TODO: Handle split packets
buffer := make([]byte, 1024)
_, err := bufio.NewReader(conn).Read(buffer)
if err != nil {
return
}
commands, err := common.ParseGameSpyMessage(message)
if err != nil {
logging.Error(moduleName, "Error parsing message:", err.Error())
logging.Error(moduleName, "Raw data:", message)
replyError(moduleName, index, gpcm.ErrParse)
return
}
commands, err := common.ParseGameSpyMessage(string(buffer))
if err != nil {
logging.Error(moduleName, "Error parsing message:", err.Error())
logging.Error(moduleName, "Raw data:", string(buffer))
replyError(moduleName, conn, gpcm.ErrParse)
return
}
for _, command := range commands {
switch command.Command {
default:
logging.Error(moduleName, "Unknown command:", command.Command)
logging.Error(moduleName, "Raw data:", message)
replyError(moduleName, index, gpcm.ErrParse)
for _, command := range commands {
switch command.Command {
default:
logging.Error(moduleName, "Unknown command:", command.Command)
logging.Error(moduleName, "Raw data:", string(buffer))
replyError(moduleName, conn, gpcm.ErrParse)
case "ka":
common.SendPacket(ServerName, index, []byte(`\ka\\final\`))
case "ka":
conn.Write([]byte(`\ka\\final\`))
case "otherslist":
common.SendPacket(ServerName, index, []byte(handleOthersList(command)))
case "otherslist":
conn.Write([]byte(handleOthersList(command)))
case "search":
conn.Write([]byte(handleSearch(command)))
}
case "search":
common.SendPacket(ServerName, index, []byte(handleSearch(command)))
}
}
}

34
main.go
View File

@ -99,8 +99,14 @@ func backendMain() {
// RPCPacket.NewConnection is called by the frontend to notify the backend of a new connection
func (r *RPCPacket) NewConnection(args RPCPacket, _ *struct{}) error {
switch args.Server {
case "serverbrowser":
serverbrowser.NewConnection(args.Index, args.Address)
case "gpcm":
gpcm.NewConnection(args.Index, args.Address)
case "gpsp":
gpsp.NewConnection(args.Index, args.Address)
case "gamestats":
gamestats.NewConnection(args.Index, args.Address)
}
return nil
@ -109,8 +115,14 @@ func (r *RPCPacket) NewConnection(args RPCPacket, _ *struct{}) error {
// RPCPacket.HandlePacket is called by the frontend to forward a packet to the backend
func (r *RPCPacket) HandlePacket(args RPCPacket, _ *struct{}) error {
switch args.Server {
case "serverbrowser":
serverbrowser.HandlePacket(args.Index, args.Data, args.Address)
case "gpcm":
gpcm.HandlePacket(args.Index, args.Data)
case "gpsp":
gpsp.HandlePacket(args.Index, args.Data)
case "gamestats":
gamestats.HandlePacket(args.Index, args.Data)
}
return nil
@ -119,8 +131,14 @@ func (r *RPCPacket) HandlePacket(args RPCPacket, _ *struct{}) error {
// rpcPacket.closeConnection is called by the frontend to notify the backend of a closed connection
func (r *RPCPacket) CloseConnection(args RPCPacket, _ *struct{}) error {
switch args.Server {
case "serverbrowser":
serverbrowser.CloseConnection(args.Index)
case "gpcm":
gpcm.CloseConnection(args.Index)
case "gpsp":
gpsp.CloseConnection(args.Index)
case "gamestats":
gamestats.CloseConnection(args.Index)
}
return nil
@ -165,10 +183,10 @@ func frontendMain() {
go startBackendProcess()
servers := []serverInfo{
// {rpcName: "serverbrowser", protocol: "tcp", port: 28910},
{rpcName: "serverbrowser", protocol: "tcp", port: 28910},
{rpcName: "gpcm", protocol: "tcp", port: 29900},
// {rpcName: "gpsp", protocol: "tcp", port: 29901},
// {rpcName: "gamestats", protocol: "tcp", port: 29920},
{rpcName: "gpsp", protocol: "tcp", port: 29901},
{rpcName: "gamestats", protocol: "tcp", port: 29920},
}
for _, server := range servers {
@ -291,6 +309,10 @@ func handleConnection(server serverInfo, conn net.Conn, index uint64) {
break
}
if n == 0 {
continue
}
rpcMutex.Lock()
rpcBusyCount.Add(1)
rpcMutex.Unlock()
@ -302,6 +324,9 @@ func handleConnection(server serverInfo, conn net.Conn, index uint64) {
if err != nil {
logging.Error("FRONTEND", "Failed to forward packet to backend:", err)
if err == rpc.ErrShutdown {
os.Exit(1)
}
break
}
}
@ -317,6 +342,9 @@ func handleConnection(server serverInfo, conn net.Conn, index uint64) {
if err != nil {
logging.Error("FRONTEND", "Failed to forward close connection to backend:", err)
if err == rpc.ErrShutdown {
os.Exit(1)
}
}
}

View File

@ -15,11 +15,11 @@ import (
// Example: dwc_mver = 90 and dwc_pid != 43 and maxplayers = 11 and numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 2 and dwc_suspend = 0 and (rk = 'vs' and ev >= 4250 and ev <= 5750 and p = 0)
func filterServers(servers []map[string]string, queryGame string, expression string, publicIP string) []map[string]string {
func filterServers(moduleName string, servers []map[string]string, queryGame string, expression string, publicIP string) []map[string]string {
// Matchmaking search
tree, err := filter.Parse(expression)
if err != nil {
logging.Error(ModuleName, "Error parsing filter:", err.Error())
logging.Error(moduleName, "Error parsing filter:", err.Error())
return []map[string]string{}
}
@ -40,7 +40,7 @@ func filterServers(servers []map[string]string, queryGame string, expression str
ret, err := filter.Eval(tree, server, queryGame)
if err != nil {
logging.Error(ModuleName, "Error evaluating filter:", err.Error())
logging.Error(moduleName, "Error evaluating filter:", err.Error())
return []map[string]string{}
}
@ -49,11 +49,14 @@ func filterServers(servers []map[string]string, queryGame string, expression str
}
}
logging.Info(ModuleName, "Matched", aurora.BrightCyan(len(filtered)), "servers")
if len(filtered) != 0 {
logging.Info(moduleName, "Matched", aurora.BrightCyan(len(filtered)), "servers")
}
return filtered
}
func filterSelfLookup(servers []map[string]string, queryGame string, dwcPid string, publicIP string) []map[string]string {
func filterSelfLookup(moduleName string, servers []map[string]string, queryGame string, dwcPid string, publicIP string) []map[string]string {
var filtered []map[string]string
// Search for where the profile ID matches
@ -64,7 +67,7 @@ func filterSelfLookup(servers []map[string]string, queryGame string, dwcPid stri
if server["dwc_pid"] == dwcPid {
// May not be a self lookup, some games search for friends like this
logging.Info(ModuleName, "Lookup", aurora.Cyan(dwcPid), "ok")
logging.Info(moduleName, "Lookup", aurora.Cyan(dwcPid), "ok")
return []map[string]string{server}
}
@ -85,10 +88,10 @@ func filterSelfLookup(servers []map[string]string, queryGame string, dwcPid stri
}
if len(filtered) == 0 {
logging.Error(ModuleName, "Could not find server with dwc_pid", aurora.Cyan(dwcPid))
logging.Error(moduleName, "Could not find server with dwc_pid", aurora.Cyan(dwcPid))
return []map[string]string{}
}
logging.Info(ModuleName, "Self lookup for", aurora.Cyan(dwcPid), "matched", aurora.BrightCyan(len(filtered)), "servers via public IP")
logging.Info(moduleName, "Self lookup for", aurora.Cyan(dwcPid), "matched", aurora.BrightCyan(len(filtered)), "servers via public IP")
return filtered
}

View File

@ -1,30 +1,17 @@
package serverbrowser
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"os"
"wwfc/common"
"wwfc/logging"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"github.com/sasha-s/go-deadlock"
)
var (
ctx = context.Background()
pool *pgxpool.Pool
userId int
)
var ServerName = "serverbrowser"
const (
ModuleName = "SB"
// Requests sent from the client
ServerListRequest = 0x00
ServerInfoRequest = 0x01
@ -42,127 +29,99 @@ const (
PlayerSearchMessage = 0x06
)
var (
connBuffers = map[uint64]*[]byte{}
mutex = deadlock.RWMutex{}
)
func StartServer() {
// Get config
config := common.GetConfig()
// Start SQL
dbString := fmt.Sprintf("postgres://%s:%s@%s/%s", config.Username, config.Password, config.DatabaseAddress, config.DatabaseName)
dbConf, err := pgxpool.ParseConfig(dbString)
if err != nil {
panic(err)
}
pool, err = pgxpool.ConnectConfig(ctx, dbConf)
if err != nil {
panic(err)
}
address := *config.GameSpyAddress + ":28910"
l, err := net.Listen("tcp", address)
if err != nil {
panic(err)
}
go func() {
// Close the listener when the application closes.
defer l.Close()
logging.Notice(ModuleName, "Listening on", address)
for {
// Listen for an incoming connection.
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting: ", err.Error())
os.Exit(1)
}
// Handle connections in a new goroutine.
go handleRequest(conn)
}
}()
}
// Handles incoming requests.
func handleRequest(conn net.Conn) {
defer conn.Close()
func NewConnection(index uint64, address string) {
}
err := conn.(*net.TCPConn).SetKeepAlive(true)
if err != nil {
logging.Error(ModuleName, "Unable to set keepalive", err.Error())
}
func CloseConnection(index uint64) {
mutex.Lock()
delete(connBuffers, index)
mutex.Unlock()
}
logging.Info(ModuleName, "Connection established from", aurora.BrightCyan(conn.RemoteAddr()))
func HandlePacket(index uint64, data []byte, address string) {
moduleName := "SB:" + address
// Here we go into the listening loop
bufferSize := 0
packetSize := uint16(0)
var buffer []byte
for {
// Remove stale data and remake the buffer
buffer = append(buffer[packetSize:], make([]byte, 1024-packetSize)...)
bufferSize -= int(packetSize)
packetSize = 0
mutex.RLock()
buffer := connBuffers[index]
mutex.RUnlock()
// Packets tend to be sent in fragments, so this loop makes sure the packets has been fully received before continuing
for {
if bufferSize > 2 {
packetSize = binary.BigEndian.Uint16(buffer[:2])
if packetSize < 3 || packetSize >= 1024 {
logging.Error(ModuleName, "Invalid packet size - terminating")
return
}
if bufferSize >= int(packetSize) {
// Got a full packet, break to continue
break
}
}
readSize, err := bufio.NewReader(conn).Read(buffer[bufferSize:])
if err != nil {
if errors.Is(err, io.EOF) {
logging.Info(ModuleName, "Connection closed")
return
}
logging.Error(ModuleName, "Connection error:", err.Error())
if buffer == nil {
buffer = &[]byte{}
defer func() {
if buffer == nil {
return
}
bufferSize += readSize
}
mutex.Lock()
connBuffers[index] = buffer
mutex.Unlock()
}()
}
switch buffer[2] {
case ServerListRequest:
logging.Info(ModuleName, "Command:", aurora.Yellow("SERVER_LIST_REQUEST"))
handleServerListRequest(conn, buffer[:packetSize])
break
if len(*buffer)+len(data) > 0x1000 {
logging.Error(moduleName, "Buffer overflow")
common.CloseConnection(ServerName, index)
buffer = nil
return
}
case ServerInfoRequest:
logging.Info(ModuleName, "Command:", aurora.Yellow("SERVER_INFO_REQUEST"))
break
*buffer = append(*buffer, data...)
case SendMessageRequest:
logging.Info(ModuleName, "Command:", aurora.Yellow("SEND_MESSAGE_REQUEST"))
handleSendMessageRequest(conn, buffer[:packetSize])
break
// Packets can be sent in fragments, so we need to check if we have a full packet
// The first two bytes signify the packet size
if len(*buffer) < 2 {
return
}
case KeepaliveReply:
logging.Info(ModuleName, "Command:", aurora.Yellow("KEEPALIVE_REPLY"))
break
packetSize := binary.BigEndian.Uint16((*buffer)[:2])
if packetSize < 3 || packetSize > 0x1000 {
logging.Error(moduleName, "Invalid packet size - terminating")
common.CloseConnection(ServerName, index)
buffer = nil
return
}
case MapLoopRequest:
logging.Info(ModuleName, "Command:", aurora.Yellow("MAPLOOP_REQUEST"))
break
if len(*buffer) < int(packetSize) {
return
}
case PlayerSearchRequest:
logging.Info(ModuleName, "Command:", aurora.Yellow("PLAYER_SEARCH_REQUEST"))
break
switch (*buffer)[2] {
case ServerListRequest:
// logging.Info(moduleName, "Command:", aurora.Yellow("SERVER_LIST_REQUEST"))
handleServerListRequest(moduleName, index, address, (*buffer)[:packetSize])
default:
logging.Error(ModuleName, "Unknown command:", aurora.Cyan(buffer[2]))
break
}
case ServerInfoRequest:
logging.Info(moduleName, "Command:", aurora.Yellow("SERVER_INFO_REQUEST"))
case SendMessageRequest:
// logging.Info(moduleName, "Command:", aurora.Yellow("SEND_MESSAGE_REQUEST"))
handleSendMessageRequest(moduleName, index, address, (*buffer)[:packetSize])
case KeepaliveReply:
logging.Info(moduleName, "Command:", aurora.Yellow("KEEPALIVE_REPLY"))
case MapLoopRequest:
logging.Info(moduleName, "Command:", aurora.Yellow("MAPLOOP_REQUEST"))
case PlayerSearchRequest:
logging.Info(moduleName, "Command:", aurora.Yellow("PLAYER_SEARCH_REQUEST"))
default:
logging.Error(moduleName, "Unknown command:", aurora.Cyan((*buffer)[2]))
}
if len(*buffer) > int(packetSize) {
*buffer = (*buffer)[packetSize:]
} else {
*buffer = []byte{}
buffer = nil
}
}

View File

@ -4,7 +4,6 @@ import (
"encoding/binary"
"errors"
"fmt"
"net"
"regexp"
"strconv"
"strings"
@ -82,40 +81,45 @@ func popUint32(buffer []byte, index int) (uint32, int, error) {
var regexSelfLookup = regexp.MustCompile(`^dwc_pid ?= ?(\d{1,10})$`)
func handleServerListRequest(conn net.Conn, buffer []byte) {
func handleServerListRequest(moduleName string, connIndex uint64, address string, buffer []byte) {
index := 9
queryGame, index, err := popString(buffer, index)
if err != nil {
logging.Error(ModuleName, "Invalid queryGame")
logging.Error(moduleName, "Invalid queryGame")
return
}
gameName, index, err := popString(buffer, index)
if err != nil {
logging.Error(ModuleName, "Invalid gameName")
logging.Error(moduleName, "Invalid gameName")
return
}
challenge, index, err := popBytes(buffer, index, 8)
if err != nil {
logging.Error(ModuleName, "Invalid challenge")
return
}
filter, index, err := popString(buffer, index)
if err != nil {
logging.Error(ModuleName, "Invalid filter")
return
}
fields, index, err := popString(buffer, index)
if err != nil {
logging.Error(ModuleName, "Invalid fields")
return
}
options, index, err := popUint32(buffer, index)
if err != nil {
logging.Error(ModuleName, "Invalid options")
logging.Error(moduleName, "Invalid challenge")
return
}
logging.Info(ModuleName, "queryGame:", aurora.Cyan(queryGame).String(), "- gameName:", aurora.Cyan(gameName).String(), "- filter:", aurora.Cyan(filter).String(), "- fields:", aurora.Cyan(fields).String())
filter, index, err := popString(buffer, index)
if err != nil {
logging.Error(moduleName, "Invalid filter")
return
}
fields, index, err := popString(buffer, index)
if err != nil {
logging.Error(moduleName, "Invalid fields")
return
}
options, index, err := popUint32(buffer, index)
if err != nil {
logging.Error(moduleName, "Invalid options")
return
}
logging.Info(moduleName, "Server list:", aurora.Cyan(queryGame), "/", aurora.Cyan(filter[:min(len(filter), 200)]))
gameInfo := common.GetGameInfoByName(gameName)
if gameInfo == nil {
@ -124,7 +128,7 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
}
var output []byte
for _, s := range strings.Split(strings.Split(conn.RemoteAddr().String(), ":")[0], ".") {
for _, s := range strings.Split(strings.Split(address, ":")[0], ".") {
val, err := strconv.Atoi(s)
if err != nil {
panic(err)
@ -134,35 +138,25 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
}
var fieldList []string
for _, field := range strings.Split(fields, "\\") {
if len(field) == 0 || field == " " {
continue
if options&NoServerListOption == 0 {
for _, field := range strings.Split(fields, "\\") {
if len(field) == 0 || field == " " {
continue
}
// Skip private fields
if field == "publicip" || field == "publicport" || strings.HasPrefix(field, "localip") || field == "localport" {
continue
}
fieldList = append(fieldList, field)
}
// Skip private fields
if field == "publicip" || field == "publicport" || strings.HasPrefix(field, "localip") || field == "localport" {
continue
}
fieldList = append(fieldList, field)
} else {
filter = ""
}
if options&NoServerListOption != 0 || len(fieldList) == 0 {
// The client requests its own public IP and game port
logging.Info(ModuleName, "Reply without server list", aurora.Cyan(conn.RemoteAddr()))
// The default game port 6500
output = binary.BigEndian.AppendUint16(output, 6500)
// Write the encrypted reply
conn.Write(common.EncryptTypeX([]byte(gameInfo.SecretKey), challenge, output))
return
}
logging.Info(ModuleName, "Reply with server list", aurora.Cyan(conn.RemoteAddr()))
// The client's port
port, err := strconv.Atoi(strings.Split(conn.RemoteAddr().String(), ":")[1])
port, err := strconv.Atoi(strings.Split(address, ":")[1])
if err != nil {
panic(err)
}
@ -176,14 +170,16 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
}
output = append(output, 0x00) // Zero length string to end the list
callerPublicIP, _ := common.IPFormatToString(conn.RemoteAddr().String())
callerPublicIP, _ := common.IPFormatToString(address)
var servers []map[string]string
if match := regexSelfLookup.FindStringSubmatch(filter); match != nil {
// Self lookup is handled differently
servers = filterSelfLookup(qr2.GetSessionServers(), queryGame, match[1], callerPublicIP)
} else {
servers = filterServers(qr2.GetSessionServers(), queryGame, filter, callerPublicIP)
servers := []map[string]string{}
if options&NoServerListOption == 0 && filter != "" && filter != " " && filter != "0" {
if match := regexSelfLookup.FindStringSubmatch(filter); match != nil {
// Self lookup is handled differently
servers = filterSelfLookup(moduleName, qr2.GetSessionServers(), queryGame, match[1], callerPublicIP)
} else {
servers = filterServers(moduleName, qr2.GetSessionServers(), queryGame, filter, callerPublicIP)
}
}
for _, server := range servers {
@ -201,7 +197,7 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
var publicip string
if publicip, exists = server["publicip"]; !exists {
logging.Error(ModuleName, "Server exists without public IP")
logging.Error(moduleName, "Server exists without public IP")
continue
}
@ -209,7 +205,7 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
// Use the real public IP if it matches the caller's
ip, err := strconv.ParseInt(publicip, 10, 32)
if err != nil {
logging.Error(ModuleName, "Server has invalid public IP value:", aurora.Cyan(publicip))
logging.Error(moduleName, "Server has invalid public IP value:", aurora.Cyan(publicip))
}
flagsBuffer = binary.BigEndian.AppendUint32(flagsBuffer, uint32(ip))
@ -219,19 +215,19 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
if !exists {
// Fall back to local port if public port doesn't exist
if port, exists = server["localport"]; !exists {
logging.Error(ModuleName, "Server exists without port (publicip =", aurora.Cyan(publicip).String()+")")
logging.Error(moduleName, "Server exists without port (publicip =", aurora.Cyan(publicip).String()+")")
continue
}
}
portValue, err := strconv.ParseUint(port, 10, 16)
if err != nil {
logging.Error(ModuleName, "Server has invalid port value:", aurora.Cyan(port))
logging.Error(moduleName, "Server has invalid port value:", aurora.Cyan(port))
continue
}
if portValue < 1024 {
logging.Error(ModuleName, "Server uses reserved port:", aurora.Cyan(portValue))
logging.Error(moduleName, "Server uses reserved port:", aurora.Cyan(portValue))
continue
}
@ -245,7 +241,7 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
// localip is written like "192.168.255.255" for example, so it needs to be parsed
ipSplit := strings.Split(localip0, ".")
if len(ipSplit) != 4 {
logging.Error(ModuleName, "Server has invalid local IP:", aurora.Cyan(localip0))
logging.Error(moduleName, "Server has invalid local IP:", aurora.Cyan(localip0))
continue
}
@ -260,7 +256,7 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
}
if err != nil {
logging.Error(ModuleName, "Server has invalid local IP value:", aurora.Cyan(localip0))
logging.Error(moduleName, "Server has invalid local IP value:", aurora.Cyan(localip0))
continue
}
}
@ -268,7 +264,7 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
if localport, exists := server["localport"]; exists {
portValue, err = strconv.ParseUint(localport, 10, 16)
if err != nil {
logging.Error(ModuleName, "Server has invalid local port value:", aurora.Cyan(localport))
logging.Error(moduleName, "Server has invalid local port value:", aurora.Cyan(localport))
continue
}
@ -282,13 +278,13 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
// Regular server, hide the public IP until match reservation is made
var searchIDStr string
if searchIDStr, exists = server["+searchid"]; !exists {
logging.Error(ModuleName, "Server exists without search ID")
logging.Error(moduleName, "Server exists without search ID")
continue
}
searchID, err := strconv.ParseInt(searchIDStr, 10, 64)
if err != nil {
logging.Error(ModuleName, "Server has invalid search ID value:", aurora.Cyan(searchIDStr))
logging.Error(moduleName, "Server has invalid search ID value:", aurora.Cyan(searchIDStr))
}
// Append low value as public IP
@ -326,19 +322,21 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
}
}
// Server with 0 flags and IP of 0xffffffff terminates the list
output = append(output, []byte{0x00, 0xff, 0xff, 0xff, 0xff}...)
if options&NoServerListOption == 0 {
// Server with 0 flags and IP of 0xffffffff terminates the list
output = append(output, []byte{0x00, 0xff, 0xff, 0xff, 0xff}...)
}
// Write the encrypted reply
conn.Write(common.EncryptTypeX([]byte(gameInfo.SecretKey), challenge, output))
common.SendPacket(ServerName, connIndex, common.EncryptTypeX([]byte(gameInfo.SecretKey), challenge, output))
}
func handleSendMessageRequest(conn net.Conn, buffer []byte) {
func handleSendMessageRequest(moduleName string, connIndex uint64, address string, buffer []byte) {
// Read search ID from buffer
searchID := uint64(binary.BigEndian.Uint32(buffer[3:7]))
searchID |= uint64(binary.BigEndian.Uint16(buffer[7:9])) << 32
logging.Notice(ModuleName, "Send message from", aurora.BrightCyan(conn.RemoteAddr()), "to", aurora.Cyan(fmt.Sprintf("%012x", searchID)))
logging.Notice(moduleName, "Send message from to", aurora.Cyan(fmt.Sprintf("%012x", searchID)))
go qr2.SendClientMessage(conn.RemoteAddr().String(), searchID, buffer[9:])
go qr2.SendClientMessage(address, searchID, buffer[9:])
}