From 95d6583dfe60f33e9458a1b03922619efc2d03ef Mon Sep 17 00:00:00 2001 From: Graham Date: Tue, 22 Dec 2020 11:04:13 +0000 Subject: [PATCH] Add initial implementation of the RS framing layer I have spent a while thinking about the best way to implement this, especially after I discovered that OSRS treats the packets sent during the handshake stage as normal packets (see share/doc/protocol/login.md for a longer explanation). I have settled on a design where the same Rs2Decoder and Rs2Encoder instances, which deal with decoding packet opcodes and lengths, will remain the same for the entire connection - unless it switches into a mode where a completely different framing scheme is used, such as JAGGRAB and JS5. Rs2Decoder and Rs2Encoder both support optional opcode encryption. The initial cipher is set to NopStreamCipher. It will be set to an IsaacRandom instance during the login process. As the scrambled opcodes used in-game overlap with the opcodes used during the login process, a class called Protocol is used to represent a set of opcodes that can be used in a particular state. The protocol can be changed at any time. Rs2Decoder sets isSingleDecode to true initially to help do this safely, but once in the final in-game state this can be disabled for performance. Unlike previous servers, like Apollo and ScapeEmulator, intermediate frame objects are not used. Rs2Decoder and Rs2Encoder call PacketCodec to directly translate an inbound ByteBuf into a POJO, or an outbound POJO into a ByteBuf. This saves a memory allocation per packet. PacketCodec implementations must provide both an encode() and a decode() method for every packet. This will ensure the library is usable as both a client and server, allowing the development of tools, such as a JS5 client, a game server stress-tester or even a proxy server for inspecting the flow of packets. PacketCodec implementations may optionally override getLength() to return their exact length, to optimise the size of the ByteBuf passed to the decode() method. (It is not necessary to override getLength() for fixed packets, as the size is already known.) The 256-element packet length array is automatically generated by the Protocol class based on the length attribute of the PacketCodec objects passed to it. This is less error-prone than populating the array manually. However, it does mean stub implementations of every packet packet will need to be provided during development - all lengths must be known in advance to ensure the client and server remain in sync. For the moment, it is legal to throw NotImplementError from a decode() method. Rs2Decoder catches this exception, prints a warning and then proceed to the next packet. Once all packets have been implemented, this code may be removed. Signed-off-by: Graham --- protocol/build.gradle.kts | 1 + .../org/openrs2/protocol/EmptyPacketCodec.kt | 16 +++ .../kotlin/org/openrs2/protocol/Packet.kt | 3 + .../org/openrs2/protocol/PacketCodec.kt | 47 +++++++ .../org/openrs2/protocol/PacketLength.kt | 6 + .../kotlin/org/openrs2/protocol/Protocol.kt | 22 +++ .../kotlin/org/openrs2/protocol/Rs2Decoder.kt | 80 +++++++++++ .../kotlin/org/openrs2/protocol/Rs2Encoder.kt | 60 +++++++++ .../org/openrs2/protocol/EmptyPacket.kt | 3 + .../org/openrs2/protocol/FixedPacket.kt | 3 + .../org/openrs2/protocol/FixedPacketCodec.kt | 18 +++ .../protocol/LengthMismatchPacketCodec.kt | 18 +++ .../org/openrs2/protocol/Rs2DecoderTest.kt | 103 ++++++++++++++ .../org/openrs2/protocol/Rs2EncoderTest.kt | 127 ++++++++++++++++++ .../openrs2/protocol/TestEmptyPacketCodec.kt | 6 + .../org/openrs2/protocol/TestStreamCipher.kt | 9 ++ .../protocol/VariableByteOptimisedPacket.kt | 3 + .../VariableByteOptimisedPacketCodec.kt | 23 ++++ .../openrs2/protocol/VariableBytePacket.kt | 18 +++ .../protocol/VariableBytePacketCodec.kt | 19 +++ .../protocol/VariableShortOptimisedPacket.kt | 3 + .../VariableShortOptimisedPacketCodec.kt | 23 ++++ .../openrs2/protocol/VariableShortPacket.kt | 18 +++ .../protocol/VariableShortPacketCodec.kt | 19 +++ 24 files changed, 648 insertions(+) create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/EmptyPacketCodec.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/Packet.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/PacketCodec.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/PacketLength.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/Protocol.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/Rs2Decoder.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/Rs2Encoder.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/EmptyPacket.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/FixedPacket.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/FixedPacketCodec.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/LengthMismatchPacketCodec.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/Rs2DecoderTest.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/Rs2EncoderTest.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/TestEmptyPacketCodec.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/TestStreamCipher.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/VariableByteOptimisedPacket.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/VariableByteOptimisedPacketCodec.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/VariableBytePacket.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/VariableBytePacketCodec.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/VariableShortOptimisedPacket.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/VariableShortOptimisedPacketCodec.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/VariableShortPacket.kt create mode 100644 protocol/src/test/kotlin/org/openrs2/protocol/VariableShortPacketCodec.kt diff --git a/protocol/build.gradle.kts b/protocol/build.gradle.kts index 33835985..de94f3bf 100644 --- a/protocol/build.gradle.kts +++ b/protocol/build.gradle.kts @@ -4,6 +4,7 @@ plugins { } dependencies { + api(project(":crypto")) api("io.netty:netty-codec:${Versions.netty}") implementation(project(":buffer")) diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/EmptyPacketCodec.kt b/protocol/src/main/kotlin/org/openrs2/protocol/EmptyPacketCodec.kt new file mode 100644 index 00000000..e6a8f8fc --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/EmptyPacketCodec.kt @@ -0,0 +1,16 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf + +public abstract class EmptyPacketCodec( + private val packet: T, + opcode: Int +) : PacketCodec(packet.javaClass, opcode, length = 0) { + override fun decode(input: ByteBuf): T { + return packet + } + + override fun encode(input: T, output: ByteBuf) { + // empty + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/Packet.kt b/protocol/src/main/kotlin/org/openrs2/protocol/Packet.kt new file mode 100644 index 00000000..d93c13f9 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/Packet.kt @@ -0,0 +1,3 @@ +package org.openrs2.protocol + +public interface Packet diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/PacketCodec.kt b/protocol/src/main/kotlin/org/openrs2/protocol/PacketCodec.kt new file mode 100644 index 00000000..5d92818b --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/PacketCodec.kt @@ -0,0 +1,47 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator + +public abstract class PacketCodec( + public val type: Class, + public val opcode: Int, + public val length: Int +) { + init { + require(opcode in 0 until 256) + require(length >= PacketLength.VARIABLE_SHORT) + } + + public abstract fun decode(input: ByteBuf): T + public abstract fun encode(input: T, output: ByteBuf) + + public open fun getLength(input: T): Int { + return length + } + + public fun allocateBuffer(alloc: ByteBufAllocator, input: T, preferDirect: Boolean): ByteBuf { + val payloadLen = getLength(input) + if (payloadLen < 0) { + return if (preferDirect) { + alloc.ioBuffer() + } else { + alloc.heapBuffer() + } + } + + val headerLen = when (length) { + PacketLength.VARIABLE_BYTE -> 2 + PacketLength.VARIABLE_SHORT -> 3 + else -> 1 + } + + val totalLen = headerLen + payloadLen + + return if (preferDirect) { + alloc.ioBuffer(totalLen, totalLen) + } else { + alloc.heapBuffer(totalLen, totalLen) + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/PacketLength.kt b/protocol/src/main/kotlin/org/openrs2/protocol/PacketLength.kt new file mode 100644 index 00000000..b04d6a10 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/PacketLength.kt @@ -0,0 +1,6 @@ +package org.openrs2.protocol + +public object PacketLength { + public const val VARIABLE_SHORT: Int = -2 + public const val VARIABLE_BYTE: Int = -1 +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/Protocol.kt b/protocol/src/main/kotlin/org/openrs2/protocol/Protocol.kt new file mode 100644 index 00000000..7d952de7 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/Protocol.kt @@ -0,0 +1,22 @@ +package org.openrs2.protocol + +public class Protocol(vararg codecs: PacketCodec<*>) { + private val decoders = arrayOfNulls>(256) + private val encoders = codecs.associateBy(PacketCodec<*>::type) + + init { + for (codec in codecs) { + decoders[codec.opcode] = codec + } + } + + public fun getDecoder(opcode: Int): PacketCodec<*>? { + require(opcode in decoders.indices) + return decoders[opcode] + } + + @Suppress("UNCHECKED_CAST") + public fun getEncoder(type: Class): PacketCodec? { + return encoders[type] as PacketCodec? + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/Rs2Decoder.kt b/protocol/src/main/kotlin/org/openrs2/protocol/Rs2Decoder.kt new file mode 100644 index 00000000..bef9ce51 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/Rs2Decoder.kt @@ -0,0 +1,80 @@ +package org.openrs2.protocol + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ByteToMessageDecoder +import io.netty.handler.codec.DecoderException +import org.openrs2.crypto.NopStreamCipher +import org.openrs2.crypto.StreamCipher + +public class Rs2Decoder(public var protocol: Protocol) : ByteToMessageDecoder() { + private enum class State { + READ_OPCODE, + READ_LENGTH, + READ_PAYLOAD + } + + public var cipher: StreamCipher = NopStreamCipher + private var state = State.READ_OPCODE + private lateinit var decoder: PacketCodec<*> + private var length = 0 + + init { + isSingleDecode = true + } + + override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { + if (state == State.READ_OPCODE) { + if (!input.isReadable) { + return + } + + val opcode = (input.readUnsignedByte().toInt() - cipher.nextInt()) and 0xFF + decoder = protocol.getDecoder(opcode) ?: throw DecoderException("Unsupported opcode: $opcode") + length = decoder.length + + state = State.READ_LENGTH + } + + if (state == State.READ_LENGTH) { + when (length) { + PacketLength.VARIABLE_BYTE -> { + if (!input.isReadable) { + return + } + + length = input.readUnsignedByte().toInt() + } + PacketLength.VARIABLE_SHORT -> { + if (input.readableBytes() < 2) { + return + } + + length = input.readUnsignedShort() + } + } + + state = State.READ_PAYLOAD + } + + if (state == State.READ_PAYLOAD) { + if (input.readableBytes() < length) { + return + } + + out += try { + decoder.decode(input.readSlice(length)) + } catch (ex: NotImplementedError) { + // TODO(gpe): remove this catch block when every packet is implemented + logger.warn { "Skipping unimplemented packet: ${decoder.javaClass}" } + } + + state = State.READ_OPCODE + } + } + + private companion object { + private val logger = InlineLogger() + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/Rs2Encoder.kt b/protocol/src/main/kotlin/org/openrs2/protocol/Rs2Encoder.kt new file mode 100644 index 00000000..813cdada --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/Rs2Encoder.kt @@ -0,0 +1,60 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.EncoderException +import io.netty.handler.codec.MessageToByteEncoder +import org.openrs2.crypto.NopStreamCipher +import org.openrs2.crypto.StreamCipher + +public class Rs2Encoder(public var protocol: Protocol) : MessageToByteEncoder(Packet::class.java) { + public var cipher: StreamCipher = NopStreamCipher + + override fun encode(ctx: ChannelHandlerContext, msg: Packet, out: ByteBuf) { + val encoder = protocol.getEncoder(msg.javaClass) + ?: throw EncoderException("Unsupported packet type: ${msg.javaClass}") + + out.writeByte(encoder.opcode + cipher.nextInt()) + + val len = encoder.length + val lenIndex = out.writerIndex() + when (len) { + PacketLength.VARIABLE_BYTE -> out.writeZero(1) + PacketLength.VARIABLE_SHORT -> out.writeZero(2) + } + + val payloadIndex = out.writerIndex() + encoder.encode(msg, out) + + val written = out.writerIndex() - payloadIndex + + when (len) { + PacketLength.VARIABLE_BYTE -> { + if (written >= 256) { + throw EncoderException("Variable byte payload too long: $written bytes") + } + + out.setByte(lenIndex, written) + } + PacketLength.VARIABLE_SHORT -> { + if (written >= 65536) { + throw EncoderException("Variable short payload too long: $written bytes") + } + + out.setShort(lenIndex, written) + } + else -> { + if (written != len) { + throw EncoderException("Fixed payload length mismatch (expected $len bytes, got $written bytes)") + } + } + } + } + + override fun allocateBuffer(ctx: ChannelHandlerContext, msg: Packet, preferDirect: Boolean): ByteBuf { + val encoder = protocol.getEncoder(msg.javaClass) + ?: throw EncoderException("Unsupported packet type: ${msg.javaClass}") + + return encoder.allocateBuffer(ctx.alloc(), msg, preferDirect) + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/EmptyPacket.kt b/protocol/src/test/kotlin/org/openrs2/protocol/EmptyPacket.kt new file mode 100644 index 00000000..32c08765 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/EmptyPacket.kt @@ -0,0 +1,3 @@ +package org.openrs2.protocol + +object EmptyPacket : Packet diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/FixedPacket.kt b/protocol/src/test/kotlin/org/openrs2/protocol/FixedPacket.kt new file mode 100644 index 00000000..48da5293 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/FixedPacket.kt @@ -0,0 +1,3 @@ +package org.openrs2.protocol + +internal data class FixedPacket(val value: Int) : Packet diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/FixedPacketCodec.kt b/protocol/src/test/kotlin/org/openrs2/protocol/FixedPacketCodec.kt new file mode 100644 index 00000000..0f428c30 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/FixedPacketCodec.kt @@ -0,0 +1,18 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf + +internal object FixedPacketCodec : PacketCodec( + type = FixedPacket::class.java, + opcode = 0, + length = 4 +) { + override fun decode(input: ByteBuf): FixedPacket { + val value = input.readInt() + return FixedPacket(value) + } + + override fun encode(input: FixedPacket, output: ByteBuf) { + output.writeInt(input.value) + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/LengthMismatchPacketCodec.kt b/protocol/src/test/kotlin/org/openrs2/protocol/LengthMismatchPacketCodec.kt new file mode 100644 index 00000000..8a4009eb --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/LengthMismatchPacketCodec.kt @@ -0,0 +1,18 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf + +internal object LengthMismatchPacketCodec : PacketCodec( + type = FixedPacket::class.java, + opcode = 0, + length = 5 +) { + override fun decode(input: ByteBuf): FixedPacket { + val value = input.readInt() + return FixedPacket(value) + } + + override fun encode(input: FixedPacket, output: ByteBuf) { + output.writeInt(input.value) + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/Rs2DecoderTest.kt b/protocol/src/test/kotlin/org/openrs2/protocol/Rs2DecoderTest.kt new file mode 100644 index 00000000..75c0b63f --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/Rs2DecoderTest.kt @@ -0,0 +1,103 @@ +package org.openrs2.protocol + +import io.netty.buffer.Unpooled +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.DecoderException +import org.junit.jupiter.api.assertThrows +import org.openrs2.buffer.wrappedBuffer +import kotlin.test.Test +import kotlin.test.assertEquals + +object Rs2DecoderTest { + @Test + fun testDecode() { + testDecode(byteArrayOf(0, 0x11, 0x22, 0x33, 0x44), FixedPacket(0x11223344)) + testDecode(byteArrayOf(1, 3, 0x11, 0x22, 0x33), VariableBytePacket(byteArrayOf(0x11, 0x22, 0x33))) + testDecode(byteArrayOf(2, 0, 3, 0x11, 0x22, 0x33), VariableShortPacket(byteArrayOf(0x11, 0x22, 0x33))) + testDecode(byteArrayOf(5), EmptyPacket) + } + + @Test + fun testFragmented() { + testFragmented(byteArrayOf(0, 0x11, 0x22, 0x33, 0x44), FixedPacket(0x11223344)) + testFragmented(byteArrayOf(1, 3, 0x11, 0x22, 0x33), VariableBytePacket(byteArrayOf(0x11, 0x22, 0x33))) + testFragmented(byteArrayOf(2, 0, 3, 0x11, 0x22, 0x33), VariableShortPacket(byteArrayOf(0x11, 0x22, 0x33))) + } + + @Test + fun testUnsupported() { + val channel = EmbeddedChannel(Rs2Decoder(Protocol())) + + assertThrows { + channel.writeInbound(wrappedBuffer(0)) + } + } + + @Test + fun testEncryptedOpcode() { + val decoder = Rs2Decoder(Protocol(FixedPacketCodec)) + decoder.cipher = TestStreamCipher + + val channel = EmbeddedChannel(decoder) + channel.writeInbound(wrappedBuffer(10, 0x11, 0x22, 0x33, 0x44)) + + val actual = channel.readInbound() + assertEquals(FixedPacket(0x11223344), actual) + } + + @Test + fun testSwitchProtocol() { + val decoder = Rs2Decoder(Protocol(FixedPacketCodec)) + val channel = EmbeddedChannel(decoder) + + channel.writeInbound(wrappedBuffer(0, 0x11, 0x22, 0x33, 0x44)) + channel.readInbound() + + assertThrows { + channel.writeInbound(wrappedBuffer(5)) + } + + decoder.protocol = Protocol(TestEmptyPacketCodec) + + channel.writeInbound(wrappedBuffer(5)) + + val actual = channel.readInbound() + assertEquals(EmptyPacket, actual) + + assertThrows { + channel.writeInbound(wrappedBuffer(0, 0x11, 0x22, 0x33, 0x44)) + } + } + + private fun testDecode(buf: ByteArray, expected: Packet) { + val channel = EmbeddedChannel(Rs2Decoder(Protocol( + FixedPacketCodec, + VariableBytePacketCodec, + VariableShortPacketCodec, + VariableByteOptimisedPacketCodec, + VariableShortOptimisedPacketCodec, + TestEmptyPacketCodec + ))) + channel.writeInbound(Unpooled.wrappedBuffer(buf)) + + val actual = channel.readInbound() + assertEquals(expected, actual) + } + + private fun testFragmented(buf: ByteArray, expected: Packet) { + val channel = EmbeddedChannel(Rs2Decoder(Protocol( + FixedPacketCodec, + VariableBytePacketCodec, + VariableShortPacketCodec, + VariableByteOptimisedPacketCodec, + VariableShortOptimisedPacketCodec + ))) + + for (b in buf) { + channel.writeInbound(wrappedBuffer(b)) + } + + val actual = channel.readInbound() + assertEquals(expected, actual) + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/Rs2EncoderTest.kt b/protocol/src/test/kotlin/org/openrs2/protocol/Rs2EncoderTest.kt new file mode 100644 index 00000000..5763b2d1 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/Rs2EncoderTest.kt @@ -0,0 +1,127 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.EncoderException +import org.junit.jupiter.api.assertThrows +import org.openrs2.buffer.use +import org.openrs2.buffer.wrappedBuffer +import kotlin.test.Test +import kotlin.test.assertEquals + +object Rs2EncoderTest { + @Test + fun testEncode() { + testEncode(FixedPacket(0x11223344), byteArrayOf(0, 0x11, 0x22, 0x33, 0x44)) + testEncode(VariableBytePacket(byteArrayOf(0x11, 0x22, 0x33)), byteArrayOf(1, 3, 0x11, 0x22, 0x33)) + testEncode(VariableShortPacket(byteArrayOf(0x11, 0x22, 0x33)), byteArrayOf(2, 0, 3, 0x11, 0x22, 0x33)) + testEncode(EmptyPacket, byteArrayOf(5)) + } + + @Test + fun testTooLong() { + val channel = EmbeddedChannel(Rs2Encoder(Protocol( + VariableBytePacketCodec, + VariableShortPacketCodec + ))) + + channel.writeOutbound(VariableShortPacket(ByteArray(255))) + channel.readOutbound().release() + + channel.writeOutbound(VariableShortPacket(ByteArray(65535))) + channel.readOutbound().release() + + assertThrows { + channel.writeOutbound(VariableBytePacket(ByteArray(256))) + } + + assertThrows { + channel.writeOutbound(VariableShortPacket(ByteArray(65536))) + } + } + + @Test + fun testUnsupported() { + val channel = EmbeddedChannel(Rs2Encoder(Protocol())) + + assertThrows { + channel.writeOutbound(FixedPacket(0x11223344)) + } + } + + @Test + fun testLengthMismatch() { + val channel = EmbeddedChannel(Rs2Encoder(Protocol(LengthMismatchPacketCodec))) + + assertThrows { + channel.writeOutbound(FixedPacket(0x11223344)) + } + } + + @Test + fun testLengthOptimised() { + testEncode(VariableByteOptimisedPacket(byteArrayOf(0x11, 0x22, 0x33)), byteArrayOf(3, 3, 0x11, 0x22, 0x33)) + testEncode(VariableShortOptimisedPacket(byteArrayOf(0x11, 0x22, 0x33)), byteArrayOf(4, 0, 3, 0x11, 0x22, 0x33)) + } + + @Test + fun testEncryptedOpcode() { + val encoder = Rs2Encoder(Protocol(FixedPacketCodec)) + encoder.cipher = TestStreamCipher + + val channel = EmbeddedChannel(encoder) + channel.writeOutbound(FixedPacket(0x11223344)) + + channel.readOutbound().use { actual -> + wrappedBuffer(10, 0x11, 0x22, 0x33, 0x44).use { expected -> + assertEquals(expected, actual) + } + } + } + + @Test + fun testSwitchProtocol() { + val encoder = Rs2Encoder(Protocol(FixedPacketCodec)) + val channel = EmbeddedChannel(encoder) + + channel.writeOutbound(FixedPacket(0x11223344)) + channel.readOutbound().release() + + assertThrows { + channel.writeOutbound(EmptyPacket) + } + + encoder.protocol = Protocol(TestEmptyPacketCodec) + + channel.writeOutbound(EmptyPacket) + + channel.readOutbound().use { actual -> + wrappedBuffer(5).use { expected -> + assertEquals(expected, actual) + } + } + + assertThrows { + channel.writeOutbound(FixedPacket(0x11223344)) + } + } + + private fun testEncode(packet: Packet, expected: ByteArray) { + val channel = EmbeddedChannel(Rs2Encoder(Protocol( + FixedPacketCodec, + VariableBytePacketCodec, + VariableShortPacketCodec, + VariableByteOptimisedPacketCodec, + VariableShortOptimisedPacketCodec, + TestEmptyPacketCodec + ))) + channel.writeOutbound(packet) + + channel.readOutbound().use { actual -> + Unpooled.wrappedBuffer(expected).use { expected -> + assertEquals(expected, actual) + } + } + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/TestEmptyPacketCodec.kt b/protocol/src/test/kotlin/org/openrs2/protocol/TestEmptyPacketCodec.kt new file mode 100644 index 00000000..ad95864e --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/TestEmptyPacketCodec.kt @@ -0,0 +1,6 @@ +package org.openrs2.protocol + +object TestEmptyPacketCodec : EmptyPacketCodec( + packet = EmptyPacket, + opcode = 5 +) diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/TestStreamCipher.kt b/protocol/src/test/kotlin/org/openrs2/protocol/TestStreamCipher.kt new file mode 100644 index 00000000..17d065ad --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/TestStreamCipher.kt @@ -0,0 +1,9 @@ +package org.openrs2.protocol + +import org.openrs2.crypto.StreamCipher + +object TestStreamCipher : StreamCipher { + override fun nextInt(): Int { + return 10 + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/VariableByteOptimisedPacket.kt b/protocol/src/test/kotlin/org/openrs2/protocol/VariableByteOptimisedPacket.kt new file mode 100644 index 00000000..ed300491 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/VariableByteOptimisedPacket.kt @@ -0,0 +1,3 @@ +package org.openrs2.protocol + +internal class VariableByteOptimisedPacket(val value: ByteArray) : Packet diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/VariableByteOptimisedPacketCodec.kt b/protocol/src/test/kotlin/org/openrs2/protocol/VariableByteOptimisedPacketCodec.kt new file mode 100644 index 00000000..0b56e87c --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/VariableByteOptimisedPacketCodec.kt @@ -0,0 +1,23 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf + +internal object VariableByteOptimisedPacketCodec : PacketCodec( + type = VariableByteOptimisedPacket::class.java, + opcode = 3, + length = PacketLength.VARIABLE_BYTE +) { + override fun decode(input: ByteBuf): VariableByteOptimisedPacket { + val value = ByteArray(input.readableBytes()) + input.readBytes(value) + return VariableByteOptimisedPacket(value) + } + + override fun encode(input: VariableByteOptimisedPacket, output: ByteBuf) { + output.writeBytes(input.value) + } + + override fun getLength(input: VariableByteOptimisedPacket): Int { + return input.value.size + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/VariableBytePacket.kt b/protocol/src/test/kotlin/org/openrs2/protocol/VariableBytePacket.kt new file mode 100644 index 00000000..ed5bf46e --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/VariableBytePacket.kt @@ -0,0 +1,18 @@ +package org.openrs2.protocol + +internal class VariableBytePacket(val value: ByteArray) : Packet { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VariableBytePacket + + if (!value.contentEquals(other.value)) return false + + return true + } + + override fun hashCode(): Int { + return value.contentHashCode() + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/VariableBytePacketCodec.kt b/protocol/src/test/kotlin/org/openrs2/protocol/VariableBytePacketCodec.kt new file mode 100644 index 00000000..46bc4974 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/VariableBytePacketCodec.kt @@ -0,0 +1,19 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf + +internal object VariableBytePacketCodec : PacketCodec( + type = VariableBytePacket::class.java, + opcode = 1, + length = PacketLength.VARIABLE_BYTE +) { + override fun decode(input: ByteBuf): VariableBytePacket { + val value = ByteArray(input.readableBytes()) + input.readBytes(value) + return VariableBytePacket(value) + } + + override fun encode(input: VariableBytePacket, output: ByteBuf) { + output.writeBytes(input.value) + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortOptimisedPacket.kt b/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortOptimisedPacket.kt new file mode 100644 index 00000000..b3e7a135 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortOptimisedPacket.kt @@ -0,0 +1,3 @@ +package org.openrs2.protocol + +internal class VariableShortOptimisedPacket(val value: ByteArray) : Packet diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortOptimisedPacketCodec.kt b/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortOptimisedPacketCodec.kt new file mode 100644 index 00000000..1f719a2c --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortOptimisedPacketCodec.kt @@ -0,0 +1,23 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf + +internal object VariableShortOptimisedPacketCodec : PacketCodec( + type = VariableShortOptimisedPacket::class.java, + opcode = 4, + length = PacketLength.VARIABLE_SHORT +) { + override fun decode(input: ByteBuf): VariableShortOptimisedPacket { + val value = ByteArray(input.readableBytes()) + input.readBytes(value) + return VariableShortOptimisedPacket(value) + } + + override fun encode(input: VariableShortOptimisedPacket, output: ByteBuf) { + output.writeBytes(input.value) + } + + override fun getLength(input: VariableShortOptimisedPacket): Int { + return input.value.size + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortPacket.kt b/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortPacket.kt new file mode 100644 index 00000000..6c364de3 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortPacket.kt @@ -0,0 +1,18 @@ +package org.openrs2.protocol + +internal class VariableShortPacket(val value: ByteArray) : Packet { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VariableShortPacket + + if (!value.contentEquals(other.value)) return false + + return true + } + + override fun hashCode(): Int { + return value.contentHashCode() + } +} diff --git a/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortPacketCodec.kt b/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortPacketCodec.kt new file mode 100644 index 00000000..76d74607 --- /dev/null +++ b/protocol/src/test/kotlin/org/openrs2/protocol/VariableShortPacketCodec.kt @@ -0,0 +1,19 @@ +package org.openrs2.protocol + +import io.netty.buffer.ByteBuf + +internal object VariableShortPacketCodec : PacketCodec( + type = VariableShortPacket::class.java, + opcode = 2, + length = PacketLength.VARIABLE_SHORT +) { + override fun decode(input: ByteBuf): VariableShortPacket { + val value = ByteArray(input.readableBytes()) + input.readBytes(value) + return VariableShortPacket(value) + } + + override fun encode(input: VariableShortPacket, output: ByteBuf) { + output.writeBytes(input.value) + } +}