mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
The protbot sent "DSGN:\x00" as the sign request type, but the server strips the last 3 characters as a version suffix. Send "DSGN:041" (ZZ client mode 41) to match the real client format. The entrance channel entry parser read 14 bytes for remaining fields but the server writes 18 bytes (9 uint16, not 7), causing a panic when parsing the server list. The channel server panicked on disconnect when a session had no decompressed save data (e.g. protbot or early client disconnect). Guard Save() against nil decompSave. Also fix docker-compose volume mount for Postgres 18 which changed its data directory layout.
143 lines
3.8 KiB
Go
143 lines
3.8 KiB
Go
package protocol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"net"
|
|
|
|
"erupe-ce/common/byteframe"
|
|
|
|
"erupe-ce/cmd/protbot/conn"
|
|
)
|
|
|
|
// ServerEntry represents a channel server from the entrance server response.
|
|
type ServerEntry struct {
|
|
IP string
|
|
Port uint16
|
|
Name string
|
|
}
|
|
|
|
// DoEntrance connects to the entrance server and retrieves the server list.
|
|
// Reference: Erupe server/entranceserver/entrance_server.go and make_resp.go.
|
|
func DoEntrance(addr string) ([]ServerEntry, error) {
|
|
c, err := conn.DialWithInit(addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("entrance connect: %w", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
// Send a minimal packet (the entrance server reads it, checks len > 5 for USR data).
|
|
// An empty/short packet triggers only SV2 response.
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(0)
|
|
if err := c.SendPacket(bf.Data()); err != nil {
|
|
return nil, fmt.Errorf("entrance send: %w", err)
|
|
}
|
|
|
|
resp, err := c.ReadPacket()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("entrance recv: %w", err)
|
|
}
|
|
|
|
return parseEntranceResponse(resp)
|
|
}
|
|
|
|
// parseEntranceResponse parses the Bin8-encrypted entrance server response.
|
|
// Reference: Erupe server/entranceserver/make_resp.go (makeHeader, makeSv2Resp)
|
|
func parseEntranceResponse(data []byte) ([]ServerEntry, error) {
|
|
if len(data) < 2 {
|
|
return nil, fmt.Errorf("entrance response too short")
|
|
}
|
|
|
|
// First byte is the Bin8 encryption key.
|
|
key := data[0]
|
|
decrypted := conn.DecryptBin8(data[1:], key)
|
|
|
|
rbf := byteframe.NewByteFrameFromBytes(decrypted)
|
|
|
|
// Read response type header: "SV2" or "SVR"
|
|
respType := string(rbf.ReadBytes(3))
|
|
if respType != "SV2" && respType != "SVR" {
|
|
return nil, fmt.Errorf("unexpected entrance response type: %s", respType)
|
|
}
|
|
|
|
entryCount := rbf.ReadUint16()
|
|
dataLen := rbf.ReadUint16()
|
|
if dataLen == 0 {
|
|
return nil, nil
|
|
}
|
|
expectedSum := rbf.ReadUint32()
|
|
serverData := rbf.ReadBytes(uint(dataLen))
|
|
|
|
actualSum := conn.CalcSum32(serverData)
|
|
if expectedSum != actualSum {
|
|
return nil, fmt.Errorf("entrance checksum mismatch: expected %08X, got %08X", expectedSum, actualSum)
|
|
}
|
|
|
|
return parseServerEntries(serverData, entryCount)
|
|
}
|
|
|
|
// parseServerEntries parses the server info binary blob.
|
|
// Reference: Erupe server/entranceserver/make_resp.go (encodeServerInfo)
|
|
func parseServerEntries(data []byte, entryCount uint16) ([]ServerEntry, error) {
|
|
bf := byteframe.NewByteFrameFromBytes(data)
|
|
var entries []ServerEntry
|
|
|
|
for i := uint16(0); i < entryCount; i++ {
|
|
ipBytes := bf.ReadBytes(4)
|
|
ip := net.IP([]byte{
|
|
byte(ipBytes[3]), byte(ipBytes[2]),
|
|
byte(ipBytes[1]), byte(ipBytes[0]),
|
|
})
|
|
|
|
_ = bf.ReadUint16() // serverIdx | 16
|
|
_ = bf.ReadUint16() // 0
|
|
channelCount := bf.ReadUint16()
|
|
_ = bf.ReadUint8() // Type
|
|
_ = bf.ReadUint8() // Season/rotation
|
|
|
|
// G1+ recommended flag
|
|
_ = bf.ReadUint8()
|
|
|
|
// G51+ (ZZ): skip 1 byte, then read 65-byte padded name
|
|
_ = bf.ReadUint8()
|
|
nameBytes := bf.ReadBytes(65)
|
|
|
|
// GG+: AllowedClientFlags
|
|
_ = bf.ReadUint32()
|
|
|
|
// Parse name (null-separated: name + description)
|
|
name := ""
|
|
for j := 0; j < len(nameBytes); j++ {
|
|
if nameBytes[j] == 0 {
|
|
break
|
|
}
|
|
name += string(nameBytes[j])
|
|
}
|
|
|
|
// Read channel entries (14 x uint16 = 28 bytes each)
|
|
for j := uint16(0); j < channelCount; j++ {
|
|
port := bf.ReadUint16()
|
|
_ = bf.ReadUint16() // channelIdx | 16
|
|
_ = bf.ReadUint16() // maxPlayers
|
|
_ = bf.ReadUint16() // currentPlayers
|
|
_ = bf.ReadBytes(18) // remaining channel fields (9 x uint16: 6 zeros + unk319 + unk254 + unk255)
|
|
_ = bf.ReadUint16() // 12345
|
|
|
|
serverIP := ip.String()
|
|
// Convert 127.0.0.1 representation
|
|
if binary.LittleEndian.Uint32(ipBytes) == 0x0100007F {
|
|
serverIP = "127.0.0.1"
|
|
}
|
|
|
|
entries = append(entries, ServerEntry{
|
|
IP: serverIP,
|
|
Port: port,
|
|
Name: fmt.Sprintf("%s ch%d", name, j+1),
|
|
})
|
|
}
|
|
}
|
|
|
|
return entries, nil
|
|
}
|