Add initial GAMELOGIC packet support

Signed-off-by: Graham <gpe@openrs2.org>
master
Graham 8 months ago
parent 508d9526ea
commit 6d43e0392e
  1. 15
      game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt
  2. 4
      protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt
  3. 19
      protocol/src/main/kotlin/org/openrs2/protocol/common/AntiAliasingMode.kt
  4. 20
      protocol/src/main/kotlin/org/openrs2/protocol/common/DisplayMode.kt
  5. 30
      protocol/src/main/kotlin/org/openrs2/protocol/common/Uid.kt
  6. 24
      protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginCodec.kt
  7. 143
      protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginPayload.kt
  8. 24
      protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameReconnectCodec.kt
  9. 2
      protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/LoginRequest.kt

@ -24,6 +24,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.openrs2.buffer.copiedBuffer
import org.openrs2.cache.Js5MasterIndex
import org.openrs2.conf.CountryCode
import org.openrs2.crypto.secureRandom
import org.openrs2.game.cluster.Cluster
@ -44,6 +45,7 @@ import org.openrs2.protocol.js5.downstream.Js5ResponseEncoder
import org.openrs2.protocol.js5.downstream.XorDecoder
import org.openrs2.protocol.js5.upstream.Js5RequestDecoder
import org.openrs2.protocol.login.downstream.LoginResponse
import org.openrs2.protocol.login.upstream.GameLoginPayload
import org.openrs2.protocol.login.upstream.LoginRequest
import org.openrs2.protocol.world.downstream.WorldListDownstream
import org.openrs2.protocol.world.downstream.WorldListResponse
@ -53,6 +55,7 @@ import java.time.LocalDate
public class LoginChannelHandler @Inject constructor(
private val cluster: Cluster,
private val store: PlayerStore,
js5MasterIndex: Js5MasterIndex,
private val js5HandlerProvider: Provider<Js5ChannelHandler>,
private val jaggrabHandler: JaggrabChannelHandler,
@CreateDownstream
@ -63,6 +66,7 @@ public class LoginChannelHandler @Inject constructor(
private val worldListDownstreamProtocol: Protocol
) : SimpleChannelInboundHandler<LoginRequest>(LoginRequest::class.java) {
private lateinit var scope: CoroutineScope
private val js5ArchiveChecksums = js5MasterIndex.entries.map(Js5MasterIndex.Entry::checksum)
private var usernameHash = 0
private var serverKey = 0L
@ -86,7 +90,9 @@ public class LoginChannelHandler @Inject constructor(
when (msg) {
is LoginRequest.InitGameConnection -> handleInitGameConnection(ctx, msg)
is LoginRequest.InitJs5RemoteConnection -> handleInitJs5RemoteConnection(ctx, msg)
is LoginRequest.GameLogin -> handleGameLogin(ctx, msg.payload, reconnect = false)
is LoginRequest.InitJaggrabConnection -> handleInitJaggrabConnection(ctx)
is LoginRequest.GameReconnect -> handleGameLogin(ctx, msg.payload, reconnect = true)
is LoginRequest.CreateCheckDateOfBirthCountry -> handleCreateCheckDateOfBirthCountry(ctx, msg)
is LoginRequest.CreateCheckName -> handleCreateCheckName(ctx, msg)
is LoginRequest.CreateAccount -> handleCreateAccount(ctx, msg)
@ -129,6 +135,15 @@ public class LoginChannelHandler @Inject constructor(
}
}
private fun handleGameLogin(ctx: ChannelHandlerContext, msg: GameLoginPayload, reconnect: Boolean) {
if (msg.build != BUILD || msg.js5ArchiveChecksums != js5ArchiveChecksums) {
ctx.write(LoginResponse.ClientOutOfDate).addListener(ChannelFutureListener.CLOSE)
return
}
// TODO
}
private fun handleInitJaggrabConnection(ctx: ChannelHandlerContext) {
ctx.pipeline().addLast(
DelimiterBasedFrameDecoder(JAGGRAB_MAX_FRAME_LENGTH, JAGGRAB_DELIMITER),

@ -71,6 +71,8 @@ import org.openrs2.protocol.login.upstream.CheckWorldSuitabilityCodec
import org.openrs2.protocol.login.upstream.CreateAccountCodec
import org.openrs2.protocol.login.upstream.CreateCheckDateOfBirthCountryCodec
import org.openrs2.protocol.login.upstream.CreateCheckNameCodec
import org.openrs2.protocol.login.upstream.GameLoginCodec
import org.openrs2.protocol.login.upstream.GameReconnectCodec
import org.openrs2.protocol.login.upstream.InitCrossDomainConnectionCodec
import org.openrs2.protocol.login.upstream.InitGameConnectionCodec
import org.openrs2.protocol.login.upstream.InitJaggrabConnectionCodec
@ -125,7 +127,9 @@ public object ProtocolModule : AbstractModule() {
LoginUpstream::class.java,
InitGameConnectionCodec::class.java,
InitJs5RemoteConnectionCodec::class.java,
GameLoginCodec::class.java,
InitJaggrabConnectionCodec::class.java,
GameReconnectCodec::class.java,
CreateCheckDateOfBirthCountryCodec::class.java,
CreateCheckNameCodec::class.java,
CreateAccountCodec::class.java,

@ -0,0 +1,19 @@
package org.openrs2.protocol.common
public enum class AntiAliasingMode {
NONE,
X2,
X4;
public companion object {
private val values = values()
public fun fromOrdinal(ordinal: Int): AntiAliasingMode? {
return if (ordinal >= 0 && ordinal < values.size) {
values[ordinal]
} else {
null
}
}
}
}

@ -0,0 +1,20 @@
package org.openrs2.protocol.common
public enum class DisplayMode {
SD,
HD_SMALL,
HD,
HD_FULLSCREEN;
public companion object {
private val values = values()
public fun fromOrdinal(ordinal: Int): DisplayMode? {
return if (ordinal >= 0 && ordinal < values.size) {
values[ordinal]
} else {
null
}
}
}
}

@ -0,0 +1,30 @@
package org.openrs2.protocol.common
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
public class Uid private constructor(
private val bytes: ByteArray,
) {
init {
require(bytes.size == LENGTH)
}
public fun write(buf: ByteBuf) {
buf.writeBytes(bytes)
}
public override fun toString(): String {
return ByteBufUtil.hexDump(bytes)
}
public companion object {
public const val LENGTH: Int = 24
public fun read(buf: ByteBuf): Uid {
val bytes = ByteArray(LENGTH)
buf.readBytes(bytes)
return Uid(bytes)
}
}
}

@ -0,0 +1,24 @@
package org.openrs2.protocol.login.upstream
import io.netty.buffer.ByteBuf
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters
import org.openrs2.crypto.StreamCipher
import org.openrs2.protocol.VariableShortPacketCodec
@Singleton
public class GameLoginCodec @Inject constructor(
private val key: RSAPrivateCrtKeyParameters,
) : VariableShortPacketCodec<LoginRequest.GameLogin>(
type = LoginRequest.GameLogin::class.java,
opcode = 16,
) {
override fun decode(input: ByteBuf, cipher: StreamCipher): LoginRequest.GameLogin {
return LoginRequest.GameLogin(GameLoginPayload.read(input, key))
}
override fun encode(input: LoginRequest.GameLogin, output: ByteBuf, cipher: StreamCipher) {
input.payload.write(output, key)
}
}

@ -0,0 +1,143 @@
package org.openrs2.protocol.login.upstream
import io.netty.buffer.ByteBuf
import org.bouncycastle.crypto.params.RSAKeyParameters
import org.openrs2.buffer.readString
import org.openrs2.buffer.use
import org.openrs2.buffer.writeString
import org.openrs2.crypto.Rsa
import org.openrs2.crypto.XteaKey
import org.openrs2.crypto.rsa
import org.openrs2.protocol.common.AntiAliasingMode
import org.openrs2.protocol.common.DisplayMode
import org.openrs2.protocol.common.Uid
import org.openrs2.util.Base37
public data class GameLoginPayload(
val build: Int,
val advertSuppressed: Boolean,
val clientSigned: Boolean,
val displayMode: DisplayMode,
val canvasWidth: Int,
val canvasHeight: Int,
val antiAliasingMode: AntiAliasingMode,
val uid: Uid,
val siteSettings: String,
val affiliate: Int,
val detailOptions: Int,
val verifyId: Int,
val js5ArchiveChecksums: List<Int>,
// TODO(gpe): XteaKey needs a better name, as it represents an ISAAC key here
val key: XteaKey,
val username: String,
val password: String,
) {
init {
require(js5ArchiveChecksums.size == JS5_ARCHIVES)
}
internal fun write(buf: ByteBuf, rsaKey: RSAKeyParameters) {
buf.writeInt(build)
buf.writeByte(0)
buf.writeBoolean(advertSuppressed)
buf.writeBoolean(clientSigned)
buf.writeByte(displayMode.ordinal)
buf.writeShort(canvasWidth)
buf.writeShort(canvasHeight)
buf.writeByte(antiAliasingMode.ordinal)
uid.write(buf)
buf.writeString(siteSettings)
buf.writeInt(affiliate)
buf.writeInt(detailOptions)
buf.writeShort(verifyId)
for (checksum in js5ArchiveChecksums) {
buf.writeInt(checksum)
}
buf.alloc().buffer().use { plaintext ->
plaintext.writeByte(Rsa.MAGIC)
plaintext.writeInt(key.k0)
plaintext.writeInt(key.k1)
plaintext.writeInt(key.k2)
plaintext.writeInt(key.k3)
plaintext.writeLong(Base37.encode(username))
plaintext.writeString(password)
plaintext.rsa(rsaKey).use { ciphertext ->
buf.writeByte(ciphertext.readableBytes())
buf.writeBytes(ciphertext)
}
}
}
public companion object {
public const val JS5_ARCHIVES: Int = 29
internal fun read(buf: ByteBuf, rsaKey: RSAKeyParameters): GameLoginPayload {
val build = buf.readInt()
require(buf.readUnsignedByte().toInt() == 0) {
"Unknown byte is non-zero"
}
val advertSuppressed = buf.readBoolean()
val clientSigned = buf.readBoolean()
val displayMode = DisplayMode.fromOrdinal(buf.readUnsignedByte().toInt())
?: throw IllegalArgumentException("Invalid DisplayMode")
val canvasWidth = buf.readUnsignedShort()
val canvasHeight = buf.readUnsignedShort()
val antiAliasingMode = AntiAliasingMode.fromOrdinal(buf.readUnsignedByte().toInt())
?: throw IllegalArgumentException("Invalid AntiAliasingMode")
val uid = Uid.read(buf)
val siteSettings = buf.readString()
val affiliate = buf.readInt()
val detailOptions = buf.readInt()
val verifyId = buf.readUnsignedShort()
val js5ArchiveChecksums = mutableListOf<Int>()
for (i in 0 until JS5_ARCHIVES) {
js5ArchiveChecksums += buf.readInt()
}
val ciphertextLen = buf.readUnsignedByte().toInt()
val ciphertext = buf.readSlice(ciphertextLen)
ciphertext.rsa(rsaKey).use { plaintext ->
require(plaintext.readUnsignedByte().toInt() == Rsa.MAGIC) {
"Invalid RSA magic"
}
val k0 = plaintext.readInt()
val k1 = plaintext.readInt()
val k2 = plaintext.readInt()
val k3 = plaintext.readInt()
val username = plaintext.readLong()
val password = plaintext.readString()
return GameLoginPayload(
build,
advertSuppressed,
clientSigned,
displayMode,
canvasWidth,
canvasHeight,
antiAliasingMode,
uid,
siteSettings,
affiliate,
detailOptions,
verifyId,
js5ArchiveChecksums,
XteaKey(k0, k1, k2, k3),
Base37.decodeLowerCase(username),
password,
)
}
}
}
}

@ -0,0 +1,24 @@
package org.openrs2.protocol.login.upstream
import io.netty.buffer.ByteBuf
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters
import org.openrs2.crypto.StreamCipher
import org.openrs2.protocol.VariableShortPacketCodec
@Singleton
public class GameReconnectCodec @Inject constructor(
private val key: RSAPrivateCrtKeyParameters,
) : VariableShortPacketCodec<LoginRequest.GameReconnect>(
type = LoginRequest.GameReconnect::class.java,
opcode = 18,
) {
override fun decode(input: ByteBuf, cipher: StreamCipher): LoginRequest.GameReconnect {
return LoginRequest.GameReconnect(GameLoginPayload.read(input, key))
}
override fun encode(input: LoginRequest.GameReconnect, output: ByteBuf, cipher: StreamCipher) {
input.payload.write(output, key)
}
}

@ -5,7 +5,9 @@ import org.openrs2.protocol.Packet
public sealed class LoginRequest : Packet {
public data class InitGameConnection(public val usernameHash: Int) : LoginRequest()
public data class InitJs5RemoteConnection(public val build: Int) : LoginRequest()
public data class GameLogin(public val payload: GameLoginPayload) : LoginRequest()
public object InitJaggrabConnection : LoginRequest()
public data class GameReconnect(public val payload: GameLoginPayload) : LoginRequest()
public data class CreateCheckDateOfBirthCountry(
public val year: Int,
public val month: Int,

Loading…
Cancel
Save