Files
Erupe/server/channelserver/handlers_commands.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

425 lines
15 KiB
Go

package channelserver
import (
"crypto/rand"
"encoding/hex"
"erupe-ce/common/byteframe"
"erupe-ce/common/mhfcid"
"erupe-ce/common/mhfcourse"
"erupe-ce/config"
"erupe-ce/network"
"erupe-ce/network/binpacket"
"erupe-ce/network/mhfpacket"
"fmt"
"math"
"slices"
"strconv"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
var (
commands map[string]_config.Command
commandsOnce sync.Once
)
func initCommands(cmds []_config.Command, logger *zap.Logger) {
commandsOnce.Do(func() {
commands = make(map[string]_config.Command)
for _, cmd := range cmds {
commands[cmd.Name] = cmd
if cmd.Enabled {
logger.Info(fmt.Sprintf("Command %s: Enabled, prefix: %s", cmd.Name, cmd.Prefix))
} else {
logger.Info(fmt.Sprintf("Command %s: Disabled", cmd.Name))
}
}
})
}
func sendDisabledCommandMessage(s *Session, cmd _config.Command) {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.disabled, cmd.Name))
}
func sendServerChatMessage(s *Session, message string) {
// Make the inside of the casted binary
bf := byteframe.NewByteFrame()
bf.SetLE()
msgBinChat := &binpacket.MsgBinChat{
Unk0: 0,
Type: 5,
Flags: 0x80,
Message: message,
SenderName: "Erupe",
}
_ = msgBinChat.Build(bf)
castedBin := &mhfpacket.MsgSysCastedBinary{
CharID: 0,
MessageType: BinaryMessageTypeChat,
RawDataPayload: bf.Data(),
}
s.QueueSendMHFNonBlocking(castedBin)
}
func parseChatCommand(s *Session, command string) {
args := strings.Split(command[len(s.server.erupeConfig.CommandPrefix):], " ")
switch args[0] {
case commands["Ban"].Prefix:
if s.isOp() {
if len(args) > 1 {
var expiry time.Time
if len(args) > 2 {
var length int
var unit string
n, err := fmt.Sscanf(args[2], `%d%s`, &length, &unit)
if err == nil && n == 2 {
switch unit {
case "s", "second", "seconds":
expiry = time.Now().Add(time.Duration(length) * time.Second)
case "m", "mi", "minute", "minutes":
expiry = time.Now().Add(time.Duration(length) * time.Minute)
case "h", "hour", "hours":
expiry = time.Now().Add(time.Duration(length) * time.Hour)
case "d", "day", "days":
expiry = time.Now().Add(time.Duration(length) * time.Hour * 24)
case "mo", "month", "months":
expiry = time.Now().Add(time.Duration(length) * time.Hour * 24 * 30)
case "y", "year", "years":
expiry = time.Now().Add(time.Duration(length) * time.Hour * 24 * 365)
}
} else {
sendServerChatMessage(s, s.server.i18n.commands.ban.error)
return
}
}
cid := mhfcid.ConvertCID(args[1])
if cid > 0 {
var uid uint32
var uname string
err := s.server.db.QueryRow(`SELECT id, username FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, cid).Scan(&uid, &uname)
if err == nil {
if expiry.IsZero() {
if _, err := s.server.db.Exec(`INSERT INTO bans VALUES ($1)
ON CONFLICT (user_id) DO UPDATE SET expires=NULL`, uid); err != nil {
s.logger.Error("Failed to ban user", zap.Error(err))
}
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname))
} else {
if _, err := s.server.db.Exec(`INSERT INTO bans VALUES ($1, $2)
ON CONFLICT (user_id) DO UPDATE SET expires=$2`, uid, expiry); err != nil {
s.logger.Error("Failed to ban user with expiry", zap.Error(err))
}
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)+fmt.Sprintf(s.server.i18n.commands.ban.length, expiry.Format(time.DateTime)))
}
s.server.DisconnectUser(uid)
} else {
sendServerChatMessage(s, s.server.i18n.commands.ban.noUser)
}
} else {
sendServerChatMessage(s, s.server.i18n.commands.ban.invalid)
}
} else {
sendServerChatMessage(s, s.server.i18n.commands.ban.error)
}
} else {
sendServerChatMessage(s, s.server.i18n.commands.noOp)
}
case commands["Timer"].Prefix:
if commands["Timer"].Enabled || s.isOp() {
var state bool
if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users WHERE id=$1`, s.userID).Scan(&state); err != nil {
s.logger.Error("Failed to get timer state", zap.Error(err))
}
if _, err := s.server.db.Exec(`UPDATE users SET timer=$1 WHERE id=$2`, !state, s.userID); err != nil {
s.logger.Error("Failed to update timer setting", zap.Error(err))
}
if state {
sendServerChatMessage(s, s.server.i18n.commands.timer.disabled)
} else {
sendServerChatMessage(s, s.server.i18n.commands.timer.enabled)
}
} else {
sendDisabledCommandMessage(s, commands["Timer"])
}
case commands["PSN"].Prefix:
if commands["PSN"].Enabled || s.isOp() {
if len(args) > 1 {
var exists int
if err := s.server.db.QueryRow(`SELECT count(*) FROM users WHERE psn_id = $1`, args[1]).Scan(&exists); err != nil {
s.logger.Error("Failed to check PSN ID existence", zap.Error(err))
}
if exists == 0 {
_, err := s.server.db.Exec(`UPDATE users SET psn_id=$1 WHERE id=$2`, args[1], s.userID)
if err == nil {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.psn.success, args[1]))
}
} else {
sendServerChatMessage(s, s.server.i18n.commands.psn.exists)
}
} else {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.psn.error, commands["PSN"].Prefix))
}
} else {
sendDisabledCommandMessage(s, commands["PSN"])
}
case commands["Reload"].Prefix:
if commands["Reload"].Enabled || s.isOp() {
sendServerChatMessage(s, s.server.i18n.commands.reload)
var temp mhfpacket.MHFPacket
deleteNotif := byteframe.NewByteFrame()
for _, object := range s.stage.objects {
if object.ownerCharID == s.charID {
continue
}
temp = &mhfpacket.MsgSysDeleteObject{ObjID: object.id}
deleteNotif.WriteUint16(uint16(temp.Opcode()))
_ = temp.Build(deleteNotif, s.clientContext)
}
for _, session := range s.server.sessions {
if s == session {
continue
}
temp = &mhfpacket.MsgSysDeleteUser{CharID: session.charID}
deleteNotif.WriteUint16(uint16(temp.Opcode()))
_ = temp.Build(deleteNotif, s.clientContext)
}
deleteNotif.WriteUint16(uint16(network.MSG_SYS_END))
s.QueueSendNonBlocking(deleteNotif.Data())
time.Sleep(500 * time.Millisecond)
reloadNotif := byteframe.NewByteFrame()
for _, session := range s.server.sessions {
if s == session {
continue
}
temp = &mhfpacket.MsgSysInsertUser{CharID: session.charID}
reloadNotif.WriteUint16(uint16(temp.Opcode()))
_ = temp.Build(reloadNotif, s.clientContext)
for i := 0; i < 3; i++ {
temp = &mhfpacket.MsgSysNotifyUserBinary{
CharID: session.charID,
BinaryType: uint8(i + 1),
}
reloadNotif.WriteUint16(uint16(temp.Opcode()))
_ = temp.Build(reloadNotif, s.clientContext)
}
}
for _, obj := range s.stage.objects {
if obj.ownerCharID == s.charID {
continue
}
temp = &mhfpacket.MsgSysDuplicateObject{
ObjID: obj.id,
X: obj.x,
Y: obj.y,
Z: obj.z,
Unk0: 0,
OwnerCharID: obj.ownerCharID,
}
reloadNotif.WriteUint16(uint16(temp.Opcode()))
_ = temp.Build(reloadNotif, s.clientContext)
}
reloadNotif.WriteUint16(uint16(network.MSG_SYS_END))
s.QueueSendNonBlocking(reloadNotif.Data())
} else {
sendDisabledCommandMessage(s, commands["Reload"])
}
case commands["KeyQuest"].Prefix:
if commands["KeyQuest"].Enabled || s.isOp() {
if s.server.erupeConfig.RealClientMode < _config.G10 {
sendServerChatMessage(s, s.server.i18n.commands.kqf.version)
} else {
if len(args) > 1 {
switch args[1] {
case "get":
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.kqf.get, s.kqf))
case "set":
if len(args) > 2 && len(args[2]) == 16 {
hexd, _ := hex.DecodeString(args[2])
s.kqf = hexd
s.kqfOverride = true
sendServerChatMessage(s, s.server.i18n.commands.kqf.set.success)
} else {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.kqf.set.error, commands["KeyQuest"].Prefix))
}
}
}
}
} else {
sendDisabledCommandMessage(s, commands["KeyQuest"])
}
case commands["Rights"].Prefix:
if commands["Rights"].Enabled || s.isOp() {
if len(args) > 1 {
v, _ := strconv.Atoi(args[1])
_, err := s.server.db.Exec("UPDATE users SET rights=$1 WHERE id=$2", v, s.userID)
if err == nil {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.success, v))
} else {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.error, commands["Rights"].Prefix))
}
} else {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.error, commands["Rights"].Prefix))
}
} else {
sendDisabledCommandMessage(s, commands["Rights"])
}
case commands["Course"].Prefix:
if commands["Course"].Enabled || s.isOp() {
if len(args) > 1 {
for _, course := range mhfcourse.Courses() {
for _, alias := range course.Aliases() {
if strings.EqualFold(args[1], alias) {
if slices.Contains(s.server.erupeConfig.Courses, _config.Course{Name: course.Aliases()[0], Enabled: true}) {
var delta, rightsInt uint32
if mhfcourse.CourseExists(course.ID, s.courses) {
ei := slices.IndexFunc(s.courses, func(c mhfcourse.Course) bool {
for _, alias := range c.Aliases() {
if strings.EqualFold(args[1], alias) {
return true
}
}
return false
})
if ei != -1 {
delta = uint32(-1 * math.Pow(2, float64(course.ID)))
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.disabled, course.Aliases()[0]))
}
} else {
delta = uint32(math.Pow(2, float64(course.ID)))
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.enabled, course.Aliases()[0]))
}
err := s.server.db.QueryRow("SELECT rights FROM users WHERE id=$1", s.userID).Scan(&rightsInt)
if err == nil {
if _, err := s.server.db.Exec("UPDATE users SET rights=$1 WHERE id=$2", rightsInt+delta, s.userID); err != nil {
s.logger.Error("Failed to update user rights", zap.Error(err))
}
}
updateRights(s)
} else {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.locked, course.Aliases()[0]))
}
return
}
}
}
} else {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.error, commands["Course"].Prefix))
}
} else {
sendDisabledCommandMessage(s, commands["Course"])
}
case commands["Raviente"].Prefix:
if commands["Raviente"].Enabled || s.isOp() {
if len(args) > 1 {
if s.server.getRaviSemaphore() != nil {
switch args[1] {
case "start":
if s.server.raviente.register[1] == 0 {
s.server.raviente.register[1] = s.server.raviente.register[3]
sendServerChatMessage(s, s.server.i18n.commands.ravi.start.success)
s.notifyRavi()
} else {
sendServerChatMessage(s, s.server.i18n.commands.ravi.start.error)
}
case "cm", "check", "checkmultiplier", "multiplier":
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ravi.multiplier, s.server.GetRaviMultiplier()))
case "sr", "sendres", "resurrection", "ss", "sendsed", "rs", "reqsed":
if s.server.erupeConfig.RealClientMode == _config.ZZ {
switch args[1] {
case "sr", "sendres", "resurrection":
if s.server.raviente.state[28] > 0 {
sendServerChatMessage(s, s.server.i18n.commands.ravi.res.success)
s.server.raviente.state[28] = 0
} else {
sendServerChatMessage(s, s.server.i18n.commands.ravi.res.error)
}
case "ss", "sendsed":
sendServerChatMessage(s, s.server.i18n.commands.ravi.sed.success)
// Total BerRavi HP
HP := s.server.raviente.state[0] + s.server.raviente.state[1] + s.server.raviente.state[2] + s.server.raviente.state[3] + s.server.raviente.state[4]
s.server.raviente.support[1] = HP
case "rs", "reqsed":
sendServerChatMessage(s, s.server.i18n.commands.ravi.request)
// Total BerRavi HP
HP := s.server.raviente.state[0] + s.server.raviente.state[1] + s.server.raviente.state[2] + s.server.raviente.state[3] + s.server.raviente.state[4]
s.server.raviente.support[1] = HP + 1
}
} else {
sendServerChatMessage(s, s.server.i18n.commands.ravi.version)
}
default:
sendServerChatMessage(s, s.server.i18n.commands.ravi.error)
}
} else {
sendServerChatMessage(s, s.server.i18n.commands.ravi.noPlayers)
}
} else {
sendServerChatMessage(s, s.server.i18n.commands.ravi.error)
}
} else {
sendDisabledCommandMessage(s, commands["Raviente"])
}
case commands["Teleport"].Prefix:
if commands["Teleport"].Enabled || s.isOp() {
if len(args) > 2 {
x, _ := strconv.ParseInt(args[1], 10, 16)
y, _ := strconv.ParseInt(args[2], 10, 16)
payload := byteframe.NewByteFrame()
payload.SetLE()
payload.WriteUint8(2) // SetState type(position == 2)
payload.WriteInt16(int16(x)) // X
payload.WriteInt16(int16(y)) // Y
payloadBytes := payload.Data()
s.QueueSendMHFNonBlocking(&mhfpacket.MsgSysCastedBinary{
CharID: s.charID,
MessageType: BinaryMessageTypeState,
RawDataPayload: payloadBytes,
})
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.teleport.success, x, y))
} else {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.teleport.error, commands["Teleport"].Prefix))
}
} else {
sendDisabledCommandMessage(s, commands["Teleport"])
}
case commands["Discord"].Prefix:
if commands["Discord"].Enabled || s.isOp() {
var _token string
err := s.server.db.QueryRow(`SELECT discord_token FROM users WHERE id=$1`, s.userID).Scan(&_token)
if err != nil {
randToken := make([]byte, 4)
_, _ = rand.Read(randToken)
_token = fmt.Sprintf("%x-%x", randToken[:2], randToken[2:])
if _, err := s.server.db.Exec(`UPDATE users SET discord_token = $1 WHERE id=$2`, _token, s.userID); err != nil {
s.logger.Error("Failed to update discord token", zap.Error(err))
}
}
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.discord.success, _token))
} else {
sendDisabledCommandMessage(s, commands["Discord"])
}
case commands["Playtime"].Prefix:
if commands["Playtime"].Enabled || s.isOp() {
playtime := s.playtime + uint32(time.Since(s.playtimeTime).Seconds())
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.playtime, playtime/60/60, playtime/60%60, playtime%60))
} else {
sendDisabledCommandMessage(s, commands["Playtime"])
}
case commands["Help"].Prefix:
if commands["Help"].Enabled || s.isOp() {
for _, command := range commands {
if command.Enabled || s.isOp() {
sendServerChatMessage(s, fmt.Sprintf("%s%s: %s", s.server.erupeConfig.CommandPrefix, command.Prefix, command.Description))
}
}
} else {
sendDisabledCommandMessage(s, commands["Help"])
}
}
}