mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
golangci-lint's errcheck rule requires explicit handling of error return values from Close, Write, and Logout calls. Use blank identifier assignment for cleanup paths where errors are intentionally discarded.
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 func() { _ = 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
|
|
}
|