Archived
1
0
Fork 0
This repository has been archived on 2025-03-02. You can view files and clone it, but cannot push or open issues or pull requests.
uranos/blokk-server/src/main/kotlin/space/blokk/net/LoginAndJoinProcedure.kt

188 lines
7.2 KiB
Kotlin

package space.blokk.net
import io.netty.buffer.Unpooled
import space.blokk.BlokkServer
import space.blokk.Difficulty
import space.blokk.DifficultySettings
import space.blokk.chat.TextComponent
import space.blokk.event.ifCancelled
import space.blokk.net.MinecraftProtocolDataTypes.writeString
import space.blokk.net.event.PlayerInitializationEvent
import space.blokk.net.event.SessionAfterLoginEvent
import space.blokk.net.packet.login.*
import space.blokk.net.packet.play.*
import space.blokk.player.BlokkPlayer
import space.blokk.player.GameMode
import space.blokk.tags.TagRegistry
import space.blokk.util.*
import space.blokk.world.Chunk
import java.security.MessageDigest
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
class LoginAndJoinProcedure(val session: BlokkSession) {
private val tagsPacket by lazy { TagsPacket(TagRegistry.tags.values) }
suspend fun start(packet: LoginStartPacket) {
session.state.getOrFail<Session.State.WaitingForLoginStart>()
if (session.server.config.authenticateAndEncrypt) {
val verifyToken = EncryptionUtils.generateVerifyToken()
session.state = Session.State.WaitingForEncryptionVerification(packet.username, verifyToken)
session.send(EncryptionRequestPacket(session.server.x509EncodedPublicKey, verifyToken))
} else {
val uuid = UUID.nameUUIDFromBytes("OfflinePlayer:${packet.username}".toByteArray())
session.send(LoginSuccessPacket(uuid, packet.username))
session.state = Session.State.LoginSucceeded(packet.username, uuid)
afterLogin()
}
}
suspend fun onEncryptionResponse(packet: EncryptionResponsePacket) {
val state: Session.State.WaitingForEncryptionVerification = session.state.getOrFail()
val cipher: Cipher = Cipher.getInstance("RSA")
cipher.init(Cipher.DECRYPT_MODE, session.server.keyPair.private)
// Decrypt verify token
val verifyToken: ByteArray = cipher.doFinal(packet.verifyToken)
if (!verifyToken.contentEquals(state.verifyToken)) {
session.disconnect(TextComponent of "Encryption failed.")
return
}
// Decrypt shared secret
val sharedSecretKey = SecretKeySpec(cipher.doFinal(packet.sharedSecret), "AES")
session.state = Session.State.Encrypted(state.username)
session.enableEncryptionCodec(sharedSecretKey)
val hash = MessageDigest.getInstance("SHA-1")
hash.update(sharedSecretKey.encoded)
hash.update(session.server.x509EncodedPublicKey)
val hashString = EncryptionUtils.toMinecraftStyleSha1String(hash.digest())
val result = AuthenticationHelper.authenticate(hashString, state.username)
session.send(SetCompressionPacket(session.server.config.packetCompressionThreshold))
session.enableCompressionCodec()
session.send(LoginSuccessPacket(result.uuid, result.username))
session.state = Session.State.LoginSucceeded(result.username, result.uuid)
afterLogin()
}
private suspend fun afterLogin() {
val state: Session.State.LoginSucceeded = session.state.getOrFail()
val event = session.emit(SessionAfterLoginEvent(session))
val initialWorldAndLocation = event.initialWorldAndLocation
when {
event.cancelled -> session.disconnect()
initialWorldAndLocation == null -> {
session.logger warn "Could not join because no spawn location was set"
session.disconnect(loggableReason = "No spawn location set")
}
else -> {
val seedAsBytes = (initialWorldAndLocation.world.seed ?: Random.nextLong()).toByteArray()
// TODO: Spawn the player entity
session.send(
JoinGamePacket(
0,
event.gameMode,
event.hardcoreHearts,
initialWorldAndLocation.world.dimension,
sha256(seedAsBytes).toLong(),
initialWorldAndLocation.world.type,
event.maxViewDistance,
event.reducedDebugInfo,
event.respawnScreenEnabled
)
)
session.sendPluginMessage(
"minecraft:brand",
Unpooled.buffer().writeString("Blokk ${BlokkServer.VERSION_WITH_V}")
)
// As this is only visual, there is no way of changing this aside from intercepting the packet.
session.send(ServerDifficultyPacket(DifficultySettings(Difficulty.NORMAL, false)))
session.send(
PlayerAbilitiesPacket(
event.invulnerable,
event.flying,
event.canFly,
// TODO: Consider allowing to modify this value
event.gameMode == GameMode.CREATIVE,
// TODO: Find out how this relates to the entity property named `generic.flying_speed`
event.flyingSpeed,
event.fieldOfView
)
)
session.state = Session.State.WaitingForClientSettings(
state.username,
state.uuid,
event.flying,
event.canFly,
event.flyingSpeed,
event.fieldOfView,
event.gameMode,
initialWorldAndLocation,
event.invulnerable,
event.reducedDebugInfo,
event.selectedHotbarSlot
)
}
}
}
suspend fun onClientSettingsReceived(packet: ClientSettingsPacket) {
val state: Session.State.WaitingForClientSettings = session.state.getOrFail()
val settings = packet.asPlayerSettings()
session.emit(PlayerInitializationEvent(session, settings)).ifCancelled {
session.disconnect(loggableReason = "PlayerInitializationEvent was cancelled")
return
}
session.state = Session.State.JoiningWorld(
BlokkPlayer(
session,
state.username,
state.uuid,
state.gameMode,
settings,
state.initialWorldAndLocation.world,
state.initialWorldAndLocation.location,
state.reducedDebugInfo,
state.fieldOfView,
state.canFly,
state.flying,
state.flyingSpeed,
state.invulnerable,
state.selectedHotbarSlot
)
)
session.send(SetSelectedHotbarSlotPacket(state.selectedHotbarSlot))
session.send(DeclareRecipesPacket(session.server.recipes))
session.send(tagsPacket)
// TODO: Send Entity Status packet with OP permission level
session.send(PlayerPositionAndLookPacket(state.initialWorldAndLocation.location))
// TODO: Send PlayerInfo packet
session.send(UpdateViewPositionPacket(Chunk.Key.from(session.player!!.location.asVoxelLocation())))
}
}