From c9b983536454ecd7726ae1cc01ddd7fb68538ccd Mon Sep 17 00:00:00 2001 From: mkwcat Date: Tue, 24 Oct 2023 22:13:01 -0400 Subject: [PATCH] Base for sending messages between clients --- database/login.go | 5 +-- database/user.go | 12 ++--- gcsp/main.go | 100 +++++++++++++++++++++++++++++++++++++++--- gpcm/login.go | 12 +++++ gpcm/main.go | 37 +++++++++++----- gpcm/profile.go | 82 +++++++++++++++++++++++++++------- master/main.go | 7 ++- master/session.go | 66 ++++++++++++++++++++++++++-- matchmaking/main.go | 45 ++++++++++++++++--- matchmaking/server.go | 13 ++++++ 10 files changed, 321 insertions(+), 58 deletions(-) diff --git a/database/login.go b/database/login.go index f4dd389..6d943af 100644 --- a/database/login.go +++ b/database/login.go @@ -3,12 +3,11 @@ package database import ( "context" "database/sql" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/logrusorgru/aurora/v3" "strconv" "wwfc/common" "wwfc/logging" - - "github.com/jackc/pgx/v4/pgxpool" - "github.com/logrusorgru/aurora/v3" ) const ( diff --git a/database/user.go b/database/user.go index bd3f873..a95736d 100644 --- a/database/user.go +++ b/database/user.go @@ -3,11 +3,10 @@ package database import ( "context" "errors" - "math/rand" - "wwfc/common" - "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" + "math/rand" + "wwfc/common" ) const ( @@ -70,13 +69,14 @@ func deleteSession(pool *pgxpool.Pool, ctx context.Context, profileId uint32) { } } -func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) User { +func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) { user := User{} row := pool.QueryRow(ctx, GetUser, profileId) err := row.Scan(&user.UserId, &user.GsbrCode, &user.Password, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName) if err != nil { - panic(err) + return User{}, false } - return user + user.ProfileId = profileId + return user, true } diff --git a/gcsp/main.go b/gcsp/main.go index 8b0ca83..7de6b49 100644 --- a/gcsp/main.go +++ b/gcsp/main.go @@ -9,8 +9,11 @@ import ( "github.com/logrusorgru/aurora/v3" "io" "net" + "strconv" + "strings" "time" "wwfc/common" + "wwfc/database" "wwfc/logging" ) @@ -62,17 +65,20 @@ func StartServer() { func handleRequest(conn net.Conn) { defer conn.Close() + moduleName := "GCSP" + knownProfileId := uint32(0) + err := conn.(*net.TCPConn).SetKeepAlive(true) if err != nil { - logging.Notice("GCSP", "Unable to set keepalive:", err.Error()) + logging.Notice(moduleName, "Unable to set keepalive:", err.Error()) } err = conn.(*net.TCPConn).SetKeepAlivePeriod(time.Hour * 1000) if err != nil { - logging.Notice("GCSP", "Unable to set keepalive:", err.Error()) + logging.Notice(moduleName, "Unable to set keepalive:", err.Error()) } - logging.Notice("GCSP", "Connection established from", conn.RemoteAddr().String()) + logging.Notice(moduleName, "Connection established from", conn.RemoteAddr().String()) // Here we go into the listening loop for { @@ -91,17 +97,97 @@ func handleRequest(conn net.Conn) { } for _, command := range commands { - logging.Notice("GCSP", "Command:", aurora.Yellow(command.Command).String()) + logging.Notice(moduleName, "Command:", aurora.Yellow(command.Command).String()) switch command.Command { case "ka": conn.Write([]byte(`\ka\\final\`)) break + case "otherslist": - // This message needs to be sorted so we can't use a regular map - payload := `\otherslist\\oldone\\o\0\uniquenick\7me4ijr5sRMCJ3d9uhvh\final\` - conn.Write([]byte(payload)) + strProfileId, ok := command.OtherValues["profileid"] + if !ok { + logging.Notice(moduleName, "Missing profileid in otherslist") + return + } + + profileId, err := strconv.ParseUint(strProfileId, 10, 32) + if err != nil { + panic(err) + } + + if knownProfileId == 0 { + knownProfileId = uint32(profileId) + moduleName = "GCSP:" + strconv.FormatUint(profileId, 10) + moduleName += "/" + common.CalcFriendCodeString(uint32(profileId), "RMCJ") + } else if uint32(profileId) != knownProfileId { + logging.Notice(moduleName, "WARN: Mismatched profile ID in otherslist:", aurora.Cyan(strProfileId).String()) + } + + conn.Write([]byte(handleOthersList(moduleName, uint32(profileId), command))) break } } } } + +func handleOthersList(moduleName string, profileId uint32, command common.GameSpyCommand) string { + empty := `\otherslist\\final\` + + _, ok := command.OtherValues["sesskey"] + if !ok { + logging.Notice(moduleName, "Missing sesskey in otherslist") + return empty + } + + numopids, ok := command.OtherValues["numopids"] + if !ok { + logging.Notice(moduleName, "Missing numopids in otherslist") + return empty + } + + opids, ok := command.OtherValues["opids"] + if !ok { + logging.Notice(moduleName, "Missing opids in otherslist") + return empty + } + + _, ok = command.OtherValues["gamename"] + if !ok { + logging.Notice(moduleName, "Missing gamename in otherslist") + return empty + } + + numOpidsValue, err := strconv.Atoi(numopids) + if err != nil { + panic(err) + } + + opidsSplit := strings.Split(opids, "|") + if len(opidsSplit) != numOpidsValue { + logging.Notice(moduleName, "Mismatch opids length with numopids:", aurora.Cyan(len(opidsSplit)).String(), "!=", aurora.Cyan(numOpidsValue).String()) + return empty + } + + payload := `\otherslist\` + for _, strOtherId := range opidsSplit { + otherId, err := strconv.ParseUint(strOtherId, 10, 32) + if err != nil { + panic(err) + } + + // TODO: Perhaps this could be condensed into one database query + // Also TODO: Check if the players are actually friends + user, ok := database.GetProfile(pool, ctx, uint32(otherId)) + if !ok { + logging.Notice(moduleName, "Other ID doesn't exist:", aurora.Cyan(strOtherId).String()) + // If the profile doesn't exist then skip adding it + continue + } + + payload += `\o\` + strconv.FormatUint(uint64(user.ProfileId), 10) + payload += `\uniquenick\` + user.UniqueNick + } + + payload += `\oldone\\final\` + return payload +} diff --git a/gpcm/login.go b/gpcm/login.go index fe3547a..f263036 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -56,6 +56,18 @@ func Login(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, com } session.User = user + // Check to see if a session is already open with this profile ID + // TODO: Test this + mutex.Lock() + _, exists := sessions[session.User.ProfileId] + if exists { + mutex.Unlock() + // TODO: Return an error + log.Fatalf("Session with profile ID %u is already open", session.User.ProfileId) + } + sessions[session.User.ProfileId] = session + mutex.Unlock() + loginTicket := strings.Replace(base64.StdEncoding.EncodeToString([]byte(common.RandomString(16))), "=", "_", -1) // Now initiate the session _ = database.CreateSession(pool, ctx, session.User.ProfileId, loginTicket) diff --git a/gpcm/main.go b/gpcm/main.go index 556b58f..fc43e3c 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -9,6 +9,7 @@ import ( "github.com/logrusorgru/aurora/v3" "io" "net" + "sync" "time" "wwfc/common" "wwfc/database" @@ -16,14 +17,19 @@ import ( ) type GameSpySession struct { + Conn net.Conn User database.User ModuleName string LoggedIn bool + Status string } var ( ctx = context.Background() pool *pgxpool.Pool + // I would use a sync.Map instead of the map mutex combo, but this performs better. + sessions = map[uint32]*GameSpySession{} + mutex = sync.RWMutex{} ) func StartServer() { @@ -64,15 +70,27 @@ func StartServer() { } } +func closeSession(session *GameSpySession) { + mutex.Lock() + defer mutex.Unlock() + + session.Conn.Close() + if session.LoggedIn { + delete(sessions, session.User.ProfileId) + } +} + // Handles incoming requests. func handleRequest(conn net.Conn) { session := GameSpySession{ + Conn: conn, User: database.User{}, ModuleName: "GPCM", LoggedIn: false, + Status: "", } - defer conn.Close() + defer closeSession(&session) // Set session ID and challenge challenge := common.RandomString(10) @@ -118,7 +136,7 @@ func handleRequest(conn net.Conn) { if session.LoggedIn == false { if command.Command != "login" { - logging.Notice(session.ModuleName, "Attempt to run command before login!!!") + logging.Notice(session.ModuleName, "Attempt to run command before login!") return } @@ -139,24 +157,19 @@ func handleRequest(conn net.Conn) { return case "updatepro": - UpdateProfile(&session, pool, ctx, command) + updateProfile(&session, pool, ctx, command) break case "status": - logging.Notice(session.ModuleName, "statstring:", aurora.Cyan(command.OtherValues["statstring"]).String()) - if command.OtherValues["locstring"] == "" { - logging.Notice(session.ModuleName, "locstring: (empty)") - } else { - logging.Notice(session.ModuleName, "locstring:", aurora.Cyan(command.OtherValues["locstring"]).String()) - } + setStatus(&session, pool, ctx, command) break case "addbuddy": - AddFriend(&session, pool, ctx, command) + addFriend(&session, pool, ctx, command) break case "delbuddy": - RemoveFriend(&session, pool, ctx, command) + removeFriend(&session, pool, ctx, command) break } } @@ -168,7 +181,7 @@ func handleRequest(conn net.Conn) { break case "getprofile": - payload := GetProfile(&session, pool, ctx, command) + payload := getProfile(&session, pool, ctx, command) conn.Write([]byte(payload)) break } diff --git a/gpcm/profile.go b/gpcm/profile.go index 9d7301a..cc59b9c 100644 --- a/gpcm/profile.go +++ b/gpcm/profile.go @@ -5,19 +5,23 @@ import ( "github.com/jackc/pgx/v4/pgxpool" "github.com/logrusorgru/aurora/v3" "strconv" + "strings" "wwfc/common" "wwfc/database" "wwfc/logging" ) -func GetProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) string { +func getProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) string { strProfileId := command.OtherValues["profileid"] profileId, err := strconv.ParseInt(strProfileId, 10, 32) if err != nil { panic(err) } - user := database.GetProfile(pool, ctx, uint32(profileId)) + user, ok := database.GetProfile(pool, ctx, uint32(profileId)) + if !ok { + return `\pi\\final\` + } _ = common.RandomHexString(32) return common.CreateGameSpyMessage(common.GameSpyCommand{ @@ -28,7 +32,7 @@ func GetProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context "nick": user.UniqueNick, "userid": strconv.FormatInt(user.UserId, 10), "email": user.Email, - "sig": "b126556e5ee62d4da9629dfad0f6b2a8", + "sig": common.RandomHexString(32), "uniquenick": user.UniqueNick, "firstname": user.FirstName, "lastname": user.LastName, @@ -41,7 +45,7 @@ func GetProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context }) } -func UpdateProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { +func updateProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { var firstName string var lastName string if v, ok := command.OtherValues["firstname"]; ok { @@ -55,7 +59,62 @@ func UpdateProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Cont database.UpdateUser(pool, ctx, firstName, lastName, session.User.UserId) } -func AddFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { +func setStatus(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { + status := command.CommandValue + + statstring, ok := command.OtherValues["statstring"] + if !ok { + logging.Notice(session.ModuleName, "Missing statstring") + statstring = "" + } else { + if statstring == "" { + logging.Notice(session.ModuleName, "statstring: (empty)") + } else { + logging.Notice(session.ModuleName, "statstring:", aurora.Cyan(statstring).String()) + } + } + + locstring, ok := command.OtherValues["locstring"] + if !ok { + logging.Notice(session.ModuleName, "Missing locstring") + locstring = "" + } else { + if locstring == "" { + logging.Notice(session.ModuleName, "locstring: (empty)") + } else { + logging.Notice(session.ModuleName, "locstring:", aurora.Cyan(locstring).String()) + } + } + + // Get the IP address for the status msg + var rawIP int + for i, s := range strings.Split(strings.Split(session.Conn.RemoteAddr().String(), ":")[0], ".") { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + + rawIP |= val << (24 - i*8) + } + + // TODO: Check if this handles negative numbers correctly + ip := strconv.FormatInt(int64(int32(rawIP)), 10) + + friendStatus := common.CreateGameSpyMessage(common.GameSpyCommand{ + Command: "bm", + CommandValue: "100", + OtherValues: map[string]string{ + "f": strconv.FormatUint(uint64(session.User.ProfileId), 10), + "msg": "|s|" + status + "|ss|" + statstring + "|ls|" + locstring + "|ip|" + ip + "|p|0|qm|0", + }, + }) + + mutex.Lock() + session.Status = friendStatus + mutex.Unlock() +} + +func addFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { strProfileId := command.OtherValues["newprofileid"] profileId, err := strconv.ParseUint(strProfileId, 10, 32) if err != nil { @@ -67,17 +126,6 @@ func AddFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, // TODO } -func RemoveFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { +func removeFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { // TODO } - -func createStatus() string { - return common.CreateGameSpyMessage(common.GameSpyCommand{ - Command: "bm", - CommandValue: "100", - OtherValues: map[string]string{ - "f": "5", - "msg": "|s|0|ss|Offline", - }, - }) -} diff --git a/master/main.go b/master/main.go index 9e8bd3e..6e15e9d 100644 --- a/master/main.go +++ b/master/main.go @@ -13,8 +13,9 @@ import ( var ( // I would use a sync.Map instead of the map mutex combo, but this performs better. - sessions = map[uint32]*Session{} - mutex = sync.RWMutex{} + sessions = map[uint32]*Session{} + mutex = sync.RWMutex{} + masterConn net.PacketConn ) const ( @@ -41,6 +42,8 @@ func StartServer() { panic(err) } + masterConn = conn + // Close the listener when the application closes. defer conn.Close() logging.Notice("MASTER", "Listening on", address) diff --git a/master/session.go b/master/session.go index 206c515..cc153e9 100644 --- a/master/session.go +++ b/master/session.go @@ -1,7 +1,11 @@ package master import ( + "encoding/binary" "github.com/logrusorgru/aurora/v3" + "net" + "strconv" + "strings" "time" "wwfc/logging" ) @@ -14,6 +18,7 @@ const ( type Session struct { SessionID uint32 + Addr net.Addr Challenge string Authenticated bool LastKeepAlive int64 @@ -43,22 +48,23 @@ func setSessionData(sessionId uint32, payload map[string]string) Session { Data: payload, } sessions[sessionId] = &data + return data } session.Data = payload session.LastKeepAlive = time.Now().Unix() + return *session } // Get a copy of the list of servers func GetSessionServers() []map[string]string { - mutex.Lock() - defer mutex.Unlock() - + var servers []map[string]string currentTime := time.Now().Unix() - var servers []map[string]string + mutex.Lock() + defer mutex.Unlock() for _, session := range sessions { if !session.Authenticated { continue @@ -74,3 +80,55 @@ func GetSessionServers() []map[string]string { return servers } + +func SendClientMessage(destIP string, message []byte) { + var rawIP int + for i, s := range strings.Split(strings.Split(destIP, ":")[0], ".") { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + + rawIP |= val << (24 - i*8) + } + + // TODO: Check if this handles negative numbers correctly + destIPIntStr := strconv.FormatInt(int64(int32(rawIP)), 10) + + currentTime := time.Now().Unix() + + mutex.Lock() + // Find the session with the IP + for _, session := range sessions { + if !session.Authenticated { + continue + } + + // If the last keep alive was over a minute ago then consider the server unreachable + if session.LastKeepAlive < currentTime-60 { + continue + } + + if session.Data["publicip"] == destIPIntStr { + // Found the client, now send the message + payload := createResponseHeader(ClientMessageRequest, session.SessionID) + mutex.Unlock() + + payload = append(payload, []byte{0, 0, 0, 0}...) + binary.BigEndian.PutUint32(payload[len(payload)-4:], uint32(time.Now().Unix())) + payload = append(payload, message...) + + destIPAddr, err := net.ResolveUDPAddr("udp", destIP) + if err != nil { + panic(err) + } + + logging.Notice("MASTER", "Sending message...") + masterConn.WriteTo(payload, destIPAddr) + return + } + } + mutex.Unlock() + + logging.Notice("MASTER", "Could not find destination server") +} diff --git a/matchmaking/main.go b/matchmaking/main.go index 296c464..54958f8 100644 --- a/matchmaking/main.go +++ b/matchmaking/main.go @@ -3,6 +3,7 @@ package matchmaking import ( "bufio" "context" + "encoding/binary" "errors" "fmt" "github.com/jackc/pgx/v4/pgxpool" @@ -95,20 +96,50 @@ func handleRequest(conn net.Conn) { } // Here we go into the listening loop + bufferSize := 0 + packetSize := uint16(0) + buffer := []byte{} for { - buffer := make([]byte, 1024) - _, err := bufio.NewReader(conn).Read(buffer) - if err != nil { - if errors.Is(err, io.EOF) { - // Client closed connection, terminate. + // Remove stale data and remake the buffer + buffer = append(buffer[packetSize:], make([]byte, 1024-packetSize)...) + bufferSize -= int(packetSize) + packetSize = 0 + + for { + if bufferSize > 2 { + packetSize = binary.BigEndian.Uint16(buffer[:2]) + if packetSize < 3 { + logging.Notice(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.Notice(ModuleName, "Connection closed") + return + } + + logging.Notice(ModuleName, "Connection error") return } + + bufferSize += readSize } + logging.Notice(ModuleName, "packet size:", aurora.Cyan(packetSize).String()) + logging.Notice(ModuleName, "buffer size:", aurora.Cyan(bufferSize).String()) + switch buffer[2] { case ServerListRequest: logging.Notice(ModuleName, "Command:", aurora.Yellow("SERVER_LIST_REQUEST").String()) - handleServerListRequest(conn, buffer) + handleServerListRequest(conn, buffer[:packetSize]) break case ServerInfoRequest: @@ -117,7 +148,7 @@ func handleRequest(conn net.Conn) { case SendMessageRequest: logging.Notice(ModuleName, "Command:", aurora.Yellow("SEND_MESSAGE_REQUEST").String()) - // TODO + handleSendMessageRequest(conn, buffer[:packetSize]) break case KeepaliveReply: diff --git a/matchmaking/server.go b/matchmaking/server.go index e93e562..d0720cc 100644 --- a/matchmaking/server.go +++ b/matchmaking/server.go @@ -2,6 +2,7 @@ package matchmaking import ( "encoding/binary" + "fmt" "github.com/logrusorgru/aurora/v3" "net" "strconv" @@ -238,3 +239,15 @@ func handleServerListRequest(conn net.Conn, buffer []byte) { // Write the encrypted reply conn.Write(common.EncryptTypeX([]byte("9r3Rmy"), challenge, output)) } + +func handleSendMessageRequest(conn net.Conn, buffer []byte) { + // Read destination IP from buffer + destIP := fmt.Sprintf("%d.%d.%d.%d:%d", buffer[3], buffer[4], buffer[5], buffer[6], binary.BigEndian.Uint16(buffer[7:9])) + + logging.Notice(ModuleName, "Send message from", aurora.Cyan(conn.RemoteAddr()).String(), "to", aurora.Cyan(destIP).String()) + + // TODO: Perform basic packet verification + // TODO SECURITY: Check if the selected IP is actually online, or at least make sure it's not a local IP + + master.SendClientMessage(destIP, buffer[9:]) +}