From dc8fcd09f6537103332ecc10008dcc51e2ac4fb5 Mon Sep 17 00:00:00 2001 From: Graham Date: Fri, 30 Sep 2022 19:46:35 +0100 Subject: [PATCH] Flesh out LoginChannelHandler This commit adds initial support for negotiating the ISAAC session key, creating accounts, and checking world suitability. Signed-off-by: Graham --- game/build.gradle.kts | 3 + .../kotlin/org/openrs2/game/GameModule.kt | 5 + .../game/net/login/LoginChannelHandler.kt | 134 ++++++++++++++++++ .../openrs2/game/store/DummyPlayerStore.kt | 29 ++++ .../org/openrs2/game/store/PlayerStore.kt | 21 +++ gradle/libs.versions.toml | 2 + 6 files changed, 194 insertions(+) create mode 100644 game/src/main/kotlin/org/openrs2/game/store/DummyPlayerStore.kt create mode 100644 game/src/main/kotlin/org/openrs2/game/store/PlayerStore.kt diff --git a/game/build.gradle.kts b/game/build.gradle.kts index 645fad521c..4fdeef3e15 100644 --- a/game/build.gradle.kts +++ b/game/build.gradle.kts @@ -20,7 +20,10 @@ dependencies { implementation(projects.protocol) implementation(projects.util) implementation(libs.guava) + implementation(libs.kotlin.coroutines.core) implementation(libs.netty.codec.http) + implementation(libs.result.core) + implementation(libs.result.coroutines) } publishing { diff --git a/game/src/main/kotlin/org/openrs2/game/GameModule.kt b/game/src/main/kotlin/org/openrs2/game/GameModule.kt index 0ffb222c96..a507b868de 100644 --- a/game/src/main/kotlin/org/openrs2/game/GameModule.kt +++ b/game/src/main/kotlin/org/openrs2/game/GameModule.kt @@ -17,6 +17,8 @@ import org.openrs2.game.cluster.Cluster import org.openrs2.game.cluster.SingleWorldCluster import org.openrs2.game.net.NetworkService import org.openrs2.game.net.js5.Js5Service +import org.openrs2.game.store.DummyPlayerStore +import org.openrs2.game.store.PlayerStore import org.openrs2.net.NetworkModule import org.openrs2.protocol.ProtocolModule @@ -47,5 +49,8 @@ public object GameModule : AbstractModule() { bind(Cluster::class.java) .to(SingleWorldCluster::class.java) + + bind(PlayerStore::class.java) + .to(DummyPlayerStore::class.java) } } diff --git a/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt b/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt index e6c84d98ca..97f481c0cd 100644 --- a/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt +++ b/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt @@ -1,5 +1,11 @@ package org.openrs2.game.net.login +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.and +import com.github.michaelbull.result.coroutines.binding.binding +import com.github.michaelbull.result.getErrorOr import io.netty.buffer.Unpooled import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext @@ -10,50 +16,88 @@ import io.netty.handler.codec.http.HttpRequestDecoder import io.netty.handler.codec.http.HttpResponseEncoder import io.netty.handler.codec.string.StringDecoder import io.netty.handler.timeout.IdleStateEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import org.openrs2.buffer.copiedBuffer +import org.openrs2.conf.CountryCode +import org.openrs2.crypto.secureRandom import org.openrs2.game.cluster.Cluster import org.openrs2.game.net.crossdomain.CrossDomainChannelHandler import org.openrs2.game.net.http.Http import org.openrs2.game.net.jaggrab.JaggrabChannelHandler import org.openrs2.game.net.js5.Js5ChannelHandler +import org.openrs2.game.store.PlayerStore import org.openrs2.protocol.Protocol import org.openrs2.protocol.Rs2Decoder import org.openrs2.protocol.Rs2Encoder +import org.openrs2.protocol.create.downstream.CreateDownstream +import org.openrs2.protocol.create.downstream.CreateResponse import org.openrs2.protocol.jaggrab.upstream.JaggrabRequestDecoder import org.openrs2.protocol.js5.downstream.Js5LoginResponse import org.openrs2.protocol.js5.downstream.Js5RemoteDownstream 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.LoginRequest import org.openrs2.protocol.world.downstream.WorldListDownstream import org.openrs2.protocol.world.downstream.WorldListResponse +import java.time.DateTimeException +import java.time.LocalDate import javax.inject.Inject import javax.inject.Provider public class LoginChannelHandler @Inject constructor( private val cluster: Cluster, + private val store: PlayerStore, private val js5HandlerProvider: Provider, private val jaggrabHandler: JaggrabChannelHandler, + @CreateDownstream + private val createDownstreamProtocol: Protocol, @Js5RemoteDownstream private val js5RemoteDownstreamProtocol: Protocol, @WorldListDownstream private val worldListDownstreamProtocol: Protocol ) : SimpleChannelInboundHandler(LoginRequest::class.java) { + private lateinit var scope: CoroutineScope + private var usernameHash = 0 + private var serverKey = 0L + + override fun handlerAdded(ctx: ChannelHandlerContext) { + scope = CoroutineScope(ctx.executor().asCoroutineDispatcher()) + } + + override fun handlerRemoved(ctx: ChannelHandlerContext) { + scope.cancel() + } + override fun channelActive(ctx: ChannelHandlerContext) { ctx.read() } override fun channelRead0(ctx: ChannelHandlerContext, msg: LoginRequest) { when (msg) { + is LoginRequest.InitGameConnection -> handleInitGameConnection(ctx, msg) is LoginRequest.InitJs5RemoteConnection -> handleInitJs5RemoteConnection(ctx, msg) is LoginRequest.InitJaggrabConnection -> handleInitJaggrabConnection(ctx) + is LoginRequest.CreateCheckDateOfBirthCountry -> handleCreateCheckDateOfBirthCountry(ctx, msg) + is LoginRequest.CreateCheckName -> handleCreateCheckName(ctx, msg) + is LoginRequest.CreateAccount -> handleCreateAccount(ctx, msg) is LoginRequest.RequestWorldList -> handleRequestWorldList(ctx, msg) + is LoginRequest.CheckWorldSuitability -> handleCheckWorldSuitability(ctx, msg) is LoginRequest.InitCrossDomainConnection -> handleInitCrossDomainConnection(ctx) else -> Unit } } + private fun handleInitGameConnection(ctx: ChannelHandlerContext, msg: LoginRequest.InitGameConnection) { + usernameHash = msg.usernameHash + serverKey = secureRandom.nextLong() + ctx.write(LoginResponse.ExchangeSessionKey(serverKey), ctx.voidPromise()) + } + private fun handleInitJs5RemoteConnection(ctx: ChannelHandlerContext, msg: LoginRequest.InitJs5RemoteConnection) { val encoder = ctx.pipeline().get(Rs2Encoder::class.java) encoder.protocol = js5RemoteDownstreamProtocol @@ -91,6 +135,88 @@ public class LoginChannelHandler @Inject constructor( ctx.pipeline().remove(this) } + private fun validateDateOfBirth(year: Int, month: Int, day: Int): Result { + val date = try { + LocalDate.of(year, month + 1, day) + } catch (ex: DateTimeException) { + return Err(CreateResponse.DateOfBirthInvalid) + } + + val now = LocalDate.now() + if (date.isAfter(now)) { + return Err(CreateResponse.DateOfBirthFuture) + } else if (date.year == now.year) { + return Err(CreateResponse.DateOfBirthThisYear) + } else if (date.year == (now.year - 1)) { + return Err(CreateResponse.DateOfBirthLastYear) + } + + return Ok(date) + } + + private fun validateCountry(id: Int): Result { + // TODO + + return Ok(CountryCode.GB) + } + + private fun handleCreateCheckDateOfBirthCountry( + ctx: ChannelHandlerContext, + msg: LoginRequest.CreateCheckDateOfBirthCountry + ) { + val encoder = ctx.pipeline().get(Rs2Encoder::class.java) + encoder.protocol = createDownstreamProtocol + + val response = validateDateOfBirth(msg.year, msg.month, msg.day) + .and(validateCountry(msg.country)) + .getErrorOr(CreateResponse.Ok) + + ctx.write(response).addListener(ChannelFutureListener.CLOSE) + } + + private fun handleCreateCheckName(ctx: ChannelHandlerContext, msg: LoginRequest.CreateCheckName) { + val encoder = ctx.pipeline().get(Rs2Encoder::class.java) + encoder.protocol = createDownstreamProtocol + + scope.launch { + val response = store.checkName(msg.username) + .getErrorOr(CreateResponse.Ok) + + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE) + } + } + + private fun handleCreateAccount(ctx: ChannelHandlerContext, msg: LoginRequest.CreateAccount) { + val encoder = ctx.pipeline().get(Rs2Encoder::class.java) + encoder.protocol = createDownstreamProtocol + + if (msg.build != BUILD) { + ctx.write(CreateResponse.ClientOutOfDate).addListener(ChannelFutureListener.CLOSE) + return + } + + scope.launch { + val response = binding { + val dateOfBirth = validateDateOfBirth(msg.year, msg.month, msg.day).bind() + val country = validateCountry(msg.country).bind() + + store.create( + msg.gameNewsletters, + msg.otherNewsletters, + msg.shareDetailsWithBusinessPartners, + msg.username, + msg.password, + msg.affiliate, + dateOfBirth, + country, + msg.email, + ).bind() + }.getErrorOr(CreateResponse.Ok) + + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE) + } + } + private fun handleRequestWorldList(ctx: ChannelHandlerContext, msg: LoginRequest.RequestWorldList) { val (worlds, players) = cluster.getWorldList() @@ -116,6 +242,14 @@ public class LoginChannelHandler @Inject constructor( ctx.write(WorldListResponse(worldList, players)).addListener(ChannelFutureListener.CLOSE) } + private fun handleCheckWorldSuitability(ctx: ChannelHandlerContext, msg: LoginRequest.CheckWorldSuitability) { + // TODO + + val (worlds, _) = cluster.getWorldList() + val id = worlds.firstKey() + ctx.write(LoginResponse.SwitchWorld(id)).addListener(ChannelFutureListener.CLOSE) + } + private fun handleInitCrossDomainConnection(ctx: ChannelHandlerContext) { ctx.pipeline().addLast( HttpRequestDecoder(), diff --git a/game/src/main/kotlin/org/openrs2/game/store/DummyPlayerStore.kt b/game/src/main/kotlin/org/openrs2/game/store/DummyPlayerStore.kt new file mode 100644 index 0000000000..3d513454af --- /dev/null +++ b/game/src/main/kotlin/org/openrs2/game/store/DummyPlayerStore.kt @@ -0,0 +1,29 @@ +package org.openrs2.game.store + +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result +import org.openrs2.conf.CountryCode +import org.openrs2.protocol.create.downstream.CreateResponse +import java.time.LocalDate +import javax.inject.Singleton + +@Singleton +public class DummyPlayerStore : PlayerStore { + override suspend fun checkName(username: String): Result { + return Ok(Unit) + } + + override suspend fun create( + gameNewsletters: Boolean, + otherNewsletters: Boolean, + shareDetailsWithBusinessPartners: Boolean, + username: String, + password: String, + affiliate: Int, + dateOfBirth: LocalDate, + country: CountryCode, + email: String + ): Result { + return Ok(Unit) + } +} diff --git a/game/src/main/kotlin/org/openrs2/game/store/PlayerStore.kt b/game/src/main/kotlin/org/openrs2/game/store/PlayerStore.kt new file mode 100644 index 0000000000..8c0d64798b --- /dev/null +++ b/game/src/main/kotlin/org/openrs2/game/store/PlayerStore.kt @@ -0,0 +1,21 @@ +package org.openrs2.game.store + +import com.github.michaelbull.result.Result +import org.openrs2.conf.CountryCode +import org.openrs2.protocol.create.downstream.CreateResponse +import java.time.LocalDate + +public interface PlayerStore { + public suspend fun checkName(username: String): Result + public suspend fun create( + gameNewsletters: Boolean, + otherNewsletters: Boolean, + shareDetailsWithBusinessPartners: Boolean, + username: String, + password: String, + affiliate: Int, + dateOfBirth: LocalDate, + country: CountryCode, + email: String, + ): Result +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07433603eb..297c2aa23e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -77,6 +77,8 @@ netty-transport = { module = "io.netty:netty-transport", version.ref = "netty" } openrs2-natives = { module = "org.openrs2:openrs2-natives-all", version = "3.2.0" } pf4j = { module = "org.pf4j:pf4j", version = "3.7.0" } postgres = { module = "org.postgresql:postgresql", version = "42.5.0" } +result-core = { module = "com.michael-bull.kotlin-result:kotlin-result", version = "1.1.16" } +result-coroutines = { module = "com.michael-bull.kotlin-result:kotlin-result-coroutines", version = "1.1.16" } runelite-client = { module = "net.runelite:client", version = "1.8.34" } sqlite = { module = "org.xerial:sqlite-jdbc", version = "3.39.3.0" } thymeleaf-core = { module = "org.thymeleaf:thymeleaf", version = "3.0.15.RELEASE" }