GPCM: Handle friend requests

This commit is contained in:
mkwcat
2023-10-31 06:40:57 -04:00
parent 1c8166959e
commit b645caf448
6 changed files with 377 additions and 94 deletions

View File

@@ -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
}

151
gpcm/error.go Normal file
View File

@@ -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)))
}

183
gpcm/friend.go Normal file
View File

@@ -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()
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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