Files
Erupe/server/signserver/session.go
Houmgaor 7ef5efc549 feat(network): add protocol packet capture and replay system
Add a recording and replay foundation for the MHF network protocol.
A RecordingConn decorator wraps network.Conn to transparently capture
all decrypted packets to binary .mhfr files, with zero handler changes
and zero overhead when disabled.

- network/pcap: binary capture format (writer, reader, filters)
- RecordingConn: thread-safe Conn decorator with direction tracking
- CaptureOptions in config (disabled by default)
- Capture wired into all three server types (sign, entrance, channel)
- cmd/replay: CLI tool with dump, json, stats, and compare modes
- 19 new tests, all passing with -race
2026-02-23 18:50:44 +01:00

208 lines
5.1 KiB
Go

package signserver
import (
"database/sql"
"encoding/hex"
"erupe-ce/common/stringsupport"
"net"
"strings"
"sync"
"erupe-ce/common/byteframe"
"erupe-ce/network"
"go.uber.org/zap"
)
type client int
const (
PC100 client = iota
VITA
PS3
PS4
WIIU
)
// Session holds state for the sign server connection.
type Session struct {
sync.Mutex
logger *zap.Logger
server *Server
rawConn net.Conn
cryptConn network.Conn
client client
psn string
captureCleanup func()
}
func (s *Session) work() {
pkt, err := s.cryptConn.ReadPacket()
if s.server.erupeConfig.DebugOptions.LogInboundMessages {
s.logger.Debug("Inbound packet", zap.Int("bytes", len(pkt)), zap.String("data", hex.Dump(pkt)))
}
if err != nil {
return
}
err = s.handlePacket(pkt)
if err != nil {
return
}
}
func (s *Session) handlePacket(pkt []byte) error {
bf := byteframe.NewByteFrameFromBytes(pkt)
reqType := string(bf.ReadNullTerminatedBytes())
switch reqType[:len(reqType)-3] {
case "DLTSKEYSIGN:", "DSGN:", "SIGN:":
s.handleDSGN(bf)
case "PS4SGN:":
s.client = PS4
s.handlePSSGN(bf)
case "PS3SGN:":
s.client = PS3
s.handlePSSGN(bf)
case "VITASGN:":
s.client = VITA
s.handlePSSGN(bf)
case "WIIUSGN:":
s.client = WIIU
s.handleWIIUSGN(bf)
case "VITACOGLNK:", "COGLNK:":
s.handlePSNLink(bf)
case "DELETE:":
token := string(bf.ReadNullTerminatedBytes())
characterID := int(bf.ReadUint32())
tokenID := bf.ReadUint32()
err := s.server.deleteCharacter(characterID, token, tokenID)
if err == nil {
s.logger.Info("Deleted character", zap.Int("CharacterID", characterID))
_ = s.cryptConn.SendPacket([]byte{0x01}) // DEL_SUCCESS
}
default:
s.logger.Warn("Unknown request", zap.String("reqType", reqType))
if s.server.erupeConfig.DebugOptions.LogInboundMessages {
s.logger.Debug("Unknown inbound packet", zap.Int("bytes", len(pkt)), zap.String("data", hex.Dump(pkt)))
}
}
return nil
}
func (s *Session) authenticate(username string, password string) {
newCharaReq := false
if username[len(username)-1] == 43 { // '+'
username = username[:len(username)-1]
newCharaReq = true
}
bf := byteframe.NewByteFrame()
uid, resp := s.server.validateLogin(username, password)
switch resp {
case SIGN_SUCCESS:
if newCharaReq {
_ = s.server.newUserChara(uid)
}
bf.WriteBytes(s.makeSignResponse(uid))
default:
bf.WriteUint8(uint8(resp))
}
if s.server.erupeConfig.DebugOptions.LogOutboundMessages {
s.logger.Debug("Outbound packet", zap.Int("bytes", len(bf.Data())), zap.String("data", hex.Dump(bf.Data())))
}
_ = s.cryptConn.SendPacket(bf.Data())
}
func (s *Session) handleWIIUSGN(bf *byteframe.ByteFrame) {
_ = bf.ReadBytes(1)
wiiuKey := string(bf.ReadBytes(64))
uid, err := s.server.userRepo.GetByWiiUKey(wiiuKey)
if err != nil {
if err == sql.ErrNoRows {
s.logger.Info("Unlinked Wii U attempted to authenticate", zap.String("Key", wiiuKey))
s.sendCode(SIGN_ECOGLINK)
return
}
s.sendCode(SIGN_EABORT)
return
}
_ = s.cryptConn.SendPacket(s.makeSignResponse(uid))
}
func (s *Session) handlePSSGN(bf *byteframe.ByteFrame) {
// Prevent reading malformed request
if s.client != PS4 {
if len(bf.DataFromCurrent()) < 128 {
s.sendCode(SIGN_EABORT)
return
}
_ = bf.ReadNullTerminatedBytes() // VITA = 0000000256, PS3 = 0000000255
_ = bf.ReadBytes(2) // VITA = 1, PS3 = !
_ = bf.ReadBytes(82)
}
s.psn = string(bf.ReadNullTerminatedBytes())
uid, err := s.server.userRepo.GetByPSNID(s.psn)
if err != nil {
if err == sql.ErrNoRows {
_ = s.cryptConn.SendPacket(s.makeSignResponse(0))
return
}
s.sendCode(SIGN_EABORT)
return
}
_ = s.cryptConn.SendPacket(s.makeSignResponse(uid))
}
func (s *Session) handlePSNLink(bf *byteframe.ByteFrame) {
_ = bf.ReadNullTerminatedBytes() // Client ID
credStr := stringsupport.SJISToUTF8Lossy(bf.ReadNullTerminatedBytes())
credentials := strings.Split(credStr, "\n")
tok := string(bf.ReadNullTerminatedBytes())
uid, resp := s.server.validateLogin(credentials[0], credentials[1])
if resp == SIGN_SUCCESS && uid > 0 {
psn, err := s.server.sessionRepo.GetPSNIDByToken(tok)
if err != nil {
s.sendCode(SIGN_ECOGLINK)
return
}
// Since we check for the psn_id, this will never run
exists, err := s.server.userRepo.CountByPSNID(psn)
if err != nil {
s.sendCode(SIGN_ECOGLINK)
return
} else if exists > 0 {
s.sendCode(SIGN_EPSI)
return
}
currentPSN, err := s.server.userRepo.GetPSNIDForUsername(credentials[0])
if err != nil {
s.sendCode(SIGN_ECOGLINK)
return
} else if currentPSN != "" {
s.sendCode(SIGN_EMBID)
return
}
err = s.server.userRepo.SetPSNID(credentials[0], psn)
if err == nil {
s.sendCode(SIGN_SUCCESS)
return
}
}
s.sendCode(SIGN_ECOGLINK)
}
func (s *Session) handleDSGN(bf *byteframe.ByteFrame) {
user := stringsupport.SJISToUTF8Lossy(bf.ReadNullTerminatedBytes())
pass := stringsupport.SJISToUTF8Lossy(bf.ReadNullTerminatedBytes())
_ = string(bf.ReadNullTerminatedBytes()) // Unk
s.authenticate(user, pass)
}
func (s *Session) sendCode(id RespID) {
_ = s.cryptConn.SendPacket([]byte{byte(id)})
}