Merge pull request #63 from MikeIsAStar/support-file-uploads-from-mario-kart-wii

SAKE: Support file uploads from Mario Kart Wii
This commit is contained in:
Palapeli 2024-09-06 11:19:51 -04:00 committed by GitHub
commit 96b10233c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 555 additions and 157 deletions

View File

@ -2,6 +2,16 @@ package common
import (
"encoding/base64"
"errors"
"strings"
)
type GameSpyBase64Encoding int
const (
GameSpyBase64EncodingDefault = iota // 0
GameSpyBase64EncodingAlternate // 1
GameSpyBase64EncodingURLSafe // 2
)
var Base64DwcEncoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-").WithPadding('*')
@ -20,6 +30,31 @@ func Base32Encode(value uint64) string {
return encoded
}
func DecodeGameSpyBase64(gameSpyBase64 string, gameSpyBase64Encoding GameSpyBase64Encoding) ([]byte, error) {
base64String, err := GameSpyBase64ToBase64(gameSpyBase64, gameSpyBase64Encoding)
if err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(base64String)
}
func GameSpyBase64ToBase64(gameSpyBase64 string, gameSpyBase64Encoding GameSpyBase64Encoding) (string, error) {
switch gameSpyBase64Encoding {
case GameSpyBase64EncodingDefault:
return gameSpyBase64, nil
case GameSpyBase64EncodingAlternate:
return strings.NewReplacer("[", "+", "]", "/", "_", "=").Replace(gameSpyBase64), nil
case GameSpyBase64EncodingURLSafe:
return strings.NewReplacer("-", "+", "_", "/" /*, "=", "="*/).Replace(gameSpyBase64), nil
default:
return "", errors.New("invalid GameSpy Base64 encoding specified")
}
}
func reverse(s string) string {
rns := []rune(s)
for i, j := 0, len(rns)-1; i < j; i, j = i+1, j-1 {

19
common/gamyspy.go Normal file
View File

@ -0,0 +1,19 @@
package common
type SakeFileResult int
const (
GameSpyMultipartBoundary = "Qr4G823s23d---<<><><<<>--7d118e0536"
GameSpyGameIdMarioKartWii = 1687
)
// https://documentation.help/GameSpy-SDK/SAKEFileResult.html
const (
SakeFileResultHeader = "Sake-File-Result"
SakeFileResultSuccess = 0
SakeFileResultMissingParameter = 3
SakeFileResultFileNotFound = 4
SakeFileResultFileTooLarge = 5
SakeFileResultServerError = 6
)

View File

@ -1,110 +1,70 @@
package common
type MarioKartWiiRegionID int
type MarioKartWiiCourseID int
const MarioKartWiiGameSpyGameID int = 1687
type MarioKartWiiLeaderboardRegionId int
type MarioKartWiiCourseId int
type MarioKartWiiControllerId int
const (
Worldwide = iota // 0
Japan = iota // 1
UnitedStates = iota // 2
Europe = iota // 3
Australia = iota // 4
Taiwan = iota // 5
Korea = iota // 6
China = iota // 7
Worldwide = iota // 0x00
Japan // 0x01
UnitedStates // 0x02
Europe // 0x03
Australia // 0x04
Taiwan // 0x05
Korea // 0x06
China // 0x07
)
const (
MarioCircuit = iota // 0x00
MooMooMeadows = iota // 0x01
MushroomGorge = iota // 0x02
GrumbleVolcano = iota // 0x03
ToadsFactory = iota // 0x04
CoconutMall = iota // 0x05
DKSummit = iota // 0x06
WarioGoldMine = iota // 0x07
LuigiCircuit = iota // 0x08
DaisyCircuit = iota // 0x09
MoonviewHighway = iota // 0x0A
MapleTreeway = iota // 0x0B
BowsersCastle = iota // 0x0C
RainbowRoad = iota // 0x0D
DryDryRuins = iota // 0x0E
KoopaCape = iota // 0x0F
GCNPeachBeach = iota // 0x10
GCNMarioCircuit = iota // 0x11
GCNWaluigiStadium = iota // 0x12
GCNDKMountain = iota // 0x13
DSYoshiFalls = iota // 0x14
DSDesertHills = iota // 0x15
DSPeachGardens = iota // 0x16
DSDelfinoSquare = iota // 0x17
SNESMarioCircuit3 = iota // 0x18
SNESGhostValley2 = iota // 0x19
N64MarioRaceway = iota // 0x1A
N64SherbetLand = iota // 0x1B
N64BowsersCastle = iota // 0x1C
N64DKsJungleParkway = iota // 0x1D
GBABowserCastle3 = iota // 0x1E
GBAShyGuyBeach = iota // 0x1F
MooMooMeadows // 0x01
MushroomGorge // 0x02
GrumbleVolcano // 0x03
ToadsFactory // 0x04
CoconutMall // 0x05
DKSummit // 0x06
WarioGoldMine // 0x07
LuigiCircuit // 0x08
DaisyCircuit // 0x09
MoonviewHighway // 0x0A
MapleTreeway // 0x0B
BowsersCastle // 0x0C
RainbowRoad // 0x0D
DryDryRuins // 0x0E
KoopaCape // 0x0F
GCNPeachBeach // 0x10
GCNMarioCircuit // 0x11
GCNWaluigiStadium // 0x12
GCNDKMountain // 0x13
DSYoshiFalls // 0x14
DSDesertHills // 0x15
DSPeachGardens // 0x16
DSDelfinoSquare // 0x17
SNESMarioCircuit3 // 0x18
SNESGhostValley2 // 0x19
N64MarioRaceway // 0x1A
N64SherbetLand // 0x1B
N64BowsersCastle // 0x1C
N64DKsJungleParkway // 0x1D
GBABowserCastle3 // 0x1E
GBAShyGuyBeach // 0x1F
)
func (regionId MarioKartWiiRegionID) IsValid() bool {
const (
WiiWheel = iota // 0x00
WiiRemoteAndNunchuck // 0x01
Classic // 0x02
GameCube // 0x03
)
func (regionId MarioKartWiiLeaderboardRegionId) IsValid() bool {
return regionId >= Worldwide && regionId <= China
}
func (courseId MarioKartWiiCourseID) IsValid() bool {
func (courseId MarioKartWiiCourseId) IsValid() bool {
return courseId >= MarioCircuit && courseId <= GBAShyGuyBeach
}
func (regionId MarioKartWiiRegionID) ToString() string {
return [...]string{
"Worldwide",
"Japan",
"United States",
"Europe",
"Australia",
"Taiwan",
"Korea",
"China",
}[regionId]
}
func (courseId MarioKartWiiCourseID) ToString() string {
return [...]string{
"Mario Circuit",
"Moo Moo Meadows",
"Mushroom Gorge",
"Grumble Volcano",
"Toad's Factory",
"Coconut Mall",
"DK Summit",
"Wario's Gold Mine",
"Luigi Circuit",
"Daisy Circuit",
"Moonview Highway",
"Maple Treeway",
"Bowser's Castle",
"Rainbow Road",
"Dry Dry Ruins",
"Koopa Cape",
"GCN Peach Beach",
"GCN Mario Circuit",
"GCN Waluigi Stadium",
"GCN DK Mountain",
"DS Yoshi Falls",
"DS Desert Hills",
"DS Peach Gardens",
"DS Delfino Square",
"SNES Mario Circuit 3",
"SNES Ghost Valley 2",
"N64 Mario Raceway",
"N64 Sherbet Land",
"N64 Bowser's Castle",
"N64 DK's Jungle Parkway",
"GBA Bowser Castle 3",
"GBA Shy Guy Beach",
}[courseId]
func (controllerId MarioKartWiiControllerId) IsValid() bool {
return controllerId >= WiiWheel && controllerId <= GameCube
}

View File

@ -0,0 +1,62 @@
package database
import (
"context"
"wwfc/common"
"github.com/jackc/pgx/v4/pgxpool"
)
type MarioKartWiiTopTenRanking struct {
Score int
PID int
PlayerInfo string
}
const (
getTopTenRankingsQuery = "" +
"SELECT score, pid, playerinfo " +
"FROM mario_kart_wii_sake " +
"WHERE ($1 = 0 OR regionid = $1) " +
"AND courseid = $2 " +
"ORDER BY score ASC " +
"LIMIT 10"
uploadGhostFileStatement = "" +
"INSERT INTO mario_kart_wii_sake (regionid, courseid, score, pid, playerinfo, ghost) " +
"VALUES ($1, $2, $3, $4, $5, $6) " +
"ON CONFLICT (courseid, pid) DO UPDATE " +
"SET regionid = EXCLUDED.regionid, score = EXCLUDED.score, playerinfo = EXCLUDED.playerinfo, ghost = EXCLUDED.ghost"
)
func GetMarioKartWiiTopTenRankings(pool *pgxpool.Pool, ctx context.Context, regionId common.MarioKartWiiLeaderboardRegionId,
courseId common.MarioKartWiiCourseId) ([]MarioKartWiiTopTenRanking, error) {
rows, err := pool.Query(ctx, getTopTenRankingsQuery, regionId, courseId)
if err != nil {
return nil, err
}
defer rows.Close()
topTenRankings := make([]MarioKartWiiTopTenRanking, 0, 10)
for rows.Next() {
var topTenRanking MarioKartWiiTopTenRanking
err = rows.Scan(&topTenRanking.Score, &topTenRanking.PID, &topTenRanking.PlayerInfo)
if err != nil {
return nil, err
}
topTenRankings = append(topTenRankings, topTenRanking)
}
err = rows.Err()
if err != nil {
return nil, err
}
return topTenRankings, nil
}
func UploadMarioKartWiiGhostFile(pool *pgxpool.Pool, ctx context.Context, regionId common.MarioKartWiiLeaderboardRegionId,
courseId common.MarioKartWiiCourseId, score int, pid int, playerInfo string, ghost []byte) error {
_, err := pool.Exec(ctx, uploadGhostFileStatement, regionId, courseId, score, pid, playerInfo, ghost)
return err
}

View File

@ -1,14 +1,39 @@
package race
import (
"context"
"fmt"
"net/http"
"path"
"wwfc/common"
"wwfc/logging"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
)
var (
ctx = context.Background()
pool *pgxpool.Pool
)
func StartServer(reload bool) {
// Get config
config := common.GetConfig()
common.ReadGameList()
// Start SQL
dbString := fmt.Sprintf("postgres://%s:%s@%s/%s", config.Username, config.Password, config.DatabaseAddress, config.DatabaseName)
dbConf, err := pgxpool.ParseConfig(dbString)
if err != nil {
panic(err)
}
pool, err = pgxpool.ConnectConfig(ctx, dbConf)
if err != nil {
panic(err)
}
}
func Shutdown() {

View File

@ -5,7 +5,9 @@ import (
"io"
"net/http"
"strconv"
"strings"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
"github.com/logrusorgru/aurora/v3"
@ -21,17 +23,17 @@ type rankingsRequestBody struct {
type rankingsRequestData struct {
XMLName xml.Name
GameId int `xml:"gameid"`
RegionId common.MarioKartWiiRegionID `xml:"regionid"`
CourseId common.MarioKartWiiCourseID `xml:"courseid"`
GameId int `xml:"gameid"`
RegionId common.MarioKartWiiLeaderboardRegionId `xml:"regionid"`
CourseId common.MarioKartWiiCourseId `xml:"courseid"`
}
type rankingsResponseRankingDataResponse struct {
XMLName xml.Name `xml:"RankingDataResponse"`
XMLNSXsi string `xml:"xmlns:xsi,attr"`
XMLNSXsd string `xml:"xmlns:xsd,attr"`
XMLNS string `xml:"xmlns,attr"`
ResponseCode int `xml:"responseCode"`
XMLName xml.Name `xml:"RankingDataResponse"`
XMLNSXSI string `xml:"xmlns:xsi,attr"`
XMLNSXSD string `xml:"xmlns:xsd,attr"`
XMLNS string `xml:"xmlns,attr"`
ResponseCode raceServiceResult `xml:"responseCode"`
DataArray rankingsResponseDataArray
}
@ -54,10 +56,40 @@ type rankingsResponseRankingData struct {
UserData string `xml:"userdata"`
}
type raceServiceResult int
// https://github.com/GameProgressive/UniSpySDK/blob/master/webservices/RacingService.h
const (
raceServiceResultSuccess = 0
raceServiceResultDatabaseError = 6
raceServiceResultParseError = 101
raceServiceResultInvalidParameters = 105
)
const (
xmlNamespaceXSI = "http://www.w3.org/2001/XMLSchema-instance"
xmlNamespaceXSD = "http://www.w3.org/2001/XMLSchema"
xmlNamespace = "http://gamespy.net/RaceService/"
)
func handleNintendoRacingServiceRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) {
soapActionHeader := request.Header.Get("SOAPAction")
if soapActionHeader == "" {
logging.Error(moduleName, "No SOAPAction header")
writeErrorResponse(raceServiceResultParseError, responseWriter)
return
}
slashIndex := strings.LastIndex(soapActionHeader, "/")
if slashIndex == -1 {
logging.Error(moduleName, "Invalid SOAPAction header")
writeErrorResponse(raceServiceResultParseError, responseWriter)
return
}
quotationMarkIndex := strings.Index(soapActionHeader[slashIndex+1:], "\"")
if quotationMarkIndex == -1 {
logging.Error(moduleName, "Invalid SOAPAction header")
writeErrorResponse(raceServiceResultParseError, responseWriter)
return
}
@ -66,78 +98,103 @@ func handleNintendoRacingServiceRequest(moduleName string, responseWriter http.R
panic(err)
}
requestXML := rankingsRequestEnvelope{}
err = xml.Unmarshal(requestBody, &requestXML)
if err != nil {
logging.Error(moduleName, "Got malformed XML")
return
}
requestData := requestXML.Body.Data
gameId := requestData.GameId
if gameId != common.MarioKartWiiGameSpyGameID {
logging.Error(moduleName, "Wrong GameSpy game id")
return
}
soapAction := requestData.XMLName.Local
soapAction := soapActionHeader[slashIndex+1 : slashIndex+1+quotationMarkIndex]
switch soapAction {
case "GetTopTenRankings":
regionId := requestData.RegionId
courseId := requestData.CourseId
if !regionId.IsValid() {
logging.Error(moduleName, "Invalid region id")
return
}
if courseId < common.MarioCircuit {
logging.Error(moduleName, "Invalid course id")
return
}
var topTenLeaderboard string
if courseId <= common.GBAShyGuyBeach {
topTenLeaderboard = courseId.ToString()
} else {
topTenLeaderboard = "a competition"
}
logging.Info(moduleName, "Received a request for the Top 10 of", aurora.BrightCyan(topTenLeaderboard))
handleGetTopTenRankingsRequest(moduleName, responseWriter)
logging.Info(moduleName, "Received a Top 10 rankings request")
handleGetTopTenRankingsRequest(moduleName, responseWriter, requestBody)
default:
logging.Info(moduleName, "Unhandled SOAPAction:", aurora.Cyan(soapAction))
}
}
func handleGetTopTenRankingsRequest(moduleName string, responseWriter http.ResponseWriter) {
rankingData := rankingsResponseRankingData{
OwnerID: 1000000404,
Rank: 1,
Time: 0,
UserData: "xC0AIABNAGkAawBlAFMAdABhAHIAIAAAhH/RTQAAAAAgB45hkAAQTEDyjqQAeLgPhq4AiiUEACAATQBpAGsAZQBTAHQAYQByACD0UwACAAE=",
func handleGetTopTenRankingsRequest(moduleName string, responseWriter http.ResponseWriter, requestBody []byte) {
requestXML := rankingsRequestEnvelope{}
err := xml.Unmarshal(requestBody, &requestXML)
if err != nil {
logging.Error(moduleName, "Got malformed XML")
writeErrorResponse(raceServiceResultParseError, responseWriter)
return
}
responseData := []rankingsResponseData{
{
requestData := requestXML.Body.Data
gameId := requestData.GameId
if gameId != common.GameSpyGameIdMarioKartWii {
logging.Error(moduleName, "Wrong GameSpy game id")
writeErrorResponse(raceServiceResultInvalidParameters, responseWriter)
return
}
regionId := requestData.RegionId
courseId := requestData.CourseId
if !regionId.IsValid() {
logging.Error(moduleName, "Invalid region id")
writeErrorResponse(raceServiceResultInvalidParameters, responseWriter)
return
}
if courseId < common.MarioCircuit || courseId > 32767 {
logging.Error(moduleName, "Invalid course id")
writeErrorResponse(raceServiceResultInvalidParameters, responseWriter)
return
}
topTenRankings, err := database.GetMarioKartWiiTopTenRankings(pool, ctx, regionId, courseId)
if err != nil {
logging.Error(moduleName, "Failed to get the Top 10 rankings")
writeErrorResponse(raceServiceResultDatabaseError, responseWriter)
return
}
numberOfRankings := len(topTenRankings)
data := make([]rankingsResponseData, 0, numberOfRankings)
for i, topTenRanking := range topTenRankings {
rankingData := rankingsResponseRankingData{
OwnerID: topTenRanking.PID,
Rank: i + 1,
Time: topTenRanking.Score,
UserData: topTenRanking.PlayerInfo,
}
responseData := rankingsResponseData{
RankingData: rankingData,
},
}
data = append(data, responseData)
}
dataArray := rankingsResponseDataArray{
NumRecords: len(responseData),
Data: responseData,
NumRecords: numberOfRankings,
Data: data,
}
rankingDataResponse := rankingsResponseRankingDataResponse{
XMLNSXsi: "http://www.w3.org/2001/XMLSchema-instance",
XMLNSXsd: "http://www.w3.org/2001/XMLSchema",
XMLNS: "http://gamespy.net/RaceService/",
ResponseCode: 0,
XMLNSXSI: xmlNamespaceXSI,
XMLNSXSD: xmlNamespaceXSD,
XMLNS: xmlNamespace,
ResponseCode: raceServiceResultSuccess,
DataArray: dataArray,
}
writeResponse(responseWriter, rankingDataResponse)
}
func writeErrorResponse(raceServiceResult raceServiceResult, responseWriter http.ResponseWriter) {
rankingDataResponse := rankingsResponseRankingDataResponse{
XMLNSXSI: xmlNamespaceXSI,
XMLNSXSD: xmlNamespaceXSD,
XMLNS: xmlNamespace,
ResponseCode: raceServiceResult,
}
writeResponse(responseWriter, rankingDataResponse)
}
func writeResponse(responseWriter http.ResponseWriter, rankingDataResponse rankingsResponseRankingDataResponse) {
responseBody, err := xml.Marshal(rankingDataResponse)
if err != nil {
logging.Error(moduleName, "Failed to XML encode the data")
return
panic(err)
}
responseBody = append([]byte(xml.Header), responseBody...)

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"strings"
"wwfc/common"
"wwfc/logging"
@ -41,9 +42,13 @@ func Shutdown() {
func HandleRequest(w http.ResponseWriter, r *http.Request) {
logging.Info("SAKE", aurora.Yellow(r.Method), aurora.Cyan(r.URL), "via", aurora.Cyan(r.Host), "from", aurora.BrightCyan(r.RemoteAddr))
switch r.URL.String() {
case "/SakeStorageServer/StorageServer.asmx":
urlPath := r.URL.Path
switch {
case urlPath == "/SakeStorageServer/StorageServer.asmx":
moduleName := "SAKE:Storage:" + r.RemoteAddr
handleStorageRequest(moduleName, w, r)
case strings.Contains(urlPath, "upload.aspx"):
moduleName := "SAKE:File:" + r.RemoteAddr
handleFileUploadRequest(moduleName, w, r)
}
}

194
sake/mario_kart_wii.go Normal file
View File

@ -0,0 +1,194 @@
package sake
import (
"bytes"
"encoding/binary"
"fmt"
"hash/crc32"
"io"
"net/http"
"strconv"
"strings"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
)
type playerInfo struct {
MiiData [0x4C]byte // 0x00
ControllerId byte // 0x4C
Unknown byte // 0x4D
StateCode byte // 0x4E
CountryCode byte // 0x4F
}
const (
playerInfoSize = 0x50
rkgdFileMaxSize = 0x2800
rkgdFileMinSize = 0x0088 + 0x0008 + 0x0004
rkgdFileName = "ghost.bin"
)
func handleMarioKartWiiFileUploadRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) {
query := request.URL.Query()
regionIdString := query.Get("regionid")
courseIdString := query.Get("courseid")
scoreString := query.Get("score")
pidString := query.Get("pid")
playerInfo := query.Get("playerinfo")
regionIdInt, err := strconv.Atoi(regionIdString)
if err != nil {
logging.Error(moduleName, "Invalid region id")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultMissingParameter))
return
}
regionId := common.MarioKartWiiLeaderboardRegionId(regionIdInt)
if !regionId.IsValid() || regionId == common.Worldwide {
logging.Error(moduleName, "Invalid region id")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultMissingParameter))
return
}
courseIdInt, err := strconv.Atoi(courseIdString)
if err != nil {
logging.Error(moduleName, "Invalid course id")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultMissingParameter))
return
}
courseId := common.MarioKartWiiCourseId(courseIdInt)
if courseId < common.MarioCircuit || courseId > 32767 {
logging.Error(moduleName, "Invalid course id")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultMissingParameter))
return
}
score, err := strconv.Atoi(scoreString)
if err != nil || score <= 0 {
logging.Error(moduleName, "Invalid score")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultMissingParameter))
return
}
pid, err := strconv.Atoi(pidString)
if err != nil || pid <= 0 {
logging.Error(moduleName, "Invalid pid")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultMissingParameter))
return
}
if !isPlayerInfoValid(playerInfo) {
logging.Error(moduleName, "Invalid player info")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultMissingParameter))
return
}
// Mario Kart Wii expects player information to be in this form
playerInfo, _ = common.GameSpyBase64ToBase64(playerInfo, common.GameSpyBase64EncodingURLSafe)
// The multipart boundary utilized by GameSpy does not conform to RFC 2045. To ensure compliance,
// we need to surround it with double quotation marks.
contentType := request.Header.Get("Content-Type")
boundary := getMultipartBoundary(contentType)
if boundary == common.GameSpyMultipartBoundary {
quotedBoundary := fmt.Sprintf("%q", boundary)
contentType := strings.Replace(contentType, boundary, quotedBoundary, 1)
request.Header.Set("Content-Type", contentType)
}
err = request.ParseMultipartForm(rkgdFileMaxSize)
if err != nil {
logging.Error(moduleName, "Failed to parse the multipart form")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultFileNotFound))
return
}
file, fileHeader, err := request.FormFile(rkgdFileName)
if err != nil {
logging.Error(moduleName, "Failed to find the ghost file")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultFileNotFound))
return
}
defer file.Close()
if fileHeader.Size < rkgdFileMinSize || fileHeader.Size > rkgdFileMaxSize {
logging.Error(moduleName, "The size of the ghost file is invalid")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultFileTooLarge))
return
}
ghostFile := make([]byte, fileHeader.Size)
_, err = io.ReadFull(file, ghostFile)
if err != nil {
logging.Error(moduleName, "Failed to read contents of the ghost file")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultFileTooLarge))
return
}
if !isRKGDFileValid(ghostFile) {
logging.Error(moduleName, "Received an invalid ghost file")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultFileTooLarge))
return
}
err = database.UploadMarioKartWiiGhostFile(pool, ctx, regionId, courseId, score, pid, playerInfo, ghostFile)
if err != nil {
logging.Error(moduleName, "Failed to insert the ghost file into the database")
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultServerError))
return
}
responseWriter.Header().Set(common.SakeFileResultHeader, strconv.Itoa(common.SakeFileResultSuccess))
}
func isPlayerInfoValid(playerInfoString string) bool {
playerInfoByteArray, err := common.DecodeGameSpyBase64(playerInfoString, common.GameSpyBase64EncodingURLSafe)
if err != nil {
return false
}
if len(playerInfoByteArray) != playerInfoSize {
return false
}
var playerInfo playerInfo
reader := bytes.NewReader(playerInfoByteArray)
err = binary.Read(reader, binary.BigEndian, &playerInfo)
if err != nil {
return false
}
if common.RFLCalculateCRC(playerInfo.MiiData[:]) != 0x0000 {
return false
}
controllerId := common.MarioKartWiiControllerId(playerInfo.ControllerId)
return controllerId.IsValid()
}
func getMultipartBoundary(contentType string) string {
startIndex := strings.Index(contentType, "boundary=")
if startIndex == -1 {
return ""
}
startIndex += len("boundary=")
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
}

View File

@ -112,6 +112,10 @@ type StorageSearchForRecordsResponse struct {
Values StorageResponseValues `xml:"values"` // ???
}
var fileUploadHandlers = map[int]func(string, http.ResponseWriter, *http.Request){
common.GameSpyGameIdMarioKartWii: handleMarioKartWiiFileUploadRequest,
}
func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Request) {
headerAction := r.Header.Get("SOAPAction")
if headerAction == "" {
@ -167,13 +171,32 @@ func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Requ
panic(err)
}
payload := append([]byte(`<?xml version="1.0" encoding="utf-8"?>`), out...)
payload := append([]byte(xml.Header), out...)
w.Header().Set("Content-Type", "text/xml")
w.Header().Set("Content-Length", strconv.Itoa(len(payload)))
w.Write(payload)
}
func handleFileUploadRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) {
query := request.URL.Query()
gameIdString := query.Get("gameid")
gameId, err := strconv.Atoi(gameIdString)
if err != nil {
logging.Error(moduleName, "Invalid GameSpy game id")
return
}
handler, handlerExists := fileUploadHandlers[gameId]
if !handlerExists {
logging.Warn(moduleName, "Unhandled file upload request for 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 {

View File

@ -52,6 +52,24 @@ ALTER TABLE ONLY public.users
ALTER TABLE public.users OWNER TO wiilink;
--
-- Name: mario_kart_wii_sake; Type: TABLE; Schema: public; Owner: wiilink
--
CREATE TABLE IF NOT EXISTS public.mario_kart_wii_sake (
regionid smallint NOT NULL CHECK (regionid >= 1 AND regionid <= 7),
courseid smallint NOT NULL CHECK (courseid >= 0 AND courseid <= 32767),
score integer NOT NULL CHECK (score > 0),
pid integer NOT NULL CHECK (pid > 0),
playerinfo varchar(108) NOT NULL CHECK (LENGTH(playerinfo) = 108),
ghost bytea CHECK (ghost IS NULL OR (OCTET_LENGTH(ghost) BETWEEN 148 AND 10240)),
CONSTRAINT one_time_per_course_constraint UNIQUE (courseid, pid)
);
ALTER TABLE public.mario_kart_wii_sake OWNER TO wiilink;
--
-- Name: users_profile_id_seq; Type: SEQUENCE; Schema: public; Owner: wiilink
--