Files
Erupe/server/channelserver/handlers_cast_binary.go
Houmgaor d456bd23e0 fix(channelserver): handle ignored DB errors and cache userID on session
Silently ignored DB errors in handlers could cause data loss (frontier
point transactions completing without DB writes), reward duplication
(stamp exchange granting items on failed UPDATE), and crashes (tower
mission page=0 causing index-out-of-bounds). House access state
defaulting to 0 on DB failure also bypassed all access controls.

HIGH risk fixes:
- frontier point buy/sell now fails with ACK on DB error
- stamp exchange/stampcard abort on failed UPDATE
- guild meal INSERT returns fail ACK instead of orphaned ID 0
- mercenary/airou creation aborts on failed sequence nextval

MEDIUM risk fixes:
- tower mission page clamped to >= 1 preventing array underflow
- tower RP donation returns early on failed guild state read
- house state defaults to 2 (password-protected) on DB failure
- playtime read failure logged instead of silently resetting RP

Also cache userID on Session at login time, eliminating ~25 redundant
subqueries of the form WHERE u.id=(SELECT c.user_id FROM characters
c WHERE c.id=$1) across shop, gacha, command, and distitem handlers.
2026-02-20 21:06:16 +01:00

161 lines
4.5 KiB
Go

package channelserver
import (
"erupe-ce/common/byteframe"
"erupe-ce/common/token"
"erupe-ce/network/binpacket"
"erupe-ce/network/mhfpacket"
"fmt"
"math"
"strings"
"go.uber.org/zap"
)
// MSG_SYS_CAST[ED]_BINARY types enum
const (
BinaryMessageTypeState = 0
BinaryMessageTypeChat = 1
BinaryMessageTypeQuest = 2
BinaryMessageTypeData = 3
BinaryMessageTypeMailNotify = 4
BinaryMessageTypeEmote = 6
)
// MSG_SYS_CAST[ED]_BINARY broadcast types enum
const (
BroadcastTypeTargeted = 0x01
BroadcastTypeStage = 0x03
BroadcastTypeServer = 0x06
BroadcastTypeWorld = 0x0a
)
func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysCastBinary)
tmp := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload)
if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeData && len(pkt.RawDataPayload) == 0x10 {
if tmp.ReadUint16() == 0x0002 && tmp.ReadUint8() == 0x18 {
var timer bool
if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users WHERE id=$1`, s.userID).Scan(&timer); err != nil {
s.logger.Error("Failed to get timer setting", zap.Error(err))
}
if timer {
_ = tmp.ReadBytes(9)
tmp.SetLE()
frame := tmp.ReadUint32()
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.timer, frame/30/60/60, frame/30/60, frame/30%60, int(math.Round(float64(frame%30*100)/3)), frame))
}
}
}
if s.server.erupeConfig.DebugOptions.QuestTools {
if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeQuest && len(pkt.RawDataPayload) > 32 {
// This is only correct most of the time
tmp.ReadBytes(20)
tmp.SetLE()
x := tmp.ReadFloat32()
y := tmp.ReadFloat32()
z := tmp.ReadFloat32()
s.logger.Debug("Coord", zap.Float32s("XYZ", []float32{x, y, z}))
}
}
// Parse out the real casted binary payload
var msgBinTargeted *binpacket.MsgBinTargeted
var message, author string
var returnToSender bool
if pkt.MessageType == BinaryMessageTypeChat {
tmp.SetLE()
_, _ = tmp.Seek(8, 0)
message = string(tmp.ReadNullTerminatedBytes())
author = string(tmp.ReadNullTerminatedBytes())
}
// Customise payload
realPayload := pkt.RawDataPayload
if pkt.BroadcastType == BroadcastTypeTargeted {
tmp.SetBE()
_, _ = tmp.Seek(0, 0)
msgBinTargeted = &binpacket.MsgBinTargeted{}
err := msgBinTargeted.Parse(tmp)
if err != nil {
s.logger.Warn("Failed to parse targeted cast binary")
return
}
realPayload = msgBinTargeted.RawDataPayload
} else if pkt.MessageType == BinaryMessageTypeChat {
if message == "@dice" {
returnToSender = true
m := binpacket.MsgBinChat{
Type: BinaryMessageTypeChat,
Flags: 4,
Message: fmt.Sprintf(`%d`, token.RNG.Intn(100)+1),
SenderName: author,
}
bf := byteframe.NewByteFrame()
bf.SetLE()
_ = m.Build(bf)
realPayload = bf.Data()
} else {
bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload)
bf.SetLE()
chatMessage := &binpacket.MsgBinChat{}
_ = chatMessage.Parse(bf)
if strings.HasPrefix(chatMessage.Message, s.server.erupeConfig.CommandPrefix) {
parseChatCommand(s, chatMessage.Message)
return
}
if (pkt.BroadcastType == BroadcastTypeStage && s.stage.id == "sl1Ns200p0a0u0") || pkt.BroadcastType == BroadcastTypeWorld {
s.server.DiscordChannelSend(chatMessage.SenderName, chatMessage.Message)
}
}
}
// Make the response to forward to the other client(s).
resp := &mhfpacket.MsgSysCastedBinary{
CharID: s.charID,
BroadcastType: pkt.BroadcastType, // (The client never uses Type0 upon receiving)
MessageType: pkt.MessageType,
RawDataPayload: realPayload,
}
// Send to the proper recipients.
switch pkt.BroadcastType {
case BroadcastTypeWorld:
s.server.WorldcastMHF(resp, s, nil)
case BroadcastTypeStage:
if returnToSender {
s.stage.BroadcastMHF(resp, nil)
} else {
s.stage.BroadcastMHF(resp, s)
}
case BroadcastTypeServer:
if pkt.MessageType == BinaryMessageTypeChat {
raviSema := s.server.getRaviSemaphore()
if raviSema != nil {
raviSema.BroadcastMHF(resp, s)
}
} else {
s.server.BroadcastMHF(resp, s)
}
case BroadcastTypeTargeted:
for _, targetID := range (*msgBinTargeted).TargetCharIDs {
char := s.server.FindSessionByCharID(targetID)
if char != nil {
char.QueueSendMHFNonBlocking(resp)
}
}
default:
s.Lock()
haveStage := s.stage != nil
if haveStage {
s.stage.BroadcastMHF(resp, s)
}
s.Unlock()
}
}
func handleMsgSysCastedBinary(s *Session, p mhfpacket.MHFPacket) {}