diff --git a/common/match_command.go b/common/match_command.go index f4ccd14..855fc69 100644 --- a/common/match_command.go +++ b/common/match_command.go @@ -73,6 +73,9 @@ type MatchCommandDataResvOK struct { ReceiverNewAID uint32 ClientCount uint32 ResvCheckValue uint32 + + // Only exists in version 3 + ProfileIDs []uint32 } type MatchCommandDataResvDeny struct { @@ -160,10 +163,14 @@ func GetMatchCommandString(command byte) string { return "UNKNOWN" } -func DecodeMatchCommand(command byte, buffer []byte) (MatchCommandData, bool) { +func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandData, bool) { + if version != 3 && version != 11 && version != 90 { + return MatchCommandData{}, false + } + switch command { case MatchReservation: - if len(buffer) != 0x24 { + if (version == 3 && len(buffer) != 0xC) || (version == 90 && len(buffer) != 0x24) { break } @@ -172,64 +179,102 @@ func DecodeMatchCommand(command byte, buffer []byte) (MatchCommandData, bool) { break } - isFriendValue := binary.LittleEndian.Uint32(buffer[0x18:0x1C]) - if isFriendValue > 1 { - break - } - isFriend := isFriendValue != 0 - publicPort := binary.LittleEndian.Uint32(buffer[0x08:0x0C]) if publicPort > 0xffff { break } - localPort := binary.LittleEndian.Uint32(buffer[0x10:0x14]) - if localPort > 0xffff { - break - } + if version == 3 { + return MatchCommandData{Reservation: &MatchCommandDataReservation{ + MatchType: byte(matchType), + PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]), + PublicPort: uint16(publicPort), + }}, true + } else if version == 90 { + localPort := binary.LittleEndian.Uint32(buffer[0x10:0x14]) + if localPort > 0xffff { + break + } - return MatchCommandData{Reservation: &MatchCommandDataReservation{ - MatchType: byte(matchType), - PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]), - PublicPort: uint16(publicPort), - LocalIP: binary.BigEndian.Uint32(buffer[0x0C:0x10]), - LocalPort: uint16(localPort), - Unknown: binary.LittleEndian.Uint32(buffer[0x14:0x18]), - IsFriend: isFriend, - LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x1C:0x20]), - ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x20:0x24]), - }}, true + isFriendValue := binary.LittleEndian.Uint32(buffer[0x18:0x1C]) + if isFriendValue > 1 { + break + } + isFriend := isFriendValue != 0 + + return MatchCommandData{Reservation: &MatchCommandDataReservation{ + MatchType: byte(matchType), + PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]), + PublicPort: uint16(publicPort), + LocalIP: binary.BigEndian.Uint32(buffer[0x0C:0x10]), + LocalPort: uint16(localPort), + Unknown: binary.LittleEndian.Uint32(buffer[0x14:0x18]), + IsFriend: isFriend, + LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x1C:0x20]), + ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x20:0x24]), + }}, true + } case MatchResvOK: - if len(buffer) != 0x34 { - break - } + switch version { + case 3: + if len(buffer) < 0xC { + break + } - publicPort := binary.LittleEndian.Uint32(buffer[0x10:0x14]) - if publicPort > 0xffff { - break - } + clientCount := binary.LittleEndian.Uint32(buffer[0x00:0x04]) + if clientCount > 29 || len(buffer) != int(0xC+clientCount*0x4) { + break + } - localPort := binary.LittleEndian.Uint32(buffer[0x18:0x1C]) - if localPort > 0xffff { - break - } + var profileIDs []uint32 + for i := uint32(0); i < clientCount; i++ { + profileIDs = append(profileIDs, binary.LittleEndian.Uint32(buffer[0x4+i*4:0x4+i*4+4])) + } - return MatchCommandData{ResvOK: &MatchCommandDataResvOK{ - MaxPlayers: binary.LittleEndian.Uint32(buffer[0x00:0x04]), - SenderAID: binary.LittleEndian.Uint32(buffer[0x04:0x08]), - ProfileID: binary.LittleEndian.Uint32(buffer[0x08:0x0C]), - PublicIP: binary.BigEndian.Uint32(buffer[0x0C:0x10]), - PublicPort: uint16(publicPort), - LocalIP: binary.BigEndian.Uint32(buffer[0x14:0x18]), - LocalPort: uint16(localPort), - Unknown: binary.LittleEndian.Uint32(buffer[0x1C:0x20]), - LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x20:0x24]), - GroupID: binary.LittleEndian.Uint32(buffer[0x24:0x28]), - ReceiverNewAID: binary.LittleEndian.Uint32(buffer[0x28:0x2C]), - ClientCount: binary.LittleEndian.Uint32(buffer[0x2C:0x30]), - ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x30:0x34]), - }}, true + publicPort := binary.LittleEndian.Uint32(buffer[0x08:0x0C]) + if publicPort > 0xffff { + break + } + + return MatchCommandData{ResvOK: &MatchCommandDataResvOK{ + PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]), + PublicPort: uint16(publicPort), + ClientCount: clientCount, + ProfileIDs: profileIDs, + }}, true + + case 90: + if len(buffer) != 0x34 { + break + } + + publicPort := binary.LittleEndian.Uint32(buffer[0x10:0x14]) + if publicPort > 0xffff { + break + } + + localPort := binary.LittleEndian.Uint32(buffer[0x18:0x1C]) + if localPort > 0xffff { + break + } + + return MatchCommandData{ResvOK: &MatchCommandDataResvOK{ + MaxPlayers: binary.LittleEndian.Uint32(buffer[0x00:0x04]), + SenderAID: binary.LittleEndian.Uint32(buffer[0x04:0x08]), + ProfileID: binary.LittleEndian.Uint32(buffer[0x08:0x0C]), + PublicIP: binary.BigEndian.Uint32(buffer[0x0C:0x10]), + PublicPort: uint16(publicPort), + LocalIP: binary.BigEndian.Uint32(buffer[0x14:0x18]), + LocalPort: uint16(localPort), + Unknown: binary.LittleEndian.Uint32(buffer[0x1C:0x20]), + LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x20:0x24]), + GroupID: binary.LittleEndian.Uint32(buffer[0x24:0x28]), + ReceiverNewAID: binary.LittleEndian.Uint32(buffer[0x28:0x2C]), + ClientCount: binary.LittleEndian.Uint32(buffer[0x2C:0x30]), + ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x30:0x34]), + }}, true + } case MatchResvDeny: if len(buffer) != 0x04 { @@ -354,41 +399,64 @@ func DecodeMatchCommand(command byte, buffer []byte) (MatchCommandData, bool) { return MatchCommandData{}, false } -func EncodeMatchCommand(command byte, data MatchCommandData) ([]byte, bool) { +func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byte, bool) { + if version != 3 && version != 11 && version != 90 { + return []byte{}, false + } + switch command { case MatchReservation: message := binary.LittleEndian.AppendUint32([]byte{}, uint32(data.Reservation.MatchType)) message = binary.BigEndian.AppendUint32(message, data.Reservation.PublicIP) message = binary.LittleEndian.AppendUint32(message, uint32(data.Reservation.PublicPort)) - message = binary.BigEndian.AppendUint32(message, data.Reservation.LocalIP) - message = binary.LittleEndian.AppendUint32(message, uint32(data.Reservation.LocalPort)) - message = binary.LittleEndian.AppendUint32(message, data.Reservation.Unknown) - isFriendInt := uint32(0) - if data.Reservation.IsFriend { - isFriendInt = 1 + if version != 3 { + message = binary.BigEndian.AppendUint32(message, data.Reservation.LocalIP) + message = binary.LittleEndian.AppendUint32(message, uint32(data.Reservation.LocalPort)) + message = binary.LittleEndian.AppendUint32(message, data.Reservation.Unknown) + + isFriendInt := uint32(0) + if data.Reservation.IsFriend { + isFriendInt = 1 + } + message = binary.LittleEndian.AppendUint32(message, isFriendInt) + + message = binary.LittleEndian.AppendUint32(message, data.Reservation.LocalPlayerCount) + message = binary.LittleEndian.AppendUint32(message, data.Reservation.ResvCheckValue) } - message = binary.LittleEndian.AppendUint32(message, isFriendInt) - - message = binary.LittleEndian.AppendUint32(message, data.Reservation.LocalPlayerCount) - message = binary.LittleEndian.AppendUint32(message, data.Reservation.ResvCheckValue) return message, true case MatchResvOK: - message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvOK.MaxPlayers) - message = binary.LittleEndian.AppendUint32(message, data.ResvOK.SenderAID) - message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ProfileID) - message = binary.BigEndian.AppendUint32(message, data.ResvOK.PublicIP) - message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.PublicPort)) - message = binary.BigEndian.AppendUint32(message, data.ResvOK.LocalIP) - message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.LocalPort)) - message = binary.LittleEndian.AppendUint32(message, data.ResvOK.Unknown) - message = binary.LittleEndian.AppendUint32(message, data.ResvOK.LocalPlayerCount) - message = binary.LittleEndian.AppendUint32(message, data.ResvOK.GroupID) - message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ReceiverNewAID) - message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ClientCount) - message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ResvCheckValue) - return message, true + switch version { + case 3: + if int(data.ResvOK.ClientCount) != len(data.ResvOK.ProfileIDs) { + return []byte{}, false + } + + message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvOK.ClientCount) + for _, pid := range data.ResvOK.ProfileIDs { + message = binary.LittleEndian.AppendUint32(message, pid) + } + message = binary.BigEndian.AppendUint32(message, data.ResvOK.PublicIP) + message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.PublicPort)) + return message, true + + case 90: + message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvOK.MaxPlayers) + message = binary.LittleEndian.AppendUint32(message, data.ResvOK.SenderAID) + message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ProfileID) + message = binary.BigEndian.AppendUint32(message, data.ResvOK.PublicIP) + message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.PublicPort)) + message = binary.BigEndian.AppendUint32(message, data.ResvOK.LocalIP) + message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.LocalPort)) + message = binary.LittleEndian.AppendUint32(message, data.ResvOK.Unknown) + message = binary.LittleEndian.AppendUint32(message, data.ResvOK.LocalPlayerCount) + message = binary.LittleEndian.AppendUint32(message, data.ResvOK.GroupID) + message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ReceiverNewAID) + message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ClientCount) + message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ResvCheckValue) + return message, true + } case MatchResvDeny: message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvDeny.Reason) diff --git a/gpcm/friend.go b/gpcm/friend.go index 6d9ce82..19985e2 100644 --- a/gpcm/friend.go +++ b/gpcm/friend.go @@ -1,6 +1,7 @@ package gpcm import ( + "encoding/binary" "github.com/logrusorgru/aurora/v3" "strconv" "strings" @@ -161,40 +162,60 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) { } // Parse message for security and room tracking purposes - if !strings.HasPrefix(msg, "GPCM") { - logging.Error(g.ModuleName, "Invalid message prefix") + var version int + var msgDataIndex int + + if strings.HasPrefix(msg, "GPCM3vMAT") { + version = 3 + msgDataIndex = 9 + } else if strings.HasPrefix(msg, "GPCM11vMAT") { + // Only used for Brawl + version = 11 + msgDataIndex = 10 + } else if strings.HasPrefix(msg, "GPCM90vMAT") { + version = 90 + msgDataIndex = 10 + } else { + logging.Error(g.ModuleName, "Invalid message prefix; message:", msg) g.replyError(ErrMessage) return } - currentIndex := strings.Index(msg, "vMAT") + 4 - isMessageHeaderValid := false - switch currentIndex { - case 9: // 1 - 9 - isMessageHeaderValid = msg[4] >= '1' && msg[4] <= '9' && len(msg) >= 11 - case 10: // 10 - 99 - isMessageHeaderValid = msg[4] >= '1' && msg[4] <= '9' && msg[5] >= '0' && msg[5] <= '9' && len(msg) >= 12 - } - if !isMessageHeaderValid { - logging.Error(g.ModuleName, "Invalid message header") - g.replyError(ErrMessage) - return + cmd := msg[msgDataIndex] + msgDataIndex++ + + var msgData []byte + + switch version { + case 3: + for _, stringValue := range strings.Split(msg[msgDataIndex:], "/") { + intValue, err := strconv.ParseUint(stringValue, 10, 32) + if err != nil { + logging.Error(g.ModuleName, "Invalid message value; message:", msg) + g.replyError(ErrMessage) + return + } + + msgData = binary.LittleEndian.AppendUint32(msgData, uint32(intValue)) + } + break + + // TODO: Version 11 (Super Smash Bros. Brawl) + + case 90: + msgData, err = common.Base64DwcEncoding.DecodeString(msg[msgDataIndex:]) + if err != nil { + logging.Error(g.ModuleName, "Invalid message base64 data; message:", msg) + g.replyError(ErrMessage) + return + } + break } - cmd := msg[currentIndex] - currentIndex++ - - msgData, err := common.Base64DwcEncoding.DecodeString(msg[currentIndex:]) - if err != nil { - logging.Error(g.ModuleName, "Invalid message base64 data") - g.replyError(ErrMessage) - return - } - - msgMatchData, ok := common.DecodeMatchCommand(cmd, msgData) + msgMatchData, ok := common.DecodeMatchCommand(cmd, msgData, version) common.LogMatchCommand(g.ModuleName, strconv.FormatInt(int64(toProfileId), 10), cmd, msgMatchData) if !ok { - logging.Error(g.ModuleName, "Invalid match command data") + logging.Error(g.ModuleName, "Invalid match command data; message:", msg) g.replyError(ErrMessage) return } diff --git a/qr2/message.go b/qr2/message.go index 9489b05..eb8318c 100644 --- a/qr2/message.go +++ b/qr2/message.go @@ -54,6 +54,12 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { return } + version := int(binary.LittleEndian.Uint32(message[0x04:0x08])) + if version != 3 && version != 11 && version != 90 { + logging.Error(moduleName, "Received invalid match version") + return + } + senderProfileID := binary.LittleEndian.Uint32(message[0x10:0x14]) moduleName = "QR2/MSG:p" + strconv.FormatUint(uint64(senderProfileID), 10) @@ -81,7 +87,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { } var ok bool - matchData, ok = common.DecodeMatchCommand(message[8], message[0x14:]) + matchData, ok = common.DecodeMatchCommand(message[8], message[0x14:], version) if !ok { logging.Error(moduleName, "Received invalid match command:", aurora.Cyan(message[8])) return @@ -99,14 +105,16 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { return } - if matchData.Reservation.PublicPort < 1024 { - logging.Error(moduleName, "RESERVATION: Public port is reserved") + if qr2Port != matchData.Reservation.PublicPort { + logging.Error(moduleName, "RESERVATION: Public port mismatch in header and command") return } - if matchData.Reservation.LocalPort < 1024 { - logging.Error(moduleName, "RESERVATION: Local port is reserved") - return + if version != 3 { + if matchData.Reservation.LocalPort < 1024 { + logging.Error(moduleName, "RESERVATION: Local port is reserved") + return + } } if useSearchID { @@ -123,19 +131,21 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { return } - if matchData.ResvOK.PublicPort < 1024 { - logging.Error(moduleName, "RESV_OK: Public port is reserved") + if qr2Port != matchData.Reservation.PublicPort { + logging.Error(moduleName, "RESERVATION: Public port mismatch in header and command") return } - if matchData.ResvOK.LocalPort < 1024 { - logging.Error(moduleName, "RESV_OK: Local port is reserved") - return - } + if version != 3 { + if matchData.ResvOK.LocalPort < 1024 { + logging.Error(moduleName, "RESV_OK: Local port is reserved") + return + } - if matchData.ResvOK.ProfileID != senderProfileID { - logging.Error(moduleName, "RESV_OK: Profile ID mismatch in header") - return + if matchData.ResvOK.ProfileID != senderProfileID { + logging.Error(moduleName, "RESV_OK: Profile ID mismatch in header") + return + } } if useSearchID { @@ -171,7 +181,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { } var matchMessage []byte - matchMessage, ok = common.EncodeMatchCommand(message[8], matchData) + matchMessage, ok = common.EncodeMatchCommand(message[8], matchData, version) if !ok { logging.Error(moduleName, "Failed to reencode match command:", aurora.Cyan(message[8])) return diff --git a/serverbrowser/filter.go b/serverbrowser/filter.go index 6414578..8c64f89 100644 --- a/serverbrowser/filter.go +++ b/serverbrowser/filter.go @@ -15,53 +15,6 @@ import ( // Example: dwc_mver = 90 and dwc_pid != 43 and maxplayers = 11 and numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 2 and dwc_suspend = 0 and (rk = 'vs' and ev >= 4250 and ev <= 5750 and p = 0) func filterServers(servers []map[string]string, queryGame string, expression string, publicIP string) []map[string]string { - if match := regexSelfLookup.FindStringSubmatch(expression); match != nil { - dwcPid := match[1] - - var filtered []map[string]string - - // Search for where the profile ID matches - for _, server := range servers { - if server["gamename"] != queryGame { - continue - } - - if server["dwc_pid"] == dwcPid { - if server["publicip"] != publicIP { - logging.Error(ModuleName, "Self lookup", aurora.Cyan(dwcPid), "from wrong IP") - return []map[string]string{} - } - - logging.Info(ModuleName, "Self lookup from", aurora.Cyan(dwcPid), "ok") - filtered = []map[string]string{server} - break - } - - // Alternatively, if the server hasn't set its dwcPid field yet, we return servers matching the request's public IP. - // If multiple servers exist with the same public IP then the client will use the one with the matching port. - // This is a bit of a hack to speed up server creation. - if _, ok := server["dwc_pid"]; !ok && server["publicip"] == publicIP { - // Create a copy of the map with some values changed - newServer := map[string]string{} - for k, v := range server { - newServer[k] = v - } - newServer["dwc_pid"] = dwcPid - newServer["dwc_mtype"] = "0" - newServer["dwc_mver"] = "0" - filtered = append(filtered, newServer) - } - } - - if len(filtered) == 0 { - logging.Error(ModuleName, "Could not find server with dwcPid", aurora.Cyan(dwcPid)) - return []map[string]string{} - } - - logging.Info(ModuleName, "Self lookup for", aurora.Cyan(dwcPid), "matched", aurora.BrightCyan(len(filtered)), "servers via public IP") - return filtered - } - // Matchmaking search tree, err := filter.Parse(expression) if err != nil { @@ -76,7 +29,7 @@ func filterServers(servers []map[string]string, queryGame string, expression str continue } - if server["dwc_hoststate"] != "0" && server["dwc_hoststate"] != "2" { + if server["dwc_mver"] == "90" && (server["dwc_hoststate"] != "0" && server["dwc_hoststate"] != "2") { continue } @@ -105,12 +58,8 @@ func filterSelfLookup(servers []map[string]string, queryGame string, dwcPid stri } if server["dwc_pid"] == dwcPid { - if server["publicip"] != publicIP { - logging.Error(ModuleName, "Self lookup", aurora.Cyan(dwcPid), "from wrong IP") - return []map[string]string{} - } - - logging.Info(ModuleName, "Self lookup from", aurora.Cyan(dwcPid), "ok") + // May not be a self lookup, some games search for friends like this + logging.Info(ModuleName, "Lookup", aurora.Cyan(dwcPid), "ok") return []map[string]string{server} }