diff --git a/go.mod b/go.mod index ff39533..c551596 100644 --- a/go.mod +++ b/go.mod @@ -13,15 +13,15 @@ require ( require ( github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.3 // indirect - github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect + github.com/jackc/pgconn v1.14.3 + github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgtype v1.14.4 // indirect github.com/jackc/puddle v1.3.0 // indirect - github.com/linkdata/deadlock v0.5.5 // indirect + github.com/linkdata/deadlock v0.5.5 github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/text v0.31.0 // indirect diff --git a/gpcm/error.go b/gpcm/error.go index dcdbbb4..18d8f0c 100644 --- a/gpcm/error.go +++ b/gpcm/error.go @@ -419,67 +419,77 @@ func (err GPError) GetMessageTranslate(gameName string, region byte, lang byte, command.OtherValues["fatal"] = "" } - if err.Fatal && err.WWFCMessage.ErrorCode != 0 { - switch gameName { - case "mariokartwii": - reason := err.Reason - wwfcMessage := err.WWFCMessage + if !err.Fatal || err.WWFCMessage.ErrorCode == 0 { + return common.CreateGameSpyMessage(command), err.WWFCMessage.ErrorCode + } - if reason == "" { - reason = "None provided." + errMsg := "" + reason := "" + var wwfcMessage *WWFCErrorMessage + switch gameName { + case "mariokartwii": + reason = err.Reason + wwfcMessage = &err.WWFCMessage - engMsg := err.WWFCMessage.MessageRMC[LangEnglish] + if reason == "" { + reason = "None provided." - if engMsg == WWFCMsgKickedCustom.MessageRMC[LangEnglish] { - wwfcMessage = WWFCMsgKickedModerator - } else if engMsg == WWFCMsgProfileRestrictedCustom.MessageRMC[LangEnglish] { - wwfcMessage = WWFCMsgProfileRestricted - } else if engMsg == WWFCMsgProfileRestrictedNowCustom.MessageRMC[LangEnglish] { - wwfcMessage = WWFCMsgProfileRestrictedNow - } - } - - errMsg := wwfcMessage.MessageRMC[lang] - if errMsg == "" && lang != LangEnglish { - if lang == LangSpanishEU { - errMsg = wwfcMessage.MessageRMC[LangSpanish] - } else if lang == LangFrenchEU { - errMsg = wwfcMessage.MessageRMC[LangFrench] - } - - if errMsg == "" { - errMsg = wwfcMessage.MessageRMC[LangEnglish] - } - } - - if errMsg != "" { - errMsg = fmt.Sprintf(errMsg, wwfcMessage.ErrorCode, ngid, reason) - errMsgUTF16 := utf16.Encode([]rune(errMsg)) - errMsgByteArray := common.UTF16ToByteArray(errMsgUTF16) - command.OtherValues["wl:errmsg"] = common.Base64DwcEncoding.EncodeToString(errMsgByteArray) + switch engMsg := err.WWFCMessage.MessageRMC[LangEnglish]; engMsg { + case WWFCMsgKickedCustom.MessageRMC[LangEnglish]: + wwfcMessage = &WWFCMsgKickedModerator + case WWFCMsgProfileRestrictedCustom.MessageRMC[LangEnglish]: + wwfcMessage = &WWFCMsgProfileRestricted + case WWFCMsgProfileRestrictedNowCustom.MessageRMC[LangEnglish]: + wwfcMessage = &WWFCMsgProfileRestrictedNow } } - command.OtherValues["wl:err"] = strconv.Itoa(err.WWFCMessage.ErrorCode) + errMsg = wwfcMessage.MessageRMC[lang] + if errMsg == "" || lang == LangEnglish { + break + } + + switch lang { + case LangSpanishEU: + errMsg = wwfcMessage.MessageRMC[LangSpanish] + case LangFrenchEU: + errMsg = wwfcMessage.MessageRMC[LangFrench] + } + + if errMsg == "" { + errMsg = wwfcMessage.MessageRMC[LangEnglish] + } } + if errMsg != "" && wwfcMessage != nil { + errMsg = fmt.Sprintf(errMsg, wwfcMessage.ErrorCode, ngid, reason) + errMsgUTF16 := utf16.Encode([]rune(errMsg)) + errMsgByteArray := common.UTF16ToByteArray(errMsgUTF16) + command.OtherValues["wl:errmsg"] = common.Base64DwcEncoding.EncodeToString(errMsgByteArray) + } + + command.OtherValues["wl:err"] = strconv.Itoa(err.WWFCMessage.ErrorCode) + return common.CreateGameSpyMessage(command), err.WWFCMessage.ErrorCode } -func (g *GameSpySession) replyError(err GPError) { - logging.Error(g.ModuleName, "Reply error:", err.ErrorString) +func (g *GameSpySession) replyError(gpErr GPError) { + logging.Error(g.ModuleName, "Reply error:", gpErr.ErrorString) if !g.LoginInfoSet { - msg := err.GetMessage() + msg := gpErr.GetMessage() // logging.Info(g.ModuleName, "Sending error message:", msg) - common.SendPacket(ServerName, g.ConnIndex, []byte(msg)) - if err.Fatal { - common.CloseConnection(ServerName, g.ConnIndex) + err := common.SendPacket(ServerName, g.ConnIndex, []byte(msg)) + if gpErr.Fatal || err != nil { + if err != nil { + logging.Error("GPCM", "Failed to send packet:", err) + } + common.ShouldNotError(common.CloseConnection(ServerName, g.ConnIndex)) } logging.Event("gpcm_returned_error", map[string]any{ "profile_id": g.User.ProfileId, - "error_code": err.ErrorCode, - "error_string": err.ErrorString, - "fatal": err.Fatal, + "error_code": gpErr.ErrorCode, + "error_string": gpErr.ErrorString, + "fatal": gpErr.Fatal, }) return } @@ -489,18 +499,21 @@ func (g *GameSpySession) replyError(err GPError) { deviceId = g.User.NgDeviceId[0] } - msg, wwfcErrorCode := err.GetMessageTranslate(g.GameName, g.Region, g.Language, g.ConsoleFriendCode, deviceId) + msg, wwfcErrorCode := gpErr.GetMessageTranslate(g.GameName, g.Region, g.Language, g.ConsoleFriendCode, deviceId) // logging.Info(g.ModuleName, "Sending error message:", msg) - common.SendPacket(ServerName, g.ConnIndex, []byte(msg)) - if err.Fatal { - common.CloseConnection(ServerName, g.ConnIndex) + err := common.SendPacket(ServerName, g.ConnIndex, []byte(msg)) + if gpErr.Fatal || err != nil { + if err != nil { + logging.Error("GPCM", "Failed to send packet:", err) + } + common.ShouldNotError(common.CloseConnection(ServerName, g.ConnIndex)) } logging.Event("gpcm_returned_error", map[string]any{ "profile_id": g.User.ProfileId, - "error_code": err.ErrorCode, - "error_string": err.ErrorString, - "fatal": err.Fatal, + "error_code": gpErr.ErrorCode, + "error_string": gpErr.ErrorString, + "fatal": gpErr.Fatal, "wiilink_error_code": wwfcErrorCode, }) } diff --git a/gpcm/friend.go b/gpcm/friend.go index d762a8e..81352a4 100644 --- a/gpcm/friend.go +++ b/gpcm/friend.go @@ -1,7 +1,6 @@ package gpcm import ( - "errors" "strconv" "strings" "wwfc/common" @@ -11,19 +10,18 @@ import ( "github.com/logrusorgru/aurora/v3" ) -func removeFromUint32Array(arrayPointer *[]uint32, index int) error { +func removeFromUint32Array(arrayPointer *[]uint32, index int) { array := *arrayPointer arrayLength := len(array) if index < 0 || index >= arrayLength { - return errors.New("index is out of bounds") + return // Ignore? } lastIndex := arrayLength - 1 array[index] = array[lastIndex] *arrayPointer = array[:lastIndex] - return nil } func (g *GameSpySession) isFriendAdded(profileId uint32) bool { @@ -261,7 +259,10 @@ func sendMessageToSession(msgType string, from uint32, session *GameSpySession, "msg": msg, }, }) - common.SendPacket(ServerName, session.ConnIndex, []byte(message)) + if err := common.SendPacket(ServerName, session.ConnIndex, []byte(message)); err != nil { + logging.Error("GPCM", "Failed to send packet:", err) + common.ShouldNotError(common.CloseConnection(ServerName, session.ConnIndex)) + } } func sendMessageToSessionBuffer(msgType string, from uint32, session *GameSpySession, msg string) { diff --git a/gpcm/kick.go b/gpcm/kick.go index 27ad174..0792c53 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(ServerName, session.ConnIndex) + common.ShouldNotError(common.CloseConnection(ServerName, session.ConnIndex)) return } diff --git a/gpcm/login.go b/gpcm/login.go index dabf63d..d3f5bce 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -247,7 +247,12 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { deviceAuth := false defaultKey := false - if g.UnitCode == UnitCodeWii { + switch g.UnitCode { + case UnitCodeDS: + g.NeedsExploit = common.DoesGameNeedExploit(g.GameName) + deviceAuth = true + + case UnitCodeWii: if isLocalhost && !payloadVerExists && !signatureExists { // Players using the DNS, need patching using a QR2 exploit if !common.DoesGameNeedExploit(g.GameName) { @@ -269,10 +274,8 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { } deviceAuth = true } - } else if g.UnitCode == UnitCodeDS { - g.NeedsExploit = common.DoesGameNeedExploit(g.GameName) - deviceAuth = true - } else { + + default: logging.Error(g.ModuleName, "Invalid unit code specified:", aurora.Cyan(unitcd)) g.replyError(ErrLogin) return @@ -393,7 +396,10 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { OtherValues: otherValues, }) - common.SendPacket(ServerName, g.ConnIndex, []byte(payload)) + if err := common.SendPacket(ServerName, g.ConnIndex, []byte(payload)); err != nil { + logging.Error("GPCM", "Failed to send login response packet") + panic(err) + } logging.Event( "logged_in", @@ -505,21 +511,22 @@ func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string if err != nil { logging.Error(g.ModuleName, "DB error:", err) - if err == database.ErrProfileIDInUse { + switch err { + case database.ErrProfileIDInUse: g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode, ErrorString: "The profile ID is already in use.", Fatal: true, WWFCMessage: WWFCMsgProfileIDInUse, }) - } else if err == database.ErrReservedProfileIDRange { + case database.ErrReservedProfileIDRange: g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode, ErrorString: "The profile ID is in a reserved range.", Fatal: true, WWFCMessage: WWFCMsgProfileIDInvalid, }) - } else if err == database.ErrDeviceIDMismatch { + case database.ErrDeviceIDMismatch: if strings.HasPrefix(g.HostPlatform, "Dolphin") { g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode, @@ -535,7 +542,7 @@ func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string WWFCMessage: WWFCMsgConsoleMismatch, }) } - } else if err == database.ErrProhibitedDeviceID { + case database.ErrProhibitedDeviceID: if strings.HasPrefix(g.HostPlatform, "Dolphin") { g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode, @@ -551,7 +558,7 @@ func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string WWFCMessage: WWFCMsgUnknownLoginError, }) } - } else if err == database.ErrProfileBannedTOS { + case database.ErrProfileBannedTOS: g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode, ErrorString: "The profile is banned from the service. Reason: " + user.BanReason, @@ -559,7 +566,7 @@ func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string WWFCMessage: WWFCMsgProfileBannedTOS, Reason: user.BanReason, }) - } else { + default: g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode, ErrorString: "There was an error logging in to the GP backend.", diff --git a/gpcm/main.go b/gpcm/main.go index f299b52..eb1b0e1 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -169,7 +169,11 @@ func NewConnection(index uint64, address string) { "id": "1", }, }) - common.SendPacket(ServerName, index, []byte(payload)) + if err := common.SendPacket(ServerName, index, []byte(payload)); err != nil { + logging.Error("GPCM", "Failed to send login challenge packet:", err) + _ = common.CloseConnection(ServerName, index) + return + } logging.Notice(session.ModuleName, "Connection established from", address) @@ -185,6 +189,7 @@ func HandlePacket(index uint64, data []byte) { if session == nil { logging.Error("GPCM", "Cannot find session for this connection index:", aurora.Cyan(index)) + _ = common.CloseConnection(ServerName, index) return } @@ -238,7 +243,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(ServerName, session.ConnIndex, []byte(`\ka\\final\`)) + _ = common.SendPacket(ServerName, session.ConnIndex, []byte(`\ka\\final\`)) }) commands = session.handleCommand("login", commands, session.login) commands = session.handleCommand("wl:exlogin", commands, session.exLogin) @@ -278,8 +283,11 @@ func HandlePacket(index uint64, data []byte) { data = append(data, session.WriteBuffer[c]) } - common.SendPacket(ServerName, session.ConnIndex, data) - session.WriteBuffer = "" + if err := common.SendPacket(ServerName, session.ConnIndex, data); err != nil { + logging.Error(session.ModuleName, "Failed to send response packet:", err) + } else { + session.WriteBuffer = "" + } } } @@ -323,7 +331,7 @@ func saveState() error { defer mutex.Unlock() err = encoder.Encode(sessions) - file.Close() + common.ShouldNotError(file.Close()) return err } @@ -339,7 +347,7 @@ func loadState() error { defer mutex.Unlock() err = decoder.Decode(&sessions) - file.Close() + common.ShouldNotError(file.Close()) if err != nil { return err } diff --git a/gpcm/message.go b/gpcm/message.go index 61c31b4..ee699f5 100644 --- a/gpcm/message.go +++ b/gpcm/message.go @@ -162,9 +162,10 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) { return } - if cmd == common.MatchReservation { + switch cmd { + case common.MatchReservation: g.QR2IP = uint64(msgMatchData.Reservation.PublicIP) | (uint64(msgMatchData.Reservation.PublicPort) << 32) - } else if cmd == common.MatchResvOK { + case common.MatchResvOK: g.QR2IP = uint64(msgMatchData.ResvOK.PublicIP) | (uint64(msgMatchData.ResvOK.PublicPort) << 32) } @@ -193,90 +194,29 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) { sameAddress := strings.Split(g.RemoteAddr, ":")[0] == strings.Split(toSession.RemoteAddr, ":")[0] - if cmd == common.MatchReservation { - if g.QR2IP == 0 { - logging.Error(g.ModuleName, "Missing QR2 IP") - g.replyError(ErrMessage) - return - } + ok = true + switch cmd { + case common.MatchReservation: + ok = g.mungeMatchReservation(toSession, &msgMatchData, uint32(toProfileId), sameAddress) - if g.User.Restricted || toSession.User.Restricted { - // Check with QR2 if the room is public or private - resvError := qr2.CheckGPReservationAllowed(g.QR2IP, g.User.ProfileId, uint32(toProfileId), msgMatchData.Reservation.MatchType) - if resvError != "ok" { - if resvError == "restricted" || resvError == "restricted_join" { - logging.Error(g.ModuleName, "RESERVATION: Restricted user tried to connect to public room") + case common.MatchResvOK, common.MatchResvDeny, common.MatchResvWait: + ok = g.mungeMatchReservationResult(cmd, toSession, &msgMatchData, uint32(toProfileId), sameAddress) - // Kick the player(s) - if g.User.Restricted { - kickPlayer(toSession.User.ProfileId, resvError) - } - if toSession.User.Restricted { - kickPlayer(g.User.ProfileId, resvError) - } - } - - logging.Warn(g.ModuleName, "RESERVATION: Not allowed:", resvError) - // Otherwise generic error? - return - } - } - - if !sameAddress { - searchId := qr2.GetSearchID(g.QR2IP) - msgMatchData.Reservation.PublicIP = uint32(searchId & 0xffffffff) - msgMatchData.Reservation.PublicPort = uint16(searchId >> 32) - msgMatchData.Reservation.LocalIP = 0 - msgMatchData.Reservation.LocalPort = 0 - } - } else if cmd == common.MatchResvOK || cmd == common.MatchResvDeny || cmd == common.MatchResvWait { - if toSession.ReservationPID != g.User.ProfileId || toSession.Reservation.Reservation == nil { - logging.Error(g.ModuleName, "Destination", aurora.Cyan(toProfileId), "has no reservation with the sender") - // Allow the message through anyway to avoid a room deadlock - } - - if toSession.Reservation.Version != msgMatchData.Version { - logging.Error(g.ModuleName, "Reservation version mismatch") - g.replyError(ErrMessage) - return - } - - if cmd == common.MatchResvOK { - if g.QR2IP == 0 || toSession.QR2IP == 0 { - logging.Error(g.ModuleName, "Missing QR2 IP") - g.replyError(ErrMessage) - return - } - - if !qr2.ProcessGPResvOK(msgMatchData.Version, *toSession.Reservation.Reservation, *msgMatchData.ResvOK, g.QR2IP, g.User.ProfileId, toSession.QR2IP, uint32(toProfileId)) { - g.replyError(ErrMessage) - return - } - - if !sameAddress { - searchId := qr2.GetSearchID(g.QR2IP) - if searchId == 0 { - logging.Error(g.ModuleName, "Could not get QR2 search ID for IP", aurora.Cyan(fmt.Sprintf("%016x", g.QR2IP))) - g.replyError(ErrMessage) - return - } - - msgMatchData.ResvOK.PublicIP = uint32(searchId & 0xffffffff) - msgMatchData.ResvOK.PublicPort = uint16(searchId >> 32) - } - } else if toSession.ReservationPID == g.User.ProfileId { - toSession.ReservationPID = 0 - } - } else if cmd == common.MatchTellAddr { + case common.MatchTellAddr: if g.QR2IP == 0 || toSession.QR2IP == 0 { logging.Error(g.ModuleName, "Missing QR2 IP") g.replyError(ErrMessage) - return + ok = false + break } qr2.ProcessGPTellAddr(g.User.ProfileId, g.QR2IP, toSession.User.ProfileId, toSession.QR2IP) } + if !ok { + return + } + newMsg, ok := common.EncodeMatchCommand(cmd, msgMatchData) if !ok || len(newMsg) > 0x200 || (len(newMsg)%4) != 0 { logging.Error(g.ModuleName, "Failed to encode match command; message:", msg) @@ -347,9 +287,98 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) { }, }) - common.SendPacket(ServerName, toSession.ConnIndex, []byte(message)) + if err := common.SendPacket(ServerName, toSession.ConnIndex, []byte(message)); err != nil { + logging.Error(g.ModuleName, "Failed to send packet:", err) + } // Append sender's profile ID to dest's RecvStatusFromList toSession.RecvStatusFromList = append(toSession.RecvStatusFromList, g.User.ProfileId) } + +func (g *GameSpySession) mungeMatchReservation(toSession *GameSpySession, msgMatchData *common.MatchCommandData, toProfileId uint32, sameAddress bool) bool { + if g.QR2IP == 0 { + logging.Error(g.ModuleName, "Missing QR2 IP") + g.replyError(ErrMessage) + return false + } + + if !sameAddress { + searchId := qr2.GetSearchID(g.QR2IP) + msgMatchData.Reservation.PublicIP = uint32(searchId & 0xffffffff) + msgMatchData.Reservation.PublicPort = uint16(searchId >> 32) + msgMatchData.Reservation.LocalIP = 0 + msgMatchData.Reservation.LocalPort = 0 + } + + if !g.User.Restricted && !toSession.User.Restricted { + return true + } + + // Check with QR2 if the room is public or private + resvError := qr2.CheckGPReservationAllowed(g.QR2IP, g.User.ProfileId, uint32(toProfileId), msgMatchData.Reservation.MatchType) + if resvError == "ok" { + return true + } + + // Figure out which error to return + if resvError == "restricted" || resvError == "restricted_join" { + logging.Error(g.ModuleName, "RESERVATION: Restricted user tried to connect to public room") + + // Kick the player(s) + if g.User.Restricted { + kickPlayer(toSession.User.ProfileId, resvError) + } + if toSession.User.Restricted { + kickPlayer(g.User.ProfileId, resvError) + } + } + + logging.Warn(g.ModuleName, "RESERVATION: Not allowed:", resvError) + // Otherwise generic error? + return false +} + +func (g *GameSpySession) mungeMatchReservationResult(cmd byte, toSession *GameSpySession, msgMatchData *common.MatchCommandData, toProfileId uint32, sameAddress bool) bool { + if toSession.ReservationPID != g.User.ProfileId || toSession.Reservation.Reservation == nil { + logging.Error(g.ModuleName, "Destination", aurora.Cyan(toProfileId), "has no reservation with the sender") + // Allow the message through anyway to avoid a room deadlock + } + + if toSession.Reservation.Version != msgMatchData.Version { + logging.Error(g.ModuleName, "Reservation version mismatch") + g.replyError(ErrMessage) + return false + } + + if cmd != common.MatchResvOK { + if toSession.ReservationPID == g.User.ProfileId { + toSession.ReservationPID = 0 + } + return true + } + + if g.QR2IP == 0 || toSession.QR2IP == 0 { + logging.Error(g.ModuleName, "Missing QR2 IP") + g.replyError(ErrMessage) + return false + } + + if !qr2.ProcessGPResvOK(msgMatchData.Version, *toSession.Reservation.Reservation, *msgMatchData.ResvOK, g.QR2IP, g.User.ProfileId, toSession.QR2IP, uint32(toProfileId)) { + g.replyError(ErrMessage) + return false + } + + if !sameAddress { + searchId := qr2.GetSearchID(g.QR2IP) + if searchId == 0 { + logging.Error(g.ModuleName, "Could not get QR2 search ID for IP", aurora.Cyan(fmt.Sprintf("%016x", g.QR2IP))) + g.replyError(ErrMessage) + return false + } + + msgMatchData.ResvOK.PublicIP = uint32(searchId & 0xffffffff) + msgMatchData.ResvOK.PublicPort = uint16(searchId >> 32) + } + return true +}