QR2: Ensure dwc_pid is set for client message

This commit is contained in:
mkwcat 2023-12-09 11:13:24 -05:00
parent 546557b119
commit 6afc0e2ae3
No known key found for this signature in database
GPG Key ID: 7A505679CE9E7AA9
2 changed files with 95 additions and 80 deletions

View File

@ -25,11 +25,16 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
receiver = sessionByPublicIP[destSearchID]
}
if receiver == nil {
if receiver == nil || !receiver.Authenticated {
logging.Error(moduleName, "Destination", aurora.Cyan(destSearchID), "does not exist")
return
}
if destPid, ok := receiver.Data["dwc_pid"]; !ok || destPid == "" {
logging.Error(moduleName, "Destination", aurora.Cyan(destSearchID), "has no profile ID")
return
}
// Decode and validate the message
isNatnegPacket := false
if bytes.Equal(message[:2], []byte{0xfd, 0xfc}) {
@ -65,11 +70,16 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
}
sender = sessionByPublicIP[(uint64(qr2Port)<<32)|uint64(qr2IP)]
if sender == nil {
if sender == nil || !sender.Authenticated {
logging.Error(moduleName, "Session does not exist with QR2 IP and port")
return
}
if !sender.setProfileID(moduleName, strconv.FormatUint(uint64(senderProfileID), 10)) {
// Error already logged
return
}
var ok bool
matchData, ok = common.DecodeMatchCommand(message[8], message[0x14:])
if !ok {

View File

@ -5,6 +5,7 @@ import (
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
"wwfc/common"
@ -62,89 +63,21 @@ func removeSession(sessionId uint32) {
func setSessionData(sessionId uint32, payload map[string]string, addr net.Addr) (Session, bool) {
moduleName := "QR2:" + strconv.FormatInt(int64(sessionId), 10)
// Perform sanity checks on the session data. This is a mess but
// This is an internal error that should not happen
publicIP := ""
var ok bool
if publicIP, ok = payload["publicip"]; !ok || publicIP == "0" {
logging.Error(moduleName, "Missing publicip in session data")
return Session{}, false
}
newPID, newPIDValid := payload["dwc_pid"]
delete(payload, "dwc_pid")
// Moving into performing operations on the session data, so lock the mutex
mutex.Lock()
defer mutex.Unlock()
session, sessionExists := sessions[sessionId]
if sessionExists && session.Data["publicip"] != publicIP {
logging.Error(moduleName, "Public IP mismatch")
if sessionExists && session.Addr.String() != addr.String() {
logging.Error(moduleName, "Session IP mismatch")
return Session{}, false
}
if newPIDValid {
var oldPID string
oldPIDValid := false
if sessionExists {
if oldPID, oldPIDValid = session.Data["dwc_pid"]; oldPIDValid && newPID != oldPID {
logging.Error(moduleName, "New dwc_pid mismatch: new:", aurora.Cyan(newPID), "old:", aurora.Cyan(oldPID))
return Session{}, false
}
}
if !oldPIDValid {
// Setting a new PID so validate it
profileID, err := strconv.ParseUint(newPID, 10, 32)
if err != nil {
logging.Error(moduleName, "Invalid dwc_pid value:", aurora.Cyan(newPID))
return Session{}, false
}
// Reformat dwc_pid string
newPID = strconv.FormatUint(profileID, 10)
payload["dwc_pid"] = newPID
// Lookup the profile ID in GPCM and verify it's logged in.
// Maybe we don't need this? It relies on GPCM being hosted in the same application, and
// makes GPCM a dependency of QR2. Perhaps we could use the database.
gpcmIP := gpcm.GetSessionIP(uint32(profileID))
if gpcmIP == "" {
logging.Error(moduleName, "Provided dwc_pid is not logged in:", aurora.Cyan(newPID))
return Session{}, false
}
gpcmIPStr, _ := common.IPFormatToString(gpcmIP)
if gpcmIPStr != publicIP {
logging.Error(moduleName, "Caller public IP does not match GPCM session")
return Session{}, false
}
// Constraint: only one session can exist with a profile ID
var outdated []uint32
for sessionID, otherSession := range sessions {
if otherPID, ok := otherSession.Data["dwc_pid"]; !ok || otherPID != newPID {
continue
}
// Remove old sessions with the PID
outdated = append(outdated, sessionID)
}
for _, sessionID := range outdated {
logging.Notice(moduleName, "Removing outdated session", aurora.BrightCyan(sessionID), "with PID", aurora.Cyan(newPID))
delete(sessions, sessionID)
}
logging.Notice(moduleName, "Opened session with PID", aurora.Cyan(newPID))
}
}
if !sessionExists {
logging.Notice(moduleName, "Creating session", aurora.Cyan(sessionId).String())
data := Session{
session = &Session{
SessionID: sessionId,
Addr: addr,
Challenge: "",
@ -154,14 +87,22 @@ func setSessionData(sessionId uint32, payload map[string]string, addr net.Addr)
Data: payload,
PacketCount: 0,
}
}
if newPIDValid && !session.setProfileID(moduleName, newPID) {
return Session{}, false
}
if !sessionExists {
logging.Notice(moduleName, "Creating session", aurora.Cyan(sessionId).String())
// Set search ID
for {
searchID := uint64(rand.Int63n(0x400 << 32))
if _, exists := sessionBySearchID[searchID]; !exists {
data.SearchID = searchID
data.Data["+searchid"] = strconv.FormatUint(searchID, 10)
sessionBySearchID[searchID] = &data
session.SearchID = searchID
session.Data["+searchid"] = strconv.FormatUint(searchID, 10)
sessionBySearchID[searchID] = session
break
}
}
@ -169,18 +110,82 @@ func setSessionData(sessionId uint32, payload map[string]string, addr net.Addr)
// Set public IP lookup
ip, port := common.IPFormatToInt(addr.String())
lookupIP := (uint64(port) << 32) | uint64(uint32(ip))
sessionByPublicIP[lookupIP] = &data
sessionByPublicIP[lookupIP] = session
sessions[sessionId] = &data
return data, true
sessions[sessionId] = session
return *session, true
}
if pid, ok := session.Data["dwc_pid"]; ok && pid != "" {
payload["dwc_pid"] = pid
}
payload["+searchid"] = session.Data["+searchid"]
session.Data = payload
session.LastKeepAlive = time.Now().Unix()
return *session, true
}
// Set the session's profile ID if it doesn't already exists.
// Returns false if the profile ID is invalid.
// Expects the global mutex to already be locked.
func (session *Session) setProfileID(moduleName string, newPID string) bool {
if oldPID, oldPIDValid := session.Data["dwc_pid"]; oldPIDValid && oldPID != "" {
if newPID != oldPID {
logging.Error(moduleName, "New dwc_pid mismatch: new:", aurora.Cyan(newPID), "old:", aurora.Cyan(oldPID))
return false
}
return true
}
// Setting a new PID so validate it
profileID, err := strconv.ParseUint(newPID, 10, 32)
if err != nil || strconv.FormatUint(profileID, 10) != newPID {
logging.Error(moduleName, "Invalid dwc_pid value:", aurora.Cyan(newPID))
return false
}
// Lookup the profile ID in GPCM and verify it's logged in.
// Maybe we don't need this? It relies on GPCM being hosted in the same application, and
// makes GPCM a dependency of QR2. Perhaps we could use the database.
gpcmIP := gpcm.GetSessionIP(uint32(profileID))
if gpcmIP == "" {
logging.Error(moduleName, "Provided dwc_pid is not logged in:", aurora.Cyan(newPID))
return false
}
if strings.Split(gpcmIP, ":")[0] != strings.Split(session.Addr.String(), ":")[0] {
logging.Error(moduleName, "Caller public IP does not match GPCM session")
return false
}
// Constraint: only one session can exist with a profile ID
var outdated []uint32
for sessionID, otherSession := range sessions {
if sessionID == session.SessionID {
continue
}
if otherPID, ok := otherSession.Data["dwc_pid"]; !ok || otherPID != newPID {
continue
}
// Remove old sessions with the PID
outdated = append(outdated, sessionID)
}
for _, sessionID := range outdated {
logging.Notice(moduleName, "Removing outdated session", aurora.BrightCyan(sessionID), "with PID", aurora.Cyan(newPID))
delete(sessions, sessionID)
}
session.Data["dwc_pid"] = newPID
logging.Notice(moduleName, "Opened session with PID", aurora.Cyan(newPID))
return true
}
// Get a copy of the list of servers
func GetSessionServers() []map[string]string {
var servers []map[string]string