diff --git a/README.md b/README.md index 0c132dc53..dafd5364a 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,7 @@ BinaryBlobAppendWriter, and an atomic BinaryBlobAtomicReplacementWriter. The append writer will continuously append to the same file, keeping only a small buffer in memory and resetting it with each successful invocation. +Call `blob.close()` after the final write when the session can no longer reconnect. If users flush it every ~5kb (you can check it via `player.session.getBinaryBlobOrNull()?.readableBytes()`), there should never be more than ~10MB of heap memory allocated for this. diff --git a/buffer/src/main/kotlin/net/rsprot/buffer/extensions/ByteBufExtensions.kt b/buffer/src/main/kotlin/net/rsprot/buffer/extensions/ByteBufExtensions.kt index 37bdf2bc4..3e726b457 100644 --- a/buffer/src/main/kotlin/net/rsprot/buffer/extensions/ByteBufExtensions.kt +++ b/buffer/src/main/kotlin/net/rsprot/buffer/extensions/ByteBufExtensions.kt @@ -11,3 +11,13 @@ public fun ByteBuf.toByteArray(): ByteArray { readBytes(array) return array } + +public inline fun ByteBuf.releaseOnFailure(block: (() -> Unit) -> T): T { + var transferred = false + return try { + block { transferred = true } + } catch (throwable: Throwable) { + if (!transferred) release() + throw throwable + } +} diff --git a/haproxy/src/main/kotlin/org/jire/netty/haproxy/HAProxyPingHandler.kt b/haproxy/src/main/kotlin/org/jire/netty/haproxy/HAProxyPingHandler.kt index 8d4d2dfcb..496960c19 100644 --- a/haproxy/src/main/kotlin/org/jire/netty/haproxy/HAProxyPingHandler.kt +++ b/haproxy/src/main/kotlin/org/jire/netty/haproxy/HAProxyPingHandler.kt @@ -34,8 +34,7 @@ public class HAProxyPingHandler private val response: ByteBuf = Unpooled.unreleasableBuffer( Unpooled - .directBuffer(1, 1) - .writeByte(responseOpcode), + .wrappedBuffer(byteArrayOf(responseOpcode.toByte())), ) override fun channelRead0( diff --git a/protocol/osrs-221/osrs-221-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-221/osrs-221-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 805795805..4c7d9c80b 100644 --- a/protocol/osrs-221/osrs-221-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-221/osrs-221-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -256,6 +267,7 @@ public class GameLoginResponseHandler( networkLog(logger) { "Channel '${ctx.channel()}' has gone inactive, skipping failed response." } + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-221/osrs-221-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-221/osrs-221-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index 22b32119a..8a6d64099 100644 --- a/protocol/osrs-221/osrs-221-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-221/osrs-221-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -108,6 +108,7 @@ public class LoginConnectionHandler( } is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() return } @@ -116,6 +117,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg continueLogin(ctx) } diff --git a/protocol/osrs-221/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-221/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 5c303dc06..7e37b87cd 100644 --- a/protocol/osrs-221/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-221/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -104,8 +104,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-222/osrs-222-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-222/osrs-222-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 805795805..4c7d9c80b 100644 --- a/protocol/osrs-222/osrs-222-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-222/osrs-222-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -256,6 +267,7 @@ public class GameLoginResponseHandler( networkLog(logger) { "Channel '${ctx.channel()}' has gone inactive, skipping failed response." } + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-222/osrs-222-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-222/osrs-222-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index 22b32119a..8a6d64099 100644 --- a/protocol/osrs-222/osrs-222-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-222/osrs-222-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -108,6 +108,7 @@ public class LoginConnectionHandler( } is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() return } @@ -116,6 +117,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg continueLogin(ctx) } diff --git a/protocol/osrs-222/osrs-222-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-222/osrs-222-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 18b8cf7b7..f1ec9b4b8 100644 --- a/protocol/osrs-222/osrs-222-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-222/osrs-222-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -107,8 +107,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(worldId, buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(worldId, buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-223/osrs-223-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-223/osrs-223-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 805795805..4c7d9c80b 100644 --- a/protocol/osrs-223/osrs-223-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-223/osrs-223-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -256,6 +267,7 @@ public class GameLoginResponseHandler( networkLog(logger) { "Channel '${ctx.channel()}' has gone inactive, skipping failed response." } + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-223/osrs-223-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-223/osrs-223-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b09373992..93101603d 100644 --- a/protocol/osrs-223/osrs-223-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-223/osrs-223-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -111,6 +111,7 @@ public class LoginConnectionHandler( } is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() return } @@ -124,6 +125,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-223/osrs-223-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-223/osrs-223-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 18b8cf7b7..f1ec9b4b8 100644 --- a/protocol/osrs-223/osrs-223-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-223/osrs-223-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -107,8 +107,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(worldId, buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(worldId, buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-224/osrs-224-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-224/osrs-224-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 805795805..4c7d9c80b 100644 --- a/protocol/osrs-224/osrs-224-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-224/osrs-224-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -256,6 +267,7 @@ public class GameLoginResponseHandler( networkLog(logger) { "Channel '${ctx.channel()}' has gone inactive, skipping failed response." } + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-224/osrs-224-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-224/osrs-224-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b09373992..93101603d 100644 --- a/protocol/osrs-224/osrs-224-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-224/osrs-224-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -111,6 +111,7 @@ public class LoginConnectionHandler( } is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() return } @@ -124,6 +125,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-224/osrs-224-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-224/osrs-224-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 18b8cf7b7..f1ec9b4b8 100644 --- a/protocol/osrs-224/osrs-224-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-224/osrs-224-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -107,8 +107,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(worldId, buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(worldId, buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index e7e92804f..42162c96e 100644 --- a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -92,80 +93,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -262,6 +273,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-226/osrs-226-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-226/osrs-226-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index e7e92804f..42162c96e 100644 --- a/protocol/osrs-226/osrs-226-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-226/osrs-226-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -92,80 +93,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -262,6 +273,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-226/osrs-226-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-226/osrs-226-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-226/osrs-226-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-226/osrs-226-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-226/osrs-226-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-226/osrs-226-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-226/osrs-226-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-226/osrs-226-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-227/osrs-227-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-227/osrs-227-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index e7e92804f..42162c96e 100644 --- a/protocol/osrs-227/osrs-227-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-227/osrs-227-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -92,80 +93,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -262,6 +273,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-227/osrs-227-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-227/osrs-227-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-227/osrs-227-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-227/osrs-227-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-227/osrs-227-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-227/osrs-227-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-227/osrs-227-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-227/osrs-227-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-228/osrs-228-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-228/osrs-228-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index c142ba59f..a181d9c12 100644 --- a/protocol/osrs-228/osrs-228-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-228/osrs-228-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -255,6 +266,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-228/osrs-228-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-228/osrs-228-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-228/osrs-228-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-228/osrs-228-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-228/osrs-228-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-228/osrs-228-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-228/osrs-228-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-228/osrs-228-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-229/osrs-229-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-229/osrs-229-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index e7e92804f..42162c96e 100644 --- a/protocol/osrs-229/osrs-229-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-229/osrs-229-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -92,80 +93,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -262,6 +273,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-229/osrs-229-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-229/osrs-229-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-229/osrs-229-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-229/osrs-229-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-229/osrs-229-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-229/osrs-229-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-229/osrs-229-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-229/osrs-229-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-230/osrs-230-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-230/osrs-230-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index e7e92804f..42162c96e 100644 --- a/protocol/osrs-230/osrs-230-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-230/osrs-230-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -92,80 +93,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -262,6 +273,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-230/osrs-230-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-230/osrs-230-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-230/osrs-230-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-230/osrs-230-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-230/osrs-230-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-230/osrs-230-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-230/osrs-230-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-230/osrs-230-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-231/osrs-231-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-231/osrs-231-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index c142ba59f..a181d9c12 100644 --- a/protocol/osrs-231/osrs-231-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-231/osrs-231-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -255,6 +266,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-231/osrs-231-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-231/osrs-231-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-231/osrs-231-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-231/osrs-231-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-231/osrs-231-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-231/osrs-231-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-231/osrs-231-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-231/osrs-231-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-232/osrs-232-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-232/osrs-232-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index c142ba59f..a181d9c12 100644 --- a/protocol/osrs-232/osrs-232-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-232/osrs-232-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -255,6 +266,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-232/osrs-232-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-232/osrs-232-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-232/osrs-232-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-232/osrs-232-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-232/osrs-232-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-232/osrs-232-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-232/osrs-232-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-232/osrs-232-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-233/osrs-233-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-233/osrs-233-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index c142ba59f..a181d9c12 100644 --- a/protocol/osrs-233/osrs-233-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-233/osrs-233-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -255,6 +266,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-233/osrs-233-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-233/osrs-233-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-233/osrs-233-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-233/osrs-233-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-233/osrs-233-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-233/osrs-233-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-233/osrs-233-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-233/osrs-233-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-234/osrs-234-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-234/osrs-234-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index c142ba59f..a181d9c12 100644 --- a/protocol/osrs-234/osrs-234-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-234/osrs-234-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -7,6 +7,7 @@ import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -91,80 +92,90 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } public fun writeSuccessfulResponse( response: LoginResponse.ReconnectOk, loginBlock: LoginBlock<*>, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() + val pipeline = ctx.channel().pipeline() - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -255,6 +266,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-234/osrs-234-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-234/osrs-234-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3f12e287..26eea8220 100644 --- a/protocol/osrs-234/osrs-234-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-234/osrs-234-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -132,6 +132,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -152,6 +153,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-234/osrs-234-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-234/osrs-234-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-234/osrs-234-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-234/osrs-234-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-235/osrs-235-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-235/osrs-235-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 379fdb23f..3ca0b1d54 100644 --- a/protocol/osrs-235/osrs-235-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-235/osrs-235-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -3,15 +3,13 @@ package net.rsprot.protocol.api.login import com.github.michaelbull.logging.InlineLogger -import io.netty.buffer.Unpooled import io.netty.buffer.UnpooledByteBufAllocator import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler -import net.rsprot.buffer.extensions.gdata -import net.rsprot.buffer.extensions.p8 +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -33,6 +31,7 @@ import net.rsprot.protocol.channel.setBinaryHeaderBuilder import net.rsprot.protocol.common.client.OldSchoolClientType import net.rsprot.protocol.loginprot.incoming.util.LoginBlock import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import java.nio.ByteBuffer import java.security.MessageDigest /** @@ -103,27 +102,30 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() - finalizeBinaryHeader(ctx.channel(), response) - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + finalizeBinaryHeader(ctx.channel(), response) + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } private fun finalizeBinaryHeader( @@ -163,25 +165,21 @@ public class GameLoginResponseHandler( userId: Long, userHash: Long, ): ByteArray { - val buffer = Unpooled.buffer(Long.SIZE_BYTES + Long.SIZE_BYTES) + val buffer = ByteBuffer.allocate(Long.SIZE_BYTES + Long.SIZE_BYTES) // User id is an incrementing value; As of writing this comment, there are somewhere between // 300-400m users, meaning the userId value for any new accounts would be in that range // This value is not sensitive in any way, but it is constant. - buffer.p8(userId) + buffer.putLong(userId) // User hash is an actual hash provided by Jagex, unique for a given account regardless of the world. // While hash on its own is not useful, there is a potential security concern in how these hashes // are generated. As such, we take an extra step and salt it with the user id, then hash the // value once more. Due to the function turning 128 bits of data to 256 bits of data, // the probability of collisions is extremely thin. - buffer.p8(userHash) - val input = ByteArray(buffer.readableBytes()) - buffer.gdata(input) + buffer.putLong(userHash) // Take the combined byte array and hash it with a SHA-256 hashing function. // This effectively ensures no one will be able to reverse the original input values, // while still ensuring we can match multiple play sessions to a single user account. - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(input) - return messageDigest.digest() + return MessageDigest.getInstance("SHA-256").digest(buffer.array()) } public fun writeSuccessfulResponse( @@ -189,62 +187,69 @@ public class GameLoginResponseHandler( loginBlock: LoginBlock<*>, previousSession: Session, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() - val oldBlob = previousSession.getBinaryBlobOrNull() - if (oldBlob != null) { - this.ctx.channel().setBinaryBlob(oldBlob) - oldBlob.stream.append( - serverToClient = true, - opcode = 0xFF, - size = Prot.VAR_SHORT, - payload = buffer.buffer.retainedSlice(start, written), - ) - } - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + val oldBlob = previousSession.getBinaryBlobOrNull() + if (oldBlob != null) { + this.ctx.channel().setBinaryBlob(oldBlob) + oldBlob.stream.append( + serverToClient = true, + opcode = 0xFF, + size = Prot.VAR_SHORT, + payload = buffer.buffer.retainedSlice(start, written), + ) + } + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -335,6 +340,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-235/osrs-235-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-235/osrs-235-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3c979906..8d35a1d38 100644 --- a/protocol/osrs-235/osrs-235-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-235/osrs-235-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -135,6 +135,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -155,6 +156,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-235/osrs-235-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-235/osrs-235-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-235/osrs-235-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-235/osrs-235-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-236/osrs-236-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-236/osrs-236-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 379fdb23f..3ca0b1d54 100644 --- a/protocol/osrs-236/osrs-236-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-236/osrs-236-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -3,15 +3,13 @@ package net.rsprot.protocol.api.login import com.github.michaelbull.logging.InlineLogger -import io.netty.buffer.Unpooled import io.netty.buffer.UnpooledByteBufAllocator import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler -import net.rsprot.buffer.extensions.gdata -import net.rsprot.buffer.extensions.p8 +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -33,6 +31,7 @@ import net.rsprot.protocol.channel.setBinaryHeaderBuilder import net.rsprot.protocol.common.client.OldSchoolClientType import net.rsprot.protocol.loginprot.incoming.util.LoginBlock import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import java.nio.ByteBuffer import java.security.MessageDigest /** @@ -103,27 +102,30 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() - finalizeBinaryHeader(ctx.channel(), response) - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + finalizeBinaryHeader(ctx.channel(), response) + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } private fun finalizeBinaryHeader( @@ -163,25 +165,21 @@ public class GameLoginResponseHandler( userId: Long, userHash: Long, ): ByteArray { - val buffer = Unpooled.buffer(Long.SIZE_BYTES + Long.SIZE_BYTES) + val buffer = ByteBuffer.allocate(Long.SIZE_BYTES + Long.SIZE_BYTES) // User id is an incrementing value; As of writing this comment, there are somewhere between // 300-400m users, meaning the userId value for any new accounts would be in that range // This value is not sensitive in any way, but it is constant. - buffer.p8(userId) + buffer.putLong(userId) // User hash is an actual hash provided by Jagex, unique for a given account regardless of the world. // While hash on its own is not useful, there is a potential security concern in how these hashes // are generated. As such, we take an extra step and salt it with the user id, then hash the // value once more. Due to the function turning 128 bits of data to 256 bits of data, // the probability of collisions is extremely thin. - buffer.p8(userHash) - val input = ByteArray(buffer.readableBytes()) - buffer.gdata(input) + buffer.putLong(userHash) // Take the combined byte array and hash it with a SHA-256 hashing function. // This effectively ensures no one will be able to reverse the original input values, // while still ensuring we can match multiple play sessions to a single user account. - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(input) - return messageDigest.digest() + return MessageDigest.getInstance("SHA-256").digest(buffer.array()) } public fun writeSuccessfulResponse( @@ -189,62 +187,69 @@ public class GameLoginResponseHandler( loginBlock: LoginBlock<*>, previousSession: Session, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() - val oldBlob = previousSession.getBinaryBlobOrNull() - if (oldBlob != null) { - this.ctx.channel().setBinaryBlob(oldBlob) - oldBlob.stream.append( - serverToClient = true, - opcode = 0xFF, - size = Prot.VAR_SHORT, - payload = buffer.buffer.retainedSlice(start, written), - ) - } - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + val oldBlob = previousSession.getBinaryBlobOrNull() + if (oldBlob != null) { + this.ctx.channel().setBinaryBlob(oldBlob) + oldBlob.stream.append( + serverToClient = true, + opcode = 0xFF, + size = Prot.VAR_SHORT, + payload = buffer.buffer.retainedSlice(start, written), + ) + } + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -335,6 +340,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-236/osrs-236-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-236/osrs-236-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3c979906..8d35a1d38 100644 --- a/protocol/osrs-236/osrs-236-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-236/osrs-236-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -135,6 +135,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -155,6 +156,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-236/osrs-236-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-236/osrs-236-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-236/osrs-236-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-236/osrs-236-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-237/osrs-237-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-237/osrs-237-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 379fdb23f..3ca0b1d54 100644 --- a/protocol/osrs-237/osrs-237-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-237/osrs-237-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -3,15 +3,13 @@ package net.rsprot.protocol.api.login import com.github.michaelbull.logging.InlineLogger -import io.netty.buffer.Unpooled import io.netty.buffer.UnpooledByteBufAllocator import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler -import net.rsprot.buffer.extensions.gdata -import net.rsprot.buffer.extensions.p8 +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -33,6 +31,7 @@ import net.rsprot.protocol.channel.setBinaryHeaderBuilder import net.rsprot.protocol.common.client.OldSchoolClientType import net.rsprot.protocol.loginprot.incoming.util.LoginBlock import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import java.nio.ByteBuffer import java.security.MessageDigest /** @@ -103,27 +102,30 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() - finalizeBinaryHeader(ctx.channel(), response) - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + finalizeBinaryHeader(ctx.channel(), response) + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } private fun finalizeBinaryHeader( @@ -163,25 +165,21 @@ public class GameLoginResponseHandler( userId: Long, userHash: Long, ): ByteArray { - val buffer = Unpooled.buffer(Long.SIZE_BYTES + Long.SIZE_BYTES) + val buffer = ByteBuffer.allocate(Long.SIZE_BYTES + Long.SIZE_BYTES) // User id is an incrementing value; As of writing this comment, there are somewhere between // 300-400m users, meaning the userId value for any new accounts would be in that range // This value is not sensitive in any way, but it is constant. - buffer.p8(userId) + buffer.putLong(userId) // User hash is an actual hash provided by Jagex, unique for a given account regardless of the world. // While hash on its own is not useful, there is a potential security concern in how these hashes // are generated. As such, we take an extra step and salt it with the user id, then hash the // value once more. Due to the function turning 128 bits of data to 256 bits of data, // the probability of collisions is extremely thin. - buffer.p8(userHash) - val input = ByteArray(buffer.readableBytes()) - buffer.gdata(input) + buffer.putLong(userHash) // Take the combined byte array and hash it with a SHA-256 hashing function. // This effectively ensures no one will be able to reverse the original input values, // while still ensuring we can match multiple play sessions to a single user account. - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(input) - return messageDigest.digest() + return MessageDigest.getInstance("SHA-256").digest(buffer.array()) } public fun writeSuccessfulResponse( @@ -189,62 +187,69 @@ public class GameLoginResponseHandler( loginBlock: LoginBlock<*>, previousSession: Session, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() - val oldBlob = previousSession.getBinaryBlobOrNull() - if (oldBlob != null) { - this.ctx.channel().setBinaryBlob(oldBlob) - oldBlob.stream.append( - serverToClient = true, - opcode = 0xFF, - size = Prot.VAR_SHORT, - payload = buffer.buffer.retainedSlice(start, written), - ) - } - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + val oldBlob = previousSession.getBinaryBlobOrNull() + if (oldBlob != null) { + this.ctx.channel().setBinaryBlob(oldBlob) + oldBlob.stream.append( + serverToClient = true, + opcode = 0xFF, + size = Prot.VAR_SHORT, + payload = buffer.buffer.retainedSlice(start, written), + ) + } + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -335,6 +340,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-237/osrs-237-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-237/osrs-237-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3c979906..8d35a1d38 100644 --- a/protocol/osrs-237/osrs-237-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-237/osrs-237-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -135,6 +135,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -155,6 +156,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-237/osrs-237-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-237/osrs-237-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-237/osrs-237-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-237/osrs-237-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-238/osrs-238-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-238/osrs-238-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 379fdb23f..3ca0b1d54 100644 --- a/protocol/osrs-238/osrs-238-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-238/osrs-238-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -3,15 +3,13 @@ package net.rsprot.protocol.api.login import com.github.michaelbull.logging.InlineLogger -import io.netty.buffer.Unpooled import io.netty.buffer.UnpooledByteBufAllocator import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler -import net.rsprot.buffer.extensions.gdata -import net.rsprot.buffer.extensions.p8 +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -33,6 +31,7 @@ import net.rsprot.protocol.channel.setBinaryHeaderBuilder import net.rsprot.protocol.common.client.OldSchoolClientType import net.rsprot.protocol.loginprot.incoming.util.LoginBlock import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import java.nio.ByteBuffer import java.security.MessageDigest /** @@ -103,27 +102,30 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() - finalizeBinaryHeader(ctx.channel(), response) - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + finalizeBinaryHeader(ctx.channel(), response) + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } private fun finalizeBinaryHeader( @@ -163,25 +165,21 @@ public class GameLoginResponseHandler( userId: Long, userHash: Long, ): ByteArray { - val buffer = Unpooled.buffer(Long.SIZE_BYTES + Long.SIZE_BYTES) + val buffer = ByteBuffer.allocate(Long.SIZE_BYTES + Long.SIZE_BYTES) // User id is an incrementing value; As of writing this comment, there are somewhere between // 300-400m users, meaning the userId value for any new accounts would be in that range // This value is not sensitive in any way, but it is constant. - buffer.p8(userId) + buffer.putLong(userId) // User hash is an actual hash provided by Jagex, unique for a given account regardless of the world. // While hash on its own is not useful, there is a potential security concern in how these hashes // are generated. As such, we take an extra step and salt it with the user id, then hash the // value once more. Due to the function turning 128 bits of data to 256 bits of data, // the probability of collisions is extremely thin. - buffer.p8(userHash) - val input = ByteArray(buffer.readableBytes()) - buffer.gdata(input) + buffer.putLong(userHash) // Take the combined byte array and hash it with a SHA-256 hashing function. // This effectively ensures no one will be able to reverse the original input values, // while still ensuring we can match multiple play sessions to a single user account. - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(input) - return messageDigest.digest() + return MessageDigest.getInstance("SHA-256").digest(buffer.array()) } public fun writeSuccessfulResponse( @@ -189,62 +187,69 @@ public class GameLoginResponseHandler( loginBlock: LoginBlock<*>, previousSession: Session, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() - val oldBlob = previousSession.getBinaryBlobOrNull() - if (oldBlob != null) { - this.ctx.channel().setBinaryBlob(oldBlob) - oldBlob.stream.append( - serverToClient = true, - opcode = 0xFF, - size = Prot.VAR_SHORT, - payload = buffer.buffer.retainedSlice(start, written), - ) - } - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + val oldBlob = previousSession.getBinaryBlobOrNull() + if (oldBlob != null) { + this.ctx.channel().setBinaryBlob(oldBlob) + oldBlob.stream.append( + serverToClient = true, + opcode = 0xFF, + size = Prot.VAR_SHORT, + payload = buffer.buffer.retainedSlice(start, written), + ) + } + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -335,6 +340,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-238/osrs-238-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-238/osrs-238-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3c979906..8d35a1d38 100644 --- a/protocol/osrs-238/osrs-238-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-238/osrs-238-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -135,6 +135,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -155,6 +156,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-238/osrs-238-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-238/osrs-238-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-238/osrs-238-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-238/osrs-238-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 379fdb23f..3ca0b1d54 100644 --- a/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -3,15 +3,13 @@ package net.rsprot.protocol.api.login import com.github.michaelbull.logging.InlineLogger -import io.netty.buffer.Unpooled import io.netty.buffer.UnpooledByteBufAllocator import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler -import net.rsprot.buffer.extensions.gdata -import net.rsprot.buffer.extensions.p8 +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -33,6 +31,7 @@ import net.rsprot.protocol.channel.setBinaryHeaderBuilder import net.rsprot.protocol.common.client.OldSchoolClientType import net.rsprot.protocol.loginprot.incoming.util.LoginBlock import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import java.nio.ByteBuffer import java.security.MessageDigest /** @@ -103,27 +102,30 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() - finalizeBinaryHeader(ctx.channel(), response) - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + finalizeBinaryHeader(ctx.channel(), response) + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } private fun finalizeBinaryHeader( @@ -163,25 +165,21 @@ public class GameLoginResponseHandler( userId: Long, userHash: Long, ): ByteArray { - val buffer = Unpooled.buffer(Long.SIZE_BYTES + Long.SIZE_BYTES) + val buffer = ByteBuffer.allocate(Long.SIZE_BYTES + Long.SIZE_BYTES) // User id is an incrementing value; As of writing this comment, there are somewhere between // 300-400m users, meaning the userId value for any new accounts would be in that range // This value is not sensitive in any way, but it is constant. - buffer.p8(userId) + buffer.putLong(userId) // User hash is an actual hash provided by Jagex, unique for a given account regardless of the world. // While hash on its own is not useful, there is a potential security concern in how these hashes // are generated. As such, we take an extra step and salt it with the user id, then hash the // value once more. Due to the function turning 128 bits of data to 256 bits of data, // the probability of collisions is extremely thin. - buffer.p8(userHash) - val input = ByteArray(buffer.readableBytes()) - buffer.gdata(input) + buffer.putLong(userHash) // Take the combined byte array and hash it with a SHA-256 hashing function. // This effectively ensures no one will be able to reverse the original input values, // while still ensuring we can match multiple play sessions to a single user account. - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(input) - return messageDigest.digest() + return MessageDigest.getInstance("SHA-256").digest(buffer.array()) } public fun writeSuccessfulResponse( @@ -189,62 +187,69 @@ public class GameLoginResponseHandler( loginBlock: LoginBlock<*>, previousSession: Session, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() - val oldBlob = previousSession.getBinaryBlobOrNull() - if (oldBlob != null) { - this.ctx.channel().setBinaryBlob(oldBlob) - oldBlob.stream.append( - serverToClient = true, - opcode = 0xFF, - size = Prot.VAR_SHORT, - payload = buffer.buffer.retainedSlice(start, written), - ) - } - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + val oldBlob = previousSession.getBinaryBlobOrNull() + if (oldBlob != null) { + this.ctx.channel().setBinaryBlob(oldBlob) + oldBlob.stream.append( + serverToClient = true, + opcode = 0xFF, + size = Prot.VAR_SHORT, + payload = buffer.buffer.retainedSlice(start, written), + ) + } + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -335,6 +340,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3c979906..8d35a1d38 100644 --- a/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-239/osrs-239-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -135,6 +135,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -155,6 +156,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-239/osrs-239-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-239/osrs-239-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-239/osrs-239-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-239/osrs-239-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt index 379fdb23f..3ca0b1d54 100644 --- a/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt +++ b/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -3,15 +3,13 @@ package net.rsprot.protocol.api.login import com.github.michaelbull.logging.InlineLogger -import io.netty.buffer.Unpooled import io.netty.buffer.UnpooledByteBufAllocator import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPipeline import io.netty.handler.timeout.IdleStateHandler -import net.rsprot.buffer.extensions.gdata -import net.rsprot.buffer.extensions.p8 +import net.rsprot.buffer.extensions.releaseOnFailure import net.rsprot.buffer.extensions.toJagByteBuf import net.rsprot.crypto.cipher.StreamCipher import net.rsprot.crypto.cipher.StreamCipherPair @@ -33,6 +31,7 @@ import net.rsprot.protocol.channel.setBinaryHeaderBuilder import net.rsprot.protocol.common.client.OldSchoolClientType import net.rsprot.protocol.loginprot.incoming.util.LoginBlock import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import java.nio.ByteBuffer import java.security.MessageDigest /** @@ -103,27 +102,30 @@ public class GameLoginResponseHandler( // game packets, due to the executor differing and race conditions taking place. val buffer = ctx.alloc().buffer(37).toJagByteBuf() - if (!networkService.betaWorld) { - buffer.p1(encoder.prot.opcode) - } - // Client expects a hardcoded 37 value for the size, even though it is not the exact size - // of the login packet - buffer.p1(37) - encoder.encode(cipher.encoderCipher, buffer, response) + buffer.buffer.releaseOnFailure { transfer -> + if (!networkService.betaWorld) { + buffer.p1(encoder.prot.opcode) + } + // Client expects a hardcoded 37 value for the size, even though it is not the exact size + // of the login packet + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) - val pipeline = ctx.channel().pipeline() - finalizeBinaryHeader(ctx.channel(), response) - val session = - createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + finalizeBinaryHeader(ctx.channel(), response) + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session } - return session } private fun finalizeBinaryHeader( @@ -163,25 +165,21 @@ public class GameLoginResponseHandler( userId: Long, userHash: Long, ): ByteArray { - val buffer = Unpooled.buffer(Long.SIZE_BYTES + Long.SIZE_BYTES) + val buffer = ByteBuffer.allocate(Long.SIZE_BYTES + Long.SIZE_BYTES) // User id is an incrementing value; As of writing this comment, there are somewhere between // 300-400m users, meaning the userId value for any new accounts would be in that range // This value is not sensitive in any way, but it is constant. - buffer.p8(userId) + buffer.putLong(userId) // User hash is an actual hash provided by Jagex, unique for a given account regardless of the world. // While hash on its own is not useful, there is a potential security concern in how these hashes // are generated. As such, we take an extra step and salt it with the user id, then hash the // value once more. Due to the function turning 128 bits of data to 256 bits of data, // the probability of collisions is extremely thin. - buffer.p8(userHash) - val input = ByteArray(buffer.readableBytes()) - buffer.gdata(input) + buffer.putLong(userHash) // Take the combined byte array and hash it with a SHA-256 hashing function. // This effectively ensures no one will be able to reverse the original input values, // while still ensuring we can match multiple play sessions to a single user account. - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(input) - return messageDigest.digest() + return MessageDigest.getInstance("SHA-256").digest(buffer.array()) } public fun writeSuccessfulResponse( @@ -189,62 +187,69 @@ public class GameLoginResponseHandler( loginBlock: LoginBlock<*>, previousSession: Session, ): Session { - // Ensure it isn't null - our decoder pre-validates it long before hitting this function, - // so this exception should never be hit. - val oldSchoolClientType = - checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { - "Login client type cannot be null" - } - val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + try { + // Ensure it isn't null - our decoder pre-validates it long before hitting this function, + // so this exception should never be hit. + val oldSchoolClientType = + checkNotNull(loginBlock.clientType.toOldSchoolClientType()) { + "Login client type cannot be null" + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) - val encoder = - networkService - .encoderRepositories - .loginMessageEncoderRepository - .getEncoder(response::class.java) + val encoder = + networkService + .encoderRepositories + .loginMessageEncoderRepository + .getEncoder(response::class.java) - // Allocate a perfectly-sized buffer for this packet - val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() - val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() - buffer.p1(encoder.prot.opcode) + // Allocate a perfectly-sized buffer for this packet + val bufLength = Byte.SIZE_BYTES + Short.SIZE_BYTES + response.content().readableBytes() + val buffer = ctx.alloc().buffer(bufLength).toJagByteBuf() + buffer.buffer.releaseOnFailure { transfer -> + buffer.p1(encoder.prot.opcode) - // Write a placeholder size of 0 bytes - val lengthPos = buffer.writerIndex() - buffer.p2(0) + // Write a placeholder size of 0 bytes + val lengthPos = buffer.writerIndex() + buffer.p2(0) - // Write the payload - val start = buffer.writerIndex() - encoder.encode(encodingCipher, buffer, response) - val end = buffer.writerIndex() - val written = end - start + // Write the payload + val start = buffer.writerIndex() + encoder.encode(encodingCipher, buffer, response) + val end = buffer.writerIndex() + val written = end - start - // Update the size with the actual number of bytes written - buffer.writerIndex(lengthPos) - buffer.p2(written) - buffer.writerIndex(end) + // Update the size with the actual number of bytes written + buffer.writerIndex(lengthPos) + buffer.p2(written) + buffer.writerIndex(end) - val pipeline = ctx.channel().pipeline() - val oldBlob = previousSession.getBinaryBlobOrNull() - if (oldBlob != null) { - this.ctx.channel().setBinaryBlob(oldBlob) - oldBlob.stream.append( - serverToClient = true, - opcode = 0xFF, - size = Prot.VAR_SHORT, - payload = buffer.buffer.retainedSlice(start, written), - ) - } - val session = - createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) - networkService.js5Authorizer.authorize(ctx.hostAddress()) - ctx.executor().submit { - ctx.write(buffer.buffer) - session.onLoginTransitionComplete() - } - networkLog(logger) { - "Successful game login from channel '${ctx.channel()}': $loginBlock" + val pipeline = ctx.channel().pipeline() + val oldBlob = previousSession.getBinaryBlobOrNull() + if (oldBlob != null) { + this.ctx.channel().setBinaryBlob(oldBlob) + oldBlob.stream.append( + serverToClient = true, + opcode = 0xFF, + size = Prot.VAR_SHORT, + payload = buffer.buffer.retainedSlice(start, written), + ) + } + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkService.js5Authorizer.authorize(ctx.hostAddress()) + ctx.executor().submit { + ctx.write(buffer.buffer) + session.onLoginTransitionComplete() + } + transfer() + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + } finally { + response.release() } - return session } private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { @@ -335,6 +340,7 @@ public class GameLoginResponseHandler( ctx.hostAddress(), LoginDisconnectionReason.GAME_CHANNEL_INACTIVE, ) + response.safeRelease() return } networkLog(logger) { diff --git a/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt index b3c979906..8d35a1d38 100644 --- a/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt +++ b/protocol/osrs-240/osrs-240-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -135,6 +135,7 @@ public class LoginConnectionHandler( is GameLogin -> { if (this.loginState != LoginState.UNINITIALIZED) { + msg.buffer.buffer.release() ctx.close() networkService .trafficMonitor @@ -155,6 +156,7 @@ public class LoginConnectionHandler( } is GameReconnect -> { + releaseLoginBlock() this.loginPacket = msg this.loginHeader = networkService diff --git a/protocol/osrs-240/osrs-240-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-240/osrs-240-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt index 153e7c8ac..87c7a0bc1 100644 --- a/protocol/osrs-240/osrs-240-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt +++ b/protocol/osrs-240/osrs-240-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -117,8 +117,13 @@ public sealed interface LoginResponse : OutgoingLoginMessage { playerInfo.ensureReconnectCalled() val allocator = playerInfo.allocator val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) - playerInfo.handleAbsolutePlayerPositions(buffer) - return buffer + return try { + playerInfo.handleAbsolutePlayerPositions(buffer) + buffer + } catch (throwable: Throwable) { + buffer.release() + throw throwable + } } } diff --git a/protocol/src/main/kotlin/net/rsprot/protocol/binary/BinaryBlob.kt b/protocol/src/main/kotlin/net/rsprot/protocol/binary/BinaryBlob.kt index da589550f..b1caae101 100644 --- a/protocol/src/main/kotlin/net/rsprot/protocol/binary/BinaryBlob.kt +++ b/protocol/src/main/kotlin/net/rsprot/protocol/binary/BinaryBlob.kt @@ -8,11 +8,15 @@ package net.rsprot.protocol.binary public data class BinaryBlob( public val header: BinaryHeader, public val stream: BinaryStream, -) { +) : AutoCloseable { /** * @return the number of readable bytes in this buffer. */ public fun readableBytes(): Int { return stream.readableBytes() } + + override fun close() { + stream.close() + } } diff --git a/protocol/src/main/kotlin/net/rsprot/protocol/binary/BinaryStream.kt b/protocol/src/main/kotlin/net/rsprot/protocol/binary/BinaryStream.kt index 0bb04e19e..9212833a1 100644 --- a/protocol/src/main/kotlin/net/rsprot/protocol/binary/BinaryStream.kt +++ b/protocol/src/main/kotlin/net/rsprot/protocol/binary/BinaryStream.kt @@ -7,6 +7,7 @@ import net.rsprot.buffer.extensions.pMidiVarLen import net.rsprot.buffer.extensions.pdata import net.rsprot.buffer.extensions.toByteArray import net.rsprot.protocol.Prot +import java.lang.ref.Cleaner import java.util.concurrent.atomic.AtomicInteger import java.util.function.Consumer import kotlin.math.max @@ -15,8 +16,12 @@ import kotlin.math.min public class BinaryStream( private var buffer: ByteBuf, private var nanoTime: Long = 0, -) { +) : AutoCloseable { private val lockCount: AtomicInteger = AtomicInteger(0) + private val cleanable = CLEANER.register(this, BufferReleaser(buffer)) + + @Volatile + private var closeRequested: Boolean = false /** * Appends a packet into the buffer in this stream. @@ -71,8 +76,8 @@ public class BinaryStream( ) lockCount.incrementAndGet() return Consumer { realSize -> - lockCount.decrementAndGet() overwritePayload(marker, realSize) + if (lockCount.decrementAndGet() == 0 && closeRequested) cleanable.clean() } } finally { payload.release() @@ -170,7 +175,20 @@ public class BinaryStream( return this.buffer.readableBytes() } + @Synchronized + override fun close() { + closeRequested = true + if (lockCount.get() == 0) cleanable.clean() + } + + private class BufferReleaser(private val buffer: ByteBuf) : Runnable { + override fun run() { + if (buffer.refCnt() > 0) buffer.release() + } + } + private companion object { + private val CLEANER: Cleaner = Cleaner.create() private const val MAX_31BIT_INT: Long = 1 shl 30 private const val NANOSECONDS_IN_MILLISECOND: Long = 1_000_000 }