Merge remote-tracking branch 'origin/main'

This commit is contained in:
Sketch 2023-12-15 08:20:40 -05:00
commit 07816bda9d
18 changed files with 418 additions and 216 deletions

View File

@ -5,6 +5,7 @@ import (
"net/http"
"net/url"
"strconv"
"wwfc/common"
"wwfc/qr2"
)
@ -31,11 +32,15 @@ func HandleGroups(w http.ResponseWriter, r *http.Request) {
filtered := map[string]string{}
filtered["pid"] = player["dwc_pid"]
filtered["name"] = player["name"]
filtered["name"] = player["+ingamesn"]
if player["gamename"] == "mariokartwii" {
filtered["ev"] = player["ev"]
filtered["eb"] = player["eb"]
pid, err := strconv.ParseUint(player["dwc_pid"], 10, 32)
if err == nil {
filtered["fc"] = common.CalcFriendCodeString(uint32(pid), "RMCJ")
}
}
group.Players[i] = filtered

158
common/auth_token.go Normal file
View File

@ -0,0 +1,158 @@
package common
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"errors"
"strings"
"time"
)
func generateRandom(n int) []byte {
key := make([]byte, n)
read, err := rand.Read(key)
if err != nil {
panic(err)
}
if read != n {
panic("short rand.Read()")
}
return key
}
var (
authTokenKey = generateRandom(16)
authTokenIV = generateRandom(16)
authTokenMagic = generateRandom(16)
loginTicketKey = generateRandom(16)
loginTicketIV = generateRandom(16)
loginTicketMagic = generateRandom(4)
)
func appendString(blob []byte, value string, maxlen int) []byte {
if len([]byte(value)) < maxlen {
blob = append(blob, append([]byte(value), make([]byte, maxlen-len(value))...)...)
} else {
blob = append(blob, []byte(value)[:maxlen]...)
}
return blob
}
func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string) (string, string) {
blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().Unix()))
blob = appendString(blob, gamecd, 4)
blob = append(blob, binary.LittleEndian.AppendUint64([]byte{}, userid)[:6]...)
blob = append(blob, byte(min(len([]byte(gsbrcd)), 16)))
blob = appendString(blob, gsbrcd, 16)
blob = append(blob, binary.LittleEndian.AppendUint64([]byte{}, cfc)[:7]...)
blob = append(blob, region, lang)
blob = append(blob, byte(min(len([]byte(ingamesn)), 75)))
blob = appendString(blob, ingamesn, 75)
challenge := RandomString(8)
blob = append(blob, []byte(challenge)...)
blob = append(blob, authTokenMagic...)
block, err := aes.NewCipher(authTokenKey)
if err != nil {
panic(err)
}
cipher.NewCBCEncrypter(block, authTokenIV).CryptBlocks(blob, blob)
return "NDS" + Base64DwcEncoding.EncodeToString(blob), challenge
}
func UnmarshalNASAuthToken(token string) (err error, gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string) {
if !strings.HasPrefix(token, "NDS") {
err = errors.New("invalid auth token prefix")
return
}
blob, err := Base64DwcEncoding.DecodeString(token[3:])
if err != nil {
return
}
if len(blob) != 0x90 {
err = errors.New("invalid auth token length")
return
}
block, err := aes.NewCipher(authTokenKey)
if err != nil {
panic(err)
}
cipher.NewCBCDecrypter(block, authTokenIV).CryptBlocks(blob, blob)
if !bytes.Equal(blob[0x80:0x90], authTokenMagic) {
err = errors.New("invalid auth token magic")
return
}
issuetime = time.Unix(int64(binary.LittleEndian.Uint64(blob[0x0:0x8])), 0)
gamecd = string(blob[0x8:0xC])
userid = binary.LittleEndian.Uint64(append(bytes.Clone(blob[0xC:0x12]), 0, 0))
gsbrcd = string(blob[0x13 : 0x13+min(blob[0x12], 16)])
cfc = binary.LittleEndian.Uint64(append(bytes.Clone(blob[0x23:0x2A]), 0))
region = blob[0x2A]
lang = blob[0x2B]
ingamesn = string(blob[0x2D : 0x2D+min(blob[0x2C], 75)])
challenge = string(blob[0x78:0x80])
return
}
func MarshalGPCMLoginTicket(profileId uint32) string {
blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().Unix()))
blob = binary.LittleEndian.AppendUint32(blob, profileId)
blob = append(blob, loginTicketMagic...)
block, err := aes.NewCipher(loginTicketKey)
if err != nil {
panic(err)
}
cipher.NewCBCEncrypter(block, loginTicketIV).CryptBlocks(blob, blob)
return Base64DwcEncoding.EncodeToString(blob)
}
func UnmarshalGPCMLoginTicket(ticket string) (err error, profileId uint32, issuetime time.Time) {
blob, err := Base64DwcEncoding.DecodeString(ticket)
if err != nil {
return
}
if len(blob) != 0x10 {
err = errors.New("invalid login ticket length")
return
}
block, err := aes.NewCipher(loginTicketKey)
if err != nil {
panic(err)
}
cipher.NewCBCDecrypter(block, loginTicketIV).CryptBlocks(blob, blob)
if !bytes.Equal(blob[0xC:0x10], loginTicketMagic) {
err = errors.New("invalid login ticket magic")
return
}
issuetime = time.Unix(int64(binary.LittleEndian.Uint64(blob[0x0:0x8])), 0)
profileId = binary.LittleEndian.Uint32(blob[0x8:0xC])
return
}

View File

@ -6,7 +6,7 @@ import (
var Base64DwcEncoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-").WithPadding('*')
func Base32Encode(value int64) string {
func Base32Encode(value uint64) string {
alpha := "0123456789abcdefghijklmnopqrstuv"
encoded := ""

View File

@ -30,6 +30,10 @@ func ParseGameSpyMessage(msg string) ([]GameSpyCommand, error) {
for len(msg) > 0 && string(msg[0]) == `\` {
keyEnd := strings.Index(msg[1:], `\`) + 1
if keyEnd < 2 {
return nil, InvalidGameSpyCommand
}
key := msg[1:keyEnd]
value := ""
msg = msg[keyEnd+1:]

View File

@ -58,6 +58,8 @@ type MatchCommandDataReservation struct {
IsFriend bool
LocalPlayerCount uint32
ResvCheckValue uint32
UserData []byte
}
type MatchCommandDataResvOK struct {
@ -75,17 +77,20 @@ type MatchCommandDataResvOK struct {
ClientCount uint32
ResvCheckValue uint32
// Only exists in version 3 and 11
// Version 3 and 11
ProfileIDs []uint32
// Version 11
IsFriend bool
UserData uint32
UserData []byte
}
type MatchCommandDataResvDeny struct {
Reason uint32
ReasonString string
UserData []byte
}
type MatchCommandDataTellAddr struct {
@ -173,13 +178,18 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
return MatchCommandData{}, false
}
// Match commands must be 4 byte aligned
if (len(buffer) & 3) != 0 {
return MatchCommandData{}, false
}
switch command {
case MatchReservation:
if version == 3 && (len(buffer) != 0x04 && len(buffer) != 0x0C) {
if version == 3 && len(buffer) < 0x0C {
break
}
if (version == 11 && len(buffer) != 0x14) || (version == 90 && len(buffer) != 0x24) {
if (version == 11 && len(buffer) < 0x14) || (version == 90 && len(buffer) < 0x24) {
break
}
@ -188,7 +198,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
break
}
if version == 3 && len(buffer) == 0x04 {
if version == 3 && len(buffer) < 0x0C {
return MatchCommandData{Reservation: &MatchCommandDataReservation{
MatchType: byte(matchType),
HasPublicIP: false,
@ -207,6 +217,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
HasPublicIP: true,
PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]),
PublicPort: uint16(publicPort),
UserData: buffer[0x0C:],
}}, true
case 11:
@ -223,6 +234,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
PublicPort: uint16(publicPort),
IsFriend: isFriend,
LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x10:0x14]),
UserData: buffer[0x14:],
}}, true
case 90:
@ -248,6 +260,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
IsFriend: isFriend,
LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x1C:0x20]),
ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x20:0x24]),
UserData: buffer[0x24:],
}}, true
}
@ -258,49 +271,50 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
}
clientCount := binary.LittleEndian.Uint32(buffer[0x00:0x04])
if version == 3 && (clientCount > 29 || len(buffer) != int(0xC+clientCount*0x4)) {
if version == 3 && (clientCount > 29 || len(buffer) < int(0x0C+clientCount*0x4)) {
break
}
if version == 11 && (clientCount > 24 || len(buffer) != int(0x20+clientCount*0x4)) {
if version == 11 && (clientCount > 24 || len(buffer) < int(0x20+clientCount*0x4)) {
break
}
var profileIDs []uint32
for i := uint32(0); i < clientCount; i++ {
profileIDs = append(profileIDs, binary.LittleEndian.Uint32(buffer[0x4+i*4:0x4+i*4+4]))
profileIDs = append(profileIDs, binary.LittleEndian.Uint32(buffer[0x04+i*4:0x04+i*4+4]))
}
index := 0x4 + clientCount*4
index := 0x04 + clientCount*4
publicPort := binary.LittleEndian.Uint32(buffer[index+0x4 : index+0x8])
publicPort := binary.LittleEndian.Uint32(buffer[index+0x04 : index+0x08])
if publicPort > 0xffff {
break
}
if version == 3 {
return MatchCommandData{ResvOK: &MatchCommandDataResvOK{
PublicIP: binary.BigEndian.Uint32(buffer[index : index+0x4]),
PublicIP: binary.BigEndian.Uint32(buffer[index : index+0x04]),
PublicPort: uint16(publicPort),
ClientCount: clientCount,
ProfileIDs: profileIDs,
UserData: buffer[index+0x8:],
}}, true
} else if version == 11 {
isFriendValue := binary.LittleEndian.Uint32(buffer[index+0x8 : index+0xC])
isFriendValue := binary.LittleEndian.Uint32(buffer[index+0x08 : index+0x0C])
if isFriendValue > 1 {
break
}
isFriend := isFriendValue != 0
return MatchCommandData{ResvOK: &MatchCommandDataResvOK{
MaxPlayers: binary.LittleEndian.Uint32(buffer[0x14:0x18]),
SenderAID: binary.LittleEndian.Uint32(buffer[index+0xC : index+0x10]),
PublicIP: binary.BigEndian.Uint32(buffer[index : index+0x4]),
MaxPlayers: binary.LittleEndian.Uint32(buffer[index+0x14 : index+0x18]),
SenderAID: binary.LittleEndian.Uint32(buffer[index+0x0C : index+0x10]),
PublicIP: binary.BigEndian.Uint32(buffer[index : index+0x04]),
PublicPort: uint16(publicPort),
GroupID: binary.LittleEndian.Uint32(buffer[0x10:0x14]),
GroupID: binary.LittleEndian.Uint32(buffer[index+0x10 : index+0x14]),
ClientCount: clientCount,
ProfileIDs: profileIDs,
IsFriend: isFriend,
UserData: binary.LittleEndian.Uint32(buffer[0x18:0x1C]),
UserData: buffer[index+0x18:],
}}, true
}
break
@ -335,6 +349,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
ReceiverNewAID: binary.LittleEndian.Uint32(buffer[0x28:0x2C]),
ClientCount: binary.LittleEndian.Uint32(buffer[0x2C:0x30]),
ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x30:0x34]),
UserData: buffer[0x34:],
}}, true
case MatchResvDeny:
@ -359,6 +374,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
return MatchCommandData{ResvDeny: &MatchCommandDataResvDeny{
Reason: reason,
ReasonString: reasonString,
UserData: buffer[0x4:],
}}, true
case MatchResvWait:
@ -468,6 +484,12 @@ func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byt
message = binary.LittleEndian.AppendUint32(message, data.Reservation.LocalPlayerCount)
message = binary.LittleEndian.AppendUint32(message, data.Reservation.ResvCheckValue)
}
message = append(message, data.Reservation.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
case MatchResvOK:
@ -483,6 +505,11 @@ func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byt
message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.PublicPort))
if version == 3 {
message = append(message, data.ResvOK.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
}
@ -496,7 +523,12 @@ func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byt
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.SenderAID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.GroupID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.MaxPlayers)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.UserData)
message = append(message, data.ResvOK.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
}
@ -514,10 +546,22 @@ func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byt
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ReceiverNewAID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ClientCount)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ResvCheckValue)
message = append(message, data.ResvOK.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
case MatchResvDeny:
message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvDeny.Reason)
message = append(message, data.ResvOK.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
case MatchResvWait:

View File

@ -2,101 +2,13 @@ package database
import (
"context"
"crypto/sha512"
"encoding/hex"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"os"
"strconv"
"wwfc/common"
"wwfc/logging"
)
const (
DoesAuthTokenExist = `SELECT EXISTS(SELECT 1 FROM logins WHERE auth_token = $1)`
DoesNASUserExist = `SELECT EXISTS(SELECT 1 FROM logins WHERE user_id = $1 AND gsbrcd = $2)`
UpdateUserLogin = `UPDATE logins SET auth_token = $1, challenge = $2 WHERE user_id = $3 AND gsbrcd = $4`
InsertUserLogin = `INSERT INTO logins (auth_token, user_id, gsbrcd, challenge) VALUES ($1, $2, $3, $4)`
GetNASUserLogin = `SELECT user_id, gsbrcd FROM logins WHERE auth_token = $1 LIMIT 1`
GetNASChallenge = `SELECT challenge FROM logins WHERE auth_token = $1`
)
var salt []byte
// GenerateAuthToken generates and stores the auth token for this user as well as a challenge.
func GenerateAuthToken(pool *pgxpool.Pool, ctx context.Context, userId int64, gsbrcd string) (string, string) {
var userExists bool
err := pool.QueryRow(ctx, DoesNASUserExist, userId, gsbrcd).Scan(&userExists)
if err != nil {
panic(err)
}
authToken := "NDS" + common.RandomString(80)
for {
// We must make sure that the auth token doesn't exist before attempting to insert it into the database.
var tokenExists bool
err := pool.QueryRow(ctx, DoesAuthTokenExist, authToken).Scan(&tokenExists)
if err != nil {
panic(err)
}
if !tokenExists {
break
}
authToken = "NDS" + common.RandomString(80)
}
challenge := common.RandomString(8)
if userExists {
// UPDATE rather than INSERT
_, err = pool.Exec(ctx, UpdateUserLogin, authToken, challenge, userId, gsbrcd)
if err != nil {
panic(err)
}
} else {
_, err = pool.Exec(ctx, InsertUserLogin, authToken, userId, gsbrcd, challenge)
if err != nil {
panic(err)
}
}
return authToken, challenge
}
func GetNASLogin(pool *pgxpool.Pool, ctx context.Context, authToken string) (int64, string) {
var userId int64
var gsbrcd string
err := pool.QueryRow(ctx, GetNASUserLogin, authToken).Scan(&userId, &gsbrcd)
if err != nil {
if err == pgx.ErrNoRows {
return 0, ""
} else {
panic(err)
}
}
return userId, gsbrcd
}
func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, authToken string) (User, bool) {
// Make sure salt is loaded
if salt == nil {
var err error
salt, err = os.ReadFile("salt.bin")
if err != nil {
panic(err)
}
}
// Query login table with the auth token.
userId, gsbrcd := GetNASLogin(pool, ctx, authToken)
if userId == 0 {
logging.Error("DATABASE", "Invalid auth token:", aurora.Cyan(authToken))
return User{}, false
}
func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string) (User, bool) {
var exists bool
err := pool.QueryRow(ctx, DoesUserExist, userId, gsbrcd).Scan(&exists)
if err != nil {
@ -104,12 +16,10 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, authToken string)
}
uniqueNickname := common.Base32Encode(userId) + gsbrcd
password := sha512.Sum512(append(salt, []byte(gsbrcd)...))
user := User{
UserId: userId,
GsbrCode: gsbrcd,
Password: hex.EncodeToString(password[:]),
Email: uniqueNickname + "@nds",
UniqueNick: uniqueNickname,
}
@ -118,30 +28,15 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, authToken string)
// Create the GPCM account
user.CreateUser(pool, ctx)
logging.Notice("DATABASE", "Created new GPCM user:", aurora.Cyan(strconv.FormatInt(user.UserId, 10)), aurora.Cyan(user.GsbrCode), "-", aurora.Cyan(strconv.FormatInt(int64(user.ProfileId), 10)))
logging.Notice("DATABASE", "Created new GPCM user:", aurora.Cyan(userId), aurora.Cyan(gsbrcd), "-", aurora.Cyan(user.ProfileId))
} else {
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId)
if err != nil {
panic(err)
}
logging.Notice("DATABASE", "Log in GPCM user:", aurora.Cyan(strconv.FormatInt(user.UserId, 10)), aurora.Cyan(user.GsbrCode), "-", aurora.Cyan(strconv.FormatInt(int64(user.ProfileId), 10)))
logging.Notice("DATABASE", "Log in GPCM user:", aurora.Cyan(userId), aurora.Cyan(user.GsbrCode), "-", aurora.Cyan(user.ProfileId))
}
return user, true
}
func GetChallenge(pool *pgxpool.Pool, ctx context.Context, authToken string) string {
var challenge string
err := pool.QueryRow(ctx, GetNASChallenge, authToken).Scan(&challenge)
if err != nil {
if err == pgx.ErrNoRows {
// Invalid auth token
return ""
} else {
panic(err)
}
}
return challenge
}

View File

@ -2,17 +2,14 @@ package database
import (
"context"
"errors"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"math/rand"
"wwfc/common"
)
const (
InsertUser = `INSERT INTO users (user_id, gsbrcd, password, email, unique_nick) VALUES ($1, $2, $3, $4, $5) RETURNING profile_id`
UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END WHERE profile_id = $1 RETURNING user_id, gsbrcd, password, email, unique_nick, firstname, lastname`
GetUser = `SELECT user_id, gsbrcd, password, email, unique_nick, firstname, lastname FROM users WHERE profile_id = $1`
UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END WHERE profile_id = $1 RETURNING user_id, gsbrcd, email, unique_nick, firstname, lastname`
GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname FROM users WHERE profile_id = $1`
CreateUserSession = `INSERT INTO sessions (session_key, profile_id, login_ticket) VALUES ($1, $2, $3)`
GetTicketSession = `SELECT session_key, profile_id FROM sessions WHERE login_ticket = $1`
DoesUserExist = `SELECT EXISTS(SELECT 1 FROM users WHERE user_id = $1 AND gsbrcd = $2)`
@ -25,9 +22,8 @@ const (
type User struct {
ProfileId uint32
UserId int64
UserId uint64
GsbrCode string
Password string
Email string
UniqueNick string
FirstName string
@ -35,15 +31,15 @@ type User struct {
}
func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) {
err := pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, user.Password, user.Email, user.UniqueNick).Scan(&user.ProfileId)
err := pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, "", user.Email, user.UniqueNick).Scan(&user.ProfileId)
if err != nil {
panic(err)
}
}
func GetUniqueUserID() int64 {
func GetUniqueUserID() uint64 {
// Not guaranteed unique but doesn't matter in practice if multiple people have the same user ID.
return rand.Int63n(0x80000000000)
return uint64(rand.Int63n(0x80000000000))
}
func UpdateProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32, data map[string]string) User {
@ -52,7 +48,7 @@ func UpdateProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32, da
user := User{}
row := pool.QueryRow(ctx, UpdateUserTable, profileId, firstName, firstNameExists, lastName, lastNameExists)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Password, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName)
if err != nil {
panic(err)
}
@ -61,38 +57,10 @@ func UpdateProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32, da
return user
}
func CreateSession(pool *pgxpool.Pool, ctx context.Context, profileId uint32, loginTicket string) string {
sessionKey := common.RandomString(8)
_, err := pool.Exec(ctx, CreateUserSession, sessionKey, profileId, loginTicket)
if err != nil {
panic(err)
}
return sessionKey
}
func GetSession(pool *pgxpool.Pool, ctx context.Context, loginTicket string) (string, uint32) {
var sessionKey string
var profileId uint32
err := pool.QueryRow(ctx, GetTicketSession, loginTicket).Scan(&sessionKey, &profileId)
if err != nil {
panic(err)
}
return sessionKey, profileId
}
func deleteSession(pool *pgxpool.Pool, ctx context.Context, profileId uint32) {
_, err := pool.Exec(ctx, DeleteUserSession, profileId)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
panic(err)
}
}
func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) {
user := User{}
row := pool.QueryRow(ctx, GetUser, profileId)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Password, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName)
if err != nil {
return User{}, false
}

2
go.mod
View File

@ -1,6 +1,6 @@
module wwfc
go 1.20
go 1.21
require (
github.com/jackc/pgx/v4 v4.18.1

View File

@ -3,6 +3,7 @@ package gpcm
import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/logrusorgru/aurora/v3"
"strconv"
"strings"
@ -263,6 +264,12 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
break
}
if len(msgData) > 0x200 || (len(msgData)&3) != 0 {
logging.Error(g.ModuleName, "Invalid length message data; message:", msg)
g.replyError(ErrMessage)
return
}
msgMatchData, ok := common.DecodeMatchCommand(cmd, msgData, version)
common.LogMatchCommand(g.ModuleName, strconv.FormatInt(int64(toProfileId), 10), cmd, msgMatchData)
if !ok {
@ -272,16 +279,23 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
}
if cmd == common.MatchReservation {
if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) == int32(msgMatchData.Reservation.PublicIP) {
g.QR2IP = uint64(msgMatchData.Reservation.PublicIP) | (uint64(msgMatchData.Reservation.PublicPort) << 32)
if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) != int32(msgMatchData.Reservation.PublicIP) {
logging.Error(g.ModuleName, "RESERVATION: Public IP mismatch")
g.replyError(ErrMessage)
return
}
} else if cmd == common.MatchResvOK {
if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) == int32(msgMatchData.ResvOK.PublicIP) {
g.QR2IP = uint64(msgMatchData.ResvOK.PublicIP) | (uint64(msgMatchData.ResvOK.PublicPort) << 32)
}
}
// TODO: Replace public IP with QR2 search ID
g.QR2IP = uint64(msgMatchData.Reservation.PublicIP) | (uint64(msgMatchData.Reservation.PublicPort) << 32)
} else if cmd == common.MatchResvOK {
if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) != int32(msgMatchData.ResvOK.PublicIP) {
logging.Error(g.ModuleName, "RESV_OK: Public IP mismatch")
g.replyError(ErrMessage)
return
}
g.QR2IP = uint64(msgMatchData.ResvOK.PublicIP) | (uint64(msgMatchData.ResvOK.PublicPort) << 32)
}
mutex.Lock()
defer mutex.Unlock()
@ -300,7 +314,10 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
}
if cmd == common.MatchReservation {
g.ReservationPID = uint32(toProfileId)
msgMatchData.Reservation.PublicIP = 0
msgMatchData.Reservation.PublicPort = 0
msgMatchData.Reservation.LocalIP = 0
msgMatchData.Reservation.LocalPort = 0
} else if cmd == common.MatchResvOK || cmd == common.MatchResvDeny || cmd == common.MatchResvWait {
if toSession.ReservationPID != g.User.ProfileId {
logging.Error(g.ModuleName, "Destination", aurora.Cyan(toProfileId), "has no reservation with the sender")
@ -319,9 +336,32 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
g.replyError(ErrMessage)
return
}
if g.QR2IP&0xffffffff != toSession.QR2IP&0xffffffff {
searchId := qr2.GetSearchID(g.QR2IP)
if searchId == 0 {
logging.Error(g.ModuleName, "Could not get QR2 search ID for IP", aurora.Cyan(fmt.Sprintf("%016x", g.QR2IP)))
g.replyError(ErrMessage)
return
}
msgMatchData.ResvOK.PublicIP = uint32(searchId & 0xffffffff)
msgMatchData.ResvOK.PublicPort = uint16(searchId >> 32)
}
}
}
newMsg, ok := common.EncodeMatchCommand(cmd, msgMatchData, version)
if !ok || len(newMsg) > 0x200 {
logging.Error(g.ModuleName, "Failed to encode match command; message:", msg)
g.replyError(ErrMessage)
return
}
if cmd == common.MatchReservation {
g.ReservationPID = uint32(toProfileId)
}
sendMessageToSession("1", g.User.ProfileId, toSession, msg)
}

View File

@ -3,12 +3,13 @@ package gpcm
import (
"crypto/md5"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/logrusorgru/aurora/v3"
"math/rand"
"strconv"
"strings"
"time"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
@ -96,7 +97,7 @@ func verifySignature(authToken string, signature string) bool {
logging.Error("GPCM", "Auth token signature failed")
return false
}
logging.Notice("GPCM", "Auth token signature verified")
logging.Notice("GPCM", "Auth token signature verified; NG ID:", aurora.Cyan(fmt.Sprintf("%08x", ngId)))
return true
}
@ -124,24 +125,27 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
}
signature, exists := command.OtherValues["wwfc_sig"]
if exists {
// TODO: This is still in testing so it's only checked if it exists
if !verifySignature(authToken, signature) {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The authentication signature is invalid.",
Fatal: true,
})
return
}
if !exists || !verifySignature(authToken, signature) {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The authentication signature is invalid.",
Fatal: true,
})
return
}
challenge := database.GetChallenge(pool, ctx, authToken)
if challenge == "" {
err, _, issueTime, userId, gsbrcd, cfc, _, _, ingamesn, challenge := common.UnmarshalNASAuthToken(authToken)
if err != nil {
g.replyError(ErrLogin)
return
}
currentTime := time.Now()
if issueTime.Before(currentTime.Add(-10*time.Minute)) || issueTime.After(currentTime) {
g.replyError(ErrLoginLoginTicketExpired)
return
}
response := generateResponse(g.Challenge, challenge, authToken, command.OtherValues["challenge"])
if response != command.OtherValues["response"] {
g.replyError(ErrLogin)
@ -151,7 +155,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
proof := generateProof(g.Challenge, challenge, command.OtherValues["authtoken"], command.OtherValues["challenge"])
// Perform the login with the database.
user, ok := database.LoginUserToGPCM(pool, ctx, authToken)
user, ok := database.LoginUserToGPCM(pool, ctx, userId, gsbrcd)
if !ok {
// There was an error logging in to the GP backend.
g.replyError(ErrLogin)
@ -175,10 +179,9 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
sessions[g.User.ProfileId] = g
mutex.Unlock()
g.LoginTicket = strings.Replace(base64.StdEncoding.EncodeToString([]byte(common.RandomString(16))), "=", "_", -1)
// Now initiate the session
_ = database.CreateSession(pool, ctx, g.User.ProfileId, g.LoginTicket)
g.LoginTicket = common.MarshalGPCMLoginTicket(g.User.ProfileId)
g.SessionKey = rand.Int31n(290000000) + 10000000
g.InGameName = ingamesn
g.LoggedIn = true
g.ModuleName = "GPCM:" + strconv.FormatInt(int64(g.User.ProfileId), 10)
@ -186,7 +189,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
// Notify QR2 of the login
// TODO: Get ingamesn and cfc from NAS
qr2.Login(g.User.ProfileId, "", "", g.Conn.RemoteAddr().String())
qr2.Login(g.User.ProfileId, ingamesn, cfc, g.Conn.RemoteAddr().String())
payload := common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "lc",
@ -194,8 +197,8 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
OtherValues: map[string]string{
"sesskey": strconv.FormatInt(int64(g.SessionKey), 10),
"proof": proof,
"userid": strconv.FormatInt(g.User.UserId, 10),
"profileid": strconv.FormatInt(int64(g.User.ProfileId), 10),
"userid": strconv.FormatUint(g.User.UserId, 10),
"profileid": strconv.FormatUint(uint64(g.User.ProfileId), 10),
"uniquenick": g.User.UniqueNick,
"lt": g.LoginTicket,
"id": command.OtherValues["id"],

View File

@ -24,6 +24,7 @@ type GameSpySession struct {
Challenge string
LoginTicket string
SessionKey int32
InGameName string
Status string
LocString string
FriendList []uint32

View File

@ -43,7 +43,7 @@ func (g *GameSpySession) getProfile(command common.GameSpyCommand) {
OtherValues: map[string]string{
"profileid": command.OtherValues["profileid"],
"nick": user.UniqueNick,
"userid": strconv.FormatInt(user.UserId, 10),
"userid": strconv.FormatUint(uint64(user.UserId), 10),
"email": user.Email,
"sig": common.RandomHexString(32),
"uniquenick": user.UniqueNick,

View File

@ -1,6 +1,8 @@
package nas
import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/logrusorgru/aurora/v3"
"net/http"
@ -10,6 +12,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf16"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
@ -30,7 +33,7 @@ func handleAuthRequest(moduleName string, w http.ResponseWriter, r *http.Request
fields := map[string]string{}
for key, values := range r.PostForm {
if len(values) != 1 {
logging.Warn(moduleName, "Ignoring multiple POST form values:", aurora.Cyan(key).String()+":", aurora.Cyan(values))
logging.Warn(moduleName, "Ignoring none or multiple POST form values:", aurora.Cyan(key).String()+":", aurora.Cyan(values))
continue
}
@ -40,8 +43,20 @@ func handleAuthRequest(moduleName string, w http.ResponseWriter, r *http.Request
replyHTTPError(w, 400, "400 Bad Request")
return
}
logging.Info(moduleName, aurora.Cyan(key).String()+":", aurora.Cyan(string(parsed)))
fields[key] = string(parsed)
value := string(parsed)
if key == "ingamesn" {
// Special handling required for the UTF-16 string
var utf16String []uint16
for i := 0; i < len(parsed)/2; i++ {
utf16String = append(utf16String, binary.BigEndian.Uint16(parsed[i*2:i*2+2]))
}
value = string(utf16.Decode(utf16String))
}
logging.Info(moduleName, aurora.Cyan(key).String()+":", aurora.Cyan(value))
fields[key] = value
}
reply := map[string]string{}
@ -139,7 +154,7 @@ func acctcreate() map[string]string {
"retry": "0",
"datetime": getDateTime(),
"returncd": "002",
"userid": strconv.FormatInt(database.GetUniqueUserID(), 10),
"userid": strconv.FormatUint(database.GetUniqueUserID(), 10),
}
}
@ -150,6 +165,13 @@ func login(moduleName string, fields map[string]string) map[string]string {
"locator": "gamespy.com",
}
gamecd, ok := fields["gamecd"]
if !ok {
logging.Error(moduleName, "No gamecd in form")
param["returncd"] = "103"
return param
}
strUserId, ok := fields["userid"]
if !ok {
logging.Error(moduleName, "No userid in form")
@ -157,8 +179,8 @@ func login(moduleName string, fields map[string]string) map[string]string {
return param
}
userId, err := strconv.ParseInt(strUserId, 10, 64)
if err != nil {
userId, err := strconv.ParseUint(strUserId, 10, 64)
if err != nil || userId >= 0x80000000000 {
logging.Error(moduleName, "Invalid userid string in form")
param["returncd"] = "103"
return param
@ -171,7 +193,45 @@ func login(moduleName string, fields map[string]string) map[string]string {
return param
}
authToken, challenge := database.GenerateAuthToken(pool, ctx, userId, gsbrcd)
cfc, ok := fields["cfc"]
if !ok {
logging.Error(moduleName, "No cfc in form")
param["returncd"] = "103"
return param
}
cfcInt, err := strconv.ParseUint(cfc, 10, 64)
if err != nil || cfcInt > 9999999999999999 {
logging.Error(moduleName, "Invalid cfc string in form")
param["returncd"] = "103"
return param
}
region, ok := fields["region"]
if !ok {
region = "ff"
}
regionByte, err := hex.DecodeString(region)
if err != nil || len(regionByte) != 1 {
logging.Error(moduleName, "Invalid region byte in form")
param["returncd"] = "103"
return param
}
lang, ok := fields["lang"]
if !ok {
lang = "ff"
}
langByte, err := hex.DecodeString(lang)
if err != nil || len(langByte) != 1 {
logging.Error(moduleName, "Invalid lang byte in form")
param["returncd"] = "103"
return param
}
authToken, challenge := common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"])
param["returncd"] = "001"
param["challenge"] = challenge

View File

@ -187,6 +187,9 @@ func GetGroups(gameName string) []GroupInfo {
for k, v := range session.Data {
mapData[k] = v
}
mapData["+ingamesn"] = session.Login.InGameName
groupInfo.Players = append(groupInfo.Players, mapData)
if mapData["dwc_hoststate"] == "2" && mapData["dwc_suspend"] == "0" {

View File

@ -3,13 +3,13 @@ package qr2
type LoginInfo struct {
ProfileID uint32
InGameName string
ConsoleFriendCode string
ConsoleFriendCode uint64
GPPublicIP string
}
var logins = map[uint32]LoginInfo{}
func Login(profileID uint32, inGameName, consoleFriendCode, publicIP string) {
func Login(profileID uint32, inGameName string, consoleFriendCode uint64, publicIP string) {
mutex.Lock()
logins[profileID] = LoginInfo{
ProfileID: profileID,

View File

@ -30,7 +30,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
var receiver *Session
senderIPInt, _ := common.IPFormatToInt(senderIP)
useSearchID := destSearchID < (0x400 << 32)
useSearchID := destSearchID < (1 << 24)
if useSearchID {
receiver = sessionBySearchID[destSearchID]
} else {
@ -62,7 +62,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
moduleName = "QR2/MSG:s" + strconv.FormatUint(uint64(natnegID), 10)
} else if bytes.Equal(message[:4], []byte{0xbb, 0x49, 0xcc, 0x4d}) || bytes.Equal(message[:4], []byte("SBCM")) {
// DWC match command
if len(message) < 0x14 {
if len(message) < 0x14 || len(message) > 0x94 {
logging.Error(moduleName, "Received invalid length match command packet")
return
}
@ -198,7 +198,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
var matchMessage []byte
matchMessage, ok = common.EncodeMatchCommand(message[8], matchData, version)
if !ok {
if !ok || len(matchMessage) > 0x80 {
logging.Error(moduleName, "Failed to reencode match command:", aurora.Cyan(printHex(message)))
return
}

View File

@ -24,6 +24,7 @@ type Session struct {
Addr net.Addr
Challenge string
Authenticated bool
Login LoginInfo
LastKeepAlive int64
Endianness byte // Some fields depend on the client's endianness
Data map[string]string
@ -107,7 +108,7 @@ func setSessionData(moduleName string, addr net.Addr, sessionId uint32, payload
// Set search ID
for {
searchID := uint64(rand.Int63n((0x400<<32)-1) + 1)
searchID := uint64(rand.Int63n((1<<24)-1) + 1)
if _, exists := sessionBySearchID[searchID]; !exists {
session.SearchID = searchID
session.Data["+searchid"] = strconv.FormatUint(searchID, 10)
@ -155,7 +156,9 @@ func (session *Session) setProfileID(moduleName string, newPID string) bool {
// Check if the public IP matches the one used for the GPCM session
var gpPublicIP string
if loginInfo, ok := logins[uint32(profileID)]; ok {
var loginInfo LoginInfo
var ok bool
if loginInfo, ok = logins[uint32(profileID)]; ok {
gpPublicIP = loginInfo.GPPublicIP
} else {
logging.Error(moduleName, "Provided dwc_pid is not logged in:", aurora.Cyan(newPID))
@ -167,6 +170,8 @@ func (session *Session) setProfileID(moduleName string, newPID string) bool {
return false
}
session.Login = loginInfo
// Constraint: only one session can exist with a profile ID
var outdated []uint64
for sessionAddr, otherSession := range sessions {
@ -231,3 +236,14 @@ func GetSessionServers() []map[string]string {
return servers
}
func GetSearchID(addr uint64) uint64 {
mutex.Lock()
defer mutex.Unlock()
if session := sessions[addr]; session != nil {
return session.SearchID
}
return 0
}

View File

@ -191,8 +191,13 @@ func getRequestIdentity(moduleName string, request StorageRequestData) (uint32,
panic("Invalid secret key")
}
// GetSession panics if it can't find the session for the login ticket
_, profileId := database.GetSession(pool, ctx, request.LoginTicket)
logging.Notice(moduleName, request.LoginTicket)
err, profileId, _ := common.UnmarshalGPCMLoginTicket(request.LoginTicket)
if err != nil {
panic(err)
}
logging.Info(moduleName, "Profile ID:", aurora.BrightCyan(profileId))
logging.Info(moduleName, "Game:", aurora.Cyan(request.GameID), "-", aurora.BrightCyan(gameInfo.Name))
logging.Info(moduleName, "Table ID:", aurora.Cyan(request.TableID))