Fix general compatibility with DS games

This commit is contained in:
mkwcat
2024-01-24 11:42:04 -05:00
parent 8702c98c48
commit 10de082278
13 changed files with 181 additions and 40 deletions

View File

@@ -29,7 +29,7 @@ func generateRandom(n int) []byte {
var (
authTokenKey = generateRandom(16)
authTokenIV = generateRandom(16)
authTokenMagic = generateRandom(15)
authTokenMagic = generateRandom(14)
loginTicketKey = generateRandom(16)
loginTicketIV = generateRandom(16)
@@ -46,7 +46,7 @@ func appendString(blob []byte, value string, maxlen int) []byte {
return blob
}
func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, isLocalhost bool) (string, string) {
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().Unix()))
blob = appendString(blob, gamecd, 4)
@@ -65,6 +65,8 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64
challenge := RandomString(8)
blob = append(blob, []byte(challenge)...)
blob = append(blob, byte(unitcd))
if isLocalhost {
blob = append(blob, 0x01)
} else {
@@ -82,7 +84,7 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64
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, isLocalhost bool) {
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, unitcd byte, isLocalhost bool) {
if !strings.HasPrefix(token, "NDS") {
err = errors.New("invalid auth token prefix")
return
@@ -119,7 +121,8 @@ func UnmarshalNASAuthToken(token string) (err error, gamecd string, issuetime ti
lang = blob[0x2B]
ingamesn = string(blob[0x2D : 0x2D+min(blob[0x2C], 75)])
challenge = string(blob[0x78:0x80])
isLocalhost = blob[0x80] == 0x01
unitcd = blob[0x80]
isLocalhost = blob[0x81] == 0x01
return
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/csv"
"os"
"strconv"
"strings"
"sync"
)
@@ -84,3 +85,20 @@ func ReadGameList() {
gameListNameLookup[entry[1]] = index
}
}
func GetExpectedUnitCode(gameName string) byte {
if strings.HasSuffix(gameName, "wii") || strings.HasSuffix(gameName, "wiiam") {
return 1
}
if gameName == "sneezieswiiw" || gameName == "wormswiiware" || gameName == "wormswiiwaream" {
return 1
}
// Games with weird other regions
if gameName == "jockracerna" || gameName == "jockracereu" || gameName == "sengo3wiijp" {
return 1
}
return 0
}

View File

@@ -45,6 +45,14 @@ func IPFormatToString(ip string) (string, string) {
return strconv.FormatInt(int64(intIP), 10), strconv.FormatUint(uint64(intPort), 10)
}
func IPFormatToStringLE(ip string) (string, string) {
intIP, intPort := IPFormatToInt(ip)
// Convert to little endian and print as big endian int
intIP = int32((uint32(intIP) >> 24) | ((uint32(intIP) & 0x00FF0000) >> 8) | ((uint32(intIP) & 0x0000FF00) << 8) | ((uint32(intIP) & 0x000000FF) << 24))
return strconv.FormatInt(int64(intIP), 10), strconv.FormatUint(uint64(intPort), 10)
}
func IPFormatBytes(ip string) []byte {
if strings.Contains(ip, ":") {
ip = strings.Split(ip, ":")[0]

View File

@@ -49,6 +49,7 @@ type MatchCommandData struct {
TellAddr *MatchCommandDataTellAddr
ServerCloseClient *MatchCommandDataServerCloseClient
SuspendMatch *MatchCommandDataSuspendMatch
Other []byte
}
type MatchCommandDataReservation struct {
@@ -189,7 +190,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
switch command {
case MatchReservation:
if version == 3 && len(buffer) < 0x0C {
if version == 3 && len(buffer) < 0x04 {
break
}
@@ -473,6 +474,25 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
},
}, true
case MatchPollTimeout:
if len(buffer) != 0x00 {
break
}
return MatchCommandData{
Version: version,
Command: command,
}, true
case MatchPollToAck:
if len(buffer) != 0x04 {
break
}
return MatchCommandData{
Version: version,
Command: command,
Other: buffer,
}, true
case MatchSuspendMatch:
if len(buffer) == 0x08 {
return MatchCommandData{
@@ -497,6 +517,13 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
},
}, true
}
default:
return MatchCommandData{
Version: version,
Command: command,
Other: buffer,
}, true
}
return MatchCommandData{}, false
@@ -640,6 +667,12 @@ func EncodeMatchCommand(command byte, data MatchCommandData) ([]byte, bool) {
}
return message, true
case MatchPollTimeout:
return []byte{}, true
case MatchPollToAck:
return data.Other, true
case MatchSuspendMatch:
message := binary.LittleEndian.AppendUint32([]byte{}, data.SuspendMatch.HostProfileID)
message = binary.LittleEndian.AppendUint32(message, data.SuspendMatch.IsHostFlag)
@@ -649,6 +682,10 @@ func EncodeMatchCommand(command byte, data MatchCommandData) ([]byte, bool) {
message = binary.LittleEndian.AppendUint32(message, data.SuspendMatch.ClientAIDValue)
}
return message, true
default:
logging.Info("Common", "Unknown match command:", aurora.Cyan(command), "data:", data.Other)
return data.Other, true
}
return []byte{}, false

View File

@@ -660,7 +660,9 @@ Celtic Kings Demo celtickingsdemo 667 TCQMZI
Celtic Lore: Sidhe Hills celticloresidhehil
Ceville ceville
Chainz 2: Relinked chainz2relinked
Champion Jockey - G1 Jockey & Gallop Racer (Wii) jockracerna yNVo3W
Champion Jockey - G1 Jockey & Gallop Racer (EU) (Wii) jockracereu eG1kq5
Champion Jockey - G1 Jockey & Gallop Racer (JP) (Wii) jockracerwii haVAVM
Champion Jockey - G1 Jockey & Gallop Racer (NA) (Wii) jockracerna yNVo3W
Champions Online champon
Championship Euchre cheuchre 317 Yw7fc9
Championship Hearts chhearts 251 Yw7fc9
1 1001 Minigolf Challenge 1001MinigolfChalle
660 Celtic Lore: Sidhe Hills celticloresidhehil
661 Ceville ceville
662 Chainz 2: Relinked chainz2relinked
663 Champion Jockey - G1 Jockey & Gallop Racer (Wii) Champion Jockey - G1 Jockey & Gallop Racer (EU) (Wii) jockracerna jockracereu yNVo3W eG1kq5
664 Champion Jockey - G1 Jockey & Gallop Racer (JP) (Wii) jockracerwii haVAVM
665 Champion Jockey - G1 Jockey & Gallop Racer (NA) (Wii) jockracerna yNVo3W
666 Champions Online champon
667 Championship Euchre cheuchre 317 Yw7fc9
668 Championship Hearts chhearts 251 Yw7fc9

View File

@@ -18,6 +18,11 @@ import (
"github.com/logrusorgru/aurora/v3"
)
const (
UnitCodeDS = 0
UnitCodeWii = 1
)
func generateResponse(gpcmChallenge, nasChallenge, authToken, clientChallenge string) string {
hasher := md5.New()
hasher.Write([]byte(nasChallenge))
@@ -137,7 +142,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
return
}
err, gamecd, issueTime, userId, gsbrcd, cfc, region, lang, ingamesn, challenge, isLocalhost := common.UnmarshalNASAuthToken(authToken)
err, gamecd, issueTime, userId, gsbrcd, cfc, region, lang, ingamesn, challenge, unitcd, isLocalhost := common.UnmarshalNASAuthToken(authToken)
if err != nil {
g.replyError(ErrLogin)
return
@@ -149,10 +154,6 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
return
}
_, payloadVerExists := command.OtherValues["payload_ver"]
_, signatureExists := command.OtherValues["wwfc_sig"]
deviceId := uint32(0)
g.GameName = command.OtherValues["gamename"]
logging.Info(g.ModuleName, "Game name:", aurora.Cyan(g.GameName))
g.GameCode = gamecd
@@ -160,6 +161,11 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
g.Language = lang
g.ConsoleFriendCode = cfc
g.InGameName = ingamesn
g.UnitCode = unitcd
_, payloadVerExists := command.OtherValues["payload_ver"]
_, signatureExists := command.OtherValues["wwfc_sig"]
deviceId := uint32(0)
if hostPlatform, exists := command.OtherValues["wwfc_host"]; exists {
g.HostPlatform = hostPlatform
@@ -169,14 +175,22 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
g.LoginInfoSet = true
if isLocalhost && !payloadVerExists && !signatureExists {
// Players using the DNS exploit, need patching using a QR2 exploit
// TODO: Check that the game is compatible with the DNS
g.NeedsExploit = true
} else {
deviceId = g.verifyExLoginInfo(command, authToken)
if deviceId == 0 {
return
if g.GameName != "mahjongkcds" && common.GetExpectedUnitCode(g.GameName) != unitcd {
logging.Error(g.ModuleName, "Incorrect unit code specified:", aurora.Cyan(unitcd))
g.replyError(ErrLogin)
return
}
if g.UnitCode == UnitCodeWii {
if isLocalhost && !payloadVerExists && !signatureExists {
// Players using the DNS exploit, need patching using a QR2 exploit
// TODO: Check that the game is compatible with the DNS
g.NeedsExploit = true
} else {
deviceId = g.verifyExLoginInfo(command, authToken)
if deviceId == 0 {
return
}
}
}
@@ -251,13 +265,19 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
// Notify QR2 of the login
qr2.Login(g.User.ProfileId, gamecd, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated, g.User.Restricted, KickPlayer)
replyUserId := g.User.UserId
if g.UnitCode == UnitCodeDS {
// Workaround for SDK bug
replyUserId = 0
}
payload := common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "lc",
CommandValue: "2",
OtherValues: map[string]string{
"sesskey": strconv.FormatInt(int64(g.SessionKey), 10),
"proof": proof,
"userid": strconv.FormatUint(g.User.UserId, 10),
"userid": strconv.FormatUint(replyUserId, 10),
"profileid": strconv.FormatUint(uint64(g.User.ProfileId), 10),
"uniquenick": g.User.UniqueNick,
"lt": g.LoginTicket,

View File

@@ -38,6 +38,7 @@ type GameSpySession struct {
ConsoleFriendCode uint64
DeviceId uint32
HostPlatform string
UnitCode byte
Status string
LocString string

View File

@@ -75,6 +75,9 @@ func handleRequest(conn net.Conn) {
switch command.Command {
default:
logging.Error(moduleName, "Unknown command:", command.Command)
logging.Error(moduleName, "Raw data:", string(buffer))
replyError(moduleName, conn, gpcm.ErrParse)
break
case "ka":
conn.Write([]byte(`\ka\\final\`))
@@ -83,6 +86,10 @@ func handleRequest(conn net.Conn) {
case "otherslist":
conn.Write([]byte(handleOthersList(command)))
break
case "search":
conn.Write([]byte(handleSearch(command)))
break
}
}
}

33
gpsp/search.go Normal file
View File

@@ -0,0 +1,33 @@
package gpsp
import (
"strconv"
"wwfc/common"
"wwfc/gpcm"
"wwfc/logging"
"github.com/logrusorgru/aurora/v3"
)
func handleSearch(command common.GameSpyCommand) string {
moduleName := "GPSP"
strProfileId, ok := command.OtherValues["profileid"]
if !ok {
logging.Error(moduleName, "Missing profileid in search")
return gpcm.ErrSearch.GetMessage()
}
profileId, err := strconv.ParseUint(strProfileId, 10, 32)
if err != nil {
logging.Error(moduleName, "Invalid profileid:", strProfileId)
return gpcm.ErrSearch.GetMessage()
}
moduleName = "GPSP:" + strconv.FormatUint(profileId, 10)
logging.Info(moduleName, "Search for", aurora.Cyan(profileId))
return common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "bsrdone",
})
}

View File

@@ -30,7 +30,7 @@ func handleAuthRequest(moduleName string, w http.ResponseWriter, r *http.Request
replyHTTPError(w, 400, "400 Bad Request")
return
}
// Need to know this here to determine UTF-16 endianness (LE for DS, BE for Wii)
// unitcd 0 = DS, 1 = Wii
unitcdValues, ok := r.PostForm["unitcd"]
@@ -264,10 +264,10 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st
// Only later DS games send this
ingamesn, ok := fields["ingamesn"]
if ok {
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], ingamesn, isLocalhost)
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], "", isLocalhost)
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))
}
@@ -299,7 +299,7 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st
return param
}
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], isLocalhost)
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"]))
}

View File

@@ -40,18 +40,30 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b
realIP, realPort := common.IPFormatToString(addr.String())
noIP := false
if ip, ok := payload["publicip"]; !ok || ip == "0" {
// Set the public IP key to the real IP
payload["publicip"] = realIP
payload["publicport"] = realPort
noIP = true
}
// Client is mistaken about its public IP
if payload["publicip"] != realIP || payload["publicport"] != realPort {
logging.Error(moduleName, "Public IP mismatch")
return
clientEndianness := common.GetExpectedUnitCode(payload["gamename"])
if !noIP && clientEndianness == ClientBigEndian {
if payload["publicip"] != realIP || payload["publicport"] != realPort {
// Client is mistaken about its public IP
logging.Error(moduleName, "Public IP mismatch")
return
}
} else if !noIP && clientEndianness == ClientLittleEndian {
realIPLE, realPortLE := common.IPFormatToStringLE(addr.String())
if payload["publicip"] != realIPLE || payload["publicport"] != realPortLE {
// Client is mistaken about its public IP
logging.Error(moduleName, "Public IP mismatch")
return
}
}
payload["publicip"] = realIP
payload["publicport"] = realPort
lookupAddr := makeLookupAddr(addr.String())
statechanged, ok := payload["statechanged"]
@@ -100,7 +112,7 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b
mutex.Unlock()
}
if !session.Authenticated {
if !session.Authenticated || noIP {
sendChallenge(conn, addr, session, lookupAddr)
} else if !session.ExploitReceived && session.Login != nil && session.Login.NeedsExploit && statechanged == "1" {
logging.Notice(moduleName, "Sending SBCM exploit to DNS patcher client")

View File

@@ -241,7 +241,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
}
s := sleep.Sleeper{}
s.AddWaker(&receiver.MessageAckWaker)
s.AddWaker(receiver.MessageAckWaker)
timeWaker := sleep.Waker{}
s.AddWaker(&timeWaker)

View File

@@ -15,9 +15,9 @@ import (
)
const (
ClientNoEndian = iota
ClientBigEndian
ClientLittleEndian
ClientLittleEndian = 0
ClientBigEndian = 1
ClientNoEndian = 2
)
type Session struct {
@@ -34,8 +34,8 @@ type Session struct {
PacketCount uint32
Reservation common.MatchCommandData
ReservationID uint64
MessageMutex deadlock.Mutex
MessageAckWaker sleep.Waker
MessageMutex *deadlock.Mutex
MessageAckWaker *sleep.Waker
GroupPointer *Group
}
@@ -116,8 +116,8 @@ func setSessionData(moduleName string, addr net.Addr, sessionId uint32, payload
PacketCount: 0,
Reservation: common.MatchCommandData{},
ReservationID: 0,
MessageMutex: deadlock.Mutex{},
MessageAckWaker: sleep.Waker{},
MessageMutex: &deadlock.Mutex{},
MessageAckWaker: &sleep.Waker{},
}
}