diff --git a/gpcm/friend.go b/gpcm/friend.go index 0baf353..1f7b1e2 100644 --- a/gpcm/friend.go +++ b/gpcm/friend.go @@ -8,6 +8,7 @@ import ( "strings" "wwfc/common" "wwfc/logging" + "wwfc/qr2" ) func (g *GameSpySession) isFriendAdded(profileId uint32) bool { @@ -182,6 +183,12 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) { return } + if len(msg) < msgDataIndex+1 { + logging.Error(g.ModuleName, "Invalid message length; message:", msg) + g.replyError(ErrMessage) + return + } + cmd := msg[msgDataIndex] msgDataIndex++ @@ -232,6 +239,18 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) { return } + if cmd == common.MatchReservation { + if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) == int32(msgMatchData.Reservation.PublicIP) { + g.QR2IP = uint64(msgMatchData.Reservation.PublicIP) | (uint64(msgMatchData.Reservation.PublicPort) << 32) + } + } else if cmd == common.MatchResvOK { + if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) == int32(msgMatchData.ResvOK.PublicIP) { + g.QR2IP = uint64(msgMatchData.ResvOK.PublicIP) | (uint64(msgMatchData.ResvOK.PublicPort) << 32) + } + } + + // TODO: Replace public IP with QR2 search ID + mutex.Lock() defer mutex.Unlock() @@ -248,6 +267,29 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) { return } + if cmd == common.MatchReservation { + g.ReservationPID = uint32(toProfileId) + } else if cmd == common.MatchResvOK || cmd == common.MatchResvDeny || cmd == common.MatchResvWait { + if toSession.ReservationPID != g.User.ProfileId { + logging.Error(g.ModuleName, "Destination", aurora.Cyan(toProfileId), "has no reservation with the sender") + 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.ResvOK, g.QR2IP, g.User.ProfileId, toSession.QR2IP, uint32(toProfileId)) { + g.replyError(ErrMessage) + return + } + } + } + sendMessageToSession("1", g.User.ProfileId, toSession, msg) } diff --git a/gpcm/main.go b/gpcm/main.go index 9e39545..4ba7c62 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -28,6 +28,9 @@ type GameSpySession struct { LocString string FriendList []uint32 AuthFriendList []uint32 + + QR2IP uint64 + ReservationPID uint32 } var ( diff --git a/qr2/group.go b/qr2/group.go new file mode 100644 index 0000000..c0a0870 --- /dev/null +++ b/qr2/group.go @@ -0,0 +1,81 @@ +package qr2 + +import ( + "fmt" + "github.com/logrusorgru/aurora/v3" + "strconv" + "wwfc/common" + "wwfc/logging" +) + +type Group struct { + GroupID uint32 + ServerAID uint8 + Players map[uint8]*Session +} + +var groups = map[*Group]bool{} + +func processResvOK(moduleName string, cmd common.MatchCommandDataResvOK, sender, destination *Session) bool { + group := sender.GroupPointer + if group == nil { + logging.Notice(moduleName, "Creating new group", aurora.Cyan(cmd.GroupID), "/", aurora.Cyan(cmd.ProfileID)) + group = &Group{ + GroupID: cmd.GroupID, + ServerAID: uint8(cmd.SenderAID), + Players: map[uint8]*Session{uint8(cmd.SenderAID): sender}, + } + sender.GroupPointer = group + groups[group] = true + } + + if uint8(cmd.SenderAID) != sender.GroupAID { + logging.Error(moduleName, "ResvOK: Invalid sender AID") + return false + } + + // TODO: Check if the sender is the actual server (host) once host migration works + + // Keep group ID updated + sender.GroupPointer.GroupID = cmd.GroupID + + logging.Info(moduleName, "New AID", aurora.Cyan(uint8(cmd.ReceiverNewAID)), "in group", aurora.Cyan(group.GroupID)) + group.Players[uint8(cmd.ReceiverNewAID)] = destination + destination.GroupPointer = group + destination.GroupAID = uint8(cmd.ReceiverNewAID) + + return true +} + +func ProcessGPResvOK(cmd common.MatchCommandDataResvOK, senderIP uint64, senderPid uint32, destIP uint64, destPid uint32) bool { + senderPidStr := strconv.FormatUint(uint64(senderPid), 10) + destPidStr := strconv.FormatUint(uint64(destPid), 10) + + moduleName := "QR2:GPMsg:" + senderPidStr + "->" + destPidStr + + mutex.Lock() + defer mutex.Unlock() + + from := sessionByPublicIP[senderIP] + if from == nil { + logging.Error(moduleName, "Sender IP does not exist:", aurora.Cyan(fmt.Sprintf("%012x", senderIP))) + return false + } + + to := sessionByPublicIP[destIP] + if to == nil { + logging.Error(moduleName, "Destination IP does not exist:", aurora.Cyan(fmt.Sprintf("%012x", destIP))) + return false + } + + // Validate dwc_pid values + if !from.setProfileID(moduleName, senderPidStr) { + return false + } + + if !to.setProfileID(moduleName, destPidStr) { + return false + } + + return processResvOK(moduleName, cmd, from, to) +} diff --git a/qr2/message.go b/qr2/message.go index da43ca7..a8a3e01 100644 --- a/qr2/message.go +++ b/qr2/message.go @@ -226,7 +226,26 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { cookie := binary.BigEndian.Uint32(message[0x2:0x6]) logging.Notice(moduleName, "Send NN cookie", aurora.Cyan(cookie), "to", aurora.BrightCyan(destPid)) } else { - common.LogMatchCommand(moduleName, destPid, message[8], matchData) + cmd := message[8] + common.LogMatchCommand(moduleName, destPid, cmd, matchData) + + if cmd == common.MatchReservation { + sender.ReservationID = receiver.SearchID + } else if cmd == common.MatchResvOK || cmd == common.MatchResvDeny || cmd == common.MatchResvWait { + if receiver.ReservationID != sender.SearchID { + logging.Error(moduleName, "Destination has no reservation with the sender") + return + } + + if cmd == common.MatchResvOK { + mutex.Lock() + if !processResvOK(moduleName, *matchData.ResvOK, sender, receiver) { + mutex.Unlock() + return + } + mutex.Unlock() + } + } } payload := createResponseHeader(ClientMessageRequest, destSessionID) diff --git a/qr2/session.go b/qr2/session.go index b1577c7..6077c35 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -28,6 +28,9 @@ type Session struct { Endianness byte // Some fields depend on the client's endianness Data map[string]string PacketCount uint32 + ReservationID uint64 + GroupPointer *Group + GroupAID uint8 } var ( @@ -43,10 +46,23 @@ func removeSession(sessionId uint32) { mutex.Lock() defer mutex.Unlock() - if sessions[sessionId] == nil { + session := sessions[sessionId] + if session == nil { return } + if session.GroupPointer != nil { + delete(session.GroupPointer.Players, session.GroupAID) + + if len(session.GroupPointer.Players) == 0 { + logging.Notice("QR2", "Deleting group", aurora.Cyan(session.GroupPointer.GroupID)) + delete(groups, session.GroupPointer) + } else if session.GroupPointer.ServerAID == session.GroupAID { + logging.Notice("QR2", "Server down in group", aurora.Cyan(session.GroupPointer.GroupID)) + // TODO: Search for new host via dwc_hoststate + } + } + // Delete search ID lookup delete(sessionBySearchID, sessions[sessionId].SearchID) @@ -85,6 +101,7 @@ func setSessionData(sessionId uint32, payload map[string]string, addr net.Addr) Endianness: ClientNoEndian, Data: payload, PacketCount: 0, + ReservationID: 0, } } @@ -97,7 +114,7 @@ func setSessionData(sessionId uint32, payload map[string]string, addr net.Addr) // Set search ID for { - searchID := uint64(rand.Int63n(0x400 << 32)) + searchID := uint64(rand.Int63n((0x400<<32)-1) + 1) if _, exists := sessionBySearchID[searchID]; !exists { session.SearchID = searchID session.Data["+searchid"] = strconv.FormatUint(searchID, 10)