mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-03-21 17:44:58 -05:00
SAKE: More thorough validation of MKW ghost data
This commit is contained in:
parent
1b63ae4a1d
commit
3d1cdc7ab5
|
|
@ -1,9 +1,22 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"hash/crc32"
|
||||
"wwfc/logging"
|
||||
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
)
|
||||
|
||||
type MarioKartWiiLeaderboardRegionId int
|
||||
type MarioKartWiiCourseId int
|
||||
type MarioKartWiiCharacterId int
|
||||
type MarioKartWiiVehicleId int
|
||||
type MarioKartWiiControllerId int
|
||||
type MarioKartWiiWeightClassId int
|
||||
|
||||
// MarioKartWiiLeaderboardRegionId
|
||||
const (
|
||||
Worldwide = iota // 0x00
|
||||
Japan // 0x01
|
||||
|
|
@ -15,6 +28,7 @@ const (
|
|||
China // 0x07
|
||||
)
|
||||
|
||||
// MarioKartWiiCourseId
|
||||
const (
|
||||
MarioCircuit = iota // 0x00
|
||||
MooMooMeadows // 0x01
|
||||
|
|
@ -50,6 +64,93 @@ const (
|
|||
GBAShyGuyBeach // 0x1F
|
||||
)
|
||||
|
||||
// MarioKartWiiCharacterId
|
||||
const (
|
||||
Mario = iota // 0x00
|
||||
BabyPeach // 0x01
|
||||
Waluigi // 0x02
|
||||
Bowser // 0x03
|
||||
BabyDaisy // 0x04
|
||||
DryBones // 0x05
|
||||
BabyMario // 0x06
|
||||
Luigi // 0x07
|
||||
Toad // 0x08
|
||||
DonkeyKong // 0x09
|
||||
Yoshi // 0x0A
|
||||
Wario // 0x0B
|
||||
BabyLuigi // 0x0C
|
||||
Toadette // 0x0D
|
||||
KoopaTroopa // 0x0E
|
||||
Daisy // 0x0F
|
||||
Peach // 0x10
|
||||
Birdo // 0x11
|
||||
DiddyKong // 0x12
|
||||
KingBoo // 0x13
|
||||
BowserJr // 0x14
|
||||
DryBowser // 0x15
|
||||
FunkyKong // 0x16
|
||||
Rosalina // 0x17
|
||||
SmallMiiOutfitAMale // 0x18
|
||||
SmallMiiOutfitAFemale // 0x19
|
||||
SmallMiiOutfitBMale // 0x1A
|
||||
SmallMiiOutfitBFemale // 0x1B
|
||||
SmallMiiOutfitCMale // 0x1C
|
||||
SmallMiiOutfitCFemale // 0x1D
|
||||
MediumMiiOutfitAMale // 0x1E
|
||||
MediumMiiOutfitAFemale // 0x1F
|
||||
MediumMiiOutfitBMale // 0x20
|
||||
MediumMiiOutfitBFemale // 0x21
|
||||
MediumMiiOutfitCMale // 0x22
|
||||
MediumMiiOutfitCFemale // 0x23
|
||||
LargeMiiOutfitAMale // 0x24
|
||||
LargeMiiOutfitAFemale // 0x25
|
||||
LargeMiiOutfitBMale // 0x26
|
||||
LargeMiiOutfitBFemale // 0x27
|
||||
LargeMiiOutfitCMale // 0x28
|
||||
LargeMiiOutfitCFemale // 0x29
|
||||
)
|
||||
|
||||
// MarioKartWiiVehicleId
|
||||
const (
|
||||
StandardKartSmall = iota // 0x00
|
||||
StandardKartMedium // 0x01
|
||||
StandardKartLarge // 0x02
|
||||
BoosterSeat // 0x03
|
||||
ClassicDragster // 0x04
|
||||
Offroader // 0x05
|
||||
MiniBeast // 0x06
|
||||
WildWing // 0x07
|
||||
FlameFlyer // 0x08
|
||||
CheepCharger // 0x09
|
||||
SuperBlooper // 0x0A
|
||||
PiranhaProwler // 0x0B
|
||||
TinyTitan // 0x0C
|
||||
Daytripper // 0x0D
|
||||
Jetsetter // 0x0E
|
||||
BlueFalcon // 0x0F
|
||||
Sprinter // 0x10
|
||||
Honeycoupe // 0x11
|
||||
StandardBikeSmall // 0x12
|
||||
StandardBikeMedium // 0x13
|
||||
StandardBikeLarge // 0x14
|
||||
BulletBike // 0x15
|
||||
MachBike // 0x16
|
||||
FlameRunner // 0x17
|
||||
BitBike // 0x18
|
||||
Sugarscoot // 0x19
|
||||
WarioBike // 0x1A
|
||||
Quacker // 0x1B
|
||||
ZipZip // 0x1C
|
||||
ShootingStar // 0x1D
|
||||
Magikruiser // 0x1E
|
||||
Sneakster // 0x1F
|
||||
Spear // 0x20
|
||||
JetBubble // 0x21
|
||||
DolphinDasher // 0x22
|
||||
Phantom // 0x23
|
||||
)
|
||||
|
||||
// MarioKartWiiControllerId
|
||||
const (
|
||||
WiiWheel = iota // 0x00
|
||||
WiiRemoteAndNunchuck // 0x01
|
||||
|
|
@ -57,6 +158,13 @@ const (
|
|||
GameCube // 0x03
|
||||
)
|
||||
|
||||
// MarioKartWiiWeightClassId
|
||||
const (
|
||||
LightWeight = iota
|
||||
MiddleWeight
|
||||
HeavyWeight
|
||||
)
|
||||
|
||||
func (regionId MarioKartWiiLeaderboardRegionId) IsValid() bool {
|
||||
return regionId >= Worldwide && regionId <= China
|
||||
}
|
||||
|
|
@ -65,6 +173,393 @@ func (courseId MarioKartWiiCourseId) IsValid() bool {
|
|||
return courseId >= MarioCircuit && courseId <= GBAShyGuyBeach
|
||||
}
|
||||
|
||||
func (characterId MarioKartWiiCharacterId) IsValid() bool {
|
||||
// Mii Outfit C is not allowed
|
||||
if characterId == SmallMiiOutfitCMale || characterId == SmallMiiOutfitCFemale || characterId == MediumMiiOutfitCMale || characterId == MediumMiiOutfitCFemale {
|
||||
return false
|
||||
}
|
||||
|
||||
return characterId >= Mario && characterId <= LargeMiiOutfitBFemale
|
||||
}
|
||||
|
||||
func (characterId MarioKartWiiCharacterId) GetWeightClass() MarioKartWiiWeightClassId {
|
||||
switch characterId {
|
||||
case BabyPeach, BabyDaisy, DryBones, BabyMario, Toad, BabyLuigi, Toadette, KoopaTroopa, SmallMiiOutfitAMale, SmallMiiOutfitAFemale, SmallMiiOutfitBMale, SmallMiiOutfitBFemale, SmallMiiOutfitCMale, SmallMiiOutfitCFemale:
|
||||
return LightWeight
|
||||
|
||||
case Mario, Luigi, Yoshi, Daisy, Peach, Birdo, DiddyKong, BowserJr, MediumMiiOutfitAMale, MediumMiiOutfitAFemale, MediumMiiOutfitBMale, MediumMiiOutfitBFemale, MediumMiiOutfitCMale, MediumMiiOutfitCFemale:
|
||||
return MiddleWeight
|
||||
|
||||
case Waluigi, Bowser, Wario, DonkeyKong, KingBoo, DryBowser, FunkyKong, Rosalina, LargeMiiOutfitAMale, LargeMiiOutfitAFemale, LargeMiiOutfitBMale, LargeMiiOutfitBFemale, LargeMiiOutfitCMale, LargeMiiOutfitCFemale:
|
||||
return HeavyWeight
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (vehicleId MarioKartWiiVehicleId) IsValid() bool {
|
||||
return vehicleId >= StandardKartSmall && vehicleId <= Phantom
|
||||
}
|
||||
|
||||
func (vehicleId MarioKartWiiVehicleId) GetWeightClass() MarioKartWiiWeightClassId {
|
||||
if vehicleId < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return MarioKartWiiWeightClassId(vehicleId % 3)
|
||||
}
|
||||
|
||||
func (controllerId MarioKartWiiControllerId) IsValid() bool {
|
||||
return controllerId >= WiiWheel && controllerId <= GameCube
|
||||
}
|
||||
|
||||
type RKGhostData []byte
|
||||
|
||||
const (
|
||||
RKGDFileMaxSize = 0x2800
|
||||
RKGDFileMinSize = 0x0088 + 0x0014 + 0x0004
|
||||
)
|
||||
|
||||
func (rkgd RKGhostData) GetBits(byteOffset int, bitOffset int, bitLength int) uint32 {
|
||||
if bitLength == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
byteIndex := byteOffset + (bitOffset / 8)
|
||||
bitIndex := bitOffset % 8
|
||||
byteCount := (bitLength + bitIndex + 7) / 8
|
||||
|
||||
var value uint32
|
||||
for i := 0; i < byteCount; i++ {
|
||||
value |= uint32(rkgd[byteIndex+i]) << uint32((byteCount-i-1)*8)
|
||||
}
|
||||
|
||||
endBitIndex := (bitIndex + bitLength) % 8
|
||||
value >>= uint32(8-endBitIndex) % 8
|
||||
value &= (1 << uint32(bitLength)) - 1
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetMinutes(lap int) int {
|
||||
if lap == 0 {
|
||||
return int(rkgd.GetBits(0x04, 0, 7))
|
||||
}
|
||||
|
||||
if lap < 1 || lap > 5 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(rkgd.GetBits(0x11+(lap-1)*3, 0, 7))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetSeconds(lap int) int {
|
||||
if lap == 0 {
|
||||
return int(rkgd.GetBits(0x04, 7, 7))
|
||||
}
|
||||
|
||||
if lap < 1 || lap > 5 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(rkgd.GetBits(0x11+(lap-1)*3, 7, 7))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetMilliseconds(lap int) int {
|
||||
if lap == 0 {
|
||||
return int(rkgd.GetBits(0x05, 6, 10))
|
||||
}
|
||||
|
||||
if lap < 1 || lap > 5 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(rkgd.GetBits(0x11+(lap-1)*3, 14, 10))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetTime(lap int) int {
|
||||
return rkgd.GetMinutes(lap)*60000 + rkgd.GetSeconds(lap)*1000 + rkgd.GetMilliseconds(lap)
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetCourse() MarioKartWiiCourseId {
|
||||
return MarioKartWiiCourseId(rkgd.GetBits(0x07, 0, 6))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetVehicle() MarioKartWiiVehicleId {
|
||||
return MarioKartWiiVehicleId(rkgd.GetBits(0x08, 0, 6))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetCharacter() MarioKartWiiCharacterId {
|
||||
return MarioKartWiiCharacterId(rkgd.GetBits(0x08, 6, 6))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetYear() int {
|
||||
return int(rkgd.GetBits(0x09, 4, 7))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetMonth() int {
|
||||
return int(rkgd.GetBits(0x0A, 3, 4))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetDay() int {
|
||||
return int(rkgd.GetBits(0x0A, 7, 5))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetController() MarioKartWiiControllerId {
|
||||
return MarioKartWiiControllerId(rkgd.GetBits(0x0B, 4, 4))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) IsCompressed() bool {
|
||||
return rkgd.GetBits(0x0C, 4, 1) == 1
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetGhostType() int {
|
||||
return int(rkgd.GetBits(0x0C, 7, 7))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetDriftType() int {
|
||||
return int(rkgd.GetBits(0x0D, 6, 1))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetInputDataLength() uint16 {
|
||||
return uint16(rkgd.GetBits(0x0E, 0, 16))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetLapCount() int {
|
||||
return int(rkgd.GetBits(0x10, 0, 8))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetCountryCode() byte {
|
||||
return byte(rkgd.GetBits(0x34, 0, 8))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetStateCode() byte {
|
||||
return byte(rkgd.GetBits(0x35, 0, 8))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetLocationCode() uint16 {
|
||||
return uint16(rkgd.GetBits(0x36, 0, 16))
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetMiiData() Mii {
|
||||
return Mii(rkgd[0x3C : 0x3C+0x4C])
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetCompressedSize() uint32 {
|
||||
return binary.BigEndian.Uint32(rkgd[0x88:0x8C])
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) GetCompressedData() []byte {
|
||||
return []byte(rkgd[0x8C : len(rkgd)-4])
|
||||
}
|
||||
|
||||
func (rkgd RKGhostData) IsRKGDFileValid(moduleName string, expectedCourse MarioKartWiiCourseId, expectedScore int) bool {
|
||||
rkgdFileMagic := []byte{'R', 'K', 'G', 'D'}
|
||||
rkgdFileLength := len(rkgd)
|
||||
|
||||
if rkgdFileLength < RKGDFileMinSize || rkgdFileLength > RKGDFileMaxSize {
|
||||
logging.Error(moduleName, "Invalid RKGD length:", aurora.Cyan(rkgdFileLength))
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Equal(rkgd[:4], rkgdFileMagic) {
|
||||
logging.Error(moduleName, "Invalid RKGD magic:", aurora.Cyan(string(rkgd[:4])))
|
||||
return false
|
||||
}
|
||||
|
||||
expectedChecksum := binary.BigEndian.Uint32(rkgd[rkgdFileLength-4:])
|
||||
checksum := crc32.ChecksumIEEE(rkgd[:rkgdFileLength-4])
|
||||
|
||||
if checksum != expectedChecksum {
|
||||
logging.Error(moduleName, "Invalid RKGD checksum:", aurora.Cyan(checksum), "expected:", aurora.Cyan(expectedChecksum))
|
||||
return false
|
||||
}
|
||||
|
||||
lapCount := rkgd.GetLapCount()
|
||||
// This will need to be changed if/when we add support for tournament ghosts
|
||||
// Always make sure this value <= 5 due to a stack buffer overflow in the game's code
|
||||
if lapCount != 3 {
|
||||
logging.Error(moduleName, "Invalid RKGD lap count:", aurora.Cyan(lapCount))
|
||||
return false
|
||||
}
|
||||
|
||||
lapScore := 0
|
||||
for lap := 0; lap <= lapCount; lap++ {
|
||||
if rkgd.GetTime(lap) == 0 {
|
||||
logging.Error(moduleName, "Zero RKGD time for lap:", aurora.Cyan(lap))
|
||||
return false
|
||||
}
|
||||
|
||||
if rkgd.GetMinutes(lap) > 5 || rkgd.GetSeconds(lap) > 59 || rkgd.GetMilliseconds(lap) > 999 {
|
||||
logging.Error(moduleName, "Invalid RKGD time for lap m =", aurora.Cyan(rkgd.GetMinutes(lap)), "s =", aurora.Cyan(rkgd.GetSeconds(lap)), "ms =", aurora.Cyan(rkgd.GetMilliseconds(lap)))
|
||||
return false
|
||||
}
|
||||
|
||||
if lap > 0 {
|
||||
lapScore += rkgd.GetTime(lap)
|
||||
}
|
||||
}
|
||||
|
||||
totalScore := rkgd.GetTime(0)
|
||||
|
||||
if expectedScore != -1 && totalScore != expectedScore {
|
||||
logging.Error(moduleName, "RKGD total score mismatch:", aurora.Cyan(rkgd.GetTime(0)), "expected:", aurora.Cyan(expectedScore))
|
||||
return false
|
||||
}
|
||||
|
||||
// We'll do a grace of ~1 millisecond for the lap score compared to the total score,
|
||||
// in case there is a rounding error in the game's code
|
||||
if lapScore+1 < totalScore || lapScore-1 > totalScore {
|
||||
logging.Error(moduleName, "RKGD lap score mismatch:", aurora.Cyan(lapScore), "expected:", aurora.Cyan(expectedScore))
|
||||
return false
|
||||
}
|
||||
|
||||
if expectedCourse != -1 && rkgd.GetCourse() != expectedCourse {
|
||||
logging.Error(moduleName, "RKGD course mismatch:", aurora.Cyan(rkgd.GetCourse()), "expected:", aurora.Cyan(expectedCourse))
|
||||
return false
|
||||
}
|
||||
|
||||
if !rkgd.GetCourse().IsValid() {
|
||||
logging.Error(moduleName, "Invalid RKGD course:", aurora.Cyan(rkgd.GetCourse()))
|
||||
return false
|
||||
}
|
||||
|
||||
if !rkgd.GetCharacter().IsValid() {
|
||||
logging.Error(moduleName, "Invalid RKGD character:", aurora.Cyan(rkgd.GetCharacter()))
|
||||
return false
|
||||
}
|
||||
|
||||
if !rkgd.GetVehicle().IsValid() {
|
||||
logging.Error(moduleName, "Invalid RKGD vehicle:", aurora.Cyan(rkgd.GetVehicle()))
|
||||
return false
|
||||
}
|
||||
|
||||
if rkgd.GetCharacter().GetWeightClass() != rkgd.GetVehicle().GetWeightClass() {
|
||||
logging.Error(moduleName, "RKGD character/vehicle weight class mismatch: c =", aurora.Cyan(rkgd.GetCharacter()), "v =", aurora.Cyan(rkgd.GetVehicle()))
|
||||
}
|
||||
|
||||
if !rkgd.GetController().IsValid() {
|
||||
logging.Error(moduleName, "Invalid RKGD controller:", aurora.Cyan(rkgd.GetController()))
|
||||
return false
|
||||
}
|
||||
|
||||
if rkgd.GetMiiData().RFLCalculateCRC() != 0x0000 {
|
||||
logging.Error(moduleName, "Invalid RKGD Mii data CRC")
|
||||
return false
|
||||
}
|
||||
|
||||
// RKG uploaded to the server must be SZS (Yaz1) compressed
|
||||
if !rkgd.IsCompressed() {
|
||||
logging.Error(moduleName, "RKGD is not compressed")
|
||||
return false
|
||||
}
|
||||
|
||||
szsData := rkgd.GetCompressedData()
|
||||
|
||||
if string(szsData[:0x4]) != "Yaz1" {
|
||||
logging.Error(moduleName, "Invalid Yaz1 magic:", aurora.Cyan(string(szsData[:4])))
|
||||
return false
|
||||
}
|
||||
|
||||
decompSize := binary.BigEndian.Uint32(szsData[0x4:0x8])
|
||||
if uint32(rkgd.GetInputDataLength()) != decompSize {
|
||||
logging.Error(moduleName, "Invalid RKGD input data length:", aurora.Cyan(rkgd.GetInputDataLength()), "actual:", aurora.Cyan(len(szsData)))
|
||||
}
|
||||
|
||||
if rkgd.GetCompressedSize() != uint32(len(szsData)) {
|
||||
logging.Error(moduleName, "Invalid RKGD compressed size:", aurora.Cyan(rkgd.GetCompressedSize()), "actual:", aurora.Cyan(len(szsData)))
|
||||
}
|
||||
|
||||
if !bytes.Equal(szsData[0x8:0x10], []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) {
|
||||
logging.Error(moduleName, "Invalid SZS header padding")
|
||||
}
|
||||
|
||||
valid, consumed := VerifyYaz1Data(moduleName, szsData[0x10:], int(decompSize), 0)
|
||||
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
|
||||
if consumed+3 < len(szsData)-0x10 {
|
||||
logging.Error(moduleName, "Too much padding at end of RKGD")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Verify that SZS compressed data fits the standard. A buffer overflow bug is the basis for a critical RCE vulnerability in the game (szsHaxx).
|
||||
func VerifyYaz1Data(moduleName string, szsData []byte, expectedDecompSize int, decoded int) (bool, int) {
|
||||
i := 0
|
||||
for decoded < expectedDecompSize {
|
||||
if i >= len(szsData) {
|
||||
logging.Error(moduleName, "Yaz1: Unexpected end of data")
|
||||
return false, i
|
||||
}
|
||||
|
||||
flags := szsData[i]
|
||||
i++
|
||||
|
||||
// This happens a lot, so might as well check for the sake of performance
|
||||
if flags == 0xFF {
|
||||
decoded += 8
|
||||
i += 8
|
||||
continue
|
||||
}
|
||||
|
||||
for j := 0; j < 8; j++ {
|
||||
if flags&0x80 == 0 {
|
||||
if i+1 >= len(szsData) {
|
||||
logging.Error(moduleName, "Yaz1: Unexpected end of data")
|
||||
return false, i
|
||||
}
|
||||
|
||||
copyLen := (szsData[i] >> 4) + 2
|
||||
|
||||
copySrc := (int(szsData[i])&0x0F)<<8 | int(szsData[i+1])
|
||||
copySrc = decoded - copySrc - 1
|
||||
|
||||
i += 2
|
||||
|
||||
if copySrc < 0 {
|
||||
logging.Error(moduleName, "Yaz1: Copy source is out of bounds")
|
||||
return false, i
|
||||
}
|
||||
|
||||
if copyLen == 2 {
|
||||
if i >= len(szsData) {
|
||||
logging.Error(moduleName, "Yaz1: Unexpected end of data")
|
||||
return false, i
|
||||
}
|
||||
|
||||
copyLen = szsData[i] + 0x12
|
||||
i += 1
|
||||
}
|
||||
|
||||
decoded += int(copyLen)
|
||||
} else {
|
||||
decoded++
|
||||
i++
|
||||
}
|
||||
|
||||
if decoded >= expectedDecompSize {
|
||||
break
|
||||
}
|
||||
|
||||
flags <<= 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if decoded > expectedDecompSize {
|
||||
logging.Error(moduleName, "Yaz1: Overran expected decompressed size")
|
||||
return false, i
|
||||
}
|
||||
|
||||
if i > len(szsData) {
|
||||
logging.Error(moduleName, "Yaz1: Unexpected end of data")
|
||||
return false, i
|
||||
}
|
||||
|
||||
return true, i
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ package common
|
|||
// https://wiibrew.org/wiki/Mii_Data
|
||||
// https://github.com/kiwi515/ogws/tree/master/src/RVLFaceLib
|
||||
|
||||
func RFLCalculateCRC(data []byte) uint16 {
|
||||
type Mii [0x4C]byte
|
||||
|
||||
func (data Mii) RFLCalculateCRC() uint16 {
|
||||
crc := uint16(0)
|
||||
|
||||
for _, val := range data {
|
||||
|
|
|
|||
|
|
@ -390,7 +390,8 @@ func ProcessUSER(senderPid uint32, senderIP uint64, packet []byte) {
|
|||
}
|
||||
|
||||
index := 0x08 + i*0x4C
|
||||
if common.RFLCalculateCRC(packet[index:index+0x4C]) != 0x0000 {
|
||||
mii := common.Mii(packet[index : index+0x4C])
|
||||
if mii.RFLCalculateCRC() != 0x0000 {
|
||||
logging.Error(moduleName, "Received USER packet with invalid Mii data CRC")
|
||||
gpErrorCallback(senderPid, "malpacket")
|
||||
return
|
||||
|
|
|
|||
55
sake/file.go
Normal file
55
sake/file.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package sake
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"wwfc/common"
|
||||
"wwfc/logging"
|
||||
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
FileRequestDownload = iota
|
||||
FileRequestUpload
|
||||
)
|
||||
|
||||
type FileRequest int
|
||||
|
||||
var fileDownloadHandlers = map[int]func(string, http.ResponseWriter, *http.Request){
|
||||
common.GetGameIDOrPanic("mariokartwii"): handleMarioKartWiiFileDownloadRequest,
|
||||
}
|
||||
|
||||
var fileUploadHandlers = map[int]func(string, http.ResponseWriter, *http.Request){
|
||||
common.GetGameIDOrPanic("mariokartwii"): handleMarioKartWiiFileUploadRequest,
|
||||
}
|
||||
|
||||
func handleFileRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request,
|
||||
fileRequest FileRequest) {
|
||||
|
||||
gameIdString := request.URL.Query().Get("gameid")
|
||||
gameId, err := strconv.Atoi(gameIdString)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Invalid GameSpy game id")
|
||||
return
|
||||
}
|
||||
|
||||
var handler func(string, http.ResponseWriter, *http.Request)
|
||||
var handlerExists bool
|
||||
switch fileRequest {
|
||||
case FileRequestDownload:
|
||||
handler, handlerExists = fileDownloadHandlers[gameId]
|
||||
case FileRequestUpload:
|
||||
handler, handlerExists = fileUploadHandlers[gameId]
|
||||
default:
|
||||
logging.Error(moduleName, "Invalid file request")
|
||||
return
|
||||
}
|
||||
|
||||
if !handlerExists {
|
||||
logging.Warn(moduleName, "Unhandled file request for GameSpy game id:", aurora.Cyan(gameId))
|
||||
return
|
||||
}
|
||||
|
||||
handler(moduleName, responseWriter, request)
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
|
@ -17,7 +16,7 @@ import (
|
|||
)
|
||||
|
||||
type playerInfo struct {
|
||||
MiiData [0x4C]byte // 0x00
|
||||
MiiData common.Mii // 0x00
|
||||
ControllerId byte // 0x4C
|
||||
Unknown byte // 0x4D
|
||||
StateCode byte // 0x4E
|
||||
|
|
@ -27,9 +26,7 @@ type playerInfo struct {
|
|||
const (
|
||||
playerInfoSize = 0x50
|
||||
|
||||
rkgdFileMaxSize = 0x2800
|
||||
rkgdFileMinSize = 0x0088 + 0x0008 + 0x0004
|
||||
rkgdFileName = "ghost.bin"
|
||||
rkgdFileName = "ghost.bin"
|
||||
)
|
||||
|
||||
func handleMarioKartWiiFileDownloadRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) {
|
||||
|
|
@ -205,7 +202,7 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http
|
|||
request.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
err = request.ParseMultipartForm(rkgdFileMaxSize)
|
||||
err = request.ParseMultipartForm(common.RKGDFileMaxSize)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Failed to parse the multipart form:", err)
|
||||
responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultFileNotFound))
|
||||
|
|
@ -220,7 +217,7 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http
|
|||
}
|
||||
defer file.Close()
|
||||
|
||||
if fileHeader.Size < rkgdFileMinSize || fileHeader.Size > rkgdFileMaxSize {
|
||||
if fileHeader.Size < common.RKGDFileMinSize || fileHeader.Size > common.RKGDFileMaxSize {
|
||||
logging.Error(moduleName, "The size of the ghost file is invalid:", aurora.Cyan(fileHeader.Size))
|
||||
responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultFileTooLarge))
|
||||
return
|
||||
|
|
@ -234,7 +231,7 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http
|
|||
return
|
||||
}
|
||||
|
||||
if !isRKGDFileValid(ghostFile) {
|
||||
if !common.RKGhostData(ghostFile).IsRKGDFileValid(moduleName, courseId, score) {
|
||||
logging.Error(moduleName, "Received an invalid ghost file")
|
||||
responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultFileTooLarge))
|
||||
return
|
||||
|
|
@ -279,7 +276,7 @@ func isPlayerInfoValid(playerInfoString string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
if common.RFLCalculateCRC(playerInfo.MiiData[:]) != 0x0000 {
|
||||
if playerInfo.MiiData.RFLCalculateCRC() != 0x0000 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -297,18 +294,3 @@ func getMultipartBoundary(contentType string) string {
|
|||
|
||||
return contentType[startIndex:]
|
||||
}
|
||||
|
||||
func isRKGDFileValid(rkgdFile []byte) bool {
|
||||
rkgdFileMagic := []byte{'R', 'K', 'G', 'D'}
|
||||
|
||||
if !bytes.Equal(rkgdFile[:4], rkgdFileMagic) {
|
||||
return false
|
||||
}
|
||||
|
||||
rkgdFileLength := len(rkgdFile)
|
||||
|
||||
expectedChecksum := binary.BigEndian.Uint32(rkgdFile[rkgdFileLength-4:])
|
||||
checksum := crc32.ChecksumIEEE(rkgdFile[:rkgdFileLength-4])
|
||||
|
||||
return checksum == expectedChecksum
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,18 +15,11 @@ import (
|
|||
"github.com/logrusorgru/aurora/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
FileRequestDownload = iota
|
||||
FileRequestUpload
|
||||
)
|
||||
|
||||
const (
|
||||
SOAPEnvNamespace = "http://schemas.xmlsoap.org/soap/envelope/"
|
||||
SakeNamespace = "http://gamespy.net/sake"
|
||||
)
|
||||
|
||||
type FileRequest int
|
||||
|
||||
type StorageRequestEnvelope struct {
|
||||
XMLName xml.Name
|
||||
Body StorageRequestBody
|
||||
|
|
@ -119,14 +112,6 @@ type StorageSearchForRecordsResponse struct {
|
|||
Values StorageResponseValues `xml:"values"` // ???
|
||||
}
|
||||
|
||||
var fileDownloadHandlers = map[int]func(string, http.ResponseWriter, *http.Request){
|
||||
common.GetGameIDOrPanic("mariokartwii"): handleMarioKartWiiFileDownloadRequest,
|
||||
}
|
||||
|
||||
var fileUploadHandlers = map[int]func(string, http.ResponseWriter, *http.Request){
|
||||
common.GetGameIDOrPanic("mariokartwii"): handleMarioKartWiiFileUploadRequest,
|
||||
}
|
||||
|
||||
func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Request) {
|
||||
headerAction := r.Header.Get("SOAPAction")
|
||||
if headerAction == "" {
|
||||
|
|
@ -189,36 +174,6 @@ func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Requ
|
|||
w.Write(payload)
|
||||
}
|
||||
|
||||
func handleFileRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request,
|
||||
fileRequest FileRequest) {
|
||||
|
||||
gameIdString := request.URL.Query().Get("gameid")
|
||||
gameId, err := strconv.Atoi(gameIdString)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Invalid GameSpy game id")
|
||||
return
|
||||
}
|
||||
|
||||
var handler func(string, http.ResponseWriter, *http.Request)
|
||||
var handlerExists bool
|
||||
switch fileRequest {
|
||||
case FileRequestDownload:
|
||||
handler, handlerExists = fileDownloadHandlers[gameId]
|
||||
case FileRequestUpload:
|
||||
handler, handlerExists = fileUploadHandlers[gameId]
|
||||
default:
|
||||
logging.Error(moduleName, "Invalid file request")
|
||||
return
|
||||
}
|
||||
|
||||
if !handlerExists {
|
||||
logging.Warn(moduleName, "Unhandled file request for GameSpy game id:", aurora.Cyan(gameId))
|
||||
return
|
||||
}
|
||||
|
||||
handler(moduleName, responseWriter, request)
|
||||
}
|
||||
|
||||
func getRequestIdentity(moduleName string, request StorageRequestData) (uint32, common.GameInfo, bool) {
|
||||
gameInfo := common.GetGameInfoByID(request.GameID)
|
||||
if gameInfo == nil {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user