mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-05-06 05:26:33 -05:00
SAKE: Create game-specific table definitions
Some checks failed
Build CI / build (push) Has been cancelled
Some checks failed
Build CI / build (push) Has been cancelled
Additionally adds verification and sanity checks for all values passed to Sake from the client
This commit is contained in:
parent
d5c1a80a06
commit
865d8a8df2
|
|
@ -2,20 +2,13 @@ package common
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GameSpyBase64Encoding int
|
||||
|
||||
const (
|
||||
GameSpyBase64EncodingDefault = iota // 0
|
||||
GameSpyBase64EncodingAlternate // 1
|
||||
GameSpyBase64EncodingURLSafe // 2
|
||||
var (
|
||||
Base64DwcEncoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-").WithPadding('*')
|
||||
Base64GamespyAlternativeEncoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789[]").WithPadding('_')
|
||||
)
|
||||
|
||||
var Base64DwcEncoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-").WithPadding('*')
|
||||
|
||||
func Base32Encode(value uint64) string {
|
||||
alpha := "0123456789abcdefghijklmnopqrstuv"
|
||||
|
||||
|
|
@ -30,29 +23,13 @@ func Base32Encode(value uint64) string {
|
|||
return encoded
|
||||
}
|
||||
|
||||
func DecodeGameSpyBase64(gameSpyBase64 string, gameSpyBase64Encoding GameSpyBase64Encoding) ([]byte, error) {
|
||||
base64String, err := GameSpyBase64ToBase64(gameSpyBase64, gameSpyBase64Encoding)
|
||||
func Base64Convert(input string, fromEncoding, toEncoding *base64.Encoding) (string, error) {
|
||||
decoded, err := fromEncoding.DecodeString(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", 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")
|
||||
}
|
||||
return toEncoding.EncodeToString(decoded), nil
|
||||
}
|
||||
|
||||
func reverse(s string) string {
|
||||
|
|
|
|||
|
|
@ -14,3 +14,25 @@ const (
|
|||
SakeFileResultFileTooLarge = 5
|
||||
SakeFileResultServerError = 6
|
||||
)
|
||||
|
||||
const (
|
||||
ResultSuccess = "Success" // 4xx51
|
||||
ResultSecretKeyInvalid = "SecretKeyInvalid" // 4xx52
|
||||
ResultServiceDisabled = "ServiceDisabled" // 4xx53
|
||||
ResultDatabaseUnavailable = "DatabaseUnavailable" // 4xx58
|
||||
ResultLoginTicketInvalid = "LoginTicketInvalid" // 4xx59
|
||||
ResultLoginTicketExpired = "LoginTicketExpired" // 4xx60
|
||||
ResultTableNotFound = "TableNotFound" // 4xx61
|
||||
ResultRecordNotFound = "RecordNotFound" // 4xx62
|
||||
ResultFieldNotFound = "FieldNotFound" // 4xx63
|
||||
ResultFieldTypeInvalid = "FieldTypeInvalid" // 4xx64
|
||||
ResultNoPermission = "NoPermission" // 4xx65
|
||||
ResultRecordLimitReached = "RecordLimitReached" // 4xx66
|
||||
ResultAlreadyRated = "AlreadyRated" // 4xx67
|
||||
ResultNotRateable = "NotRateable" // 4xx68
|
||||
ResultNotOwned = "NotOwned" // 4xx69
|
||||
ResultFilterInvalid = "FilterInvalid" // 4xx70
|
||||
ResultSortInvalid = "SortInvalid" // 4xx71
|
||||
ResultTargetFilterInvalid = "TargetFilterInvalid" // 4xx80
|
||||
ResultUnknownError = "UnknownError" // 4xx72
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package sake
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -34,63 +35,63 @@ var (
|
|||
ghostDataFilterRegex = regexp.MustCompile(`^course = ([1-9]\d?|0) and gameid = 1687 and time < ([1-9][0-9]{0,5})$`)
|
||||
)
|
||||
|
||||
func getMarioKartWiiGhostDataRecord(moduleName string, request StorageRequestData) (database.SakeRecord, bool) {
|
||||
func getMarioKartWiiGhostDataRecord(moduleName string, request StorageRequestData) ([]database.SakeRecord, bool) {
|
||||
if request.Sort != "time desc" {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid sort string:", aurora.Cyan(request.Sort))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Offset != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid offset value:", aurora.Cyan(request.Offset))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Max != 1 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid number of records to return:", aurora.Cyan(request.Max))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Surrounding != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid number of surrounding records to return:", aurora.Cyan(request.Surrounding))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if len(request.OwnerIDs.OwnerID) != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid owner id array:", aurora.Cyan(request.OwnerIDs))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.CacheFlag != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid cache value:", aurora.Cyan(request.CacheFlag))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
match := ghostDataFilterRegex.FindStringSubmatch(request.Filter)
|
||||
if match == nil {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid filter string:", aurora.Cyan(request.Filter))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
courseIdInt, _ := strconv.Atoi(match[1])
|
||||
courseId := common.MarioKartWiiCourseId(courseIdInt)
|
||||
if !courseId.IsValid() {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid course ID:", aurora.Cyan(match[1]))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
time, _ := strconv.Atoi(match[2])
|
||||
if time >= 360000 /* 6 minutes */ {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid time:", aurora.Cyan(match[2]))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
fileId, err := database.GetMarioKartWiiGhostData(pool, ctx, courseId, time)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Failed to get the ghost data from the database:", err)
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
return database.SakeRecord{
|
||||
return []database.SakeRecord{{
|
||||
GameId: 1687,
|
||||
TableId: "GhostData",
|
||||
RecordId: 0,
|
||||
|
|
@ -101,51 +102,51 @@ func getMarioKartWiiGhostDataRecord(moduleName string, request StorageRequestDat
|
|||
Value: strconv.FormatInt(int64(int32(fileId)), 10),
|
||||
},
|
||||
},
|
||||
}, true
|
||||
}}, true
|
||||
}
|
||||
|
||||
func getMarioKartWiiStoredGhostDataRecord(moduleName string, request StorageRequestData) (database.SakeRecord, bool) {
|
||||
func getMarioKartWiiStoredGhostDataRecord(moduleName string, request StorageRequestData) ([]database.SakeRecord, bool) {
|
||||
if request.Sort != "time" {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid sort string:", aurora.Cyan(request.Sort))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Offset != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid offset value:", aurora.Cyan(request.Offset))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Max != 1 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid number of records to return:", aurora.Cyan(request.Max))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Surrounding != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid number of surrounding records to return:", aurora.Cyan(request.Surrounding))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if len(request.OwnerIDs.OwnerID) != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid owner id array:", aurora.Cyan(request.OwnerIDs))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.CacheFlag != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid cache value:", aurora.Cyan(request.CacheFlag))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`^course = ([1-9]\d?|0) and gameid = 1687(?: and region = ([1-7]))?$`).FindStringSubmatch(request.Filter)
|
||||
if match == nil {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid filter string:", aurora.Cyan(request.Filter))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
courseIdInt, _ := strconv.Atoi(match[1])
|
||||
courseId := common.MarioKartWiiCourseId(courseIdInt)
|
||||
if !courseId.IsValid() {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid course ID:", aurora.Cyan(match[1]))
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
var regionId common.MarioKartWiiLeaderboardRegionId
|
||||
|
|
@ -159,10 +160,10 @@ func getMarioKartWiiStoredGhostDataRecord(moduleName string, request StorageRequ
|
|||
pid, fileId, err := database.GetMarioKartWiiStoredGhostData(pool, ctx, regionId, courseId)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Failed to get the stored ghost data from the database:", err)
|
||||
return database.SakeRecord{}, false
|
||||
return []database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
return database.SakeRecord{
|
||||
return []database.SakeRecord{{
|
||||
GameId: 1687,
|
||||
TableId: "StoredGhostData",
|
||||
RecordId: 0,
|
||||
|
|
@ -171,7 +172,7 @@ func getMarioKartWiiStoredGhostDataRecord(moduleName string, request StorageRequ
|
|||
"profile": {Type: database.SakeFieldTypeInt, Value: strconv.FormatInt(int64(int32(pid)), 10)},
|
||||
"fileid": {Type: database.SakeFieldTypeInt, Value: strconv.FormatInt(int64(int32(fileId)), 10)},
|
||||
},
|
||||
}, true
|
||||
}}, true
|
||||
}
|
||||
|
||||
func handleMarioKartWiiFileDownloadRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) {
|
||||
|
|
@ -335,7 +336,7 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http
|
|||
return
|
||||
}
|
||||
// Mario Kart Wii expects player information to be in this form
|
||||
playerInfo, _ = common.GameSpyBase64ToBase64(playerInfo, common.GameSpyBase64EncodingURLSafe)
|
||||
playerInfo, _ = common.Base64Convert(playerInfo, base64.URLEncoding, base64.StdEncoding)
|
||||
|
||||
// The multipart boundary utilized by GameSpy does not conform to RFC 2045. To ensure compliance,
|
||||
// we need to surround it with double quotation marks.
|
||||
|
|
@ -405,7 +406,7 @@ func downloadedGhostFileHeader() []byte {
|
|||
}
|
||||
|
||||
func isPlayerInfoValid(playerInfoString string) bool {
|
||||
playerInfoByteArray, err := common.DecodeGameSpyBase64(playerInfoString, common.GameSpyBase64EncodingURLSafe)
|
||||
playerInfoByteArray, err := base64.URLEncoding.DecodeString(playerInfoString)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
136
sake/storage.go
136
sake/storage.go
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
|
@ -16,34 +15,6 @@ import (
|
|||
"github.com/logrusorgru/aurora/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxSakeRecordsPerProfile = 96
|
||||
MaxSakeFieldsPerRecord = 64
|
||||
MaxSakeFieldValueLength = 4096
|
||||
)
|
||||
|
||||
const (
|
||||
ResultSuccess = "Success"
|
||||
ResultSecretKeyInvalid = "SecretKeyInvalid"
|
||||
ResultServiceDisabled = "ServiceDisabled"
|
||||
ResultDatabaseUnavailable = "DatabaseUnavailable"
|
||||
ResultLoginTicketInvalid = "LoginTicketInvalid"
|
||||
ResultLoginTicketExpired = "LoginTicketExpired"
|
||||
ResultTableNotFound = "TableNotFound"
|
||||
ResultRecordNotFound = "RecordNotFound"
|
||||
ResultFieldNotFound = "FieldNotFound"
|
||||
ResultFieldTypeInvalid = "FieldTypeInvalid"
|
||||
ResultNoPermission = "NoPermission"
|
||||
ResultRecordLimitReached = "RecordLimitReached"
|
||||
ResultAlreadyRated = "AlreadyRated"
|
||||
ResultNotRateable = "NotRateable"
|
||||
ResultNotOwned = "NotOwned"
|
||||
ResultFilterInvalid = "FilterInvalid"
|
||||
ResultSortInvalid = "SortInvalid"
|
||||
ResultTargetFilterInvalid = "TargetFilterInvalid"
|
||||
ResultUnknownError = "UnknownError"
|
||||
)
|
||||
|
||||
const (
|
||||
SOAPEnvNamespace = "http://schemas.xmlsoap.org/soap/envelope/"
|
||||
SakeNamespace = "http://gamespy.net/sake"
|
||||
|
|
@ -274,17 +245,25 @@ func createRecord(moduleName string, profileId uint32, gameInfo common.GameInfo,
|
|||
}}
|
||||
}
|
||||
|
||||
if gameInfo.Name == "mariokartwii" && (request.TableID == "GhostData" || request.TableID == "StoredGhostData") {
|
||||
table := GetTable(gameInfo.Name, request.TableID)
|
||||
if table != nil && table.Reserved {
|
||||
// Reserved for special handler
|
||||
logging.Error(moduleName, "Attempt to create record in reserved table", aurora.Cyan(request.TableID))
|
||||
logging.Error(moduleName, "Attempt to create record in reserved table", aurora.Cyan(request.TableID), "in game", aurora.BrightCyan(gameInfo.Name))
|
||||
return StorageResponseBody{CreateRecordResponse: &StorageCreateRecordResponse{
|
||||
CreateRecordResult: ResultTableNotFound,
|
||||
CreateRecordResult: ResultNoPermission,
|
||||
}}
|
||||
}
|
||||
|
||||
if !table.AllowsPublicCreate() {
|
||||
logging.Error(moduleName, "Attempt to create record in table that doesn't allow public create", aurora.Cyan(request.TableID), "in game", aurora.BrightCyan(gameInfo.Name))
|
||||
return StorageResponseBody{CreateRecordResponse: &StorageCreateRecordResponse{
|
||||
CreateRecordResult: ResultNoPermission,
|
||||
}}
|
||||
}
|
||||
|
||||
var record database.SakeRecord
|
||||
var result string
|
||||
record.Fields, result = getInputFields(moduleName, request)
|
||||
record.Fields, result = getInputFields(moduleName, request, table, true)
|
||||
if result != ResultSuccess {
|
||||
return StorageResponseBody{CreateRecordResponse: &StorageCreateRecordResponse{
|
||||
CreateRecordResult: result,
|
||||
|
|
@ -357,16 +336,25 @@ func updateRecord(moduleName string, profileId uint32, gameInfo common.GameInfo,
|
|||
}}
|
||||
}
|
||||
|
||||
if gameInfo.Name == "mariokartwii" && (request.TableID == "GhostData" || request.TableID == "StoredGhostData") {
|
||||
table := GetTable(gameInfo.Name, request.TableID)
|
||||
if table != nil && table.Reserved {
|
||||
// Reserved for special handler
|
||||
logging.Error(moduleName, "Attempt to update record in reserved table", aurora.Cyan(request.TableID), "in game", aurora.BrightCyan(gameInfo.Name))
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultTableNotFound,
|
||||
UpdateRecordResult: ResultNoPermission,
|
||||
}}
|
||||
}
|
||||
|
||||
if !table.AllowsOwnerUpdate() {
|
||||
logging.Error(moduleName, "Attempt to update record in table that doesn't allow owner update", aurora.Cyan(request.TableID), "in game", aurora.BrightCyan(gameInfo.Name))
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultNoPermission,
|
||||
}}
|
||||
}
|
||||
|
||||
var record database.SakeRecord
|
||||
var result string
|
||||
record.Fields, result = getInputFields(moduleName, request)
|
||||
record.Fields, result = getInputFields(moduleName, request, table, false)
|
||||
if result != ResultSuccess {
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: result,
|
||||
|
|
@ -403,46 +391,39 @@ func updateRecord(moduleName string, profileId uint32, gameInfo common.GameInfo,
|
|||
}}
|
||||
}
|
||||
|
||||
var searchRecordHandlers = map[string]func(string, StorageRequestData) (database.SakeRecord, bool){
|
||||
"mariokartwii/GhostData": getMarioKartWiiGhostDataRecord,
|
||||
"mariokartwii/StoredGhostData": getMarioKartWiiStoredGhostDataRecord,
|
||||
}
|
||||
|
||||
var (
|
||||
standardFilterRegex = regexp.MustCompile(`^ownerid\s*=\s*-?(\d{1,10})$`)
|
||||
)
|
||||
|
||||
// TODO: Test this
|
||||
func searchForRecords(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestData) StorageResponseBody {
|
||||
var records []database.SakeRecord
|
||||
|
||||
if handler, ok := searchRecordHandlers[gameInfo.Name+"/"+request.TableID]; ok {
|
||||
record, ok := handler(moduleName, request)
|
||||
if request.TableID == "" {
|
||||
logging.Error(moduleName, "No table ID provided")
|
||||
return StorageResponseBody{SearchForRecordsResponse: &StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: ResultTableNotFound,
|
||||
}}
|
||||
}
|
||||
|
||||
table := GetTable(gameInfo.Name, request.TableID)
|
||||
|
||||
if table != nil && table.SearchForRecordsHandler != nil {
|
||||
var ok bool
|
||||
records, ok = table.SearchForRecordsHandler(moduleName, request)
|
||||
if !ok {
|
||||
return StorageResponseBody{SearchForRecordsResponse: &StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: ResultUnknownError,
|
||||
}}
|
||||
}
|
||||
records = append(records, record)
|
||||
} else if table != nil && table.Reserved {
|
||||
logging.Error(moduleName, "Attempt to search for records in reserved table", aurora.Cyan(request.TableID))
|
||||
return StorageResponseBody{SearchForRecordsResponse: &StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: ResultTableNotFound,
|
||||
}}
|
||||
} else {
|
||||
filter := request.Filter
|
||||
ownerIds := request.OwnerIDs.OwnerID
|
||||
|
||||
// Parse standard filter
|
||||
if matches := standardFilterRegex.FindStringSubmatch(request.Filter); matches != nil {
|
||||
ownerIdParsed, err := strconv.ParseInt(matches[1], 10, 32)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Invalid owner ID in filter:", aurora.Cyan(request.Filter))
|
||||
return StorageResponseBody{SearchForRecordsResponse: &StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: ResultFilterInvalid,
|
||||
}}
|
||||
}
|
||||
ownerIds = append(ownerIds, int32(ownerIdParsed))
|
||||
filter = ""
|
||||
if !table.AllowsPublicRead() {
|
||||
ownerIds = []int32{int32(profileId)}
|
||||
}
|
||||
|
||||
var err error
|
||||
records, err = database.GetSakeRecords(pool, ctx, gameInfo.GameID, ownerIds, request.TableID, nil, request.Fields.Fields, filter)
|
||||
records, err = database.GetSakeRecords(pool, ctx, gameInfo.GameID, ownerIds, request.TableID, nil, request.Fields.Fields, request.Filter)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Failed to get sake records from the database:", err)
|
||||
return StorageResponseBody{SearchForRecordsResponse: &StorageSearchForRecordsResponse{
|
||||
|
|
@ -451,7 +432,7 @@ func searchForRecords(moduleName string, profileId uint32, gameInfo common.GameI
|
|||
}
|
||||
}
|
||||
|
||||
// Sort the records now
|
||||
// Sort the records now. TODO: This can be done more effectively in the database query.
|
||||
sort.Slice(records, func(l, r int) bool {
|
||||
lVal, lExists := records[l].Fields[request.Sort]
|
||||
rVal, rExists := records[r].Fields[request.Sort]
|
||||
|
|
@ -490,21 +471,20 @@ func searchForRecords(moduleName string, profileId uint32, gameInfo common.GameI
|
|||
return StorageResponseBody{SearchForRecordsResponse: &response}
|
||||
}
|
||||
|
||||
func getInputFields(moduleName string, request StorageRequestData) (map[string]database.SakeField, string) {
|
||||
record := database.SakeRecord{
|
||||
Fields: make(map[string]database.SakeField),
|
||||
}
|
||||
func getInputFields(moduleName string, request StorageRequestData, table *SakeTable, useDefault bool) (map[string]database.SakeField, string) {
|
||||
if len(request.Values.RecordFields) > MaxSakeFieldsPerRecord {
|
||||
logging.Error(moduleName, "Too many fields in record:", aurora.Cyan(len(request.Values.RecordFields)))
|
||||
return nil, ResultFieldTypeInvalid
|
||||
}
|
||||
var fields map[string]database.SakeField
|
||||
if useDefault {
|
||||
fields = table.GetDefaultFields()
|
||||
} else {
|
||||
fields = make(map[string]database.SakeField)
|
||||
}
|
||||
|
||||
for _, field := range request.Values.RecordFields {
|
||||
value := field.Value.Value.Value
|
||||
if len(value) > MaxSakeFieldValueLength {
|
||||
logging.Error(moduleName, "Field value too long for field", aurora.Cyan(field.Name), "with length", aurora.Cyan(len(value)))
|
||||
return nil, ResultFieldTypeInvalid
|
||||
}
|
||||
fieldType, ok := tagToSakeType[field.Value.Value.XMLName.Local]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "Invalid field type tag:", aurora.Cyan(field.Value.Value.XMLName.Local))
|
||||
|
|
@ -514,9 +494,17 @@ func getInputFields(moduleName string, request StorageRequestData) (map[string]d
|
|||
logging.Error(moduleName, "Attempt to set reserved field:", aurora.Cyan(field.Name))
|
||||
return nil, ResultNoPermission
|
||||
}
|
||||
record.Fields[field.Name] = database.SakeField{Type: fieldType, Value: field.Value.Value.Value}
|
||||
sakeField := database.SakeField{
|
||||
Type: fieldType,
|
||||
Value: value,
|
||||
}
|
||||
if result := table.CheckValidField(field.Name, sakeField); result != ResultSuccess {
|
||||
logging.Error(moduleName, "Invalid value for field", aurora.Cyan(field.Name).String()+":", aurora.Cyan(value))
|
||||
return nil, result
|
||||
}
|
||||
fields[field.Name] = sakeField
|
||||
}
|
||||
return record.Fields, ResultSuccess
|
||||
return fields, ResultSuccess
|
||||
}
|
||||
|
||||
func fillResponseValues(records []database.SakeRecord, request StorageRequestData) StorageResponseValues {
|
||||
|
|
|
|||
389
sake/tables.go
Normal file
389
sake/tables.go
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
package sake
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"time"
|
||||
"wwfc/database"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxSakeRecordsPerProfile = 96
|
||||
MaxSakeFieldsPerRecord = 64
|
||||
MaxSakeFieldValueLength = 4096
|
||||
)
|
||||
|
||||
const DateAndTimeFormat = "2006-01-02T15:04:05.000"
|
||||
|
||||
const (
|
||||
RateableUnknown = iota
|
||||
RateableYes
|
||||
RateableNo
|
||||
)
|
||||
|
||||
type Rateable byte
|
||||
|
||||
const (
|
||||
OwnerTypeProfile = iota
|
||||
OwnerTypeBackend
|
||||
)
|
||||
|
||||
type OwnerType byte
|
||||
|
||||
const (
|
||||
PermissionDefault = iota
|
||||
PermissionAllowed
|
||||
PermissionDenied
|
||||
)
|
||||
|
||||
type Permission byte
|
||||
|
||||
type SakeFieldDefinition struct {
|
||||
// Type of the field.
|
||||
Type database.SakeFieldType
|
||||
// If empty, no default value will be set.
|
||||
// "{EMPTY}" can be used to set an empty string default value.
|
||||
// "{CURRENT_TIMESTAMP}" can be used to set the current timestamp as the default value (only for DateAndTime type).
|
||||
Default string
|
||||
// If zero, the default field length limit will be used.
|
||||
LengthLimit int
|
||||
|
||||
// Optional function for custom validation.
|
||||
IsValidFunc func(value string) bool
|
||||
// Optional function for custom filtering. This function receives the value from the client AFTER validation, before inserting into the database.
|
||||
FilterFromClientFunc func(value string) (string, error)
|
||||
// Optional function for custom filtering. This function receives the value from the database before sending to the client.
|
||||
FilterFromDatabaseFunc func(value string) (string, error)
|
||||
}
|
||||
|
||||
type SakeTable struct {
|
||||
// Determines whether the 'average_rating', 'my_rating', 'num_ratings', 'sum_ratings' fields are automatically added.
|
||||
Rateable Rateable
|
||||
// Defaults to profile-owned records if not specified.
|
||||
OwnerType OwnerType
|
||||
// Defaults to allowed if OwnerType is OwnerTypeProfile, denied otherwise.
|
||||
PublicPermCreate Permission
|
||||
// Defaults to allowed.
|
||||
PublicPermRead Permission
|
||||
// Defaults to allowed.
|
||||
OwnerPermUpdate Permission
|
||||
// Defaults to allowed.
|
||||
OwnerPermDelete Permission
|
||||
// Override the default maximum number of records per owner.
|
||||
LimitPerOwner int
|
||||
// If true, fields not specified in this table definition will be rejected.
|
||||
Hardened bool
|
||||
// If true, Sake will return a NoPermission error for requests that don't have a custom handler
|
||||
Reserved bool
|
||||
// Custom handler for SearchForRecords. Returns an array of response Sake records.
|
||||
SearchForRecordsHandler func(string, StorageRequestData) ([]database.SakeRecord, bool)
|
||||
// Field definitions for this table. The key is the field name.
|
||||
Fields map[string]SakeFieldDefinition
|
||||
}
|
||||
|
||||
var TableDefinitions = map[string]SakeTable{
|
||||
"micchannelwii/userinfo": {
|
||||
Rateable: RateableYes,
|
||||
OwnerType: OwnerTypeProfile,
|
||||
PublicPermCreate: PermissionAllowed,
|
||||
PublicPermRead: PermissionAllowed,
|
||||
OwnerPermUpdate: PermissionAllowed,
|
||||
OwnerPermDelete: PermissionAllowed,
|
||||
Fields: map[string]SakeFieldDefinition{
|
||||
"wiiid": {
|
||||
Type: database.SakeFieldTypeInt64,
|
||||
},
|
||||
"username": {
|
||||
Type: database.SakeFieldTypeBinaryData,
|
||||
},
|
||||
"friendkey": {
|
||||
Type: database.SakeFieldTypeInt64,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"mariokartwii/FriendInfo": {
|
||||
Rateable: RateableNo,
|
||||
OwnerType: OwnerTypeProfile,
|
||||
PublicPermCreate: PermissionAllowed,
|
||||
PublicPermRead: PermissionAllowed,
|
||||
OwnerPermUpdate: PermissionAllowed,
|
||||
OwnerPermDelete: PermissionAllowed,
|
||||
LimitPerOwner: 1,
|
||||
Hardened: false, // To allow modding extra fields
|
||||
Fields: map[string]SakeFieldDefinition{
|
||||
"info": {
|
||||
Type: database.SakeFieldTypeBinaryData,
|
||||
},
|
||||
},
|
||||
},
|
||||
"mariokartwii/GhostData": {
|
||||
Rateable: RateableNo,
|
||||
OwnerType: OwnerTypeProfile,
|
||||
PublicPermCreate: PermissionAllowed,
|
||||
PublicPermRead: PermissionAllowed,
|
||||
OwnerPermUpdate: PermissionAllowed,
|
||||
OwnerPermDelete: PermissionAllowed,
|
||||
Hardened: true,
|
||||
Reserved: true,
|
||||
SearchForRecordsHandler: getMarioKartWiiStoredGhostDataRecord,
|
||||
Fields: map[string]SakeFieldDefinition{
|
||||
"fileid": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"profile": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"course": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"region": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"time": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
},
|
||||
},
|
||||
"mariokartwii/StoredGhostData": {
|
||||
Rateable: RateableNo,
|
||||
OwnerType: OwnerTypeProfile,
|
||||
PublicPermCreate: PermissionAllowed,
|
||||
PublicPermRead: PermissionAllowed,
|
||||
OwnerPermUpdate: PermissionAllowed,
|
||||
OwnerPermDelete: PermissionAllowed,
|
||||
Hardened: true,
|
||||
Reserved: true,
|
||||
SearchForRecordsHandler: getMarioKartWiiStoredGhostDataRecord,
|
||||
Fields: map[string]SakeFieldDefinition{
|
||||
"fileid": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"profile": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"course": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"region": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"time": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"guinnesswrds/RecordTable": {
|
||||
Rateable: RateableUnknown,
|
||||
OwnerType: OwnerTypeProfile,
|
||||
PublicPermCreate: PermissionAllowed,
|
||||
PublicPermRead: PermissionAllowed,
|
||||
OwnerPermUpdate: PermissionAllowed,
|
||||
OwnerPermDelete: PermissionAllowed,
|
||||
Fields: map[string]SakeFieldDefinition{
|
||||
"Score": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
},
|
||||
"GameID": {
|
||||
Type: database.SakeFieldTypeByte,
|
||||
},
|
||||
"Region": {
|
||||
Type: database.SakeFieldTypeByte,
|
||||
},
|
||||
"Country": {
|
||||
Type: database.SakeFieldTypeByte,
|
||||
},
|
||||
"OwnerName": {
|
||||
Type: database.SakeFieldTypeUnicodeString,
|
||||
},
|
||||
"AvatarName": {
|
||||
Type: database.SakeFieldTypeUnicodeString,
|
||||
},
|
||||
"AvatarModel": {
|
||||
Type: database.SakeFieldTypeByte,
|
||||
},
|
||||
"AvatarParts": {
|
||||
Type: database.SakeFieldTypeBinaryData,
|
||||
},
|
||||
"DateTimeSet": {
|
||||
Type: database.SakeFieldTypeDateAndTime,
|
||||
Default: "{CURRENT_TIMESTAMP}",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func GetTable(gameName string, tableId string) *SakeTable {
|
||||
tableDef, exists := TableDefinitions[gameName+"/"+tableId]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return &tableDef
|
||||
}
|
||||
|
||||
func (t *SakeTable) AllowsPublicCreate() bool {
|
||||
if t == nil {
|
||||
return true
|
||||
}
|
||||
if t.PublicPermCreate == PermissionDefault {
|
||||
return t.OwnerType == OwnerTypeProfile
|
||||
}
|
||||
return t.PublicPermCreate == PermissionAllowed
|
||||
}
|
||||
|
||||
func (t *SakeTable) AllowsPublicRead() bool {
|
||||
if t == nil {
|
||||
return true
|
||||
}
|
||||
return t.PublicPermRead == PermissionAllowed || t.PublicPermRead == PermissionDefault
|
||||
}
|
||||
|
||||
func (t *SakeTable) AllowsOwnerUpdate() bool {
|
||||
if t == nil {
|
||||
return true
|
||||
}
|
||||
return t.OwnerPermUpdate == PermissionAllowed || t.OwnerPermUpdate == PermissionDefault
|
||||
}
|
||||
|
||||
func (t *SakeTable) AllowsOwnerDelete() bool {
|
||||
if t == nil {
|
||||
return true
|
||||
}
|
||||
return t.OwnerPermDelete == PermissionAllowed || t.OwnerPermDelete == PermissionDefault
|
||||
}
|
||||
|
||||
func (t *SakeTable) GetDefaultFields() map[string]database.SakeField {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
defaultFields := make(map[string]database.SakeField)
|
||||
for fieldName, fieldDef := range t.Fields {
|
||||
if fieldDef.Default != "" {
|
||||
value := fieldDef.Default
|
||||
if value == "{CURRENT_TIMESTAMP}" && fieldDef.Type == database.SakeFieldTypeDateAndTime {
|
||||
value = time.Now().UTC().Format(DateAndTimeFormat)
|
||||
} else if value == "{EMPTY}" {
|
||||
value = ""
|
||||
}
|
||||
|
||||
defaultFields[fieldName] = database.SakeField{
|
||||
Type: fieldDef.Type,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
if t.Rateable == RateableYes {
|
||||
defaultFields["average_rating"] = database.SakeField{
|
||||
Type: database.SakeFieldTypeFloat,
|
||||
Value: "0",
|
||||
}
|
||||
defaultFields["num_ratings"] = database.SakeField{
|
||||
Type: database.SakeFieldTypeInt,
|
||||
Value: "0",
|
||||
}
|
||||
}
|
||||
|
||||
return defaultFields
|
||||
}
|
||||
|
||||
func (t *SakeTable) CheckValidField(fieldName string, field database.SakeField) string {
|
||||
lengthLimit := MaxSakeFieldValueLength
|
||||
var verifyFunc func(value string) bool
|
||||
if t != nil && len(t.Fields) != 0 {
|
||||
fieldDef, exists := t.Fields[fieldName]
|
||||
if !exists {
|
||||
if t.Hardened {
|
||||
return ResultFieldNotFound
|
||||
}
|
||||
} else if fieldDef.Type != field.Type {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
if fieldDef.LengthLimit > 0 {
|
||||
lengthLimit = fieldDef.LengthLimit
|
||||
}
|
||||
verifyFunc = fieldDef.IsValidFunc
|
||||
}
|
||||
if len(field.Value) > lengthLimit {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
|
||||
// These values may not be written to
|
||||
if fieldName == "average_rating" || fieldName == "my_rating" || fieldName == "num_ratings" || fieldName == "sum_ratings" {
|
||||
return ResultFieldNotFound
|
||||
}
|
||||
|
||||
switch field.Type {
|
||||
case database.SakeFieldTypeByte:
|
||||
if len(field.Value) == 0 || len(field.Value) > 3 {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
if parsed, err := strconv.ParseUint(field.Value, 10, 8); err != nil || strconv.FormatUint(parsed, 10) != field.Value {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
|
||||
case database.SakeFieldTypeShort:
|
||||
if len(field.Value) == 0 || len(field.Value) > 6 {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
if parsed, err := strconv.ParseInt(field.Value, 10, 16); err != nil || strconv.FormatInt(parsed, 10) != field.Value {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
|
||||
case database.SakeFieldTypeInt:
|
||||
if len(field.Value) == 0 || len(field.Value) > 11 {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
if parsed, err := strconv.ParseInt(field.Value, 10, 32); err != nil || strconv.FormatInt(parsed, 10) != field.Value {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
|
||||
case database.SakeFieldTypeInt64:
|
||||
if len(field.Value) == 0 || len(field.Value) > 20 {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
if parsed, err := strconv.ParseInt(field.Value, 10, 64); err != nil || strconv.FormatInt(parsed, 10) != field.Value {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
|
||||
case database.SakeFieldTypeFloat:
|
||||
if len(field.Value) == 0 || len(field.Value) > 24 {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
_, err := strconv.ParseFloat(field.Value, 32)
|
||||
if err != nil {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
|
||||
case database.SakeFieldTypeBoolean:
|
||||
if field.Value != "true" && field.Value != "false" && field.Value != "1" && field.Value != "0" {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
|
||||
case database.SakeFieldTypeDateAndTime:
|
||||
if len(field.Value) == 0 || len(field.Value) > 24 {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
_, err := time.Parse(DateAndTimeFormat, field.Value)
|
||||
if err != nil {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
|
||||
case database.SakeFieldTypeBinaryData:
|
||||
if len(field.Value) == 0 {
|
||||
return ResultSuccess
|
||||
}
|
||||
binaryData, err := base64.StdEncoding.Strict().DecodeString(field.Value)
|
||||
if err != nil {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
if len(binaryData) > lengthLimit {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
if verifyFunc != nil && !verifyFunc(field.Value) {
|
||||
return ResultFieldTypeInvalid
|
||||
}
|
||||
return ResultSuccess
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user