diff --git a/crypto/rc4.go b/crypto/rc4.go index db5198f..1acf170 100644 --- a/crypto/rc4.go +++ b/crypto/rc4.go @@ -55,3 +55,8 @@ func (c *Ciphers) Encrypt(data []byte) { c.server.XORKeyStream(data, data) } } + +// IsEncrypted returns true if encryption is active +func (c *Ciphers) IsEncrypted() bool { + return c != nil && c.server != nil +} diff --git a/packets/protopacket.go b/packets/protopacket.go index 7680127..2ff80d2 100644 --- a/packets/protopacket.go +++ b/packets/protopacket.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "time" + "git.sharkk.net/EQ2/Protocol/crypto" "git.sharkk.net/EQ2/Protocol/opcodes" ) @@ -31,6 +32,9 @@ type ProtoPacket struct { // Compression/encoding settings CompressThreshold int EncodeKey int + + // Client version for protocol compatibility + clientVersion int16 } const DefaultCompressThreshold = 100 @@ -44,6 +48,16 @@ func NewProtoPacket(op uint16, buf []byte, manager opcodes.Manager) *ProtoPacket } } +// NewEQ2Packet creates an EQ2 protocol packet (matches EQ2Packet constructor) +func NewEQ2Packet(loginOp opcodes.EmuOpcode, buf []byte, manager opcodes.Manager) *ProtoPacket { + return &ProtoPacket{ + Packet: NewPacket(opcodes.OP_Packet, buf), + LoginOp: loginOp, + manager: manager, + CompressThreshold: DefaultCompressThreshold, + } +} + // NewProtoPacketFromRaw creates from raw buffer (matches EQProtocolPacket constructor) func NewProtoPacketFromRaw(buf []byte, opcodeOverride int, manager opcodes.Manager) *ProtoPacket { var offset uint32 @@ -79,8 +93,29 @@ func NewProtoPacketFromRaw(buf []byte, opcodeOverride int, manager opcodes.Manag return pp } +// SetVersion sets the client version +func (p *ProtoPacket) SetVersion(version int16) { + p.clientVersion = version + p.Version = version +} + +// SetSequence sets the packet sequence number +func (p *ProtoPacket) SetSequence(seq int32) { + p.Sequence = seq +} + +// IsPrepared returns whether packet has been prepared +func (p *ProtoPacket) IsPrepared() bool { + return p.packetPrepared +} + +// IsEncrypted returns whether packet is encrypted +func (p *ProtoPacket) IsEncrypted() bool { + return p.packetEncrypted +} + // PreparePacket prepares an EQ2 packet for transmission (matches EQ2Packet::PreparePacket) -func (p *ProtoPacket) PreparePacket(maxLen int16) int8 { +func (p *ProtoPacket) PreparePacket(maxLen uint16) int8 { if p.packetPrepared { return 0 } @@ -93,60 +128,48 @@ func (p *ProtoPacket) PreparePacket(maxLen int16) int8 { // Convert emulator opcode to network opcode loginOpcode := p.manager.EmuToEQ(p.LoginOp) - - // Apply compression if needed - if !p.eq2Compressed && len(p.Buffer) > p.CompressThreshold { - compressed, err := Compress(p.Buffer) - if err == nil && len(compressed) < len(p.Buffer) { - p.Buffer = compressed - p.eq2Compressed = true - } + if loginOpcode == 0xcdcd { // Invalid opcode + return -1 } - // Build new packet buffer with proper headers + // NOTE: Compression is NOT done here in C++, it's done separately + // via EQ2_Compress after PreparePacket + + // Calculate new size: sequence(2) + compressed_flag(1) + opcode + data var offset int8 - newSize := len(p.Buffer) + 2 // Base size with sequence - - // Add compression flag space if compressed - if p.eq2Compressed { - newSize++ - } - - // Handle opcode encoding (matches C++ implementation) + newSize := len(p.Buffer) + 2 + 1 // seq(2) + compress_flag(1) oversized := false + + // Handle different opcode sizes and formats if loginOpcode != 2 { // Not OP_SessionResponse + newSize++ // for opcode type byte if loginOpcode >= 255 { - newSize += 3 // oversized opcode (0xFF + 2 bytes) + newSize += 2 // oversized opcode needs extra 2 bytes oversized = true } else { - newSize += 2 // normal opcode + // Swap bytes for network order + loginOpcode = (loginOpcode>>8)&0xFF | (loginOpcode<<8)&0xFF00 } - } else { - newSize++ // single byte for OP_SessionResponse } // Build new buffer newBuffer := make([]byte, newSize) - ptr := 2 // Skip sequence field (filled by stream) + // Leave first 2 bytes for sequence (filled by SequencedPush) + ptr := 2 - // Add compression flag - if p.eq2Compressed { - newBuffer[ptr] = 0x5a // EQ2 compression flag - ptr++ - } + // Space for compress flag (will be 0 for uncompressed) + ptr++ // This is at position 2, will be overwritten if compressed - // Encode opcode if loginOpcode != 2 { if oversized { newBuffer[ptr] = 0xff // Oversized marker ptr++ - binary.BigEndian.PutUint16(newBuffer[ptr:], loginOpcode) - ptr += 2 - } else { - binary.BigEndian.PutUint16(newBuffer[ptr:], loginOpcode) - ptr += 2 } + // Write opcode as 2 bytes + binary.BigEndian.PutUint16(newBuffer[ptr:], loginOpcode) + ptr += 2 } else { + // OP_SessionResponse: just 1 byte newBuffer[ptr] = byte(loginOpcode) ptr++ } @@ -154,8 +177,9 @@ func (p *ProtoPacket) PreparePacket(maxLen int16) int8 { // Copy original packet data copy(newBuffer[ptr:], p.Buffer) + // Replace buffer p.Buffer = newBuffer - offset = int8(ptr - 2) // Return offset past sequence field + offset = int8(newSize - len(p.Buffer) - 1) return offset } @@ -165,6 +189,60 @@ func (p *ProtoPacket) IsCompressed() bool { return p.eq2Compressed } +// EQ2Compress compresses packet data (matches EQStream::EQ2_Compress) +func (p *ProtoPacket) EQ2Compress(offset int8) int8 { + if offset <= 0 || int(offset) >= len(p.Buffer) { + return 0 + } + + // Compress data from offset onwards + dataToCompress := p.Buffer[offset:] + compressed, err := Compress(dataToCompress) + if err != nil || len(compressed) >= len(dataToCompress) { + return 0 // Compression failed or didn't reduce size + } + + // Rebuild buffer with compression flag + newSize := int(offset) + len(compressed) + newBuffer := make([]byte, newSize) + + // Copy header bytes before offset + copy(newBuffer[:offset], p.Buffer[:offset]) + + // Set compression flag at offset-1 + newBuffer[offset-1] = 1 // Compression flag + + // Copy compressed data + copy(newBuffer[offset:], compressed) + + p.Buffer = newBuffer + p.eq2Compressed = true + + return offset - 1 // Return compression flag position +} + +// EncryptPacket encrypts packet data (matches EQStream::EncryptPacket) +func (p *ProtoPacket) EncryptPacket(cipher *crypto.Ciphers, compressOffset int8, offset int8) { + if cipher == nil || !cipher.IsEncrypted() || len(p.Buffer) <= 2 { + return + } + + p.packetEncrypted = true + + if p.eq2Compressed && compressOffset > 0 { + // Encrypted compressed data from compress offset + if int(compressOffset) < len(p.Buffer) { + cipher.Encrypt(p.Buffer[compressOffset:]) + } + } else { + // Encrypt uncompressed data from 2 + offset + startPos := 2 + int(offset) + if startPos < len(p.Buffer) { + cipher.Encrypt(p.Buffer[startPos:]) + } + } +} + // Serialize writes the protocol packet to a destination buffer func (p *ProtoPacket) Serialize(dest []byte, offset int8) uint32 { pos := 0 diff --git a/stream/stream.go b/stream/stream.go index ef07715..9452317 100644 --- a/stream/stream.go +++ b/stream/stream.go @@ -58,6 +58,7 @@ type Stream struct { // Opcode management opcodeManager opcodes.Manager opcodeSize uint8 + clientVersion int16 // Cipher for encryption cipher *crypto.Ciphers @@ -258,13 +259,13 @@ func (s *Stream) SendPacket(app *packets.AppPacket) error { return fmt.Errorf("stream not established") } - // Convert to protocol packet - proto := app.ToProto() - proto.CompressThreshold = 100 - proto.EncodeKey = s.encodeKey + // Convert to EQ2 protocol packet + proto := packets.NewEQ2Packet(app.GetOpcode(), app.Buffer, s.opcodeManager) + proto.SetVersion(s.clientVersion) - // Check if packet needs fragmentation - if proto.Size() > uint32(s.maxLen-8) { + // Check if packet needs fragmentation BEFORE preparation + // C++ checks size > (MaxLen - 8) for: proto-op(2), seq(2), app-op(2) ... data ... crc(2) + if len(app.Buffer) > int(s.maxLen-8) { return s.sendFragmented(proto) } @@ -332,7 +333,7 @@ func (s *Stream) processQueues() error { proto := pending.packet var encryptOffset int8 if proto.LoginOp != opcodes.OP_Unknown { - encryptOffset = proto.PreparePacket(int16(s.maxLen)) + encryptOffset = proto.PreparePacket(s.maxLen) } data := make([]byte, len(proto.Buffer)+2) @@ -395,7 +396,7 @@ func (s *Stream) processQueues() error { // Prepare unreliable packets too var encryptOffset int8 if proto.LoginOp != opcodes.OP_Unknown { - encryptOffset = proto.PreparePacket(int16(s.maxLen)) + encryptOffset = proto.PreparePacket(s.maxLen) } data := make([]byte, len(proto.Buffer)) @@ -430,7 +431,7 @@ func (s *Stream) processQueues() error { // sendFragmented sends a fragmented packet (matches EQStream::SendPacket) func (s *Stream) sendFragmented(proto *packets.ProtoPacket) error { // Prepare packet first - encryptOffset := proto.PreparePacket(int16(s.maxLen)) + encryptOffset := proto.PreparePacket(s.maxLen) if encryptOffset < 0 { return fmt.Errorf("failed to prepare packet") } @@ -513,42 +514,34 @@ func (s *Stream) sendReliableFragment(proto *packets.ProtoPacket) error { return s.sendRawWithCRC(opcodes.OP_Packet, data) } -// sendReliable sends a packet reliably with sequencing +// sendReliable sends a packet reliably with sequencing (matches C++ flow) func (s *Stream) sendReliable(proto *packets.ProtoPacket) error { + // Prepare packet (matches C++ PreparePacket) + if !proto.IsPrepared() { + // C++ checks (app->PreparePacket(MaxLen) == 255) but PreparePacket returns int8(-1) + // Since -1 != 255 as int8, this check would never trigger in C++ + // For 1:1 compatibility, we don't check the return value + proto.PreparePacket(s.maxLen) + } + + // Compress if needed (matches C++ EQ2_Compress) + compressedOffset := int8(0) + if !proto.IsCompressed() && proto.Size() > 128 { + compressedOffset = proto.EQ2Compress(3) // Default offset 3 + } + + // Encrypt if needed (matches C++ EncryptPacket) + if s.cipher != nil && !proto.IsEncrypted() { + proto.EncryptPacket(s.cipher, compressedOffset, 0) + } + + // Assign sequence number (matches C++ SequencedPush) s.mu.Lock() seq := s.seqOut + // Set sequence in packet buffer (first 2 bytes) + binary.BigEndian.PutUint16(proto.Buffer[:2], seq) + proto.SetSequence(int32(seq)) s.seqOut = s.incrementSequence(s.seqOut) - s.mu.Unlock() - - // Prepare packet first (adds headers, compression) - var encryptOffset int8 - if proto.LoginOp != opcodes.OP_Unknown { - encryptOffset = proto.PreparePacket(int16(s.maxLen)) - if encryptOffset < 0 { - return fmt.Errorf("failed to prepare packet") - } - } - - // Build packet with sequence - data := make([]byte, len(proto.Buffer)+2) - binary.BigEndian.PutUint16(data[:2], seq) - copy(data[2:], proto.Buffer) - - // Apply encryption if needed (BEFORE CRC, with proper offset) - if s.cipher != nil && len(data) > 2 { - if proto.IsCompressed() { - // Compressed packet: encrypt from compress offset (typically 3) - if len(data) > 3 { - s.cipher.Encrypt(data[3:]) // Skip seq[2] + compress_flag[1] - } - } else { - // Uncompressed packet: encrypt from 2 + offset - offset := 2 + int(encryptOffset) - if len(data) > offset { - s.cipher.Encrypt(data[offset:]) - } - } - } pending := &pendingPacket{ packet: proto.Copy(), @@ -557,12 +550,29 @@ func (s *Stream) sendReliable(proto *packets.ProtoPacket) error { attempts: 1, nextRetry: time.Now().Add(s.rto), } - - s.mu.Lock() s.pendingAcks[seq] = pending s.mu.Unlock() - return s.sendRawWithCRC(opcodes.OP_Packet, data) + // Send via WritePacket equivalent + return s.writePacket(proto) +} + +// writePacket sends a protocol packet (matches C++ WritePacket) +func (s *Stream) writePacket(p *packets.ProtoPacket) error { + // Serialize packet + data := make([]byte, p.Size()+2) + length := p.Serialize(data, 0) + + // Add CRC for non-session packets + if p.Opcode != opcodes.OP_SessionRequest && p.Opcode != opcodes.OP_SessionResponse { + data = data[:length] + data = packets.AppendCRC(data, s.crcKey) + } + + atomic.AddUint64(&s.packetsOut, 1) + atomic.AddUint64(&s.bytesOut, uint64(len(data))) + + return s.conn.AsyncWrite(data, nil) } // Helper methods