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

@ -6,23 +6,25 @@ import (
)
type Config struct {
Username string `xml:"username"`
Password string `xml:"password"`
DatabaseAddress string `xml:"databaseAddress"`
DatabaseName string `xml:"databaseName"`
DefaultAddress string `xml:"address"`
GameSpyAddress *string `xml:"gsAddress,omitempty"`
NASAddress *string `xml:"nasAddress,omitempty"`
NASPort string `xml:"nasPort"`
NASAddressHTTPS *string `xml:"nasAddressHttps,omitempty"`
NASPortHTTPS string `xml:"nasPortHttps"`
EnableHTTPS bool `xml:"enableHttps"`
EnableHTTPSExploit *bool `xml:"enableHttpsExploit,omitempty"`
LogLevel *int `xml:"logLevel"`
CertPath string `xml:"certPath"`
KeyPath string `xml:"keyPath"`
CertPathWii string `xml:"certDerPathWii"`
KeyPathWii string `xml:"keyPathWii"`
Username string `xml:"username"`
Password string `xml:"password"`
DatabaseAddress string `xml:"databaseAddress"`
DatabaseName string `xml:"databaseName"`
DefaultAddress string `xml:"address"`
GameSpyAddress *string `xml:"gsAddress,omitempty"`
NASAddress *string `xml:"nasAddress,omitempty"`
NASPort string `xml:"nasPort"`
NASAddressHTTPS *string `xml:"nasAddressHttps,omitempty"`
NASPortHTTPS string `xml:"nasPortHttps"`
EnableHTTPS bool `xml:"enableHttps"`
EnableHTTPSExploit *bool `xml:"enableHttpsExploit,omitempty"`
LogLevel *int `xml:"logLevel"`
CertPath string `xml:"certPath"`
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,20 +20,26 @@ 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`
)
type User struct {
ProfileId uint32
UserId uint64
GsbrCode string
NgDeviceId uint32
Email string
UniqueNick string
FirstName string
LastName string
ProfileId uint32
UserId uint64
GsbrCode string
NgDeviceId uint32
Email string
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,12 +27,21 @@ type GameSpySession struct {
AuthToken string
LoginTicket string
SessionKey int32
GameCode string
InGameName string
Status string
LocString string
FriendList []uint32
AuthFriendList []uint32
LoginInfoSet bool
GameName string
GameCode string
Region byte
Language byte
InGameName string
ConsoleFriendCode uint64
DeviceId uint32
HostPlatform string
Status string
LocString string
FriendList []uint32
AuthFriendList []uint32
QR2IP uint64
Reservation common.MatchCommandData
@ -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