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>
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.protocol)
implementation(projects.util) implementation(projects.util)
implementation(libs.guava) implementation(libs.guava)
implementation(libs.kotlin.coroutines.core)
implementation(libs.netty.codec.http) implementation(libs.netty.codec.http)
implementation(libs.result.core)
implementation(libs.result.coroutines)
} }
publishing { publishing {

@ -17,6 +17,8 @@ import org.openrs2.game.cluster.Cluster
import org.openrs2.game.cluster.SingleWorldCluster import org.openrs2.game.cluster.SingleWorldCluster
import org.openrs2.game.net.NetworkService import org.openrs2.game.net.NetworkService
import org.openrs2.game.net.js5.Js5Service 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.net.NetworkModule
import org.openrs2.protocol.ProtocolModule import org.openrs2.protocol.ProtocolModule
@ -47,5 +49,8 @@ public object GameModule : AbstractModule() {
bind(Cluster::class.java) bind(Cluster::class.java)
.to(SingleWorldCluster::class.java) .to(SingleWorldCluster::class.java)
bind(PlayerStore::class.java)
.to(DummyPlayerStore::class.java)
} }
} }

@ -1,5 +1,11 @@
package org.openrs2.game.net.login 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.buffer.Unpooled
import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandlerContext 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.http.HttpResponseEncoder
import io.netty.handler.codec.string.StringDecoder import io.netty.handler.codec.string.StringDecoder
import io.netty.handler.timeout.IdleStateEvent 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.buffer.copiedBuffer
import org.openrs2.conf.CountryCode
import org.openrs2.crypto.secureRandom
import org.openrs2.game.cluster.Cluster import org.openrs2.game.cluster.Cluster
import org.openrs2.game.net.crossdomain.CrossDomainChannelHandler import org.openrs2.game.net.crossdomain.CrossDomainChannelHandler
import org.openrs2.game.net.http.Http import org.openrs2.game.net.http.Http
import org.openrs2.game.net.jaggrab.JaggrabChannelHandler import org.openrs2.game.net.jaggrab.JaggrabChannelHandler
import org.openrs2.game.net.js5.Js5ChannelHandler import org.openrs2.game.net.js5.Js5ChannelHandler
import org.openrs2.game.store.PlayerStore
import org.openrs2.protocol.Protocol import org.openrs2.protocol.Protocol
import org.openrs2.protocol.Rs2Decoder import org.openrs2.protocol.Rs2Decoder
import org.openrs2.protocol.Rs2Encoder 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.jaggrab.upstream.JaggrabRequestDecoder
import org.openrs2.protocol.js5.downstream.Js5LoginResponse import org.openrs2.protocol.js5.downstream.Js5LoginResponse
import org.openrs2.protocol.js5.downstream.Js5RemoteDownstream import org.openrs2.protocol.js5.downstream.Js5RemoteDownstream
import org.openrs2.protocol.js5.downstream.Js5ResponseEncoder import org.openrs2.protocol.js5.downstream.Js5ResponseEncoder
import org.openrs2.protocol.js5.downstream.XorDecoder import org.openrs2.protocol.js5.downstream.XorDecoder
import org.openrs2.protocol.js5.upstream.Js5RequestDecoder 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.login.upstream.LoginRequest
import org.openrs2.protocol.world.downstream.WorldListDownstream import org.openrs2.protocol.world.downstream.WorldListDownstream
import org.openrs2.protocol.world.downstream.WorldListResponse import org.openrs2.protocol.world.downstream.WorldListResponse
import java.time.DateTimeException
import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
public class LoginChannelHandler @Inject constructor( public class LoginChannelHandler @Inject constructor(
private val cluster: Cluster, private val cluster: Cluster,
private val store: PlayerStore,
private val js5HandlerProvider: Provider<Js5ChannelHandler>, private val js5HandlerProvider: Provider<Js5ChannelHandler>,
private val jaggrabHandler: JaggrabChannelHandler, private val jaggrabHandler: JaggrabChannelHandler,
@CreateDownstream
private val createDownstreamProtocol: Protocol,
@Js5RemoteDownstream @Js5RemoteDownstream
private val js5RemoteDownstreamProtocol: Protocol, private val js5RemoteDownstreamProtocol: Protocol,
@WorldListDownstream @WorldListDownstream
private val worldListDownstreamProtocol: Protocol private val worldListDownstreamProtocol: Protocol
) : SimpleChannelInboundHandler<LoginRequest>(LoginRequest::class.java) { ) : 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) { override fun channelActive(ctx: ChannelHandlerContext) {
ctx.read() ctx.read()
} }
override fun channelRead0(ctx: ChannelHandlerContext, msg: LoginRequest) { override fun channelRead0(ctx: ChannelHandlerContext, msg: LoginRequest) {
when (msg) { when (msg) {
is LoginRequest.InitGameConnection -> handleInitGameConnection(ctx, msg)
is LoginRequest.InitJs5RemoteConnection -> handleInitJs5RemoteConnection(ctx, msg) is LoginRequest.InitJs5RemoteConnection -> handleInitJs5RemoteConnection(ctx, msg)
is LoginRequest.InitJaggrabConnection -> handleInitJaggrabConnection(ctx) 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.RequestWorldList -> handleRequestWorldList(ctx, msg)
is LoginRequest.CheckWorldSuitability -> handleCheckWorldSuitability(ctx, msg)
is LoginRequest.InitCrossDomainConnection -> handleInitCrossDomainConnection(ctx) is LoginRequest.InitCrossDomainConnection -> handleInitCrossDomainConnection(ctx)
else -> Unit 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) { private fun handleInitJs5RemoteConnection(ctx: ChannelHandlerContext, msg: LoginRequest.InitJs5RemoteConnection) {
val encoder = ctx.pipeline().get(Rs2Encoder::class.java) val encoder = ctx.pipeline().get(Rs2Encoder::class.java)
encoder.protocol = js5RemoteDownstreamProtocol encoder.protocol = js5RemoteDownstreamProtocol
@ -91,6 +135,88 @@ public class LoginChannelHandler @Inject constructor(
ctx.pipeline().remove(this) 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) { private fun handleRequestWorldList(ctx: ChannelHandlerContext, msg: LoginRequest.RequestWorldList) {
val (worlds, players) = cluster.getWorldList() val (worlds, players) = cluster.getWorldList()
@ -116,6 +242,14 @@ public class LoginChannelHandler @Inject constructor(
ctx.write(WorldListResponse(worldList, players)).addListener(ChannelFutureListener.CLOSE) 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) { private fun handleInitCrossDomainConnection(ctx: ChannelHandlerContext) {
ctx.pipeline().addLast( ctx.pipeline().addLast(
HttpRequestDecoder(), 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" } openrs2-natives = { module = "org.openrs2:openrs2-natives-all", version = "3.2.0" }
pf4j = { module = "org.pf4j:pf4j", version = "3.7.0" } pf4j = { module = "org.pf4j:pf4j", version = "3.7.0" }
postgres = { module = "org.postgresql:postgresql", version = "42.5.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" } runelite-client = { module = "net.runelite:client", version = "1.8.34" }
sqlite = { module = "org.xerial:sqlite-jdbc", version = "3.39.3.0" } sqlite = { module = "org.xerial:sqlite-jdbc", version = "3.39.3.0" }
thymeleaf-core = { module = "org.thymeleaf:thymeleaf", version = "3.0.15.RELEASE" } thymeleaf-core = { module = "org.thymeleaf:thymeleaf", version = "3.0.15.RELEASE" }

Loading…
Cancel
Save