205 lines
7.6 KiB
Kotlin
205 lines
7.6 KiB
Kotlin
package space.blokk.net
|
|
|
|
import io.netty.buffer.Unpooled
|
|
import space.blokk.BlokkServer
|
|
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.tag.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
|
|
|
|
class LoginAndJoinProcedure(val session: BlokkSession) {
|
|
private val tagsPacket by lazy { TagsPacket(TagRegistry.tags) }
|
|
|
|
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 -> {
|
|
// TODO: Spawn the player entity
|
|
session.send(
|
|
JoinGamePacket(
|
|
0,
|
|
event.gameMode,
|
|
event.hardcoreHearts,
|
|
initialWorldAndLocation.world,
|
|
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 it 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
|
|
}
|
|
|
|
val player = 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.state = Session.State.Joining(player)
|
|
|
|
session.send(SetSelectedHotbarSlotPacket(state.selectedHotbarSlot))
|
|
|
|
session.send(DeclareRecipesPacket(session.server.recipeRegistry.items.values))
|
|
session.send(tagsPacket)
|
|
|
|
// DeclareCommands
|
|
// UnlockRecipes
|
|
|
|
session.send(PlayerPositionAndLookPacket(state.initialWorldAndLocation.location))
|
|
|
|
session.send(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer((session.server.players + player).map {
|
|
it.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
|
|
it.name,
|
|
it.gameMode,
|
|
it.rtt,
|
|
it.playerListName,
|
|
emptyMap() // TODO: Load skin
|
|
)
|
|
}.toMap())))
|
|
|
|
session.send(
|
|
PlayerInfoPacket(
|
|
PlayerInfoPacket.Action.UpdateLatency(session.server.players.map { it.uuid to it.rtt }.toMap())
|
|
)
|
|
)
|
|
|
|
session.send(UpdateViewPositionPacket(Chunk.Key.from(player.location.asVoxelLocation())))
|
|
|
|
player.sendChunksAndLight()
|
|
|
|
// WorldBorder
|
|
// TODO: Send SpawnPosition packet
|
|
|
|
session.send(PlayerPositionAndLookPacket(state.initialWorldAndLocation.location))
|
|
|
|
// TODO: Wait for ClientStatus(action=0) packet
|
|
}
|
|
}
|