188 lines
7.2 KiB
Kotlin
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())))
|
|
}
|
|
}
|