mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-08-25 10:04:14 -05:00
Merge branch 'feat/nas-refactor'
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strings"
|
||||
@@ -29,143 +30,141 @@ func generateRandom(n int) []byte {
|
||||
var (
|
||||
authTokenKey = generateRandom(16)
|
||||
authTokenIV = generateRandom(16)
|
||||
authTokenMagic = generateRandom(14)
|
||||
authTokenMagic = generateRandom(8)
|
||||
|
||||
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]...)
|
||||
}
|
||||
var (
|
||||
ErrTokenMagic = errors.New("invalid auth token or login ticket magic")
|
||||
ErrTokenExpired = errors.New("auth token or login ticket expired")
|
||||
ErrTokenLength = errors.New("invalid auth token or login ticket length")
|
||||
)
|
||||
|
||||
return blob
|
||||
type NASAuthToken struct {
|
||||
IssueTime uint64
|
||||
UserID uint64
|
||||
ConsoleFriendCode uint64
|
||||
Region byte
|
||||
Lang byte
|
||||
UnitCode byte
|
||||
GameCode [4]byte
|
||||
GsbrCode [16]byte
|
||||
Challenge [8]byte
|
||||
InGameScreenName [64]byte
|
||||
Magic [8]byte
|
||||
}
|
||||
|
||||
func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, unitcd byte, isLocalhost bool) (string, string) {
|
||||
blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().UTC().Unix()))
|
||||
var nasAuthTokenSize = (binary.Size(NASAuthToken{}) + aes.BlockSize - 1) & ^(aes.BlockSize - 1)
|
||||
|
||||
blob = appendString(blob, gamecd, 4)
|
||||
func (t NASAuthToken) Marshal() string {
|
||||
t.IssueTime = uint64(time.Now().UTC().Unix())
|
||||
copy(t.Magic[:], authTokenMagic)
|
||||
|
||||
blob = append(blob, binary.LittleEndian.AppendUint64([]byte{}, userid)[:6]...)
|
||||
var buf bytes.Buffer
|
||||
ShouldNotError(binary.Write(&buf, binary.LittleEndian, t))
|
||||
|
||||
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, byte(unitcd))
|
||||
|
||||
if isLocalhost {
|
||||
blob = append(blob, 0x01)
|
||||
} else {
|
||||
blob = append(blob, 0x00)
|
||||
}
|
||||
|
||||
blob = append(blob, authTokenMagic...)
|
||||
// Pad to CBC block size
|
||||
data := append(buf.Bytes(), make([]byte, nasAuthTokenSize-len(buf.Bytes()))...)
|
||||
|
||||
block, err := aes.NewCipher(authTokenKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ShouldNotError(err)
|
||||
cipher.NewCBCEncrypter(block, authTokenIV).CryptBlocks(data, data)
|
||||
|
||||
cipher.NewCBCEncrypter(block, authTokenIV).CryptBlocks(blob, blob)
|
||||
return "NDS" + Base64DwcEncoding.EncodeToString(blob), challenge
|
||||
return "NDS" + Base64DwcEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string, unitcd byte, isLocalhost bool, err error) {
|
||||
if !strings.HasPrefix(token, "NDS") {
|
||||
err = errors.New("invalid auth token prefix")
|
||||
return
|
||||
func (t *NASAuthToken) Unmarshal(data string) error {
|
||||
if !strings.HasPrefix(data, "NDS") {
|
||||
return ErrTokenLength
|
||||
}
|
||||
|
||||
blob, err := Base64DwcEncoding.DecodeString(token[3:])
|
||||
blob, err := Base64DwcEncoding.DecodeString(data[3:])
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if len(blob) != 0x90 {
|
||||
err = errors.New("invalid auth token length")
|
||||
return
|
||||
if len(blob) != nasAuthTokenSize {
|
||||
return ErrTokenLength
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(authTokenKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ShouldNotError(err)
|
||||
cipher.NewCBCDecrypter(block, authTokenIV).CryptBlocks(blob, blob)
|
||||
|
||||
if !bytes.Equal(blob[0x90-len(authTokenMagic):0x90], authTokenMagic) {
|
||||
err = errors.New("invalid auth token magic")
|
||||
return
|
||||
reader := bytes.NewReader(blob)
|
||||
if err := binary.Read(reader, binary.LittleEndian, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
unitcd = blob[0x80]
|
||||
isLocalhost = blob[0x81] == 0x01
|
||||
return
|
||||
if !bytes.Equal(t.Magic[:], authTokenMagic) {
|
||||
return ErrTokenMagic
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC()
|
||||
issueTime := time.Unix(int64(t.IssueTime), 0)
|
||||
if issueTime.Before(currentTime.Add(-10*time.Minute)) || issueTime.After(currentTime) {
|
||||
return ErrTokenExpired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MarshalGPCMLoginTicket(profileId uint32) string {
|
||||
blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().UTC().Unix()))
|
||||
blob = binary.LittleEndian.AppendUint32(blob, profileId)
|
||||
blob = append(blob, loginTicketMagic...)
|
||||
type GPCMLoginTicket struct {
|
||||
IssueTime uint64
|
||||
ProfileID uint32
|
||||
Magic [4]byte
|
||||
}
|
||||
|
||||
var gpcmLoginTicketSize = (binary.Size(GPCMLoginTicket{}) + aes.BlockSize - 1) & ^(aes.BlockSize - 1)
|
||||
|
||||
func (t GPCMLoginTicket) Marshal() string {
|
||||
t.IssueTime = uint64(time.Now().UTC().Unix())
|
||||
copy(t.Magic[:], loginTicketMagic)
|
||||
|
||||
var buf bytes.Buffer
|
||||
ShouldNotError(binary.Write(&buf, binary.LittleEndian, t))
|
||||
|
||||
// Pad to CBC block size
|
||||
data := append(buf.Bytes(), make([]byte, gpcmLoginTicketSize-len(buf.Bytes()))...)
|
||||
|
||||
block, err := aes.NewCipher(loginTicketKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ShouldNotError(err)
|
||||
cipher.NewCBCEncrypter(block, loginTicketIV).CryptBlocks(data, data)
|
||||
|
||||
cipher.NewCBCEncrypter(block, loginTicketIV).CryptBlocks(blob, blob)
|
||||
return Base64DwcEncoding.EncodeToString(blob)
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func UnmarshalGPCMLoginTicket(ticket string) (profileId uint32, issuetime time.Time, err error) {
|
||||
var blob []byte
|
||||
blob, err = Base64DwcEncoding.DecodeString(ticket)
|
||||
func (t *GPCMLoginTicket) Unmarshal(ticket string) error {
|
||||
blob, err := base64.StdEncoding.DecodeString(ticket)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if len(blob) != 0x10 {
|
||||
err = errors.New("invalid login ticket length")
|
||||
return
|
||||
}
|
||||
|
||||
var block cipher.Block
|
||||
block, err = aes.NewCipher(loginTicketKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if len(blob) != gpcmLoginTicketSize {
|
||||
return ErrTokenLength
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(loginTicketKey)
|
||||
ShouldNotError(err)
|
||||
cipher.NewCBCDecrypter(block, loginTicketIV).CryptBlocks(blob, blob)
|
||||
|
||||
if !bytes.Equal(blob[0xC:0x10], loginTicketMagic) {
|
||||
err = errors.New("invalid login ticket magic")
|
||||
return
|
||||
reader := bytes.NewReader(blob)
|
||||
if err := binary.Read(reader, binary.LittleEndian, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issuetime = time.Unix(int64(binary.LittleEndian.Uint64(blob[0x0:0x8])), 0)
|
||||
profileId = binary.LittleEndian.Uint32(blob[0x8:0xC])
|
||||
return
|
||||
if !bytes.Equal(t.Magic[:], loginTicketMagic) {
|
||||
return ErrTokenMagic
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC()
|
||||
issueTime := time.Unix(int64(t.IssueTime), 0)
|
||||
if issueTime.Before(currentTime.Add(-48*time.Hour)) || issueTime.After(currentTime) {
|
||||
return ErrTokenExpired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package common
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -40,3 +42,34 @@ func reverse(s string) string {
|
||||
|
||||
return string(rns)
|
||||
}
|
||||
|
||||
func UTF16Encode(s string, order binary.ByteOrder) []byte {
|
||||
encoded := utf16.Encode([]rune(s))
|
||||
buf := make([]byte, len(encoded)*2)
|
||||
for i, v := range encoded {
|
||||
order.PutUint16(buf[i*2:], v)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func UTF16Decode(u []byte, order binary.ByteOrder) string {
|
||||
decoded := make([]uint16, len(u)/2)
|
||||
for i := range decoded {
|
||||
v := order.Uint16(u[i*2:])
|
||||
if v == 0 {
|
||||
decoded = decoded[:i]
|
||||
break
|
||||
}
|
||||
decoded[i] = v
|
||||
}
|
||||
return string(utf16.Decode(decoded))
|
||||
}
|
||||
|
||||
func NullTerminatedString(u []byte) string {
|
||||
for i, b := range u {
|
||||
if b == 0 {
|
||||
return string(u[:i])
|
||||
}
|
||||
}
|
||||
return string(u)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package gamestats
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
"wwfc/common"
|
||||
"wwfc/gpcm"
|
||||
"wwfc/logging"
|
||||
@@ -62,21 +61,15 @@ func (g *GameStatsSession) authp(command common.GameSpyCommand) {
|
||||
return
|
||||
}
|
||||
|
||||
_, issueTime, userId, gsbrcd, _, _, _, _, _, _, _, err := common.UnmarshalNASAuthToken(authToken)
|
||||
authTokenObj := common.NASAuthToken{}
|
||||
err := authTokenObj.Unmarshal(authToken)
|
||||
if err != nil {
|
||||
logging.Error(g.ModuleName, "Error unmarshalling authtoken:", err.Error())
|
||||
g.Write(errorCmd)
|
||||
return
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC()
|
||||
if issueTime.Before(currentTime.Add(-10*time.Minute)) || issueTime.After(currentTime) {
|
||||
logging.Error(g.ModuleName, "Authtoken has expired")
|
||||
g.Write(errorCmd)
|
||||
return
|
||||
}
|
||||
|
||||
g.User, err = db.LoginUserToGameStats(userId, gsbrcd)
|
||||
g.User, err = db.LoginUserToGameStats(authTokenObj.UserID, common.NullTerminatedString(authTokenObj.GsbrCode[:]))
|
||||
if err != nil {
|
||||
logging.Error(g.ModuleName, "Error logging in user:", err.Error())
|
||||
g.Write(errorCmd)
|
||||
|
||||
@@ -189,26 +189,32 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
|
||||
return
|
||||
}
|
||||
|
||||
gamecd, issueTime, userId, gsbrcd, cfc, region, lang, ingamesn, challenge, unitcd, isLocalhost, err := common.UnmarshalNASAuthToken(authToken)
|
||||
authTokenObj := common.NASAuthToken{}
|
||||
err := authTokenObj.Unmarshal(authToken)
|
||||
if err != nil {
|
||||
logging.Error(g.ModuleName, "Failed to unmarshal auth token:", err)
|
||||
if err == common.ErrTokenExpired {
|
||||
g.replyError(ErrLoginLoginTicketExpired)
|
||||
return
|
||||
}
|
||||
g.replyError(ErrLogin)
|
||||
return
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC()
|
||||
if issueTime.Before(currentTime.Add(-10*time.Minute)) || issueTime.After(currentTime) {
|
||||
g.replyError(ErrLoginLoginTicketExpired)
|
||||
return
|
||||
}
|
||||
|
||||
g.GameName = command.OtherValues["gamename"]
|
||||
logging.Info(g.ModuleName, "Game name:", aurora.Cyan(g.GameName))
|
||||
g.GameCode = gamecd
|
||||
g.Region = region
|
||||
g.Language = lang
|
||||
g.ConsoleFriendCode = cfc
|
||||
g.InGameName = ingamesn
|
||||
g.UnitCode = unitcd
|
||||
g.GameCode = common.NullTerminatedString(authTokenObj.GameCode[:])
|
||||
g.Region = authTokenObj.Region
|
||||
g.Language = authTokenObj.Lang
|
||||
g.ConsoleFriendCode = authTokenObj.ConsoleFriendCode
|
||||
g.UnitCode = authTokenObj.UnitCode
|
||||
|
||||
var endianness binary.ByteOrder = binary.LittleEndian
|
||||
if g.UnitCode == UnitCodeWii {
|
||||
endianness = binary.BigEndian
|
||||
}
|
||||
|
||||
g.InGameName = common.UTF16Decode(authTokenObj.InGameScreenName[:], endianness)
|
||||
|
||||
_, payloadVerExists := command.OtherValues["wl:ver"]
|
||||
_, signatureExists := command.OtherValues["wl:sig"]
|
||||
@@ -229,18 +235,18 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
|
||||
logging.Event(
|
||||
"received_login_info",
|
||||
map[string]any{
|
||||
"user_id": userId,
|
||||
"user_id": authTokenObj.UserID,
|
||||
"game_name": g.GameName,
|
||||
"wii_number": cfc,
|
||||
"in_game_name": ingamesn,
|
||||
"unit_code": unitcd,
|
||||
"wii_number": g.ConsoleFriendCode,
|
||||
"in_game_name": g.InGameName,
|
||||
"unit_code": g.UnitCode,
|
||||
"ip_address": g.RemoteAddr,
|
||||
},
|
||||
)
|
||||
|
||||
expectedUnitCode := common.GetExpectedUnitCode(g.GameName)
|
||||
if (g.UnitCode != UnitCodeDS && g.UnitCode != UnitCodeWii) || (g.UnitCode != expectedUnitCode && expectedUnitCode != UnitCodeDSAndWii) {
|
||||
logging.Error(g.ModuleName, "Incorrect unit code specified:", aurora.Cyan(unitcd))
|
||||
logging.Error(g.ModuleName, "Incorrect unit code specified:", aurora.Cyan(g.UnitCode))
|
||||
g.replyError(ErrLogin)
|
||||
return
|
||||
}
|
||||
@@ -253,7 +259,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
|
||||
deviceAuth = true
|
||||
|
||||
case UnitCodeWii:
|
||||
if isLocalhost && !payloadVerExists && !signatureExists {
|
||||
if !payloadVerExists && !signatureExists {
|
||||
// Players using the DNS, need patching using a QR2 exploit
|
||||
if !common.DoesGameNeedExploit(g.GameName) {
|
||||
logging.Error(g.ModuleName, "Using DNS for incompatible game:", aurora.Cyan(g.GameName))
|
||||
@@ -276,18 +282,20 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
|
||||
}
|
||||
|
||||
default:
|
||||
logging.Error(g.ModuleName, "Invalid unit code specified:", aurora.Cyan(unitcd))
|
||||
logging.Error(g.ModuleName, "Invalid unit code specified:", aurora.Cyan(g.UnitCode))
|
||||
g.replyError(ErrLogin)
|
||||
return
|
||||
}
|
||||
|
||||
response := generateResponse(g.Challenge, challenge, authToken, command.OtherValues["challenge"])
|
||||
nasChallenge := common.NullTerminatedString(authTokenObj.Challenge[:])
|
||||
|
||||
response := generateResponse(g.Challenge, nasChallenge, authToken, command.OtherValues["challenge"])
|
||||
if response != command.OtherValues["response"] {
|
||||
g.replyError(ErrLogin)
|
||||
return
|
||||
}
|
||||
|
||||
proof := generateProof(g.Challenge, challenge, command.OtherValues["authtoken"], command.OtherValues["challenge"])
|
||||
proof := generateProof(g.Challenge, nasChallenge, command.OtherValues["authtoken"], command.OtherValues["challenge"])
|
||||
|
||||
cmdProfileId := uint32(0)
|
||||
if cmdProfileIdStr, exists := command.OtherValues["profileid"]; exists {
|
||||
@@ -305,7 +313,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
|
||||
cmdProfileId = uint32(cmdProfileId2)
|
||||
}
|
||||
|
||||
if !g.performLoginWithDatabase(userId, gsbrcd, cmdProfileId, defaultKey, deviceId, deviceAuth) {
|
||||
if !g.performLoginWithDatabase(authTokenObj.UserID, common.NullTerminatedString(authTokenObj.GsbrCode[:]), cmdProfileId, defaultKey, deviceId, deviceAuth) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -353,16 +361,17 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
|
||||
mutex.Unlock()
|
||||
|
||||
g.AuthToken = authToken
|
||||
g.LoginTicket = common.MarshalGPCMLoginTicket(g.User.ProfileId)
|
||||
g.LoginTicket = common.GPCMLoginTicket{ProfileID: g.User.ProfileId}.Marshal()
|
||||
g.SessionKey = rand.Int31n(290000000) + 10000000
|
||||
|
||||
g.DeviceAuthenticated = deviceAuth
|
||||
g.LoggedIn = true
|
||||
|
||||
g.ModuleName = "GPCM:" + strconv.FormatInt(int64(g.User.ProfileId), 10)
|
||||
g.ModuleName += "/" + common.CalcFriendCodeString(g.User.ProfileId, g.User.GsbrCode[:4])
|
||||
|
||||
// Notify QR2 of the login
|
||||
qr2.Login(g.User.ProfileId, gamecd, ingamesn, cfc, g.User.GsbrCode[:4], g.RemoteAddr, g.NeedsExploit, g.DeviceAuthenticated, g.User.Restricted)
|
||||
qr2.Login(g.User.ProfileId, g.GameCode, g.InGameName, g.ConsoleFriendCode, g.User.GsbrCode[:4], g.RemoteAddr, g.NeedsExploit, g.DeviceAuthenticated, g.User.Restricted)
|
||||
|
||||
replyUserId := g.User.UserId
|
||||
if g.UnitCode == UnitCodeDS {
|
||||
|
||||
244
nas/account.go
Normal file
244
nas/account.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package nas
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"wwfc/common"
|
||||
"wwfc/database"
|
||||
"wwfc/logging"
|
||||
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
)
|
||||
|
||||
var accountActions = map[string]func(moduleName string, fields map[string][]byte) map[string]string{
|
||||
"acctcreate": acctcreate,
|
||||
"login": login,
|
||||
"svcloc": svcloc,
|
||||
}
|
||||
|
||||
func handleAuthAccountEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
moduleName := getModuleName(r)
|
||||
|
||||
fields, err := parseAuthRequest(r)
|
||||
if err != nil {
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
action := string(fields["action"])
|
||||
if action == "" {
|
||||
logging.Error(moduleName, "No action in form")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
if actionFunc, exists := accountActions[action]; exists {
|
||||
reply := actionFunc(moduleName, fields)
|
||||
writeAuthResponse(w, reply)
|
||||
return
|
||||
}
|
||||
|
||||
logging.Error(moduleName, "Unknown action:", aurora.Cyan(action))
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
}
|
||||
|
||||
func acctcreate(moduleName string, fields map[string][]byte) map[string]string {
|
||||
return map[string]string{
|
||||
"retry": "0",
|
||||
"datetime": getDateTime(),
|
||||
"returncd": "002",
|
||||
"userid": strconv.FormatUint(database.GetUniqueUserID(), 10),
|
||||
}
|
||||
}
|
||||
|
||||
func login(moduleName string, fields map[string][]byte) map[string]string {
|
||||
param := map[string]string{
|
||||
"retry": "0",
|
||||
"datetime": getDateTime(),
|
||||
"locator": "gamespy.com",
|
||||
}
|
||||
|
||||
token := common.NASAuthToken{}
|
||||
|
||||
gamecd, ok := fields["gamecd"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No gamecd in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
copy(token.GameCode[:], gamecd)
|
||||
|
||||
strUserId, ok := fields["userid"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No userid in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
var err error
|
||||
token.UserID, err = strconv.ParseUint(string(strUserId), 10, 64)
|
||||
if err != nil || token.UserID >= 0x80000000000 {
|
||||
logging.Error(moduleName, "Invalid userid string in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
gsbrcd, ok := fields["gsbrcd"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No gsbrcd in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
if (len(gsbrcd) < 4 && len(gsbrcd) != 0) || strings.ContainsRune(string(gsbrcd), 0) {
|
||||
logging.Error(moduleName, "Invalid gsbrcd string in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
// Some games like Fortune Street make login requests without a gsbr code, so we temporarily fake one
|
||||
if len(gsbrcd) == 0 {
|
||||
if len(gamecd) < 4 {
|
||||
logging.Error(moduleName, "Invalid gamecd string in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
gsbrcd = append(gamecd[:3], 'J')
|
||||
}
|
||||
|
||||
copy(token.GsbrCode[:], gsbrcd)
|
||||
|
||||
lang, ok := fields["lang"]
|
||||
if !ok {
|
||||
lang = []byte("ff")
|
||||
}
|
||||
|
||||
langByte, err := hex.DecodeString(string(lang))
|
||||
if err != nil || len(langByte) != 1 {
|
||||
logging.Error(moduleName, "Invalid lang byte in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
token.Lang = langByte[0]
|
||||
|
||||
unitcd, ok := fields["unitcd"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No unitcd in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
isWii := len(unitcd) > 1 || unitcd[0] != '0'
|
||||
var endianness binary.ByteOrder
|
||||
switch isWii {
|
||||
case false:
|
||||
token.UnitCode = 0
|
||||
endianness = binary.LittleEndian
|
||||
case true:
|
||||
token.UnitCode = 1
|
||||
endianness = binary.BigEndian
|
||||
}
|
||||
|
||||
hasProfaneName := false
|
||||
ingamesn, hasIngamesn := fields["ingamesn"]
|
||||
ingamesnStr := ""
|
||||
if hasIngamesn {
|
||||
ingamesnStr = common.UTF16Decode(ingamesn, endianness)
|
||||
if hasProfaneName, _ = IsBadWord(ingamesnStr); hasProfaneName {
|
||||
logging.Info(moduleName, "Provided in-game screen name has a profane word:", aurora.Red(ingamesnStr).String())
|
||||
// Continue with different return code
|
||||
}
|
||||
}
|
||||
|
||||
switch isWii {
|
||||
case false:
|
||||
devname, ok := fields["devname"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No devname in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
// Only later DS games send ingamesn
|
||||
if !hasIngamesn {
|
||||
ingamesn = devname
|
||||
}
|
||||
logging.Notice(moduleName, "Login (DS)", aurora.Cyan(token.UserID), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname), "name:", aurora.Cyan(ingamesnStr))
|
||||
|
||||
case true:
|
||||
cfc, ok := fields["cfc"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No cfc in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
token.ConsoleFriendCode, err = strconv.ParseUint(string(cfc), 10, 64)
|
||||
if err != nil || token.ConsoleFriendCode > 9999999999999999 {
|
||||
logging.Error(moduleName, "Invalid cfc string in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
region, ok := fields["region"]
|
||||
if !ok {
|
||||
region = []byte("ff")
|
||||
}
|
||||
|
||||
regionByte, err := hex.DecodeString(string(region))
|
||||
if err != nil || len(regionByte) != 1 {
|
||||
logging.Error(moduleName, "Invalid region byte in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
token.Region = regionByte[0]
|
||||
|
||||
logging.Notice(moduleName, "Login (Wii)", aurora.Cyan(token.UserID), aurora.Cyan(string(gsbrcd)), "name:", aurora.Cyan(ingamesnStr))
|
||||
}
|
||||
|
||||
challenge := common.RandomString(8)
|
||||
copy(token.Challenge[:], []byte(challenge))
|
||||
copy(token.InGameScreenName[:], ingamesn)
|
||||
|
||||
if hasProfaneName {
|
||||
param["returncd"] = "040"
|
||||
} else {
|
||||
param["returncd"] = "001"
|
||||
}
|
||||
|
||||
param["challenge"] = challenge
|
||||
param["token"] = token.Marshal()
|
||||
|
||||
return param
|
||||
}
|
||||
|
||||
func svcloc(moduleName string, fields map[string][]byte) map[string]string {
|
||||
param := map[string]string{
|
||||
"retry": "0",
|
||||
"datetime": getDateTime(),
|
||||
"returncd": "007",
|
||||
"statusdata": "Y",
|
||||
}
|
||||
|
||||
authToken := "NDS/SVCLOC/TOKEN"
|
||||
|
||||
switch string(fields["svc"]) {
|
||||
default:
|
||||
param["servicetoken"] = authToken
|
||||
param["svchost"] = "n/a"
|
||||
|
||||
case "9000":
|
||||
param["token"] = authToken
|
||||
param["svchost"] = "dls1.nintendowifi.net"
|
||||
|
||||
case "9001":
|
||||
param["servicetoken"] = authToken
|
||||
param["svchost"] = "dls1.nintendowifi.net"
|
||||
}
|
||||
|
||||
return param
|
||||
}
|
||||
469
nas/auth.go
469
nas/auth.go
@@ -2,18 +2,14 @@ package nas
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"wwfc/common"
|
||||
"wwfc/database"
|
||||
"wwfc/logging"
|
||||
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
@@ -23,470 +19,83 @@ var (
|
||||
dlcDir = "./dlc"
|
||||
)
|
||||
|
||||
func handleAuthRequest(w http.ResponseWriter, r *http.Request) {
|
||||
func parseAuthRequest(r *http.Request) (map[string][]byte, error) {
|
||||
moduleName := getModuleName(r)
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Failed to parse form")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
return nil, errors.New("failed to parse form")
|
||||
}
|
||||
|
||||
// Need to know this here to determine UTF-16 endianness (LE for DS, BE for Wii)
|
||||
// unitcd 0 = DS, 1 = Wii
|
||||
unitcd := "1"
|
||||
isWii := false
|
||||
if unitcdValues, ok := r.PostForm["unitcd"]; ok {
|
||||
unitcdDecoded, err := common.Base64DwcEncoding.DecodeString(unitcdValues[0])
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Invalid unitcd string in form")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
return nil, errors.New("invalid unitcd string in form")
|
||||
}
|
||||
unitcd = string(unitcdDecoded)
|
||||
isWii = len(unitcdDecoded) != 1 || unitcdDecoded[0] != '0'
|
||||
}
|
||||
|
||||
fields := map[string]string{}
|
||||
var endianness binary.ByteOrder = binary.LittleEndian
|
||||
if isWii {
|
||||
endianness = binary.BigEndian
|
||||
}
|
||||
|
||||
fields := map[string][]byte{}
|
||||
for key, values := range r.PostForm {
|
||||
if len(values) != 1 {
|
||||
logging.Warn(moduleName, "Ignoring none or multiple POST form values:", aurora.Cyan(key).String()+":", aurora.Cyan(values))
|
||||
continue
|
||||
}
|
||||
|
||||
var value string
|
||||
if !strings.HasPrefix(key, "_") {
|
||||
parsed, err := common.Base64DwcEncoding.DecodeString(values[0])
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Invalid POST form value:", aurora.Cyan(key).String()+":", aurora.Cyan(values[0]))
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
if key == "ingamesn" || key == "devname" || key == "words" {
|
||||
// Special handling required for the UTF-16 string
|
||||
var utf16String []uint16
|
||||
if unitcd == "0" {
|
||||
for i := 0; i < len(parsed)/2; i++ {
|
||||
utf16String = append(utf16String, binary.LittleEndian.Uint16(parsed[i*2:i*2+2]))
|
||||
}
|
||||
} else {
|
||||
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))
|
||||
} else {
|
||||
value = string(parsed)
|
||||
}
|
||||
} else {
|
||||
// Values unique to CTGP/the Wiimmfi payload, for compatibility reasons. Some of these are not base64 encoded.
|
||||
value = values[0]
|
||||
if strings.HasPrefix(key, "_") {
|
||||
// Values unique to CTGP/the Wiimmfi payload. Ignored for compatibility reasons.
|
||||
continue
|
||||
}
|
||||
|
||||
logging.Info(moduleName, aurora.Cyan(key).String()+":", aurora.Cyan(value))
|
||||
fields[key] = value
|
||||
parsed, err := common.Base64DwcEncoding.DecodeString(values[0])
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Invalid POST form value:", aurora.Cyan(key).String()+":", aurora.Cyan(values[0]))
|
||||
return nil, errors.New("invalid POST form value: " + key)
|
||||
}
|
||||
|
||||
fields[key] = parsed
|
||||
|
||||
reported := string(parsed)
|
||||
if key == "ingamesn" || key == "devname" || key == "words" {
|
||||
// Special handling required for reporting the UTF-16 strings
|
||||
reported = common.UTF16Decode(parsed, endianness)
|
||||
}
|
||||
logging.Info(moduleName, aurora.Cyan(key).String()+":", aurora.Cyan(reported))
|
||||
}
|
||||
|
||||
reply := map[string]string{}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func writeAuthResponse(w http.ResponseWriter, reply map[string]string) {
|
||||
var response []byte
|
||||
|
||||
switch r.URL.String() {
|
||||
case "/ac":
|
||||
action, ok := fields["action"]
|
||||
if !ok || action == "" {
|
||||
logging.Error(moduleName, "No action in form")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(action) {
|
||||
case "acctcreate":
|
||||
reply = acctcreate()
|
||||
|
||||
case "login":
|
||||
isLocalhost := strings.HasPrefix(r.RemoteAddr, "127.0.0.1:") || strings.HasPrefix(r.RemoteAddr, "[::1]:")
|
||||
reply = login(moduleName, fields, isLocalhost)
|
||||
|
||||
case "svcloc":
|
||||
reply = svcloc(fields)
|
||||
|
||||
default:
|
||||
logging.Error(moduleName, "Unknown action:", aurora.Cyan(action))
|
||||
reply = map[string]string{
|
||||
"retry": "0",
|
||||
"returncd": "109",
|
||||
}
|
||||
}
|
||||
|
||||
case "/pr":
|
||||
words, ok := fields["words"]
|
||||
if words == "" || !ok {
|
||||
logging.Error(moduleName, "No words in form")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
reply = handleProfanity(r.PostForm, unitcd)
|
||||
|
||||
case "/download":
|
||||
action, ok := fields["action"]
|
||||
if !ok || action == "" {
|
||||
logging.Error(moduleName, "No action in form")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
rhgamecd, ok := fields["rhgamecd"]
|
||||
if !ok || !isValidRhgamecd(rhgamecd) {
|
||||
logging.Error(moduleName, "Missing or invalid rhgamecd")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(action) {
|
||||
case "count":
|
||||
response = []byte(dlsCount(fields))
|
||||
|
||||
default:
|
||||
logging.Error(moduleName, "Unknown action:", aurora.Cyan(action))
|
||||
reply = map[string]string{
|
||||
"retry": "0",
|
||||
"returncd": "109",
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("X-DLS-Host", "http://127.0.0.1/")
|
||||
}
|
||||
|
||||
if len(response) == 0 {
|
||||
param := url.Values{}
|
||||
for key, value := range reply {
|
||||
param.Set(key, common.Base64DwcEncoding.EncodeToString([]byte(value)))
|
||||
}
|
||||
response = []byte(param.Encode())
|
||||
response = []byte(strings.ReplaceAll(string(response), "%2A", "*"))
|
||||
param := url.Values{}
|
||||
for key, value := range reply {
|
||||
param.Set(key, common.Base64DwcEncoding.EncodeToString([]byte(value)))
|
||||
}
|
||||
response = []byte(param.Encode())
|
||||
response = []byte(strings.ReplaceAll(string(response), "%2A", "*"))
|
||||
|
||||
// DWC treats the response like a null terminated string
|
||||
response = append(response, 0x00)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
_, err = w.Write(response)
|
||||
_, err := w.Write(response)
|
||||
if err != nil {
|
||||
logging.Error("NAS", "Error writing response:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func acctcreate() map[string]string {
|
||||
return map[string]string{
|
||||
"retry": "0",
|
||||
"datetime": getDateTime(),
|
||||
"returncd": "002",
|
||||
"userid": strconv.FormatUint(database.GetUniqueUserID(), 10),
|
||||
}
|
||||
}
|
||||
|
||||
func login(moduleName string, fields map[string]string, isLocalhost bool) map[string]string {
|
||||
param := map[string]string{
|
||||
"retry": "0",
|
||||
"datetime": getDateTime(),
|
||||
"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")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
gsbrcd, ok := fields["gsbrcd"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No gsbrcd in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
if (len(gsbrcd) < 4 && len(gsbrcd) != 0) || strings.ContainsRune(gsbrcd, 0) {
|
||||
logging.Error(moduleName, "Invalid gsbrcd string in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
// Some games like Fortune Street make login requests without a gsbr code, so we temporarily fake one
|
||||
if len(gsbrcd) == 0 {
|
||||
if len(gamecd) < 4 {
|
||||
logging.Error(moduleName, "Invalid gamecd string in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
gsbrcd = gamecd[:3] + "J"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
unitcd, ok := fields["unitcd"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No unitcd in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
unitcdInt, err := strconv.ParseUint(unitcd, 10, 64)
|
||||
if err != nil || unitcdInt > 1 {
|
||||
logging.Error(moduleName, "Invalid unitcd string in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
hasProfaneName := false
|
||||
ingamesn, ok := fields["ingamesn"]
|
||||
if ok {
|
||||
if hasProfaneName, _ = IsBadWord(ingamesn); hasProfaneName {
|
||||
logging.Info(moduleName, aurora.Cyan(strconv.FormatUint(userId, 10)), "has a profane name ("+aurora.Red(ingamesn).String()+")")
|
||||
}
|
||||
}
|
||||
|
||||
var authToken, challenge string
|
||||
switch unitcdInt {
|
||||
// ds
|
||||
case 0:
|
||||
devname, ok := fields["devname"]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "No devname in form")
|
||||
param["returncd"] = "103"
|
||||
return param
|
||||
}
|
||||
|
||||
// Only later DS games send this
|
||||
ingamesn, ok := fields["ingamesn"]
|
||||
if ok {
|
||||
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], ingamesn, 0, isLocalhost)
|
||||
logging.Notice(moduleName, "Login (DS)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname), "ingamesn:", aurora.Cyan(ingamesn))
|
||||
} else {
|
||||
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], "", 0, isLocalhost)
|
||||
logging.Notice(moduleName, "Login (DS)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname))
|
||||
}
|
||||
|
||||
// wii
|
||||
case 1:
|
||||
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
|
||||
}
|
||||
|
||||
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], 1, isLocalhost)
|
||||
logging.Notice(moduleName, "Login (Wii)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "ingamesn:", aurora.Cyan(fields["ingamesn"]))
|
||||
}
|
||||
|
||||
if hasProfaneName {
|
||||
param["returncd"] = "040"
|
||||
} else {
|
||||
param["returncd"] = "001"
|
||||
}
|
||||
param["challenge"] = challenge
|
||||
param["token"] = authToken
|
||||
|
||||
return param
|
||||
}
|
||||
|
||||
func svcloc(fields map[string]string) map[string]string {
|
||||
param := map[string]string{
|
||||
"retry": "0",
|
||||
"datetime": getDateTime(),
|
||||
"returncd": "007",
|
||||
"statusdata": "Y",
|
||||
}
|
||||
|
||||
authToken := "NDS/SVCLOC/TOKEN"
|
||||
|
||||
switch fields["svc"] {
|
||||
default:
|
||||
param["servicetoken"] = authToken
|
||||
param["svchost"] = "n/a"
|
||||
|
||||
case "9000":
|
||||
param["token"] = authToken
|
||||
param["svchost"] = "dls1.nintendowifi.net"
|
||||
|
||||
case "9001":
|
||||
param["servicetoken"] = authToken
|
||||
param["svchost"] = "dls1.nintendowifi.net"
|
||||
}
|
||||
|
||||
return param
|
||||
}
|
||||
|
||||
func handleProfanity(form url.Values, unitcd string) map[string]string {
|
||||
var wordsEncoding string
|
||||
var wordsDefaultEncoding string
|
||||
var wordsBytes []byte
|
||||
var words string
|
||||
var wordsRegion string
|
||||
var prwords string
|
||||
|
||||
if unitcd == "0" {
|
||||
wordsEncoding = "UTF-16LE"
|
||||
wordsDefaultEncoding = "UTF-16LE"
|
||||
} else {
|
||||
wordsEncoding = "UTF-16BE"
|
||||
wordsDefaultEncoding = "UTF-16BE"
|
||||
}
|
||||
|
||||
if wencValues, ok := form["wenc"]; ok {
|
||||
// It's okay for this to error, the real server
|
||||
// just falls back to the default encoding in
|
||||
// this case even if it cant properly handle it
|
||||
wencDecoded, err := common.Base64DwcEncoding.DecodeString(wencValues[0])
|
||||
if err == nil {
|
||||
wordsEncoding = string(wencDecoded)
|
||||
}
|
||||
}
|
||||
|
||||
if wordsEncoding != "UTF-8" && wordsEncoding != "UTF-16LE" && wordsEncoding != "UTF-16BE" {
|
||||
wordsEncoding = wordsDefaultEncoding
|
||||
}
|
||||
|
||||
// It's okay for this to not exist/be valid, the real
|
||||
// server will just treat the missing input as a single
|
||||
// non-profane word
|
||||
if wordsValues, ok := form["words"]; ok {
|
||||
wordsDecoded, err := common.Base64DwcEncoding.DecodeString(wordsValues[0])
|
||||
if err == nil {
|
||||
wordsBytes = wordsDecoded
|
||||
}
|
||||
}
|
||||
|
||||
// This field is entirely optional, unsure what
|
||||
// specifically it does. Adds extra data to the
|
||||
// reply, probably used for handling the word
|
||||
// list differently for different regions?
|
||||
if wordsRegionValues, ok := form["wregion"]; ok {
|
||||
wordsRegionDecoded, err := common.Base64DwcEncoding.DecodeString(wordsRegionValues[0])
|
||||
if err == nil {
|
||||
wordsRegion = string(wordsRegionDecoded)
|
||||
}
|
||||
}
|
||||
|
||||
if wordsEncoding == "UTF-8" {
|
||||
words = string(wordsBytes)
|
||||
} else {
|
||||
var utf16String []uint16
|
||||
if wordsEncoding == "UTF-16LE" {
|
||||
for i := 0; i < len(wordsBytes)/2; i++ {
|
||||
utf16String = append(utf16String, binary.LittleEndian.Uint16(wordsBytes[i*2:i*2+2]))
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < len(wordsBytes)/2; i++ {
|
||||
utf16String = append(utf16String, binary.BigEndian.Uint16(wordsBytes[i*2:i*2+2]))
|
||||
}
|
||||
}
|
||||
|
||||
words = string(utf16.Decode(utf16String))
|
||||
}
|
||||
|
||||
// TODO - Handle wtype? Unsure what this field does, seems to always be an emtpy string
|
||||
|
||||
for _, word := range strings.Split(words, "\t") {
|
||||
if isBadWord, _ := IsBadWord(word); isBadWord {
|
||||
prwords += "1"
|
||||
} else {
|
||||
prwords += "0"
|
||||
}
|
||||
}
|
||||
|
||||
var returncd string
|
||||
if strings.Contains(prwords, "1") {
|
||||
returncd = "040"
|
||||
} else {
|
||||
returncd = "000"
|
||||
}
|
||||
|
||||
reply := map[string]string{
|
||||
"returncd": returncd,
|
||||
"prwords": prwords,
|
||||
}
|
||||
|
||||
// Only known value of this field that works this way
|
||||
if wordsRegion == "A" {
|
||||
// TODO - The real server seems to handle the input words differently per region? These values are supposed to differ from prwords
|
||||
reply["prwordsA"] = prwords
|
||||
reply["prwordsC"] = prwords
|
||||
reply["prwordsE"] = prwords
|
||||
reply["prwordsJ"] = prwords
|
||||
reply["prwordsK"] = prwords
|
||||
reply["prwordsP"] = prwords
|
||||
}
|
||||
|
||||
return reply
|
||||
}
|
||||
|
||||
func dlsCount(fields map[string]string) string {
|
||||
dlcFolder := filepath.Join(dlcDir, fields["rhgamecd"])
|
||||
|
||||
dir, ok := os.ReadDir(dlcFolder)
|
||||
if ok != nil {
|
||||
return "0"
|
||||
}
|
||||
|
||||
return strconv.Itoa(len(dir))
|
||||
}
|
||||
|
||||
func isValidRhgamecd(rhgamecd string) bool {
|
||||
if len(rhgamecd) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
return common.IsUppercaseAlphanumeric(rhgamecd)
|
||||
}
|
||||
|
||||
func getDateTime() string {
|
||||
t := time.Now().UTC()
|
||||
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
|
||||
|
||||
73
nas/dls.go
Normal file
73
nas/dls.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package nas
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"wwfc/common"
|
||||
"wwfc/logging"
|
||||
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
)
|
||||
|
||||
var dlsActions = map[string]func(moduleName string, fields map[string][]byte) []byte{
|
||||
"count": dlsCount,
|
||||
}
|
||||
|
||||
func handleDownloadEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
moduleName := getModuleName(r)
|
||||
|
||||
fields, err := parseAuthRequest(r)
|
||||
if err != nil {
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
action := string(fields["action"])
|
||||
if action == "" {
|
||||
logging.Error(moduleName, "No action in form")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
rhgamecd, ok := fields["rhgamecd"]
|
||||
if !ok || !isValidRHGameCode(string(rhgamecd)) {
|
||||
logging.Error(moduleName, "Missing or invalid rhgamecd")
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
if actionFunc, exists := dlsActions[action]; exists {
|
||||
reply := actionFunc(moduleName, fields)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(reply)))
|
||||
_, err := w.Write(reply)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Error writing response:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logging.Error(moduleName, "Unknown action:", aurora.Cyan(action))
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
}
|
||||
|
||||
func dlsCount(moduleName string, fields map[string][]byte) []byte {
|
||||
dlcFolder := filepath.Join(dlcDir, string(fields["rhgamecd"]))
|
||||
|
||||
dir, err := os.ReadDir(dlcFolder)
|
||||
if err != nil {
|
||||
return []byte{'0', 0}
|
||||
}
|
||||
|
||||
return append([]byte(strconv.Itoa(len(dir))), 0)
|
||||
}
|
||||
|
||||
func isValidRHGameCode(rhgamecd string) bool {
|
||||
if len(rhgamecd) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
return common.IsUppercaseAlphanumeric(rhgamecd)
|
||||
}
|
||||
23
nas/main.go
23
nas/main.go
@@ -24,11 +24,20 @@ var (
|
||||
|
||||
var (
|
||||
authMux = http.NewServeMux()
|
||||
dlsMux = http.NewServeMux()
|
||||
sakeMux = http.NewServeMux()
|
||||
gamestatsMux = http.NewServeMux()
|
||||
raceMux = http.NewServeMux()
|
||||
)
|
||||
|
||||
var hostMuxes = map[*regexp.Regexp]*http.ServeMux{
|
||||
regexp.MustCompile(`^(nas|naswii)\.`): authMux,
|
||||
regexp.MustCompile(`^dls1\.`): dlsMux,
|
||||
regexp.MustCompile(`(\.|^)gamestats2?\.(gs\.|gamespy\.com$)`): gamestatsMux,
|
||||
regexp.MustCompile(`(\.|^)sake\.(gs\.|gamespy\.com$)`): sakeMux,
|
||||
regexp.MustCompile(`(\.|^)race\.(gs\.|gamespy\.com$)`): raceMux,
|
||||
}
|
||||
|
||||
func StartServer(reload bool) {
|
||||
// Get config
|
||||
config := common.GetConfig()
|
||||
@@ -52,9 +61,10 @@ func StartServer(reload bool) {
|
||||
ReadTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
authMux.HandleFunc("/ac", handleAuthRequest)
|
||||
authMux.HandleFunc("/pr", handleAuthRequest)
|
||||
authMux.HandleFunc("/download", handleAuthRequest)
|
||||
authMux.HandleFunc("/ac", handleAuthAccountEndpoint)
|
||||
authMux.HandleFunc("/pr", handleAuthProfanityEndpoint)
|
||||
|
||||
dlsMux.HandleFunc("/download", handleDownloadEndpoint)
|
||||
|
||||
if payloadServerAddress != "" {
|
||||
// Forward the request to the payload server
|
||||
@@ -111,13 +121,6 @@ func Shutdown() {
|
||||
}
|
||||
}
|
||||
|
||||
var hostMuxes = map[*regexp.Regexp]*http.ServeMux{
|
||||
regexp.MustCompile(`^(nas|naswii|dls1)\.`): authMux,
|
||||
regexp.MustCompile(`(\.|^)gamestats2?\.(gs\.|gamespy\.com$)`): gamestatsMux,
|
||||
regexp.MustCompile(`(\.|^)sake\.(gs\.|gamespy\.com$)`): sakeMux,
|
||||
regexp.MustCompile(`(\.|^)race\.(gs\.|gamespy\.com$)`): raceMux,
|
||||
}
|
||||
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// Check for host-specific muxes
|
||||
for regex, mux := range hostMuxes {
|
||||
|
||||
104
nas/profanity.go
104
nas/profanity.go
@@ -2,7 +2,9 @@ package nas
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -101,3 +103,105 @@ func isProfanityFileCached() bool {
|
||||
}
|
||||
return profanityFileLines != nil && !fileInfo.ModTime().After(lastModTime)
|
||||
}
|
||||
|
||||
func handleAuthProfanityEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
form, err := parseAuthRequest(r)
|
||||
if err != nil {
|
||||
replyHTTPError(w, 400, "400 Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
unitcd := form["unitcd"]
|
||||
var wordsEncoding string
|
||||
var wordsDefaultEncoding string
|
||||
if len(unitcd) != 1 || unitcd[0] != '0' {
|
||||
wordsEncoding = "UTF-16BE"
|
||||
wordsDefaultEncoding = "UTF-16BE"
|
||||
} else {
|
||||
wordsEncoding = "UTF-16LE"
|
||||
wordsDefaultEncoding = "UTF-16LE"
|
||||
}
|
||||
|
||||
if wencValues, ok := form["wenc"]; ok {
|
||||
// It's okay for this to error, the real server
|
||||
// just falls back to the default encoding in
|
||||
// this case even if it cant properly handle it
|
||||
wencDecoded, err := common.Base64DwcEncoding.DecodeString(string(wencValues[0]))
|
||||
if err == nil {
|
||||
wordsEncoding = string(wencDecoded)
|
||||
}
|
||||
}
|
||||
|
||||
if wordsEncoding != "UTF-8" && wordsEncoding != "UTF-16LE" && wordsEncoding != "UTF-16BE" {
|
||||
wordsEncoding = wordsDefaultEncoding
|
||||
}
|
||||
|
||||
// It's okay for this to not exist/be valid, the real
|
||||
// server will just treat the missing input as a single
|
||||
// non-profane word
|
||||
wordsBytes := []byte{}
|
||||
if wordsValues, ok := form["words"]; ok {
|
||||
wordsDecoded, err := common.Base64DwcEncoding.DecodeString(string(wordsValues[0]))
|
||||
if err == nil {
|
||||
wordsBytes = wordsDecoded
|
||||
}
|
||||
}
|
||||
|
||||
// This field is entirely optional, unsure what
|
||||
// specifically it does. Adds extra data to the
|
||||
// reply, probably used for handling the word
|
||||
// list differently for different regions?
|
||||
var wordsRegion string
|
||||
if wordsRegionValues, ok := form["wregion"]; ok {
|
||||
wordsRegionDecoded, err := common.Base64DwcEncoding.DecodeString(string(wordsRegionValues[0]))
|
||||
if err == nil {
|
||||
wordsRegion = string(wordsRegionDecoded)
|
||||
}
|
||||
}
|
||||
|
||||
var words string
|
||||
switch wordsEncoding {
|
||||
case "UTF-8":
|
||||
words = string(wordsBytes)
|
||||
case "UTF-16LE":
|
||||
words = common.UTF16Decode(wordsBytes, binary.LittleEndian)
|
||||
case "UTF-16BE":
|
||||
words = common.UTF16Decode(wordsBytes, binary.BigEndian)
|
||||
}
|
||||
|
||||
// TODO - Handle wtype? Unsure what this field does, seems to always be an empty string
|
||||
|
||||
prwords := ""
|
||||
for _, word := range strings.Split(words, "\t") {
|
||||
if isBadWord, _ := IsBadWord(word); isBadWord {
|
||||
prwords += "1"
|
||||
} else {
|
||||
prwords += "0"
|
||||
}
|
||||
}
|
||||
|
||||
returncd := ""
|
||||
if strings.Contains(prwords, "1") {
|
||||
returncd = "040"
|
||||
} else {
|
||||
returncd = "000"
|
||||
}
|
||||
|
||||
reply := map[string]string{
|
||||
"returncd": returncd,
|
||||
"prwords": prwords,
|
||||
}
|
||||
|
||||
// Only known value of this field that works this way
|
||||
if wordsRegion == "A" {
|
||||
// TODO - The real server seems to handle the input words differently per region? These values are supposed to differ from prwords
|
||||
reply["prwordsA"] = prwords
|
||||
reply["prwordsC"] = prwords
|
||||
reply["prwordsE"] = prwords
|
||||
reply["prwordsJ"] = prwords
|
||||
reply["prwordsK"] = prwords
|
||||
reply["prwordsP"] = prwords
|
||||
}
|
||||
|
||||
writeAuthResponse(w, reply)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
"wwfc/common"
|
||||
"wwfc/database"
|
||||
"wwfc/logging"
|
||||
@@ -251,17 +250,13 @@ func getRequestIdentity(moduleName string, request StorageRequestCommon) (uint32
|
||||
return 0, common.GameInfo{}, ResultSecretKeyInvalid
|
||||
}
|
||||
|
||||
profileId, issueTime, err := common.UnmarshalGPCMLoginTicket(request.LoginTicket)
|
||||
if err != nil {
|
||||
loginTicket := common.GPCMLoginTicket{}
|
||||
if err := loginTicket.Unmarshal(request.LoginTicket); err != nil {
|
||||
logging.Error(moduleName, err)
|
||||
return 0, common.GameInfo{}, ResultLoginTicketInvalid
|
||||
}
|
||||
|
||||
if issueTime.Add(48 * time.Hour).Before(time.Now()) {
|
||||
return 0, common.GameInfo{}, ResultLoginTicketExpired
|
||||
}
|
||||
|
||||
return profileId, *gameInfo, ResultSuccess
|
||||
return loginTicket.ProfileID, *gameInfo, ResultSuccess
|
||||
}
|
||||
|
||||
func createRecord(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestCommon) StorageResponseBody {
|
||||
|
||||
Reference in New Issue
Block a user