mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-04-24 23:47:50 -05:00
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:
commit
96b10233c4
|
|
@ -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
19
common/gamyspy.go
Normal 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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
62
database/mario_kart_wii.go
Normal file
62
database/mario_kart_wii.go
Normal 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
|
||||
}
|
||||
25
race/main.go
25
race/main.go
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
|
|
|
|||
|
|
@ -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
194
sake/mario_kart_wii.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
18
schema.sql
18
schema.sql
|
|
@ -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
|
||||
--
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user