diff --git a/compression.go b/compression.go new file mode 100644 index 0000000..bd4350f --- /dev/null +++ b/compression.go @@ -0,0 +1,209 @@ +package eq2net + +import ( + "bytes" + "compress/zlib" + "encoding/binary" + "io" +) + +const ( + // Compression flags + CompressionFlagZlib = 0x5A // Zlib compression + CompressionFlagSimple = 0xA5 // Simple encoding (no actual compression) + + // Compression threshold - packets larger than this use zlib + CompressionThreshold = 30 +) + +// CompressPacket compresses a packet using zlib or simple encoding +func CompressPacket(data []byte) ([]byte, error) { + if len(data) < 2 { + return data, nil + } + + // Determine opcode size + flagOffset := 1 + if data[0] == 0 { + flagOffset = 2 // Two-byte opcode + } + + // Don't compress if too small + if len(data) <= flagOffset { + return data, nil + } + + result := make([]byte, 0, len(data)+1) + + // Copy opcode bytes + result = append(result, data[:flagOffset]...) + + if len(data) > CompressionThreshold { + // Use zlib compression for larger packets + result = append(result, CompressionFlagZlib) + + // Compress the data after opcode + var compressed bytes.Buffer + w := zlib.NewWriter(&compressed) + if _, err := w.Write(data[flagOffset:]); err != nil { + return nil, err + } + if err := w.Close(); err != nil { + return nil, err + } + + result = append(result, compressed.Bytes()...) + } else { + // Use simple encoding for smaller packets + result = append(result, CompressionFlagSimple) + result = append(result, data[flagOffset:]...) + } + + return result, nil +} + +// DecompressPacket decompresses a packet +func DecompressPacket(data []byte) ([]byte, error) { + if len(data) < 3 { + return data, nil + } + + // Determine opcode size and compression flag position + flagOffset := 1 + if data[0] == 0 { + flagOffset = 2 + } + + if len(data) <= flagOffset { + return data, nil + } + + compressionFlag := data[flagOffset] + + // Check compression type + switch compressionFlag { + case CompressionFlagZlib: + // Zlib decompression + result := make([]byte, 0, len(data)*2) + + // Copy opcode + result = append(result, data[:flagOffset]...) + + // Decompress data (skip flag byte) + r, err := zlib.NewReader(bytes.NewReader(data[flagOffset+1:])) + if err != nil { + return nil, err + } + defer r.Close() + + decompressed, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + result = append(result, decompressed...) + return result, nil + + case CompressionFlagSimple: + // Simple encoding - just remove the flag byte + result := make([]byte, 0, len(data)-1) + result = append(result, data[:flagOffset]...) + result = append(result, data[flagOffset+1:]...) + return result, nil + + default: + // No compression + return data, nil + } +} + +// IsCompressed checks if a packet is compressed +func IsCompressed(data []byte) bool { + if len(data) < 2 { + return false + } + + flagOffset := 1 + if data[0] == 0 { + flagOffset = 2 + } + + if len(data) <= flagOffset { + return false + } + + flag := data[flagOffset] + return flag == CompressionFlagZlib || flag == CompressionFlagSimple +} + +// ChatEncode encodes chat data using XOR encryption with rolling key +func ChatEncode(data []byte, encodeKey uint32) []byte { + // Skip certain packet types + if len(data) >= 2 && (data[1] == 0x01 || data[0] == 0x02 || data[0] == 0x1d) { + return data + } + + // Work with data after opcode + if len(data) <= 2 { + return data + } + + result := make([]byte, len(data)) + copy(result[:2], data[:2]) // Copy opcode + + key := encodeKey + offset := 2 + + // Process 4-byte blocks with rolling key + for i := offset; i+4 <= len(data); i += 4 { + block := binary.LittleEndian.Uint32(data[i : i+4]) + encrypted := block ^ key + binary.LittleEndian.PutUint32(result[i:i+4], encrypted) + key = encrypted // Update key with encrypted data + } + + // Handle remaining bytes + keyByte := byte(key & 0xFF) + alignedEnd := offset + ((len(data)-offset)/4)*4 + for i := alignedEnd; i < len(data); i++ { + result[i] = data[i] ^ keyByte + } + + return result +} + +// ChatDecode decodes chat data using XOR encryption with rolling key +func ChatDecode(data []byte, decodeKey uint32) []byte { + // Skip certain packet types + if len(data) >= 2 && (data[1] == 0x01 || data[0] == 0x02 || data[0] == 0x1d) { + return data + } + + // Work with data after opcode + if len(data) <= 2 { + return data + } + + result := make([]byte, len(data)) + copy(result[:2], data[:2]) // Copy opcode + + key := decodeKey + offset := 2 + + // Process 4-byte blocks with rolling key + for i := offset; i+4 <= len(data); i += 4 { + encrypted := binary.LittleEndian.Uint32(data[i : i+4]) + decrypted := encrypted ^ key + binary.LittleEndian.PutUint32(result[i:i+4], decrypted) + key = encrypted // Update key with encrypted data (before decryption) + } + + // Handle remaining bytes + keyByte := byte(key & 0xFF) + alignedEnd := offset + ((len(data)-offset)/4)*4 + for i := alignedEnd; i < len(data); i++ { + result[i] = data[i] ^ keyByte + } + + return result +} \ No newline at end of file diff --git a/crc16.go b/crc16.go new file mode 100644 index 0000000..7b7e47f --- /dev/null +++ b/crc16.go @@ -0,0 +1,93 @@ +package eq2net + +// CRC16 table for CCITT polynomial (0x1021) +var crc16Table [256]uint16 + +func init() { + // Initialize CRC16 lookup table + for i := 0; i < 256; i++ { + crc := uint16(i << 8) + for j := 0; j < 8; j++ { + if (crc & 0x8000) != 0 { + crc = (crc << 1) ^ 0x1021 + } else { + crc = crc << 1 + } + } + crc16Table[i] = crc + } +} + +// CRC16 calculates the CRC16-CCITT checksum with a key +func CRC16(data []byte, length int, key uint32) uint16 { + if length <= 0 || len(data) < length { + return 0 + } + + // Mix the key into initial CRC value + crc := uint16(0xFFFF) + keyBytes := []byte{ + byte(key), + byte(key >> 8), + byte(key >> 16), + byte(key >> 24), + } + + // Process key bytes first + for _, b := range keyBytes { + tableIndex := (uint8(crc>>8) ^ b) & 0xFF + crc = (crc << 8) ^ crc16Table[tableIndex] + } + + // Process data + for i := 0; i < length; i++ { + tableIndex := (uint8(crc>>8) ^ data[i]) & 0xFF + crc = (crc << 8) ^ crc16Table[tableIndex] + } + + return crc ^ 0xFFFF +} + +// ValidateCRC checks if a packet has a valid CRC +func ValidateCRC(buffer []byte, key uint32) bool { + if len(buffer) < 3 { + return false + } + + // Check for CRC-exempt packets + if len(buffer) >= 2 && buffer[0] == 0x00 { + switch buffer[1] { + case byte(OPSessionRequest), byte(OPSessionResponse), byte(OPOutOfSession): + return true // Session packets don't require CRC + } + } + + // Check for combined application packets (also CRC-exempt) + if len(buffer) >= 4 && buffer[2] == 0x00 && buffer[3] == 0x19 { + return true + } + + // Calculate CRC for the packet (excluding last 2 CRC bytes) + dataLen := len(buffer) - 2 + calculatedCRC := CRC16(buffer, dataLen, key) + + // Extract packet CRC (big-endian in last 2 bytes) + packetCRC := uint16(buffer[dataLen])<<8 | uint16(buffer[dataLen+1]) + + // Valid if no CRC present (0) or CRCs match + return packetCRC == 0 || calculatedCRC == packetCRC +} + +// AppendCRC adds CRC to the end of a packet +func AppendCRC(buffer []byte, key uint32) []byte { + // Calculate CRC for current buffer + crc := CRC16(buffer, len(buffer), key) + + // Append CRC in big-endian format + result := make([]byte, len(buffer)+2) + copy(result, buffer) + result[len(buffer)] = byte(crc >> 8) + result[len(buffer)+1] = byte(crc) + + return result +} \ No newline at end of file diff --git a/go.mod b/go.mod index 296403e..c8387b4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module git.sharkk.net/EQ2/Protocol -go 1.25.0 +go 1.21 diff --git a/packet.go b/packet.go new file mode 100644 index 0000000..1b1db0a --- /dev/null +++ b/packet.go @@ -0,0 +1,299 @@ +// Package eq2net implements the EverQuest 2 network protocol +package eq2net + +import ( + "encoding/binary" + "fmt" + "net" + "time" +) + +// Protocol opcodes for low-level packet control +const ( + OPSessionRequest = 0x0001 + OPSessionResponse = 0x0002 + OPCombined = 0x0003 + OPSessionDisconnect = 0x0005 + OPKeepAlive = 0x0006 + OPSessionStatRequest = 0x0007 + OPSessionStatResponse = 0x0008 + OPPacket = 0x0009 + OPFragment = 0x000D + OPOutOfOrderAck = 0x0011 + OPAck = 0x0015 + OPAppCombined = 0x0019 + OPOutOfSession = 0x001D +) + +// EQPacket is the base packet type for all EverQuest packets +type EQPacket struct { + // Core packet data + Buffer []byte + Size uint32 + Opcode uint16 + + // Network information + SrcIP net.IP + DstIP net.IP + SrcPort uint16 + DstPort uint16 + + // Metadata + Priority uint32 + Timestamp time.Time + Version int16 +} + +// NewEQPacket creates a new packet with the specified opcode and data +func NewEQPacket(opcode uint16, data []byte) *EQPacket { + p := &EQPacket{ + Opcode: opcode, + Timestamp: time.Now(), + } + + if len(data) > 0 { + p.Buffer = make([]byte, len(data)) + copy(p.Buffer, data) + p.Size = uint32(len(data)) + } + + return p +} + +// TotalSize returns the total packet size including opcode +func (p *EQPacket) TotalSize() uint32 { + return p.Size + 2 // +2 for opcode +} + +// SetNetworkInfo sets the source and destination network information +func (p *EQPacket) SetNetworkInfo(srcIP net.IP, srcPort uint16, dstIP net.IP, dstPort uint16) { + p.SrcIP = srcIP + p.SrcPort = srcPort + p.DstIP = dstIP + p.DstPort = dstPort +} + +// CopyInfo copies network and timing information from another packet +func (p *EQPacket) CopyInfo(other *EQPacket) { + p.SrcIP = other.SrcIP + p.SrcPort = other.SrcPort + p.DstIP = other.DstIP + p.DstPort = other.DstPort + p.Timestamp = other.Timestamp + p.Version = other.Version +} + +// EQProtocolPacket handles low-level protocol operations +type EQProtocolPacket struct { + *EQPacket + + // Protocol state flags + Compressed bool + Prepared bool + Encrypted bool + Acked bool + + // Reliability tracking + SentTime time.Time + AttemptCount uint8 + Sequence uint32 +} + +// NewEQProtocolPacket creates a new protocol packet +func NewEQProtocolPacket(opcode uint16, data []byte) *EQProtocolPacket { + return &EQProtocolPacket{ + EQPacket: NewEQPacket(opcode, data), + } +} + +// NewEQProtocolPacketFromBuffer creates a protocol packet from raw buffer +func NewEQProtocolPacketFromBuffer(buffer []byte, opcodeOverride int) (*EQProtocolPacket, error) { + if len(buffer) < 2 { + return nil, fmt.Errorf("buffer too small for opcode") + } + + var opcode uint16 + var dataOffset int + + if opcodeOverride >= 0 { + opcode = uint16(opcodeOverride) + dataOffset = 0 + } else { + opcode = binary.BigEndian.Uint16(buffer[:2]) + dataOffset = 2 + } + + var data []byte + if len(buffer) > dataOffset { + data = buffer[dataOffset:] + } + + return NewEQProtocolPacket(opcode, data), nil +} + +// Serialize writes the protocol packet to a byte buffer +func (p *EQProtocolPacket) Serialize(offset int) []byte { + // Allocate buffer for opcode + data + result := make([]byte, 2+len(p.Buffer)-offset) + + // Write opcode (big-endian) + if p.Opcode > 0xFF { + binary.BigEndian.PutUint16(result[0:2], p.Opcode) + } else { + result[0] = 0 + result[1] = byte(p.Opcode) + } + + // Copy packet data + if len(p.Buffer) > offset { + copy(result[2:], p.Buffer[offset:]) + } + + return result +} + +// IsProtocolPacket checks if the opcode is a valid protocol packet +func IsProtocolPacket(opcode uint16) bool { + switch opcode { + case OPSessionRequest, OPSessionDisconnect, OPKeepAlive, + OPSessionStatResponse, OPPacket, OPCombined, OPFragment, + OPAck, OPOutOfOrderAck, OPOutOfSession: + return true + default: + return false + } +} + +// Copy creates a deep copy of the protocol packet +func (p *EQProtocolPacket) Copy() *EQProtocolPacket { + newPacket := &EQProtocolPacket{ + EQPacket: NewEQPacket(p.Opcode, p.Buffer), + Compressed: p.Compressed, + Prepared: p.Prepared, + Encrypted: p.Encrypted, + Acked: p.Acked, + SentTime: p.SentTime, + AttemptCount: p.AttemptCount, + Sequence: p.Sequence, + } + newPacket.CopyInfo(p.EQPacket) + return newPacket +} + +// EQApplicationPacket represents high-level application packets +type EQApplicationPacket struct { + *EQPacket + + // Cached emulator opcode + EmuOpcode uint16 + + // Opcode size (1 or 2 bytes) + OpcodeSize uint8 +} + +// DefaultOpcodeSize is the default size for application opcodes +var DefaultOpcodeSize uint8 = 2 + +// NewEQApplicationPacket creates a new application packet +func NewEQApplicationPacket(opcode uint16, data []byte) *EQApplicationPacket { + return &EQApplicationPacket{ + EQPacket: NewEQPacket(opcode, data), + EmuOpcode: opcode, + OpcodeSize: DefaultOpcodeSize, + } +} + +// Serialize writes the application packet to a byte buffer +func (p *EQApplicationPacket) Serialize() []byte { + opcodeBytes := p.OpcodeSize + + // Special handling for opcodes with low byte = 0x00 + if p.OpcodeSize == 2 && (p.Opcode&0x00FF) == 0 { + opcodeBytes = 3 + } + + result := make([]byte, uint32(opcodeBytes)+p.Size) + + if p.OpcodeSize == 1 { + result[0] = byte(p.Opcode) + } else { + if (p.Opcode & 0x00FF) == 0 { + result[0] = 0 + binary.BigEndian.PutUint16(result[1:3], p.Opcode) + } else { + binary.BigEndian.PutUint16(result[0:2], p.Opcode) + } + } + + // Copy data after opcode + if p.Size > 0 { + copy(result[opcodeBytes:], p.Buffer) + } + + return result +} + +// Copy creates a deep copy of the application packet +func (p *EQApplicationPacket) Copy() *EQApplicationPacket { + newPacket := &EQApplicationPacket{ + EQPacket: NewEQPacket(p.Opcode, p.Buffer), + EmuOpcode: p.EmuOpcode, + OpcodeSize: p.OpcodeSize, + } + newPacket.CopyInfo(p.EQPacket) + return newPacket +} + +// PacketCombiner handles combining multiple packets for efficient transmission +type PacketCombiner struct { + maxSize int +} + +// NewPacketCombiner creates a new packet combiner with max size limit +func NewPacketCombiner(maxSize int) *PacketCombiner { + return &PacketCombiner{maxSize: maxSize} +} + +// CombineProtocolPackets combines multiple protocol packets into one +func (c *PacketCombiner) CombineProtocolPackets(packets []*EQProtocolPacket) *EQProtocolPacket { + if len(packets) == 0 { + return nil + } + + if len(packets) == 1 { + return packets[0] + } + + // Calculate total size needed + totalSize := 0 + for _, p := range packets { + totalSize += 1 + int(p.TotalSize()) // 1 byte for size prefix + } + + if totalSize > c.maxSize { + return nil // Too large to combine + } + + // Build combined packet buffer + buffer := make([]byte, totalSize) + offset := 0 + + for _, p := range packets { + // Write size prefix + buffer[offset] = byte(p.TotalSize()) + offset++ + + // Serialize packet + serialized := p.Serialize(0) + copy(buffer[offset:], serialized) + offset += len(serialized) + } + + // Create combined packet + combined := NewEQProtocolPacket(OPCombined, buffer) + if len(packets) > 0 { + combined.CopyInfo(packets[0].EQPacket) + } + + return combined +} \ No newline at end of file diff --git a/packet_test.go b/packet_test.go new file mode 100644 index 0000000..639d694 --- /dev/null +++ b/packet_test.go @@ -0,0 +1,207 @@ +package eq2net + +import ( + "bytes" + "testing" +) + +func TestEQPacket(t *testing.T) { + data := []byte("Hello, World!") + packet := NewEQPacket(OPPacket, data) + + if packet.Opcode != OPPacket { + t.Errorf("Expected opcode %04x, got %04x", OPPacket, packet.Opcode) + } + + if packet.Size != uint32(len(data)) { + t.Errorf("Expected size %d, got %d", len(data), packet.Size) + } + + if !bytes.Equal(packet.Buffer, data) { + t.Errorf("Buffer mismatch") + } +} + +func TestProtocolPacketSerialization(t *testing.T) { + data := []byte("Test Data") + packet := NewEQProtocolPacket(OPPacket, data) + + serialized := packet.Serialize(0) + + // Check opcode (first 2 bytes) + if len(serialized) < 2 { + t.Fatal("Serialized packet too small") + } + + // OPPacket = 0x0009, should be [0x00, 0x09] in big-endian + if serialized[0] != 0x00 || serialized[1] != 0x09 { + t.Errorf("Opcode not serialized correctly: %02x %02x", serialized[0], serialized[1]) + } + + // Check data + if !bytes.Equal(serialized[2:], data) { + t.Error("Data not serialized correctly") + } +} + +func TestCRC16(t *testing.T) { + tests := []struct { + data []byte + key uint32 + }{ + {[]byte{0x00, 0x09, 0x00, 0x00, 0x00}, 0x12345678}, + {[]byte{0x00, 0x01}, 0}, + {[]byte("Hello"), 0}, + } + + for _, tt := range tests { + // Just test that CRC16 produces consistent results + got1 := CRC16(tt.data, len(tt.data), tt.key) + got2 := CRC16(tt.data, len(tt.data), tt.key) + if got1 != got2 { + t.Errorf("CRC16 not consistent: %04x != %04x", got1, got2) + } + // Test that different keys produce different CRCs + if tt.key != 0 { + got3 := CRC16(tt.data, len(tt.data), 0) + if got1 == got3 { + t.Errorf("Different keys should produce different CRCs") + } + } + } +} + +func TestValidateCRC(t *testing.T) { + // Test session packet (CRC exempt) + sessionPacket := []byte{0x00, byte(OPSessionRequest), 0x00, 0x00} + if !ValidateCRC(sessionPacket, 0) { + t.Error("Session packet should be CRC exempt") + } + + // Test packet with valid CRC + data := []byte{0x00, 0x09, 0x48, 0x65, 0x6C, 0x6C, 0x6F} // "Hello" + crc := CRC16(data, len(data), 0x1234) + dataWithCRC := append(data, byte(crc>>8), byte(crc)) + + if !ValidateCRC(dataWithCRC, 0x1234) { + t.Error("Packet with valid CRC should validate") + } + + // Test packet with invalid CRC + dataWithCRC[len(dataWithCRC)-1] ^= 0xFF // Corrupt CRC + if ValidateCRC(dataWithCRC, 0x1234) { + t.Error("Packet with invalid CRC should not validate") + } +} + +func TestCompression(t *testing.T) { + // Test simple encoding (small packet) + smallData := []byte{0x00, 0x09, 0x01, 0x02, 0x03} + compressed, err := CompressPacket(smallData) + if err != nil { + t.Fatal(err) + } + + if compressed[2] != CompressionFlagSimple { + t.Errorf("Small packet should use simple encoding, got flag %02x", compressed[2]) + } + + decompressed, err := DecompressPacket(compressed) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(decompressed, smallData) { + t.Error("Decompressed data doesn't match original") + } + + // Test zlib compression (large packet) + largeData := make([]byte, 100) + largeData[0] = 0x00 + largeData[1] = 0x09 + for i := 2; i < len(largeData); i++ { + largeData[i] = byte(i) + } + + compressed, err = CompressPacket(largeData) + if err != nil { + t.Fatal(err) + } + + if compressed[2] != CompressionFlagZlib { + t.Errorf("Large packet should use zlib compression, got flag %02x", compressed[2]) + } + + decompressed, err = DecompressPacket(compressed) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(decompressed, largeData) { + t.Error("Decompressed large data doesn't match original") + } +} + +func TestChatEncryption(t *testing.T) { + // Test chat encoding/decoding + original := []byte{0x00, 0x09, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x21} // "Hello!" + key := uint32(0x12345678) + + encoded := ChatEncode(original, key) + if bytes.Equal(encoded[2:], original[2:]) { + t.Error("Encoded data should differ from original") + } + + decoded := ChatDecode(encoded, key) + if !bytes.Equal(decoded, original) { + t.Errorf("Decoded data doesn't match original\nOriginal: %v\nDecoded: %v", original, decoded) + } + + // Test exempt packet types + exemptPacket := []byte{0x00, 0x01, 0x12, 0x34} + encoded = ChatEncode(exemptPacket, key) + if !bytes.Equal(encoded, exemptPacket) { + t.Error("Exempt packet should not be encoded") + } +} + +func TestPacketCombiner(t *testing.T) { + combiner := NewPacketCombiner(256) + + p1 := NewEQProtocolPacket(OPPacket, []byte{0x01, 0x02}) + p2 := NewEQProtocolPacket(OPAck, []byte{0x03, 0x04}) + p3 := NewEQProtocolPacket(OPKeepAlive, []byte{0x05}) + + combined := combiner.CombineProtocolPackets([]*EQProtocolPacket{p1, p2, p3}) + + if combined == nil { + t.Fatal("Failed to combine packets") + } + + if combined.Opcode != OPCombined { + t.Errorf("Combined packet should have opcode %04x, got %04x", OPCombined, combined.Opcode) + } + + // Verify combined packet structure + buffer := combined.Buffer + offset := 0 + + // First packet + if buffer[offset] != byte(p1.TotalSize()) { + t.Errorf("First packet size incorrect: %d", buffer[offset]) + } + offset++ + offset += int(p1.TotalSize()) + + // Second packet + if buffer[offset] != byte(p2.TotalSize()) { + t.Errorf("Second packet size incorrect: %d", buffer[offset]) + } + offset++ + offset += int(p2.TotalSize()) + + // Third packet + if buffer[offset] != byte(p3.TotalSize()) { + t.Errorf("Third packet size incorrect: %d", buffer[offset]) + } +} \ No newline at end of file