mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-08-26 02:24:24 -05:00
Don't use database for auth tokens
This commit is contained in:
@@ -31,7 +31,7 @@ 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"]
|
||||
|
||||
158
common/auth_token.go
Normal file
158
common/auth_token.go
Normal 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
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
var Base64DwcEncoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-").WithPadding('*')
|
||||
|
||||
func Base32Encode(value int64) string {
|
||||
func Base32Encode(value uint64) string {
|
||||
alpha := "0123456789abcdefghijklmnopqrstuv"
|
||||
|
||||
encoded := ""
|
||||
|
||||
@@ -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:]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
2
go.mod
@@ -1,6 +1,6 @@
|
||||
module wwfc
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v4 v4.18.1
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -24,6 +24,7 @@ type GameSpySession struct {
|
||||
Challenge string
|
||||
LoginTicket string
|
||||
SessionKey int32
|
||||
InGameName string
|
||||
Status string
|
||||
LocString string
|
||||
FriendList []uint32
|
||||
|
||||
@@ -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,
|
||||
|
||||
74
nas/auth.go
74
nas/auth.go
@@ -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
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user