mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
Wire ExcludeOpcodes config into RecordingConn so configured opcodes (e.g. ping, nop, position) are filtered at record time. Add padded metadata with in-place PatchMetadata to populate CharID/UserID after login. Implement --mode replay using protbot's encrypted connection with timing-aware packet sending, auto-ping response, concurrent S→C collection, and byte-level payload diff reporting.
313 lines
8.6 KiB
Go
313 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"erupe-ce/network/pcap"
|
|
)
|
|
|
|
func createTestCapture(t *testing.T, records []pcap.PacketRecord) string {
|
|
t.Helper()
|
|
f, err := os.CreateTemp(t.TempDir(), "test-*.mhfr")
|
|
if err != nil {
|
|
t.Fatalf("CreateTemp: %v", err)
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
hdr := pcap.FileHeader{
|
|
Version: pcap.FormatVersion,
|
|
ServerType: pcap.ServerTypeChannel,
|
|
ClientMode: 40,
|
|
SessionStartNs: 1000000000,
|
|
}
|
|
meta := pcap.SessionMetadata{Host: "127.0.0.1", Port: 54001}
|
|
|
|
w, err := pcap.NewWriter(f, hdr, meta)
|
|
if err != nil {
|
|
t.Fatalf("NewWriter: %v", err)
|
|
}
|
|
for _, r := range records {
|
|
if err := w.WritePacket(r); err != nil {
|
|
t.Fatalf("WritePacket: %v", err)
|
|
}
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
t.Fatalf("Flush: %v", err)
|
|
}
|
|
return f.Name()
|
|
}
|
|
|
|
func TestRunDump(t *testing.T) {
|
|
path := createTestCapture(t, []pcap.PacketRecord{
|
|
{TimestampNs: 1000000100, Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13}},
|
|
{TimestampNs: 1000000200, Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xFF}},
|
|
})
|
|
// Just verify it doesn't error.
|
|
if err := runDump(path); err != nil {
|
|
t.Fatalf("runDump: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunStats(t *testing.T) {
|
|
path := createTestCapture(t, []pcap.PacketRecord{
|
|
{TimestampNs: 1000000100, Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13}},
|
|
{TimestampNs: 1000000200, Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xFF}},
|
|
{TimestampNs: 1000000300, Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13, 0xAA}},
|
|
})
|
|
if err := runStats(path); err != nil {
|
|
t.Fatalf("runStats: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunStatsEmpty(t *testing.T) {
|
|
path := createTestCapture(t, nil)
|
|
if err := runStats(path); err != nil {
|
|
t.Fatalf("runStats empty: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunJSON(t *testing.T) {
|
|
path := createTestCapture(t, []pcap.PacketRecord{
|
|
{TimestampNs: 1000000100, Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13}},
|
|
})
|
|
// Capture stdout.
|
|
old := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
if err := runJSON(path); err != nil {
|
|
os.Stdout = old
|
|
t.Fatalf("runJSON: %v", err)
|
|
}
|
|
|
|
_ = w.Close()
|
|
os.Stdout = old
|
|
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
if buf.Len() == 0 {
|
|
t.Error("runJSON produced no output")
|
|
}
|
|
// Should be valid JSON containing "packets".
|
|
if !bytes.Contains(buf.Bytes(), []byte(`"packets"`)) {
|
|
t.Error("runJSON output missing 'packets' key")
|
|
}
|
|
}
|
|
|
|
func TestComparePackets(t *testing.T) {
|
|
expected := []pcap.PacketRecord{
|
|
{Direction: pcap.DirClientToServer, Opcode: 0x0013, Payload: []byte{0x00, 0x13}},
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xAA}},
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0061, Payload: []byte{0x00, 0x61}},
|
|
}
|
|
actual := []pcap.PacketRecord{
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xBB, 0xCC}}, // size diff
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0099, Payload: []byte{0x00, 0x99}}, // opcode mismatch
|
|
}
|
|
|
|
diffs := ComparePackets(expected, actual)
|
|
if len(diffs) != 2 {
|
|
t.Fatalf("expected 2 diffs, got %d", len(diffs))
|
|
}
|
|
|
|
// First diff: size delta.
|
|
if diffs[0].SizeDelta != 1 {
|
|
t.Errorf("diffs[0] SizeDelta = %d, want 1", diffs[0].SizeDelta)
|
|
}
|
|
|
|
// Second diff: opcode mismatch.
|
|
if !diffs[1].OpcodeMismatch {
|
|
t.Error("diffs[1] expected OpcodeMismatch=true")
|
|
}
|
|
}
|
|
|
|
func TestComparePacketsMissingResponse(t *testing.T) {
|
|
expected := []pcap.PacketRecord{
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12}},
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0061, Payload: []byte{0x00, 0x61}},
|
|
}
|
|
actual := []pcap.PacketRecord{
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12}},
|
|
}
|
|
|
|
diffs := ComparePackets(expected, actual)
|
|
if len(diffs) != 1 {
|
|
t.Fatalf("expected 1 diff, got %d", len(diffs))
|
|
}
|
|
if diffs[0].Actual != nil {
|
|
t.Error("expected nil Actual for missing response")
|
|
}
|
|
}
|
|
|
|
func TestComparePacketsPayloadDiff(t *testing.T) {
|
|
expected := []pcap.PacketRecord{
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xAA, 0xBB}},
|
|
}
|
|
actual := []pcap.PacketRecord{
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xCC, 0xBB}},
|
|
}
|
|
|
|
diffs := ComparePackets(expected, actual)
|
|
if len(diffs) != 1 {
|
|
t.Fatalf("expected 1 diff, got %d", len(diffs))
|
|
}
|
|
if len(diffs[0].PayloadDiffs) != 1 {
|
|
t.Fatalf("expected 1 payload diff, got %d", len(diffs[0].PayloadDiffs))
|
|
}
|
|
bd := diffs[0].PayloadDiffs[0]
|
|
if bd.Offset != 2 || bd.Expected != 0xAA || bd.Actual != 0xCC {
|
|
t.Errorf("ByteDiff = {Offset:%d, Expected:0x%02X, Actual:0x%02X}, want {2, 0xAA, 0xCC}",
|
|
bd.Offset, bd.Expected, bd.Actual)
|
|
}
|
|
}
|
|
|
|
func TestComparePacketsIdentical(t *testing.T) {
|
|
records := []pcap.PacketRecord{
|
|
{Direction: pcap.DirServerToClient, Opcode: 0x0012, Payload: []byte{0x00, 0x12, 0xAA}},
|
|
}
|
|
diffs := ComparePackets(records, records)
|
|
if len(diffs) != 0 {
|
|
t.Errorf("expected 0 diffs for identical packets, got %d", len(diffs))
|
|
}
|
|
}
|
|
|
|
func TestPacketDiffString(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
diff PacketDiff
|
|
contains string
|
|
}{
|
|
{
|
|
name: "missing response",
|
|
diff: PacketDiff{
|
|
Index: 0,
|
|
Expected: pcap.PacketRecord{Opcode: 0x0012},
|
|
Actual: nil,
|
|
},
|
|
contains: "no response",
|
|
},
|
|
{
|
|
name: "opcode mismatch",
|
|
diff: PacketDiff{
|
|
Index: 1,
|
|
Expected: pcap.PacketRecord{Opcode: 0x0012},
|
|
Actual: &pcap.PacketRecord{Opcode: 0x0099},
|
|
OpcodeMismatch: true,
|
|
},
|
|
contains: "opcode mismatch",
|
|
},
|
|
{
|
|
name: "size delta",
|
|
diff: PacketDiff{
|
|
Index: 2,
|
|
Expected: pcap.PacketRecord{Opcode: 0x0012},
|
|
Actual: &pcap.PacketRecord{Opcode: 0x0012},
|
|
SizeDelta: 5,
|
|
},
|
|
contains: "size delta",
|
|
},
|
|
{
|
|
name: "payload diffs",
|
|
diff: PacketDiff{
|
|
Index: 3,
|
|
Expected: pcap.PacketRecord{Opcode: 0x0012},
|
|
Actual: &pcap.PacketRecord{Opcode: 0x0012},
|
|
PayloadDiffs: []ByteDiff{
|
|
{Offset: 2, Expected: 0xAA, Actual: 0xBB},
|
|
},
|
|
},
|
|
contains: "byte diff",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := tc.diff.String()
|
|
if !strings.Contains(s, tc.contains) {
|
|
t.Errorf("String() = %q, want it to contain %q", s, tc.contains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunReplayWithMockServer(t *testing.T) {
|
|
// Start a mock TCP server that echoes a response for each received packet.
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("Listen: %v", err)
|
|
}
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
serverDone := make(chan struct{})
|
|
go func() {
|
|
defer close(serverDone)
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() { _ = c.Close() }()
|
|
|
|
// This mock doesn't do Blowfish encryption — it just reads raw and echoes.
|
|
// Since the replay uses protbot's CryptConn (Blowfish), we need a real crypto echo.
|
|
// For a simpler test, just verify the function handles connection errors gracefully.
|
|
// Read a bit and close.
|
|
buf := make([]byte, 1024)
|
|
_, _ = c.Read(buf)
|
|
}()
|
|
|
|
// Create a minimal capture with one C→S packet.
|
|
path := createTestCapture(t, []pcap.PacketRecord{
|
|
{TimestampNs: 1000000100, Direction: pcap.DirClientToServer, Opcode: 0x0013,
|
|
Payload: []byte{0x00, 0x13, 0xDE, 0xAD}},
|
|
})
|
|
|
|
// Run replay — the connection will fail (no Blowfish on mock), but it should not panic.
|
|
err = runReplay(path, ln.Addr().String(), 0)
|
|
// We expect an error or graceful handling since the mock doesn't speak Blowfish.
|
|
// The important thing is no panic.
|
|
_ = err
|
|
}
|
|
|
|
func TestComparePayloads(t *testing.T) {
|
|
a := []byte{0x00, 0x12, 0xAA, 0xBB, 0xCC}
|
|
b := []byte{0x00, 0x12, 0xAA, 0xDD, 0xCC}
|
|
|
|
diffs := comparePayloads(a, b)
|
|
if len(diffs) != 1 {
|
|
t.Fatalf("expected 1 diff, got %d", len(diffs))
|
|
}
|
|
if diffs[0].Offset != 3 {
|
|
t.Errorf("Offset = %d, want 3", diffs[0].Offset)
|
|
}
|
|
}
|
|
|
|
func TestComparePayloadsMaxDiffs(t *testing.T) {
|
|
// All bytes different — should cap at maxPayloadDiffs.
|
|
a := make([]byte, 100)
|
|
b := make([]byte, 100)
|
|
for i := range b {
|
|
b[i] = 0xFF
|
|
}
|
|
|
|
diffs := comparePayloads(a, b)
|
|
if len(diffs) != maxPayloadDiffs {
|
|
t.Errorf("expected %d diffs (capped), got %d", maxPayloadDiffs, len(diffs))
|
|
}
|
|
}
|
|
|
|
func TestBuildPingResponse(t *testing.T) {
|
|
pong := buildPingResponse()
|
|
if len(pong) < 2 {
|
|
t.Fatal("ping response too short")
|
|
}
|
|
opcode := binary.BigEndian.Uint16(pong[:2])
|
|
if opcode != opcodeSysPing {
|
|
t.Errorf("opcode = 0x%04X, want 0x%04X", opcode, opcodeSysPing)
|
|
}
|
|
}
|