From 6afc0e2ae3134b9934b3672bcbd1d13f55c352fc Mon Sep 17 00:00:00 2001 From: mkwcat Date: Sat, 9 Dec 2023 11:13:24 -0500 Subject: [PATCH] QR2: Ensure dwc_pid is set for client message --- qr2/message.go | 14 ++++- qr2/session.go | 161 +++++++++++++++++++++++++------------------------ 2 files changed, 95 insertions(+), 80 deletions(-) diff --git a/qr2/message.go b/qr2/message.go index d313499..9489b05 100644 --- a/qr2/message.go +++ b/qr2/message.go @@ -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 { diff --git a/qr2/session.go b/qr2/session.go index b425f28..19a7646 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -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