Open-source multiplayer game server compatible with the RuneScape client https://www.openrs2.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
openrs2/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt

318 lines
12 KiB

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
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.DelimiterBasedFrameDecoder
import io.netty.handler.codec.http.HttpObjectAggregator
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 jakarta.inject.Inject
import jakarta.inject.Provider
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
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
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.GameLoginPayload
import org.openrs2.protocol.login.upstream.LoginRequest
import org.openrs2.protocol.world.downstream.WorldListDownstream
import org.openrs2.protocol.world.downstream.WorldListResponse
import org.openrs2.util.Base37
import java.time.DateTimeException
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
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 val js5ArchiveChecksums = js5MasterIndex.entries.map(Js5MasterIndex.Entry::checksum)
private var usernameHash = 0
private var serverKey = 0L
override fun handlerAdded(ctx: ChannelHandlerContext) {
val exceptionHandler = CoroutineExceptionHandler { _, ex ->
ctx.fireExceptionCaught(ex)
}
scope = CoroutineScope(ctx.executor().asCoroutineDispatcher() + exceptionHandler)
}
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.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)
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())
ctx.read()
}
private fun handleInitJs5RemoteConnection(ctx: ChannelHandlerContext, msg: LoginRequest.InitJs5RemoteConnection) {
val encoder = ctx.pipeline().get(Rs2Encoder::class.java)
encoder.protocol = js5RemoteDownstreamProtocol
if (msg.build != BUILD) {
ctx.write(Js5LoginResponse.ClientOutOfDate).addListener(ChannelFutureListener.CLOSE)
return
}
ctx.pipeline().addLast(
XorDecoder(),
Js5RequestDecoder(),
Js5ResponseEncoder,
js5HandlerProvider.get()
)
ctx.pipeline().remove(Rs2Decoder::class.java)
ctx.write(Js5LoginResponse.Ok).addListener { future ->
if (future.isSuccess) {
ctx.pipeline().remove(encoder)
ctx.pipeline().remove(this)
}
}
}
private fun handleGameLogin(ctx: ChannelHandlerContext, msg: GameLoginPayload, reconnect: Boolean) {
if (msg.build != BUILD || msg.js5ArchiveChecksums != js5ArchiveChecksums) {
ctx.write(LoginResponse.ClientOutOfDate).addListener(ChannelFutureListener.CLOSE)
return
}
val usernameHash = ((Base37.encode(msg.username) shr 16) and 0x1F).toInt()
if (this.usernameHash != usernameHash) {
ctx.write(LoginResponse.InvalidLoginPacket).addListener(ChannelFutureListener.CLOSE)
return
}
val serverKey = (msg.key.k2.toLong() shl 32) or (msg.key.k3.toLong() and 0xFFFFFFFF)
if (this.serverKey != serverKey) {
ctx.write(LoginResponse.InvalidLoginPacket).addListener(ChannelFutureListener.CLOSE)
return
}
// TODO
}
private fun handleInitJaggrabConnection(ctx: ChannelHandlerContext) {
ctx.pipeline().addLast(
DelimiterBasedFrameDecoder(JAGGRAB_MAX_FRAME_LENGTH, JAGGRAB_DELIMITER),
StringDecoder(Charsets.UTF_8),
JaggrabRequestDecoder,
jaggrabHandler
)
ctx.pipeline().remove(Rs2Decoder::class.java)
ctx.pipeline().remove(Rs2Encoder::class.java)
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()
var checksum = worlds.hashCode()
/*
* Fix a 1 in 2^32 chance that we'll fail to fetch the original world
* list on startup.
*/
if (checksum == 0) {
checksum = 1
}
val worldList = if (checksum != msg.checksum) {
WorldListResponse.WorldList(worlds, checksum)
} else {
null
}
val encoder = ctx.pipeline().get(Rs2Encoder::class.java)
encoder.protocol = worldListDownstreamProtocol
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(),
HttpResponseEncoder(),
HttpObjectAggregator(Http.MAX_CONTENT_LENGTH),
CrossDomainChannelHandler
)
ctx.fireChannelRead(Unpooled.wrappedBuffer(G))
ctx.pipeline().remove(Rs2Decoder::class.java)
ctx.pipeline().remove(Rs2Encoder::class.java)
ctx.pipeline().remove(this)
}
override fun channelReadComplete(ctx: ChannelHandlerContext) {
ctx.flush()
}
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is IdleStateEvent) {
ctx.close()
}
}
private companion object {
private const val BUILD = 550
private const val JAGGRAB_MAX_FRAME_LENGTH = 4096
private val JAGGRAB_DELIMITER = Unpooled.unreleasableBuffer(copiedBuffer("\n\n"))
private val G = byteArrayOf('G'.code.toByte())
}
}