mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
Fix rasta_id=0 overwriting NULL in SaveMercenary, which prevented game state saving for characters without a mercenary (#163). Also includes: - CHANGELOG updated with all 10 post-RC1 commits - Setup wizard fmt.Printf replaced with zap structured logging - technical-debt.md updated with 6 newly completed items - Scenario binary format documented (docs/scenario-format.md) - Tests: alliance nil-guard (#171), handler dispatch table, migrations (sorted/SQL/baseline), setup wizard (10 tests), protbot protocol sign/entrance/channel (23 tests)
248 lines
6.6 KiB
Go
248 lines
6.6 KiB
Go
package protocol
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"erupe-ce/cmd/protbot/conn"
|
|
"erupe-ce/common/byteframe"
|
|
)
|
|
|
|
// encryptBin8 encrypts plaintext using the Bin8 algorithm.
|
|
// Since Bin8 is a symmetric XOR cipher, DecryptBin8(plaintext, key) produces ciphertext.
|
|
func encryptBin8(plaintext []byte, key byte) []byte {
|
|
return conn.DecryptBin8(plaintext, key)
|
|
}
|
|
|
|
// buildEntranceResponse constructs a valid Bin8-encrypted entrance server response.
|
|
// Format: [key byte] [encrypted: "SV2" + uint16 entryCount + uint16 dataLen + uint32 checksum + serverData]
|
|
func buildEntranceResponse(key byte, respType string, entries []testServerEntry) []byte {
|
|
// Build server data blob first (to compute checksum and length).
|
|
serverData := buildServerData(entries)
|
|
|
|
// Build the plaintext (before encryption).
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteBytes([]byte(respType)) // "SV2" or "SVR"
|
|
bf.WriteUint16(uint16(len(entries)))
|
|
bf.WriteUint16(uint16(len(serverData)))
|
|
if len(serverData) > 0 {
|
|
bf.WriteUint32(conn.CalcSum32(serverData))
|
|
bf.WriteBytes(serverData)
|
|
}
|
|
|
|
plaintext := bf.Data()
|
|
encrypted := encryptBin8(plaintext, key)
|
|
|
|
// Final response: key byte + encrypted data.
|
|
result := make([]byte, 1+len(encrypted))
|
|
result[0] = key
|
|
copy(result[1:], encrypted)
|
|
return result
|
|
}
|
|
|
|
type testServerEntry struct {
|
|
ip [4]byte // big-endian IP bytes (reversed for MHF format)
|
|
name string
|
|
channelCount uint16
|
|
channelPorts []uint16
|
|
}
|
|
|
|
// buildServerData constructs the binary server entry data blob.
|
|
// Format mirrors Erupe server/entranceserver/make_resp.go:encodeServerInfo.
|
|
func buildServerData(entries []testServerEntry) []byte {
|
|
if len(entries) == 0 {
|
|
return nil
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
for _, e := range entries {
|
|
// IP bytes (stored reversed in the protocol — client reads and reverses)
|
|
bf.WriteBytes(e.ip[:])
|
|
|
|
bf.WriteUint16(0x0010) // serverIdx | 16
|
|
bf.WriteUint16(0) // zero
|
|
bf.WriteUint16(e.channelCount)
|
|
bf.WriteUint8(0) // Type
|
|
bf.WriteUint8(0) // Season
|
|
|
|
// G1+ recommended
|
|
bf.WriteUint8(0)
|
|
|
|
// G51+ (ZZ): skip byte + 65-byte name
|
|
bf.WriteUint8(0)
|
|
nameBytes := make([]byte, 65)
|
|
copy(nameBytes, []byte(e.name))
|
|
bf.WriteBytes(nameBytes)
|
|
|
|
// GG+: AllowedClientFlags
|
|
bf.WriteUint32(0)
|
|
|
|
// Channel entries (28 bytes each)
|
|
for j := uint16(0); j < e.channelCount; j++ {
|
|
port := uint16(54001)
|
|
if j < uint16(len(e.channelPorts)) {
|
|
port = e.channelPorts[j]
|
|
}
|
|
bf.WriteUint16(port) // port
|
|
bf.WriteUint16(0x0010) // channelIdx | 16
|
|
bf.WriteUint16(100) // maxPlayers
|
|
bf.WriteUint16(5) // currentPlayers
|
|
bf.WriteBytes(make([]byte, 18)) // remaining fields (9 x uint16)
|
|
bf.WriteUint16(12345) // sentinel
|
|
}
|
|
}
|
|
return bf.Data()
|
|
}
|
|
|
|
func TestParseEntranceResponse_ValidSV2(t *testing.T) {
|
|
entries := []testServerEntry{
|
|
{
|
|
ip: [4]byte{1, 0, 0, 127}, // 127.0.0.1 reversed
|
|
name: "TestServer",
|
|
channelCount: 2,
|
|
channelPorts: []uint16{54001, 54002},
|
|
},
|
|
}
|
|
|
|
data := buildEntranceResponse(0x42, "SV2", entries)
|
|
|
|
result, err := parseEntranceResponse(data)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(result) != 2 {
|
|
t.Fatalf("entry count: got %d, want 2", len(result))
|
|
}
|
|
|
|
if result[0].Port != 54001 {
|
|
t.Errorf("entry[0].Port: got %d, want 54001", result[0].Port)
|
|
}
|
|
if result[1].Port != 54002 {
|
|
t.Errorf("entry[1].Port: got %d, want 54002", result[1].Port)
|
|
}
|
|
if result[0].Name != "TestServer ch1" {
|
|
t.Errorf("entry[0].Name: got %q, want %q", result[0].Name, "TestServer ch1")
|
|
}
|
|
if result[1].Name != "TestServer ch2" {
|
|
t.Errorf("entry[1].Name: got %q, want %q", result[1].Name, "TestServer ch2")
|
|
}
|
|
}
|
|
|
|
func TestParseEntranceResponse_ValidSVR(t *testing.T) {
|
|
entries := []testServerEntry{
|
|
{
|
|
ip: [4]byte{1, 0, 0, 127},
|
|
name: "World1",
|
|
channelCount: 1,
|
|
channelPorts: []uint16{54001},
|
|
},
|
|
}
|
|
|
|
data := buildEntranceResponse(0x10, "SVR", entries)
|
|
|
|
result, err := parseEntranceResponse(data)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("entry count: got %d, want 1", len(result))
|
|
}
|
|
if result[0].Port != 54001 {
|
|
t.Errorf("entry[0].Port: got %d, want 54001", result[0].Port)
|
|
}
|
|
}
|
|
|
|
func TestParseEntranceResponse_InvalidType(t *testing.T) {
|
|
// Build a response with an invalid type string "BAD" instead of "SV2"/"SVR".
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteBytes([]byte("BAD"))
|
|
bf.WriteUint16(0) // entryCount
|
|
bf.WriteUint16(0) // dataLen
|
|
|
|
plaintext := bf.Data()
|
|
key := byte(0x55)
|
|
encrypted := encryptBin8(plaintext, key)
|
|
|
|
data := make([]byte, 1+len(encrypted))
|
|
data[0] = key
|
|
copy(data[1:], encrypted)
|
|
|
|
_, err := parseEntranceResponse(data)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid response type, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseEntranceResponse_EmptyData(t *testing.T) {
|
|
_, err := parseEntranceResponse(nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for nil data, got nil")
|
|
}
|
|
|
|
_, err = parseEntranceResponse([]byte{})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty data, got nil")
|
|
}
|
|
|
|
_, err = parseEntranceResponse([]byte{0x42}) // only key, no encrypted data
|
|
if err == nil {
|
|
t.Fatal("expected error for single-byte data, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseEntranceResponse_ZeroEntries(t *testing.T) {
|
|
// Valid response with zero entries and zero data length.
|
|
data := buildEntranceResponse(0x30, "SV2", nil)
|
|
|
|
result, err := parseEntranceResponse(data)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("expected nil result for zero entries, got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestParseServerEntries_MultipleServers(t *testing.T) {
|
|
entries := []testServerEntry{
|
|
{
|
|
ip: [4]byte{100, 1, 168, 192}, // 192.168.1.100 reversed
|
|
name: "Server1",
|
|
channelCount: 1,
|
|
channelPorts: []uint16{54001},
|
|
},
|
|
{
|
|
ip: [4]byte{200, 1, 168, 192}, // 192.168.1.200 reversed
|
|
name: "Server2",
|
|
channelCount: 1,
|
|
channelPorts: []uint16{54010},
|
|
},
|
|
}
|
|
|
|
serverData := buildServerData(entries)
|
|
|
|
result, err := parseServerEntries(serverData, 2)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(result) != 2 {
|
|
t.Fatalf("entry count: got %d, want 2", len(result))
|
|
}
|
|
|
|
if result[0].Port != 54001 {
|
|
t.Errorf("entry[0].Port: got %d, want 54001", result[0].Port)
|
|
}
|
|
if result[0].Name != "Server1 ch1" {
|
|
t.Errorf("entry[0].Name: got %q, want %q", result[0].Name, "Server1 ch1")
|
|
}
|
|
if result[1].Port != 54010 {
|
|
t.Errorf("entry[1].Port: got %d, want 54010", result[1].Port)
|
|
}
|
|
if result[1].Name != "Server2 ch1" {
|
|
t.Errorf("entry[1].Name: got %q, want %q", result[1].Name, "Server2 ch1")
|
|
}
|
|
}
|