From fc69fe9a6159b9425e24d979bc6e4c11e8f38671 Mon Sep 17 00:00:00 2001 From: mkwcat Date: Mon, 6 May 2024 18:13:14 -0400 Subject: [PATCH] Add RPC support for GPSP, GameStats, ServerBrowser --- gamestats/error.go | 6 +- gamestats/main.go | 235 +++++++++++++++++++--------------------- gpcm/error.go | 8 +- gpcm/friend.go | 2 +- gpcm/kick.go | 4 +- gpcm/login.go | 4 +- gpcm/main.go | 14 +-- gpcm/message.go | 2 +- gpsp/error.go | 10 +- gpsp/main.go | 93 ++++++---------- main.go | 34 +++++- serverbrowser/filter.go | 19 ++-- serverbrowser/main.go | 199 ++++++++++++++-------------------- serverbrowser/server.go | 138 ++++++++++++----------- 14 files changed, 360 insertions(+), 408 deletions(-) diff --git a/gamestats/error.go b/gamestats/error.go index 8e16018..5b8fed7 100644 --- a/gamestats/error.go +++ b/gamestats/error.go @@ -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) + } } diff --git a/gamestats/main.go b/gamestats/main.go index 4149de5..43c7fab 100644 --- a/gamestats/main.go +++ b/gamestats/main.go @@ -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{} } } diff --git a/gpcm/error.go b/gpcm/error.go index 99c73af..9ec12bc 100644 --- a/gpcm/error.go +++ b/gpcm/error.go @@ -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) } } diff --git a/gpcm/friend.go b/gpcm/friend.go index a201918..555617d 100644 --- a/gpcm/friend.go +++ b/gpcm/friend.go @@ -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) { diff --git a/gpcm/kick.go b/gpcm/kick.go index b5b2778..a4fe21b 100644 --- a/gpcm/kick.go +++ b/gpcm/kick.go @@ -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) } } diff --git a/gpcm/login.go b/gpcm/login.go index 69b1773..8335629 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -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) { diff --git a/gpcm/main.go b/gpcm/main.go index bf08797..6fc4c3b 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -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 = "" } } diff --git a/gpcm/message.go b/gpcm/message.go index b822be5..80c75e7 100644 --- a/gpcm/message.go +++ b/gpcm/message.go @@ -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) diff --git a/gpsp/error.go b/gpsp/error.go index e828022..0410e42 100644 --- a/gpsp/error.go +++ b/gpsp/error.go @@ -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) + } } diff --git a/gpsp/main.go b/gpsp/main.go index 1d74233..affe99d 100644 --- a/gpsp/main.go +++ b/gpsp/main.go @@ -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))) } } } diff --git a/main.go b/main.go index 16716b7..2bb1e68 100644 --- a/main.go +++ b/main.go @@ -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) + } } } diff --git a/serverbrowser/filter.go b/serverbrowser/filter.go index 6ea30b7..e4b9b6d 100644 --- a/serverbrowser/filter.go +++ b/serverbrowser/filter.go @@ -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 } diff --git a/serverbrowser/main.go b/serverbrowser/main.go index 838f65a..dc5a047 100644 --- a/serverbrowser/main.go +++ b/serverbrowser/main.go @@ -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 } } diff --git a/serverbrowser/server.go b/serverbrowser/server.go index bcd18d7..931c7fb 100644 --- a/serverbrowser/server.go +++ b/serverbrowser/server.go @@ -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:]) }