Flesh out LoginChannelHandler

This commit adds initial support for negotiating the ISAAC session key,
creating accounts, and checking world suitability.

Signed-off-by: Graham <gpe@openrs2.org>
master
Graham 2 years ago
parent cf7c05441c
commit dc8fcd09f6
  1. 3
      game/build.gradle.kts
  2. 5
      game/src/main/kotlin/org/openrs2/game/GameModule.kt
  3. 134
      game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt
  4. 29
      game/src/main/kotlin/org/openrs2/game/store/DummyPlayerStore.kt
  5. 21
      game/src/main/kotlin/org/openrs2/game/store/PlayerStore.kt
  6. 2
      gradle/libs.versions.toml

@ -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 {

@ -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)
}
}

@ -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<Js5ChannelHandler>,
private val jaggrabHandler: JaggrabChannelHandler,
@CreateDownstream
private val createDownstreamProtocol: Protocol,
@Js5RemoteDownstream
private val js5RemoteDownstreamProtocol: Protocol,
@WorldListDownstream
private val worldListDownstreamProtocol: Protocol
) : SimpleChannelInboundHandler<LoginRequest>(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<LocalDate, CreateResponse> {
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<CountryCode, CreateResponse> {
// 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(),

@ -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<Unit, CreateResponse> {
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<Unit, CreateResponse> {
return Ok(Unit)
}
}

@ -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<Unit, CreateResponse>
public suspend fun create(
gameNewsletters: Boolean,
otherNewsletters: Boolean,
shareDetailsWithBusinessPartners: Boolean,
username: String,
password: String,
affiliate: Int,
dateOfBirth: LocalDate,
country: CountryCode,
email: String,
): Result<Unit, CreateResponse>
}

@ -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" }

Loading…
Cancel
Save