Implement banning and custom error messages

This commit is contained in:
mkwcat 2024-01-10 03:22:17 -05:00
parent 46330bf375
commit 89f4044f04
No known key found for this signature in database
GPG Key ID: 7A505679CE9E7AA9
17 changed files with 896 additions and 88 deletions

126
api/ban.go Normal file
View File

@ -0,0 +1,126 @@
package api
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"time"
"wwfc/database"
"wwfc/gpcm"
)
func HandleBan(w http.ResponseWriter, r *http.Request) {
errorString := handleBanImpl(w, r)
if errorString != "" {
jsonData, _ := json.Marshal(map[string]string{"error": errorString})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
} else {
jsonData, _ := json.Marshal(map[string]string{"success": "true"})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
}
}
func handleBanImpl(w http.ResponseWriter, r *http.Request) string {
// TODO: Actual authentication rather than a fixed secret
// TODO: Use POST instead of GET
u, err := url.Parse(r.URL.String())
if err != nil {
return "Bad request"
}
query, err := url.ParseQuery(u.RawQuery)
if err != nil {
return "Bad request"
}
if apiSecret == "" || query.Get("secret") != apiSecret {
return "Invalid API secret"
}
pidStr := query.Get("pid")
if pidStr == "" {
return "Missing pid in request"
}
pid, err := strconv.ParseUint(pidStr, 10, 32)
if err != nil {
return "Invalid pid"
}
tosStr := query.Get("tos")
if tosStr == "" {
return "Missing tos in request"
}
tos, err := strconv.ParseBool(tosStr)
if err != nil {
return "Invalid tos"
}
minutes := uint64(0)
if query.Get("minutes") != "" {
minutesStr := query.Get("minutes")
minutes, err = strconv.ParseUint(minutesStr, 10, 32)
if err != nil {
return "Invalid minutes"
}
}
hours := uint64(0)
if query.Get("hours") != "" {
hoursStr := query.Get("hours")
hours, err = strconv.ParseUint(hoursStr, 10, 32)
if err != nil {
return "Invalid hours"
}
}
days := uint64(0)
if query.Get("days") != "" {
daysStr := query.Get("days")
days, err = strconv.ParseUint(daysStr, 10, 32)
if err != nil {
return "Invalid days"
}
}
reason := query.Get("reason")
if "reason" == "" {
return "Missing ban reason"
}
// reason_hidden is optional
reasonHidden := query.Get("reason_hidden")
moderator := query.Get("moderator")
if "moderator" == "" {
moderator = "admin"
}
minutes = days*24*60 + hours*60 + minutes
if minutes == 0 {
return "Missing ban length"
}
length := time.Duration(minutes) * time.Minute
if !database.BanUser(pool, ctx, uint32(pid), tos, length, reason, reasonHidden, moderator) {
return "Failed to ban user"
}
if tos {
gpcm.KickPlayer(uint32(pid), "banned")
} else {
gpcm.KickPlayer(uint32(pid), "restricted")
}
return ""
}

58
api/kick.go Normal file
View File

@ -0,0 +1,58 @@
package api
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"wwfc/gpcm"
)
func HandleKick(w http.ResponseWriter, r *http.Request) {
errorString := handleKickImpl(w, r)
if errorString != "" {
jsonData, _ := json.Marshal(map[string]string{"error": errorString})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
} else {
jsonData, _ := json.Marshal(map[string]string{"success": "true"})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
}
}
func handleKickImpl(w http.ResponseWriter, r *http.Request) string {
// TODO: Actual authentication rather than a fixed secret
// TODO: Use POST instead of GET
u, err := url.Parse(r.URL.String())
if err != nil {
return "Bad request"
}
query, err := url.ParseQuery(u.RawQuery)
if err != nil {
return "Bad request"
}
if apiSecret == "" || query.Get("secret") != apiSecret {
return "Invalid API secret"
}
pidStr := query.Get("pid")
if pidStr == "" {
return "Missing pid in request"
}
pid, err := strconv.ParseUint(pidStr, 10, 32)
if err != nil {
return "Invalid pid"
}
gpcm.KickPlayer(uint32(pid), "moderator_kick")
return ""
}

34
api/main.go Normal file
View File

@ -0,0 +1,34 @@
package api
import (
"context"
"fmt"
"wwfc/common"
"github.com/jackc/pgx/v4/pgxpool"
)
var (
ctx = context.Background()
pool *pgxpool.Pool
apiSecret string
)
func StartServer() {
// Get config
config := common.GetConfig()
apiSecret = config.APISecret
// 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)
}
}

58
api/unban.go Normal file
View File

@ -0,0 +1,58 @@
package api
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"wwfc/database"
)
func HandleUnban(w http.ResponseWriter, r *http.Request) {
errorString := handleUnbanImpl(w, r)
if errorString != "" {
jsonData, _ := json.Marshal(map[string]string{"error": errorString})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
} else {
jsonData, _ := json.Marshal(map[string]string{"success": "true"})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
}
}
func handleUnbanImpl(w http.ResponseWriter, r *http.Request) string {
// TODO: Actual authentication rather than a fixed secret
// TODO: Use POST instead of GET
u, err := url.Parse(r.URL.String())
if err != nil {
return "Bad request"
}
query, err := url.ParseQuery(u.RawQuery)
if err != nil {
return "Bad request"
}
if apiSecret == "" || query.Get("secret") != apiSecret {
return "Invalid API secret"
}
pidStr := query.Get("pid")
if pidStr == "" {
return "Missing pid in request"
}
pid, err := strconv.ParseUint(pidStr, 10, 32)
if err != nil {
return "Invalid pid"
}
database.UnbanUser(pool, ctx, uint32(pid))
return ""
}

View File

@ -23,6 +23,8 @@ type Config struct {
KeyPath string `xml:"keyPath"`
CertPathWii string `xml:"certDerPathWii"`
KeyPathWii string `xml:"keyPathWii"`
APISecret string `xml:"apiSecret"`
AllowDefaultDolphinKeys bool `xml:"allowDefaultDolphinKeys"`
}
func GetConfig() Config {
@ -32,6 +34,8 @@ func GetConfig() Config {
}
var config Config
config.AllowDefaultDolphinKeys = true
err = xml.Unmarshal(data, &config)
if err != nil {
panic(err)

View File

@ -20,6 +20,9 @@
<certDerPathWii>naswii-cert.der</certDerPathWii>
<keyPathWii>naswii-key.pem</keyPathWii>
<!-- Allow default Dolphin device keys to be used -->
<allowDefaultDolphinKeys>true</allowDefaultDolphinKeys>
<!-- Database Credentials -->
<username>username</username>
<password>password</password>
@ -30,4 +33,7 @@
<!-- Log verbosity -->
<logLevel>4</logLevel>
<!-- API secret -->
<apiSecret>hQ3f57b3tW2WnjJH3v</apiSecret>
</Config>

View File

@ -2,19 +2,27 @@ package database
import (
"context"
"errors"
"fmt"
"time"
"wwfc/common"
"wwfc/logging"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
)
func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32, ngDeviceId uint32) (User, bool) {
var (
ErrDeviceIDMismatch = errors.New("NG device ID mismatch")
ErrProfileBannedTOS = errors.New("Profile is banned for violating the Terms of Service")
)
func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32, ngDeviceId uint32, ipAddress string, ingamesn string) (User, error) {
var exists bool
err := pool.QueryRow(ctx, DoesUserExist, userId, gsbrcd).Scan(&exists)
if err != nil {
return User{}, false
return User{}, err
}
user := User{
@ -32,7 +40,7 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
err := user.CreateUser(pool, ctx)
if err != nil {
logging.Error("DATABASE", "Error creating user:", aurora.Cyan(userId), aurora.Cyan(gsbrcd), aurora.Cyan(user.ProfileId), "\nerror:", err.Error())
return User{}, false
return User{}, err
}
logging.Notice("DATABASE", "Created new GPCM user:", aurora.Cyan(userId), aurora.Cyan(gsbrcd), aurora.Cyan(user.ProfileId))
@ -42,7 +50,7 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
var lastName *string
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &expectedNgId, &user.Email, &user.UniqueNick, &firstName, &lastName)
if err != nil {
panic(err)
return User{}, err
}
if firstName != nil {
@ -53,17 +61,17 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
user.LastName = *lastName
}
if expectedNgId != nil && user.NgDeviceId != 0 {
if expectedNgId != nil && *expectedNgId != 0 {
user.NgDeviceId = *expectedNgId
if ngDeviceId != 0 && user.NgDeviceId != ngDeviceId {
logging.Error("DATABASE", "NG device ID mismatch for profile", aurora.Cyan(user.ProfileId), "- expected", aurora.Cyan(fmt.Sprintf("%08x", user.NgDeviceId)), "but got", aurora.Cyan(fmt.Sprintf("%08x", ngDeviceId)))
return User{}, false
return User{}, ErrDeviceIDMismatch
}
} else if ngDeviceId != 0 {
user.NgDeviceId = ngDeviceId
_, err := pool.Exec(ctx, UpdateUserNGDeviceID, user.ProfileId, ngDeviceId)
if err != nil {
panic(err)
return User{}, err
}
}
@ -86,5 +94,36 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
})
}
return user, true
// Update the user's last IP address and ingamesn
_, err = pool.Exec(ctx, UpdateUserLastIPAddress, user.ProfileId, ipAddress, ingamesn)
if err != nil {
return User{}, err
}
// Find ban from device ID or IP address
var banExists bool
var banTOS bool
var bannedDeviceId uint32
timeNow := time.Now()
err = pool.QueryRow(ctx, SearchUserBan, user.ProfileId, user.NgDeviceId, ipAddress, timeNow).Scan(&banExists, &banTOS, &bannedDeviceId)
if err != nil {
if err != pgx.ErrNoRows {
return User{}, err
}
banExists = false
}
if banExists {
if banTOS {
logging.Warn("DATABASE", "Profile", aurora.Cyan(user.ProfileId), "is banned")
return User{RestrictedDeviceId: bannedDeviceId}, ErrProfileBannedTOS
}
logging.Warn("DATABASE", "Profile", aurora.Cyan(user.ProfileId), "is restricted")
user.Restricted = true
user.RestrictedDeviceId = bannedDeviceId
}
return user, nil
}

22
database/schema.go Normal file
View File

@ -0,0 +1,22 @@
package database
import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
)
func UpdateTables(pool *pgxpool.Pool, ctx context.Context) {
pool.Exec(ctx, `
ALTER TABLE ONLY public.users
ADD IF NOT EXISTS last_ip_address character varying DEFAULT ''::character varying,
ADD IF NOT EXISTS last_ingamesn character varying DEFAULT ''::character varying,
ADD IF NOT EXISTS has_ban boolean DEFAULT false,
ADD IF NOT EXISTS ban_issued timestamp without time zone,
ADD IF NOT EXISTS ban_expires timestamp without time zone,
ADD IF NOT EXISTS ban_reason character varying,
ADD IF NOT EXISTS ban_reason_hidden character varying,
ADD IF NOT EXISTS ban_moderator character varying,
ADD IF NOT EXISTS ban_tos boolean
`)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"math/rand"
"time"
"github.com/jackc/pgx/v4/pgxpool"
)
@ -19,6 +20,10 @@ const (
IsProfileIDInUse = `SELECT EXISTS(SELECT 1 FROM users WHERE profile_id = $1)`
DeleteUserSession = `DELETE FROM sessions WHERE profile_id = $1`
GetUserProfileID = `SELECT profile_id, ng_device_id, email, unique_nick, firstname, lastname FROM users WHERE user_id = $1 AND gsbrcd = $2`
UpdateUserLastIPAddress = `UPDATE users SET last_ip_address = $2, last_ingamesn = $3 WHERE profile_id = $1`
UpdateUserBan = `UPDATE users SET has_ban = true, ban_issued = $2, ban_expires = $3, ban_reason = $4, ban_reason_hidden = $5, ban_moderator = $6, ban_tos = $7 WHERE profile_id = $1`
SearchUserBan = `SELECT has_ban, ban_tos, ng_device_id FROM users WHERE has_ban = true AND (profile_id = $1 OR ng_device_id = $2 OR last_ip_address = $3) AND (ban_expires IS NULL OR ban_expires > $4) ORDER BY ban_tos DESC LIMIT 1`
DisableUserBan = `UPDATE users SET has_ban = false WHERE profile_id = $1`
GetMKWFriendInfoQuery = `SELECT mariokartwii_friend_info FROM users WHERE profile_id = $1`
UpdateMKWFriendInfoQuery = `UPDATE users SET mariokartwii_friend_info = $2 WHERE profile_id = $1`
@ -33,6 +38,8 @@ type User struct {
UniqueNick string
FirstName string
LastName string
Restricted bool
RestrictedDeviceId uint32
}
var (
@ -130,6 +137,24 @@ func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User
return user, true
}
func BanUser(pool *pgxpool.Pool, ctx context.Context, profileId uint32, tos bool, length time.Duration, reason string, reasonHidden string, moderator string) bool {
_, err := pool.Exec(ctx, UpdateUserBan, profileId, time.Now(), time.Now().Add(length), reason, reasonHidden, moderator, tos)
if err != nil {
return false
}
return true
}
func UnbanUser(pool *pgxpool.Pool, ctx context.Context, profileId uint32) bool {
_, err := pool.Exec(ctx, DisableUserBan, profileId)
if err != nil {
return false
}
return true
}
func GetMKWFriendInfo(pool *pgxpool.Pool, ctx context.Context, profileId uint32) string {
var info string
err := pool.QueryRow(ctx, GetMKWFriendInfoQuery, profileId).Scan(&info)

View File

@ -1,15 +1,39 @@
package gpcm
import (
"fmt"
"strconv"
"wwfc/common"
"wwfc/logging"
)
const (
LangJapanese = 0x00
LangEnglish = 0x01
LangGerman = 0x02
LangFrench = 0x03
LangSpanish = 0x04
LangItalian = 0x05
LangDutch = 0x06
LangSimpChinese = 0x07
LangTradChinese = 0x08
LangKorean = 0x09
LangEnglishEU = 0x81
LangFrenchEU = 0x83
LangSpanishEU = 0x84
)
type WWFCErrorMessage struct {
ErrorCode int
MessageRMC map[byte]string
}
type GPError struct {
ErrorCode int
ErrorString string
Fatal bool
WWFCMessage WWFCErrorMessage
}
func MakeGPError(errorCode int, errorString string, fatal bool) GPError {
@ -139,6 +163,181 @@ var (
ErrRemoveBlockNotBlocked = MakeGPError(0x1301, "The profile specified was not a member of the blocked list.", false)
)
var (
// WWFC errors with custom messages
WWFCMsgUnknownLoginError = WWFCErrorMessage{
ErrorCode: 22000,
MessageRMC: map[byte]string{
LangEnglish: "" +
"An unknown error has occurred\n" +
"while logging in to WiiLink WFC.\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgDolphinSetupRequired = WWFCErrorMessage{
ErrorCode: 22001,
MessageRMC: map[byte]string{
LangEnglish: "" +
"Additional setup is required\n" +
"to use WiiLink WFC on Dolphin.\n" +
"Visit wfc.wiilink24.com/dolphin\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgProfileBannedTOS = WWFCErrorMessage{
ErrorCode: 22002,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You are banned from WiiLink WFC\n" +
"due to a violation of the\n" +
"Terms of Service.\n" +
"Visit wfc.wiilink24.com/tos\n" +
"\n" +
"Error Code: %[1]d\n" +
"Support Info: NG%08[2]x",
},
}
WWFCMsgProfileBannedTOSNow = WWFCErrorMessage{
ErrorCode: 22002,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You have been banned from\n" +
"WiiLink WFC due to a violation\n" +
"of the Terms of Service.\n" +
"Visit wfc.wiilink24.com/tos\n" +
"\n" +
"Error Code: %[1]d\n" +
"Support Info: NG%08[2]x",
},
}
WWFCMsgProfileRestricted = WWFCErrorMessage{
ErrorCode: 22003,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You are banned from public\n" +
"matches due to a violation\n" +
"of the WiiLink WFC Rules.\n" +
"Visit wfc.wiilink24.com/rules\n" +
"\n" +
"Error Code: %[1]d\n" +
"Support Info: NG%08[2]x",
},
}
WWFCMsgProfileRestrictedNow = WWFCErrorMessage{
ErrorCode: 22003,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You have been banned from public\n" +
"matches due to a violation\n" +
"of the WiiLink WFC Rules.\n" +
"Visit wfc.wiilink24.com/rules\n" +
"\n" +
"Error Code: %[1]d\n" +
"Support Info: NG%08[2]x",
},
}
WWFCMsgKickedGeneric = WWFCErrorMessage{
ErrorCode: 22004,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You have been kicked from\n" +
"WiiLink WFC.\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgKickedModerator = WWFCErrorMessage{
ErrorCode: 22004,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You have been kicked from\n" +
"WiiLink WFC by a moderator.\n" +
"Visit wfc.wiilink24.com/rules\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgKickedRoomHost = WWFCErrorMessage{
ErrorCode: 22004,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You have been kicked from the\n" +
"friend room by the room creator.\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgConsoleMismatch = WWFCErrorMessage{
ErrorCode: 22005,
MessageRMC: map[byte]string{
LangEnglish: "" +
"The console you are using is not\n" +
"the device used to register this\n" +
"profile.\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgConsoleMismatchDolphin = WWFCErrorMessage{
ErrorCode: 22005,
MessageRMC: map[byte]string{
LangEnglish: "" +
"The console you are using is not\n" +
"the device used to register this\n" +
"profile. Please make sure you've\n" +
"set up your NAND correctly.\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgProfileIDInvalid = WWFCErrorMessage{
ErrorCode: 22006,
MessageRMC: map[byte]string{
LangEnglish: "" +
"The profile ID you are trying to\n" +
"register is invalid.\n" +
"Please create a new license.\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgProfileIDInUse = WWFCErrorMessage{
ErrorCode: 22007,
MessageRMC: map[byte]string{
LangEnglish: "" +
"The friend code you are trying to\n" +
"register is already in use.\n" +
"\n" +
"Error Code: %[1]d",
},
}
WWFCMsgPayloadInvalid = WWFCErrorMessage{
ErrorCode: 22008,
MessageRMC: map[byte]string{
LangEnglish: "" +
"The WiiLink WFC payload is invalid.\n" +
"Try restarting your game.\n" +
"\n" +
"Error Code: %[1]d",
},
}
)
func (err GPError) GetMessage() string {
command := common.GameSpyCommand{
Command: "error",
@ -156,7 +355,53 @@ func (err GPError) GetMessage() string {
return common.CreateGameSpyMessage(command)
}
func (err GPError) GetMessageTranslate(gameName string, region byte, lang byte, cfc uint64, ngid uint32) string {
command := common.GameSpyCommand{
Command: "error",
CommandValue: "",
OtherValues: map[string]string{
"err": strconv.Itoa(err.ErrorCode),
"errmsg": err.ErrorString,
},
}
if err.Fatal {
command.OtherValues["fatal"] = ""
}
if err.Fatal && err.WWFCMessage.ErrorCode != 0 {
switch gameName {
case "mariokartwii":
errMsg := err.WWFCMessage.MessageRMC[lang]
if errMsg == "" {
errMsg = err.WWFCMessage.MessageRMC[LangEnglish]
}
errMsg = fmt.Sprintf(errMsg, err.WWFCMessage.ErrorCode, ngid)
command.OtherValues["wwfc_err"] = strconv.Itoa(err.WWFCMessage.ErrorCode)
command.OtherValues["wwfc_errmsg"] = errMsg
}
}
return common.CreateGameSpyMessage(command)
}
func (g *GameSpySession) replyError(err GPError) {
logging.Error(g.ModuleName, "Reply error:", err.ErrorString)
g.Conn.Write([]byte(err.GetMessage()))
if !g.LoginInfoSet {
msg := err.GetMessage()
// logging.Info(g.ModuleName, "Sending error message:", msg)
g.Conn.Write([]byte(msg))
return
}
deviceId := g.User.RestrictedDeviceId
if deviceId == 0 {
deviceId = g.User.NgDeviceId
}
msg := err.GetMessageTranslate(g.GameName, g.Region, g.Language, g.ConsoleFriendCode, deviceId)
// logging.Info(g.ModuleName, "Sending error message:", msg)
g.Conn.Write([]byte(msg))
}

35
gpcm/kick.go Normal file
View File

@ -0,0 +1,35 @@
package gpcm
func KickPlayer(profileID uint32, reason string) {
mutex.Lock()
defer mutex.Unlock()
if session, exists := sessions[profileID]; exists {
errorMessage := WWFCMsgKickedGeneric
switch reason {
case "banned":
errorMessage = WWFCMsgProfileBannedTOSNow
case "restricted":
errorMessage = WWFCMsgProfileRestrictedNow
case "restricted_join":
errorMessage = WWFCMsgProfileRestricted
case "moderator_kick":
errorMessage = WWFCMsgKickedModerator
case "room_kick":
errorMessage = WWFCMsgKickedRoomHost
}
session.replyError(GPError{
ErrorCode: ErrConnectionClosed.ErrorCode,
ErrorString: "The player was kicked from the game. Reason: " + reason,
Fatal: true,
WWFCMessage: errorMessage,
})
session.Conn.Close()
}
}

View File

@ -44,6 +44,11 @@ var msPublicKey = []byte{
0xF9, 0x5B, 0x4D, 0x11, 0x04, 0x44, 0x64, 0x35, 0xC0, 0xED, 0xA4, 0x2F,
}
var commonDeviceIds = []uint32{
0x02000001,
0x0403ac68,
}
func verifySignature(moduleName string, authToken string, signature string) uint32 {
sigBytes, err := common.Base64DwcEncoding.DecodeString(signature)
if err != nil || len(sigBytes) != 0x144 {
@ -51,6 +56,16 @@ func verifySignature(moduleName string, authToken string, signature string) uint
}
ngId := sigBytes[0x000:0x004]
if !allowDefaultDolphinKeys {
// Skip authentication signature verification for common device IDs (the caller should handle this)
for _, defaultDeviceId := range commonDeviceIds {
if binary.BigEndian.Uint32(ngId) == defaultDeviceId {
return defaultDeviceId
}
}
}
ngTimestamp := sigBytes[0x004:0x008]
caId := sigBytes[0x008:0x00C]
msId := sigBytes[0x00C:0x010]
@ -122,7 +137,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
return
}
err, gamecd, issueTime, userId, gsbrcd, cfc, _, _, ingamesn, challenge, isLocalhost := common.UnmarshalNASAuthToken(authToken)
err, gamecd, issueTime, userId, gsbrcd, cfc, region, lang, ingamesn, challenge, isLocalhost := common.UnmarshalNASAuthToken(authToken)
if err != nil {
g.replyError(ErrLogin)
return
@ -134,39 +149,32 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
return
}
payloadVer, payloadVerExists := command.OtherValues["payload_ver"]
signature, signatureExists := command.OtherValues["wwfc_sig"]
_, payloadVerExists := command.OtherValues["payload_ver"]
_, signatureExists := command.OtherValues["wwfc_sig"]
deviceId := uint32(0)
g.GameName = command.OtherValues["gamename"]
g.GameCode = gamecd
g.Region = region
g.Language = lang
g.ConsoleFriendCode = cfc
g.InGameName = ingamesn
if hostPlatform, exists := command.OtherValues["wwfc_host"]; exists {
g.HostPlatform = hostPlatform
} else {
g.HostPlatform = "Wii"
}
g.LoginInfoSet = true
if isLocalhost && !payloadVerExists && !signatureExists {
// Players using the DNS exploit, need patching using a QR2 exploit
// TODO: Check that the game is compatible with the DNS
g.NeedsExploit = true
} else {
if !payloadVerExists || payloadVer != "2" {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The payload version is invalid.",
Fatal: true,
})
return
}
if !signatureExists {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "Missing authentication signature.",
Fatal: true,
})
return
}
if deviceId = verifySignature(g.ModuleName, authToken, signature); deviceId == 0 {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The authentication signature is invalid.",
Fatal: true,
})
deviceId = g.verifyExLoginInfo(command, authToken)
if deviceId == 0 {
return
}
}
@ -187,6 +195,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The provided profile ID is invalid.",
Fatal: true,
WWFCMessage: WWFCMsgUnknownLoginError,
})
return
}
@ -194,14 +203,9 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
cmdProfileId = uint32(cmdProfileId2)
}
// Perform the login with the database.
user, ok := database.LoginUserToGPCM(pool, ctx, userId, gsbrcd, cmdProfileId, deviceId)
if !ok {
// There was an error logging in to the GP backend.
g.replyError(ErrLogin)
if !g.performLoginWithDatabase(userId, gsbrcd, cmdProfileId, deviceId) {
return
}
g.User = user
g.ModuleName = "GPCM:" + strconv.FormatInt(int64(g.User.ProfileId), 10) + "*"
g.ModuleName += "/" + common.CalcFriendCodeString(g.User.ProfileId, "RMCJ") + "*"
@ -237,8 +241,6 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
g.AuthToken = authToken
g.LoginTicket = common.MarshalGPCMLoginTicket(g.User.ProfileId)
g.SessionKey = rand.Int31n(290000000) + 10000000
g.GameCode = gamecd
g.InGameName = ingamesn
g.DeviceAuthenticated = !g.NeedsExploit
g.LoggedIn = true
@ -246,7 +248,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
g.ModuleName += "/" + common.CalcFriendCodeString(g.User.ProfileId, "RMCJ")
// Notify QR2 of the login
qr2.Login(g.User.ProfileId, gamecd, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated)
qr2.Login(g.User.ProfileId, gamecd, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated, g.User.Restricted)
payload := common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "lc",
@ -273,6 +275,20 @@ func (g *GameSpySession) exLogin(command common.GameSpyCommand) {
return
}
deviceId := g.verifyExLoginInfo(command, g.AuthToken)
if deviceId == 0 {
return
}
if !g.performLoginWithDatabase(g.User.UserId, g.User.GsbrCode, 0, deviceId) {
return
}
g.DeviceAuthenticated = true
qr2.SetDeviceAuthenticated(g.User.ProfileId)
}
func (g *GameSpySession) verifyExLoginInfo(command common.GameSpyCommand, authToken string) uint32 {
payloadVer, payloadVerExists := command.OtherValues["payload_ver"]
signature, signatureExists := command.OtherValues["wwfc_sig"]
deviceId := uint32(0)
@ -282,8 +298,9 @@ func (g *GameSpySession) exLogin(command common.GameSpyCommand) {
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The payload version is invalid.",
Fatal: true,
WWFCMessage: WWFCMsgPayloadInvalid,
})
return
return 0
}
if !signatureExists {
@ -291,21 +308,112 @@ func (g *GameSpySession) exLogin(command common.GameSpyCommand) {
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "Missing authentication signature.",
Fatal: true,
WWFCMessage: WWFCMsgUnknownLoginError,
})
return
return 0
}
if deviceId = verifySignature(g.ModuleName, g.AuthToken, signature); deviceId == 0 {
if deviceId = verifySignature(g.ModuleName, authToken, signature); deviceId == 0 {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The authentication signature is invalid.",
Fatal: true,
WWFCMessage: WWFCMsgUnknownLoginError,
})
return
return 0
}
g.DeviceAuthenticated = true
qr2.SetDeviceAuthenticated(g.User.ProfileId)
g.DeviceId = deviceId
if !allowDefaultDolphinKeys {
// Check common device IDs
for _, defaultDeviceId := range commonDeviceIds {
if deviceId != defaultDeviceId {
continue
}
if strings.HasPrefix(g.HostPlatform, "Dolphin") {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "Prohibited device ID used in signature.",
Fatal: true,
WWFCMessage: WWFCMsgDolphinSetupRequired,
})
} else {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "Prohibited device ID used in signature.",
Fatal: true,
WWFCMessage: WWFCMsgUnknownLoginError,
})
}
}
}
return deviceId
}
func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string, profileId uint32, deviceId uint32) bool {
// Get IP address without port
ipAddress := g.Conn.RemoteAddr().String()
if strings.Contains(ipAddress, ":") {
ipAddress = ipAddress[:strings.Index(ipAddress, ":")]
}
user, err := database.LoginUserToGPCM(pool, ctx, userId, gsbrCode, profileId, deviceId, ipAddress, g.InGameName)
g.User = user
if err != nil {
if err == database.ErrProfileIDInUse {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The profile ID is already in use.",
Fatal: true,
WWFCMessage: WWFCMsgProfileIDInUse,
})
} else if err == database.ErrReservedProfileIDRange {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The profile ID is in a reserved range.",
Fatal: true,
WWFCMessage: WWFCMsgProfileIDInvalid,
})
} else if err == database.ErrDeviceIDMismatch {
if strings.HasPrefix(g.HostPlatform, "Dolphin") {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The device ID does not match the one on record.",
Fatal: true,
WWFCMessage: WWFCMsgConsoleMismatchDolphin,
})
} else {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The device ID does not match the one on record.",
Fatal: true,
WWFCMessage: WWFCMsgConsoleMismatch,
})
}
} else if err == database.ErrProfileBannedTOS {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The profile is banned from the service.",
Fatal: true,
WWFCMessage: WWFCMsgProfileBannedTOS,
})
} else {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "There was an error logging in to the GP backend.",
Fatal: true,
WWFCMessage: WWFCMsgUnknownLoginError,
})
}
return false
}
return true
}
func IsLoggedIn(profileID uint32) bool {

View File

@ -27,8 +27,17 @@ type GameSpySession struct {
AuthToken string
LoginTicket string
SessionKey int32
LoginInfoSet bool
GameName string
GameCode string
Region byte
Language byte
InGameName string
ConsoleFriendCode uint64
DeviceId uint32
HostPlatform string
Status string
LocString string
FriendList []uint32
@ -47,6 +56,8 @@ var (
// I would use a sync.Map instead of the map mutex combo, but this performs better.
sessions = map[uint32]*GameSpySession{}
mutex = deadlock.Mutex{}
allowDefaultDolphinKeys bool
)
func StartServer() {
@ -65,6 +76,10 @@ func StartServer() {
panic(err)
}
database.UpdateTables(pool, ctx)
allowDefaultDolphinKeys = config.AllowDefaultDolphinKeys
address := *config.GameSpyAddress + ":29900"
l, err := net.Listen("tcp", address)
if err != nil {

View File

@ -2,6 +2,7 @@ package main
import (
"sync"
"wwfc/api"
"wwfc/common"
"wwfc/gpcm"
"wwfc/gpsp"
@ -18,7 +19,7 @@ func main() {
logging.SetLevel(*config.LogLevel)
wg := &sync.WaitGroup{}
actions := []func(){nas.StartServer, gpcm.StartServer, qr2.StartServer, gpsp.StartServer, serverbrowser.StartServer, sake.StartServer, natneg.StartServer}
actions := []func(){nas.StartServer, gpcm.StartServer, qr2.StartServer, gpsp.StartServer, serverbrowser.StartServer, sake.StartServer, natneg.StartServer, api.StartServer}
wg.Add(5)
for _, action := range actions {
go func(ac func()) {

View File

@ -59,6 +59,9 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
return
}
logging.Info("NAS", aurora.Yellow(r.Method), aurora.Cyan(r.URL), "via", aurora.Cyan(r.Host), "from", aurora.BrightCyan(r.RemoteAddr))
moduleName := "NAS:" + r.RemoteAddr
// Handle conntest server
if strings.HasPrefix(r.Host, "conntest.") {
handleConnectionTest(w)
@ -77,8 +80,23 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
return
}
logging.Info("NAS", aurora.Yellow(r.Method), aurora.Cyan(r.URL), "via", aurora.Cyan(r.Host), "from", aurora.BrightCyan(r.RemoteAddr))
moduleName := "NAS:" + r.RemoteAddr
// Check for /api/ban
if r.URL.Path == "/api/ban" {
api.HandleBan(w, r)
return
}
// Check for /api/unban
if r.URL.Path == "/api/unban" {
api.HandleUnban(w, r)
return
}
// Check for /api/kick
if r.URL.Path == "/api/kick" {
api.HandleKick(w, r)
return
}
if r.URL.String() == "/ac" || r.URL.String() == "/pr" || r.URL.String() == "/download" {
handleAuthRequest(moduleName, w, r)

View File

@ -8,12 +8,13 @@ type LoginInfo struct {
GPPublicIP string
NeedsExploit bool
DeviceAuthenticated bool
Restricted bool
Session *Session
}
var logins = map[uint32]*LoginInfo{}
func Login(profileID uint32, gameCode string, inGameName string, consoleFriendCode uint64, publicIP string, needsExploit bool, deviceAuthenticated bool) {
func Login(profileID uint32, gameCode string, inGameName string, consoleFriendCode uint64, publicIP string, needsExploit bool, deviceAuthenticated bool, restricted bool) {
mutex.Lock()
defer mutex.Unlock()
@ -25,6 +26,7 @@ func Login(profileID uint32, gameCode string, inGameName string, consoleFriendCo
GPPublicIP: publicIP,
NeedsExploit: needsExploit,
DeviceAuthenticated: deviceAuthenticated,
Restricted: restricted,
Session: nil,
}
}

View File

@ -24,7 +24,7 @@ SET default_table_access_method = heap;
-- Name: users; Type: TABLE; Schema: public; Owner: wiilink
--
CREATE TABLE public.users (
CREATE TABLE IF NOT EXISTS public.users (
profile_id bigint NOT NULL,
user_id bigint NOT NULL,
gsbrcd character varying NOT NULL,
@ -38,13 +38,25 @@ CREATE TABLE public.users (
);
ALTER TABLE ONLY public.users
ADD IF NOT EXISTS last_ip_address character varying DEFAULT ''::character varying,
ADD IF NOT EXISTS last_ingamesn character varying DEFAULT ''::character varying,
ADD IF NOT EXISTS has_ban boolean DEFAULT false,
ADD IF NOT EXISTS ban_issued timestamp without time zone,
ADD IF NOT EXISTS ban_expires timestamp without time zone,
ADD IF NOT EXISTS ban_reason character varying,
ADD IF NOT EXISTS ban_reason_hidden character varying,
ADD IF NOT EXISTS ban_moderator character varying,
ADD IF NOT EXISTS ban_tos boolean
ALTER TABLE public.users OWNER TO wiilink;
--
-- Name: users_profile_id_seq; Type: SEQUENCE; Schema: public; Owner: wiilink
--
CREATE SEQUENCE public.users_profile_id_seq
CREATE SEQUENCE IF NOT EXISTS public.users_profile_id_seq
AS integer
START WITH 1
INCREMENT BY 1