From b645caf4484290667b7c08a61221aba5c4f46024 Mon Sep 17 00:00:00 2001 From: mkwcat Date: Tue, 31 Oct 2023 06:40:57 -0400 Subject: [PATCH] GPCM: Handle friend requests --- database/user.go | 11 ++- gpcm/error.go | 151 ++++++++++++++++++++++++++++++++++++++ gpcm/friend.go | 183 +++++++++++++++++++++++++++++++++++++++++++++++ gpcm/main.go | 11 +++ gpcm/profile.go | 107 +++++---------------------- gpsp/main.go | 8 ++- 6 files changed, 377 insertions(+), 94 deletions(-) create mode 100644 gpcm/error.go create mode 100644 gpcm/friend.go diff --git a/database/user.go b/database/user.go index a95736d..c5fd76d 100644 --- a/database/user.go +++ b/database/user.go @@ -11,7 +11,7 @@ import ( const ( InsertUser = `INSERT INTO users (user_id, gsbrcd, password, email, unique_nick) VALUES ($1, $2, $3, $4, $5) RETURNING profile_id` - UpdateUserTable = `UPDATE users SET firstname = $1, lastname = $2 WHERE user_id = $3` + UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END WHERE profile_id = $1 RETURNING user_id, gsbrcd, password, email, unique_nick, firstname, lastname` GetUser = `SELECT user_id, gsbrcd, password, email, unique_nick, firstname, lastname FROM users WHERE profile_id = $1` CreateUserSession = `INSERT INTO sessions (session_key, profile_id, login_ticket) VALUES ($1, $2, $3)` DoesUserExist = `SELECT EXISTS(SELECT 1 FROM users WHERE user_id = $1 AND gsbrcd = $2)` @@ -42,13 +42,18 @@ func GetUniqueUserID() int64 { return rand.Int63n(0x80000000000) } -func UpdateUser(pool *pgxpool.Pool, ctx context.Context, firstName string, lastName string, userId int64) User { +func UpdateProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32, data map[string]string) User { + firstName, firstNameExists := data["firstname"] + lastName, lastNameExists := data["lastname"] + user := User{} - _, err := pool.Exec(ctx, UpdateUserTable, firstName, lastName, userId) + row := pool.QueryRow(ctx, UpdateUserTable, profileId, firstName, firstNameExists, lastName, lastNameExists) + err := row.Scan(&user.UserId, &user.GsbrCode, &user.Password, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName) if err != nil { panic(err) } + user.ProfileId = profileId return user } diff --git a/gpcm/error.go b/gpcm/error.go new file mode 100644 index 0000000..fc597f8 --- /dev/null +++ b/gpcm/error.go @@ -0,0 +1,151 @@ +package gpcm + +import ( + "strconv" + "wwfc/common" +) + +var gpcmErrors = map[int]string{ + // General errors + 0: "There was an unknown error.", + 1: "There was an error parsing an incoming request.", + 2: "This request cannot be processed because you are not logged in.", + 3: "This request cannot be processed because the session key is invalid.", + 4: "This request cannot be processed because of a database error.", + 5: "There was an error connecting a network socket.", + 6: "This profile has been disconnected by another login.", + 7: "The server has closed the connection.", + 8: "There was a problem with the UDP layer.", + + // Log-in errors + 256: "There was an error logging in to the GP backend.", + 257: "The login attempt timed out.", + 258: "The nickname provided was incorrect.", + 259: "The email address provided was incorrect.", + 260: "The password provided is incorrect.", + 261: "The profile provided was incorrect.", + 262: "The profile has been deleted.", + 263: "The server has refused the connection.", + 264: "The server could not be authenticated.", + 265: "The uniquenick provided is incorrect.", + 266: "There was an error validating the pre-authentication.", + 267: "The login ticket was unable to be validated.", + 268: "The login ticket had expired and could not be used.", + + // New user errors + 512: "There was an error creating a new user.", + 513: "A profile with that nick already exists.", + 514: "The password does not match the email address.", + 515: "The uniquenick is invalid.", + 516: "The uniquenick is already in use.", + + // User updating errors + 768: "There was an error updating the user information.", + 769: "A user with the email adress provided already exists.", + + // New profile errors + 1024: "There was an error creating a new profile.", + 1025: "The nickname to be replaced does not exist.", + 1026: "A profile with the nickname provided already exists.", + + // Updating profile errors + 1280: "There was an error updating the profile information. ", + 1281: "A user with the nickname provided already exists.", + + // Buddy list errors + 1536: "There was an error adding a buddy.", + 1537: "The profile requesting to add a buddy is invalid.", + 1538: "The profile requested is invalid.", + 1539: "The profile requested is already a buddy.", + 1540: "The profile requested is on the local profile's block list.", + 1541: "The profile requested is blocking you.", + + // Errors while Buddy add auth + 1792: "There was an error authorizing an add buddy request.", + 1793: "The profile being authorized is invalid.", + 1794: "The signature for the authorization is invalid.", + 1795: "The profile requesting authorization is on a block list.", + 1796: "The profile requested is blocking you.", + + // Status errors + 2048: "There was an error with the status string.", + + // Buddy message errors + 2304: "There was an error sending a buddy message.", + 2305: "The profile the message was to be sent to is not a buddy.", + 2306: "The profile does not support extended info keys.", + 2307: "The buddy to send a message to is offline.", + + // Profile information getting errors + 2560: "There was an error getting profile info.", + 2561: "The profile info was requested on is invalid.", + + // Deleting buddy errors + 2816: "There was an error deleting the buddy.", + 2817: "The buddy to be deleted is not a buddy.", + + // Deleting profile errors + 3072: "There was an error deleting the profile.", + 3073: "The last profile cannot be deleted.", + + // Profile searching errors + 3328: "There was an error searching for a profile.", + 3329: "The search attempt failed to connect to the server.", + 3330: "The search did not return in a timely fashion.", + + // User checking errors + 3584: "There was an error checking the user account.", + 3585: "No account exists with the provided e-mail address.", + 3586: "No such profile exists for the provided e-mail adress.", + 3587: "The password is incorrect.", + + // Revoking buddy errors + 3840: "There was an error revoking the buddy.", + 3841: "You are not a buddy of the profile.", + + // Registering new unique nick errors + 4096: "There was an error registering the uniquenick.", + 4097: "The uniquenick is already taken.", + 4098: "The uniquenick is reserved.", + 4099: "Tried to register a nick with no namespace set.", + + // CD key errors + 4352: "There was an error registering the cdkey.", + 4353: "The cdkey is invalid.", + 4354: "The profile has already been registered with a different cdkey.", + 4355: "The cdkey has already been registered to another profile.", + + // Adding to block list errors + 4608: "There was an error adding the player to the blocked list.", + 4609: "The profile specified is already blocked.", + + // Removing from block list errors + 4864: "There was an error removing the player from the blocked list.", + 4865: "The profile specified was not a member of the blocked list.", +} + +func createGameSpyError(err int) string { + errMsg, ok := gpcmErrors[err] + if !ok { + errMsg = "" + } + + command := common.GameSpyCommand{ + Command: "error", + CommandValue: "", + OtherValues: map[string]string{ + "err": strconv.Itoa(err), + "errmsg": errMsg, + }, + } + + if err < 300 { + command.OtherValues["fatal"] = "" + } + + return common.CreateGameSpyMessage(command) +} + +func (g *GameSpySession) replyError(err int) { + g.Conn.Write([]byte(createGameSpyError(err))) +} diff --git a/gpcm/friend.go b/gpcm/friend.go new file mode 100644 index 0000000..b978290 --- /dev/null +++ b/gpcm/friend.go @@ -0,0 +1,183 @@ +package gpcm + +import ( + "context" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/logrusorgru/aurora/v3" + "strconv" + "strings" + "wwfc/common" + "wwfc/logging" +) + +func (g *GameSpySession) isFriendAdded(profileId uint32) bool { + for _, storedPid := range g.FriendList { + if storedPid == profileId { + return true + } + } + return false +} + +func sendMessageToSession(msgType string, from uint32, session *GameSpySession, msg string) { + message := common.CreateGameSpyMessage(common.GameSpyCommand{ + Command: "bm", + CommandValue: msgType, + OtherValues: map[string]string{ + "f": strconv.FormatUint(uint64(from), 10), + "msg": msg, + }, + }) + session.Conn.Write([]byte(message)) +} + +func sendMessageToProfileId(msgType string, from uint32, to uint32, msg string) bool { + if session, ok := sessions[to]; ok && session.LoggedIn { + sendMessageToSession(msgType, from, session, msg) + return true + } + + logging.Info("GPCM", "Destination", aurora.Cyan(to), "from", aurora.Cyan(from), "is not online") + return false +} + +func (g *GameSpySession) addFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { + strNewProfileId := command.OtherValues["newprofileid"] + newProfileId, err := strconv.ParseUint(strNewProfileId, 10, 32) + if err != nil { + g.replyError(1538) + return + } + + fc := common.CalcFriendCodeString(uint32(newProfileId), "RMCJ") + logging.Notice(g.ModuleName, "Add friend:", aurora.Cyan(strNewProfileId), aurora.Cyan(fc)) + + if g.isFriendAdded(uint32(newProfileId)) { + g.replyError(1539) + return + } + + mutex.Lock() + defer mutex.Unlock() + + // TODO: Add a limit + g.FriendList = append(g.FriendList, uint32(newProfileId)) + sendMessageToProfileId("2", g.User.ProfileId, uint32(newProfileId), "\r\n\r\n|signed|"+common.RandomHexString(32)) +} + +func (g *GameSpySession) removeFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { + // TODO +} + +func (g *GameSpySession) bestieMessage(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { + if command.CommandValue != "1" { + logging.Notice(g.ModuleName, "Received unknown bestie message type:", aurora.Cyan(command.CommandValue)) + return + } + + strToProfileId := command.OtherValues["t"] + toProfileId, err := strconv.ParseUint(strToProfileId, 10, 32) + if err != nil { + g.replyError(2304) + return + } + + mutex.Lock() + defer mutex.Unlock() + + if toSession, ok := sessions[uint32(toProfileId)]; ok && toSession.LoggedIn { + // TODO: Check if mutual friends + // TODO SECURITY: Sanitize message (there's a stack overflow exploit in DWC here) + sendMessageToSession("1", g.User.ProfileId, toSession, command.OtherValues["msg"]) + return + } + + g.replyError(2307) +} + +func (g *GameSpySession) authAddFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { + strFromProfileId := command.OtherValues["fromprofileid"] + fromProfileId, err := strconv.ParseUint(strFromProfileId, 10, 32) + if err != nil { + g.replyError(1793) + return + } + + mutex.Lock() + defer mutex.Unlock() + + sendMessageToProfileId("4", g.User.ProfileId, uint32(fromProfileId), "") + // Send status as well + if session, ok := sessions[uint32(fromProfileId)]; ok && session.LoggedIn && g.Status != "" { + // TODO: Check if on friend list + session.Conn.Write([]byte(g.Status)) + } +} + +func (g *GameSpySession) setStatus(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { + status := command.CommandValue + + statstring, ok := command.OtherValues["statstring"] + if !ok { + logging.Notice(g.ModuleName, "Missing statstring") + statstring = "" + } else { + if statstring == "" { + logging.Notice(g.ModuleName, "statstring: (empty)") + } else { + logging.Notice(g.ModuleName, "statstring:", aurora.Cyan(statstring)) + } + } + + locstring, ok := command.OtherValues["locstring"] + if !ok { + logging.Notice(g.ModuleName, "Missing locstring") + locstring = "" + } else { + if locstring == "" { + logging.Notice(g.ModuleName, "locstring: (empty)") + } else { + logging.Notice(g.ModuleName, "locstring:", aurora.Cyan(locstring)) + } + } + + // Get the IP address for the status msg + var rawIP int + for i, s := range strings.Split(strings.Split(g.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(g.User.ProfileId), 10), + "msg": "|s|" + status + "|ss|" + statstring + "|ls|" + locstring + "|ip|" + ip + "|p|0|qm|0", + }, + }) + + mutex.Lock() + g.LocString = locstring + g.Status = friendStatus + + for _, storedPid := range g.FriendList { + if session, ok := sessions[uint32(storedPid)]; ok && session.LoggedIn { + if !session.isFriendAdded(g.User.ProfileId) { + continue + } + + // TODO: Check if on friend list + session.Conn.Write([]byte(friendStatus)) + } + } + + mutex.Unlock() +} diff --git a/gpcm/main.go b/gpcm/main.go index 457a236..3bca78b 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -22,6 +22,8 @@ type GameSpySession struct { ModuleName string LoggedIn bool Status string + LocString string + FriendList []uint32 } var ( @@ -88,6 +90,7 @@ func handleRequest(conn net.Conn) { ModuleName: "GPCM", LoggedIn: false, Status: "", + FriendList: []uint32{}, } defer session.closeSession() @@ -171,6 +174,14 @@ func handleRequest(conn net.Conn) { case "delbuddy": session.removeFriend(pool, ctx, command) break + + case "bm": + session.bestieMessage(pool, ctx, command) + break + + case "authadd": + session.authAddFriend(pool, ctx, command) + break } } diff --git a/gpcm/profile.go b/gpcm/profile.go index 0387300..8627965 100644 --- a/gpcm/profile.go +++ b/gpcm/profile.go @@ -5,7 +5,6 @@ import ( "github.com/jackc/pgx/v4/pgxpool" "github.com/logrusorgru/aurora/v3" "strconv" - "strings" "wwfc/common" "wwfc/database" "wwfc/logging" @@ -13,17 +12,26 @@ import ( func (g *GameSpySession) getProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) string { strProfileId := command.OtherValues["profileid"] - profileId, err := strconv.ParseInt(strProfileId, 10, 32) + profileId, err := strconv.ParseUint(strProfileId, 10, 32) if err != nil { - panic(err) + return createGameSpyError(2560) } - user, ok := database.GetProfile(pool, ctx, uint32(profileId)) - if !ok { - return `\pi\\final\` + logging.Notice(g.ModuleName, "Looking up the profile of", aurora.Cyan(profileId).String()) + + user := database.User{} + locstring := "" + + mutex.Lock() + if session, ok := sessions[uint32(profileId)]; ok && session.LoggedIn { + locstring = session.LocString + user = session.User + mutex.Unlock() + } else { + mutex.Unlock() + user, _ = database.GetProfile(pool, ctx, uint32(profileId)) } - _ = common.RandomHexString(32) return common.CreateGameSpyMessage(common.GameSpyCommand{ Command: "pi", CommandValue: "", @@ -39,93 +47,12 @@ func (g *GameSpySession) getProfile(pool *pgxpool.Pool, ctx context.Context, com "pid": "11", "lon": "0.000000", "lat": "0.000000", - "loc": "", + "loc": locstring, "id": command.OtherValues["id"], }, }) } func (g *GameSpySession) updateProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { - var firstName string - var lastName string - if v, ok := command.OtherValues["firstname"]; ok { - firstName = v - } - - if v, ok := command.OtherValues["lastname"]; ok { - lastName = v - } - - database.UpdateUser(pool, ctx, firstName, lastName, g.User.UserId) -} - -func (g *GameSpySession) setStatus(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { - status := command.CommandValue - - statstring, ok := command.OtherValues["statstring"] - if !ok { - logging.Notice(g.ModuleName, "Missing statstring") - statstring = "" - } else { - if statstring == "" { - logging.Notice(g.ModuleName, "statstring: (empty)") - } else { - logging.Notice(g.ModuleName, "statstring:", aurora.Cyan(statstring)) - } - } - - locstring, ok := command.OtherValues["locstring"] - if !ok { - logging.Notice(g.ModuleName, "Missing locstring") - locstring = "" - } else { - if locstring == "" { - logging.Notice(g.ModuleName, "locstring: (empty)") - } else { - logging.Notice(g.ModuleName, "locstring:", aurora.Cyan(locstring)) - } - } - - // Get the IP address for the status msg - var rawIP int - for i, s := range strings.Split(strings.Split(g.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(g.User.ProfileId), 10), - "msg": "|s|" + status + "|ss|" + statstring + "|ls|" + locstring + "|ip|" + ip + "|p|0|qm|0", - }, - }) - - mutex.Lock() - g.Status = friendStatus - mutex.Unlock() -} - -func (g *GameSpySession) addFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { - strProfileId := command.OtherValues["newprofileid"] - profileId, err := strconv.ParseUint(strProfileId, 10, 32) - if err != nil { - panic(err) - } - - fc := common.CalcFriendCodeString(uint32(profileId), "RMCJ") - logging.Notice(g.ModuleName, "Add friend:", aurora.Cyan(strProfileId), aurora.Cyan(fc)) - // TODO -} - -func (g *GameSpySession) removeFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) { - // TODO + g.User = database.UpdateProfile(pool, ctx, g.User.ProfileId, command.OtherValues) } diff --git a/gpsp/main.go b/gpsp/main.go index a0bb61d..5c2790d 100644 --- a/gpsp/main.go +++ b/gpsp/main.go @@ -162,7 +162,13 @@ func handleOthersList(moduleName string, profileId uint32, command common.GameSp panic(err) } - opidsSplit := strings.Split(opids, "|") + opidsSplit := []string{} + if strings.Contains(opids, "|") { + opidsSplit = strings.Split(opids, "|") + } else if opids != "" && opids != "0" { + opidsSplit = append(opidsSplit, opids) + } + if len(opidsSplit) != numOpidsValue { logging.Error(moduleName, "Mismatch opids length with numopids:", aurora.Cyan(len(opidsSplit)), "!=", aurora.Cyan(numOpidsValue)) return empty