348 lines
9.8 KiB
Go
348 lines
9.8 KiB
Go
package packets
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
|
|
"git.sharkk.net/EQ2/Protocol/crypto"
|
|
"git.sharkk.net/EQ2/Protocol/opcodes"
|
|
)
|
|
|
|
// ValidateCRC validates packet CRC using EQ2's custom CRC16 (matches C++ EQProtocolPacket::ValidateCRC)
|
|
func ValidateCRC(buffer []byte, key uint32) bool {
|
|
if len(buffer) < 2 {
|
|
return false
|
|
}
|
|
|
|
// Session packets are not CRC protected
|
|
if len(buffer) >= 2 && buffer[0] == 0x00 &&
|
|
(buffer[1] == byte(opcodes.OP_SessionRequest) ||
|
|
buffer[1] == byte(opcodes.OP_SessionResponse) ||
|
|
buffer[1] == byte(opcodes.OP_OutOfSession)) {
|
|
return true
|
|
}
|
|
|
|
// Combined application packets are also exempt (OP_AppCombined = 0x0019)
|
|
if len(buffer) >= 4 && buffer[2] == 0x00 && buffer[3] == 0x19 {
|
|
return true
|
|
}
|
|
|
|
// All other packets must have valid CRC
|
|
// Extract CRC from last 2 bytes (network byte order)
|
|
packetCRC := binary.BigEndian.Uint16(buffer[len(buffer)-2:])
|
|
|
|
// Calculate CRC on data portion (excluding CRC bytes)
|
|
data := buffer[:len(buffer)-2]
|
|
calculatedCRC := crypto.CalculateCRC(data, key)
|
|
|
|
// Valid if no CRC present (packetCRC == 0) or CRCs match
|
|
return packetCRC == 0 || calculatedCRC == packetCRC
|
|
}
|
|
|
|
// AppendCRC appends CRC16 to packet buffer using EQ2's custom CRC
|
|
func AppendCRC(buffer []byte, key uint32) []byte {
|
|
crc := crypto.CalculateCRC(buffer, key)
|
|
result := make([]byte, len(buffer)+2)
|
|
copy(result, buffer)
|
|
binary.BigEndian.PutUint16(result[len(buffer):], crc)
|
|
return result
|
|
}
|
|
|
|
// StripCRC removes CRC16 from packet buffer
|
|
func StripCRC(buffer []byte) []byte {
|
|
if len(buffer) < 2 {
|
|
return buffer
|
|
}
|
|
return buffer[:len(buffer)-2]
|
|
}
|
|
|
|
// Compress compresses packet data using zlib or simple encoding (matches C++ EQProtocolPacket::Compress)
|
|
// Uses zlib for packets > 30 bytes, simple encoding for smaller packets
|
|
func Compress(src []byte) ([]byte, error) {
|
|
if len(src) == 0 {
|
|
return src, nil
|
|
}
|
|
|
|
// C++ uses 30 bytes as threshold between simple encoding and zlib
|
|
if len(src) > 30 {
|
|
// Use zlib compression for larger packets
|
|
var buf bytes.Buffer
|
|
|
|
// Write uncompressed length first (4 bytes) - EQ protocol requirement
|
|
if err := binary.Write(&buf, binary.BigEndian, uint32(len(src))); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Compress the data
|
|
w := zlib.NewWriter(&buf)
|
|
if _, err := w.Write(src); err != nil {
|
|
w.Close()
|
|
return nil, err
|
|
}
|
|
|
|
if err := w.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// Use simple encoding for smaller packets (just return data as-is)
|
|
// The 0xa5 flag is added by the caller, not here
|
|
return src, nil
|
|
}
|
|
|
|
// Decompress decompresses packet data using zlib
|
|
// This is called after the compression flag has already been checked and removed
|
|
func Decompress(src []byte) ([]byte, error) {
|
|
if len(src) < 4 {
|
|
return nil, fmt.Errorf("compressed data too small")
|
|
}
|
|
|
|
// Read uncompressed length (first 4 bytes)
|
|
uncompressedLen := binary.BigEndian.Uint32(src[:4])
|
|
|
|
// Sanity check
|
|
if uncompressedLen > MaxPacketSize {
|
|
return nil, fmt.Errorf("uncompressed size %d exceeds max packet size", uncompressedLen)
|
|
}
|
|
|
|
// Create reader for compressed data (skip length prefix)
|
|
r, err := zlib.NewReader(bytes.NewReader(src[4:]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Close()
|
|
|
|
// Read decompressed data
|
|
decompressed := make([]byte, uncompressedLen)
|
|
if _, err := io.ReadFull(r, decompressed); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return decompressed, nil
|
|
}
|
|
|
|
// CompressPacket adds compression with proper flag handling (matches C++ EQProtocolPacket::Compress)
|
|
// Uses zlib (0x5a) for packets > 30 bytes, simple encoding (0xa5) for smaller packets
|
|
func CompressPacket(buffer []byte) ([]byte, error) {
|
|
if len(buffer) < 2 {
|
|
return buffer, nil
|
|
}
|
|
|
|
// Determine flag offset based on opcode size
|
|
flagOffset := 1
|
|
if buffer[0] == 0x00 {
|
|
flagOffset = 2 // Two-byte opcode
|
|
}
|
|
|
|
if len(buffer) <= flagOffset {
|
|
return buffer, nil // Too small to compress
|
|
}
|
|
|
|
// Get data to compress (after opcode)
|
|
dataToCompress := buffer[flagOffset:]
|
|
|
|
// Choose compression method based on size (C++ uses 30 byte threshold)
|
|
if len(dataToCompress) > 30 {
|
|
// Use zlib compression
|
|
compressed, err := Compress(dataToCompress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only use if compression actually reduced size
|
|
if len(compressed) < len(dataToCompress) {
|
|
result := make([]byte, flagOffset+1+len(compressed))
|
|
copy(result[:flagOffset], buffer[:flagOffset]) // Copy opcode
|
|
result[flagOffset] = 0x5a // Zlib flag
|
|
copy(result[flagOffset+1:], compressed) // Compressed data
|
|
return result, nil
|
|
}
|
|
} else {
|
|
// Use simple encoding - just add flag
|
|
result := make([]byte, len(buffer)+1)
|
|
copy(result[:flagOffset], buffer[:flagOffset]) // Copy opcode
|
|
result[flagOffset] = 0xa5 // Simple encoding flag
|
|
copy(result[flagOffset+1:], dataToCompress) // Original data
|
|
return result, nil
|
|
}
|
|
|
|
// No compression if it doesn't help
|
|
return buffer, nil
|
|
}
|
|
|
|
// DecompressPacket handles decompression with flag checking (matches C++ EQProtocolPacket::Decompress)
|
|
// Supports both zlib compression (0x5a) and simple encoding (0xa5)
|
|
func DecompressPacket(buffer []byte) ([]byte, error) {
|
|
if len(buffer) < 3 {
|
|
return buffer, nil // Too small to be compressed
|
|
}
|
|
|
|
// Determine flag offset based on opcode size
|
|
flagOffset := uint32(1)
|
|
if buffer[0] == 0x00 {
|
|
flagOffset = 2 // Two-byte opcode
|
|
}
|
|
|
|
if uint32(len(buffer)) <= flagOffset {
|
|
return buffer, nil // No room for compression flag
|
|
}
|
|
|
|
compressionFlag := buffer[flagOffset]
|
|
|
|
// Check compression type
|
|
if compressionFlag == 0x5a {
|
|
// Zlib compression
|
|
// Decompress data after flag, excluding last 2 CRC bytes
|
|
dataStart := flagOffset + 1
|
|
dataEnd := uint32(len(buffer)) - 2
|
|
if dataEnd <= dataStart {
|
|
return nil, fmt.Errorf("invalid compressed packet size")
|
|
}
|
|
|
|
decompressed, err := Decompress(buffer[dataStart:dataEnd])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("zlib decompression failed: %w", err)
|
|
}
|
|
|
|
// Rebuild packet: opcode + decompressed data + CRC
|
|
result := make([]byte, flagOffset+uint32(len(decompressed))+2)
|
|
copy(result[:flagOffset], buffer[:flagOffset]) // Copy opcode
|
|
copy(result[flagOffset:], decompressed) // Copy decompressed data
|
|
// Copy CRC bytes
|
|
result[len(result)-2] = buffer[len(buffer)-2]
|
|
result[len(result)-1] = buffer[len(buffer)-1]
|
|
|
|
return result, nil
|
|
} else if compressionFlag == 0xa5 {
|
|
// Simple encoding - just remove the encoding flag
|
|
result := make([]byte, len(buffer)-1)
|
|
copy(result[:flagOffset], buffer[:flagOffset]) // Copy opcode
|
|
copy(result[flagOffset:], buffer[flagOffset+1:]) // Skip flag, copy rest
|
|
return result, nil
|
|
}
|
|
|
|
// No compression
|
|
return buffer, nil
|
|
}
|
|
|
|
// ChatEncode encodes chat data using EQ's XOR-based encoding (matches C++ EQProtocolPacket::ChatEncode)
|
|
// Uses 4-byte block XOR with rolling key that updates from encrypted data
|
|
func ChatEncode(buffer []byte, encodeKey int) {
|
|
if len(buffer) <= 2 || encodeKey == 0 {
|
|
return
|
|
}
|
|
|
|
// Skip encoding for certain packet types (matches C++ conditions)
|
|
if buffer[1] == 0x01 || buffer[0] == 0x02 || buffer[0] == 0x1d {
|
|
return
|
|
}
|
|
|
|
// Skip first 2 bytes (opcode)
|
|
data := buffer[2:]
|
|
size := len(data)
|
|
key := int32(encodeKey)
|
|
|
|
// Encode 4-byte blocks with rolling key
|
|
i := 0
|
|
for ; i+4 <= size; i += 4 {
|
|
// Read 4 bytes as int32 (little-endian like C++)
|
|
pt := binary.LittleEndian.Uint32(data[i:i+4])
|
|
encrypted := pt ^ uint32(key)
|
|
key = int32(encrypted) // Update key with encrypted data
|
|
binary.LittleEndian.PutUint32(data[i:i+4], encrypted)
|
|
}
|
|
|
|
// Encode remaining bytes with last key byte
|
|
keyByte := byte(key & 0xFF)
|
|
for ; i < size; i++ {
|
|
data[i] ^= keyByte
|
|
}
|
|
}
|
|
|
|
// ChatDecode decodes chat data using EQ's XOR-based encoding (matches C++ EQProtocolPacket::ChatDecode)
|
|
// Uses 4-byte block XOR with rolling key that updates from encrypted data
|
|
func ChatDecode(buffer []byte, decodeKey int) {
|
|
if len(buffer) <= 2 || decodeKey == 0 {
|
|
return
|
|
}
|
|
|
|
// Skip decoding for certain packet types (matches C++ conditions)
|
|
if buffer[1] == 0x01 || buffer[0] == 0x02 || buffer[0] == 0x1d {
|
|
return
|
|
}
|
|
|
|
// Skip first 2 bytes (opcode)
|
|
data := buffer[2:]
|
|
size := len(data)
|
|
key := int32(decodeKey)
|
|
|
|
// Decode 4-byte blocks with rolling key
|
|
i := 0
|
|
for ; i+4 <= size; i += 4 {
|
|
// Read 4 bytes as int32 (little-endian like C++)
|
|
encrypted := binary.LittleEndian.Uint32(data[i:i+4])
|
|
decrypted := encrypted ^ uint32(key)
|
|
key = int32(encrypted) // Update key with encrypted data (before decryption)
|
|
binary.LittleEndian.PutUint32(data[i:i+4], decrypted)
|
|
}
|
|
|
|
// Decode remaining bytes with last key byte
|
|
keyByte := byte(key & 0xFF)
|
|
for ; i < size; i++ {
|
|
data[i] ^= keyByte
|
|
}
|
|
}
|
|
|
|
// IsChatPacket checks if opcode is a chat-related packet
|
|
func IsChatPacket(opcode uint16) bool {
|
|
chatOpcodes := map[uint16]bool{
|
|
0x0300: true, // OP_ChatMsg
|
|
0x0302: true, // OP_TellMsg
|
|
0x0307: true, // OP_ChatLeaveChannelMsg
|
|
0x0308: true, // OP_ChatTellChannelMsg
|
|
0x0309: true, // OP_ChatTellUserMsg
|
|
0x0e07: true, // OP_GuildsayMsg
|
|
}
|
|
return chatOpcodes[opcode]
|
|
}
|
|
|
|
// longToIP converts uint32 IP to string
|
|
func longToIP(ip uint32) string {
|
|
return fmt.Sprintf("%d.%d.%d.%d",
|
|
byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
|
|
}
|
|
|
|
// IsProtocolPacket checks if buffer contains a valid protocol packet (matches C++ EQProtocolPacket::IsProtocolPacket)
|
|
func IsProtocolPacket(buffer []byte) bool {
|
|
if len(buffer) < 2 {
|
|
return false
|
|
}
|
|
|
|
opcode := binary.BigEndian.Uint16(buffer[:2])
|
|
|
|
// Check against known protocol opcodes
|
|
switch opcode {
|
|
case opcodes.OP_SessionRequest,
|
|
opcodes.OP_SessionResponse,
|
|
opcodes.OP_Combined,
|
|
opcodes.OP_SessionDisconnect,
|
|
opcodes.OP_KeepAlive,
|
|
opcodes.OP_SessionStatRequest,
|
|
opcodes.OP_SessionStatResponse,
|
|
opcodes.OP_Packet,
|
|
opcodes.OP_Fragment,
|
|
opcodes.OP_Ack,
|
|
opcodes.OP_AppCombined,
|
|
opcodes.OP_OutOfOrderAck,
|
|
opcodes.OP_OutOfSession:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|