Archived
1
0
Fork 0

Add more join procedure logic and packets

This commit is contained in:
Moritz Ruth 2020-08-22 13:13:58 +02:00
parent 61d9ec8d2b
commit f80ee719f4
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
40 changed files with 678 additions and 92 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
# Project exclude paths
/.gradle/
/build/
/blokk-api/build/
/blokk-server/build/
/data

View file

@ -0,0 +1,8 @@
package space.blokk
enum class GameMode {
SURVIVAL,
ADVENTURE,
CREATIVE,
SPECTATOR
}

View file

@ -2,4 +2,24 @@ package space.blokk.events
interface EventTarget<T : Event> {
val eventBus: EventBus<T>
/**
* Shorthand for [`eventBus.emit(event)`][EventBus.emit].
*/
suspend fun emit(event: T) = eventBus.emit(event)
/**
* Shorthand for [`eventBus.emitAsync(event)`][EventBus.emitAsync].
*/
fun emitAsync(event: T) = eventBus.emitAsync(event)
/**
* Shorthand for [`eventBus.register(listener)`][EventBus.register].
*/
fun registerListener(listener: Listener) = eventBus.register(listener)
/**
* Shorthand for [`eventBus.unregister(listener)`][EventBus.unregister].
*/
fun unregisterListener(listener: Listener) = eventBus.unregister(listener)
}

View file

@ -60,7 +60,7 @@ object MinecraftDataTypes {
*
* @see <a href="https://wiki.vg/Protocol#VarInt_and_VarLong">https://wiki.vg/Protocol#VarInt_and_VarLong</a>
*/
fun ByteBuf.writeVarInt(value: Int) {
fun ByteBuf.writeVarInt(value: Int): ByteBuf {
var v = value
var part: Byte
while (true) {
@ -74,6 +74,8 @@ object MinecraftDataTypes {
break
}
}
return this
}
/**
@ -95,9 +97,11 @@ object MinecraftDataTypes {
*
* @see <a href="https://wiki.vg/Protocol#Data_types">https://wiki.vg/Protocol#Data_types</a>
*/
fun ByteBuf.writeString(value: String) {
fun ByteBuf.writeString(value: String): ByteBuf {
val bytes = value.toByteArray(Charsets.UTF_8)
writeVarInt(bytes.size)
writeBytes(bytes)
return this
}
}

View file

@ -0,0 +1,3 @@
package space.blokk.net
class PacketDecodingException : Exception("Packet decoding failed")

View file

@ -1,5 +1,6 @@
package space.blokk.net
import io.netty.buffer.ByteBuf
import kotlinx.coroutines.CoroutineScope
import space.blokk.events.EventTarget
import space.blokk.net.events.SessionEvent
@ -11,9 +12,31 @@ interface Session : EventTarget<SessionEvent> {
/**
* The protocol this session is currently using.
*/
var currentProtocol: Protocol
val currentProtocol: Protocol
/**
* The IP address of this session
*/
val address: InetAddress
/**
* The coroutine scope of this session. It is cancelled when the session disconnects.
*/
val scope: CoroutineScope
/**
* Sends a packet.
*/
suspend fun send(packet: OutgoingPacket)
/**
* Sends a plugin message packet.
*/
suspend fun sendPluginMessage(channel: String, data: ByteBuf)
/**
* The brand name the client optionally sent during the login procedure.
* [ClientBrandReceivedEvent][space.blokk.net.events.ClientBrandReceivedEvent] is emitted when this value changes.
*/
val brand: String?
}

View file

@ -0,0 +1,11 @@
package space.blokk.net.events
import space.blokk.net.Session
/**
* Emitted when the client sends his brand during the login process.
*/
class ClientBrandReceivedEvent(
session: Session,
val brand: String
) : SessionEvent(session)

View file

@ -4,6 +4,9 @@ import space.blokk.events.Cancellable
import space.blokk.net.Session
import space.blokk.net.protocols.IncomingPacket
/**
* Emitted when a packet is received. **You should only listen to this event when there is no other option.**
*/
class PacketReceivedEvent<T : IncomingPacket>(session: Session, var packet: T) : SessionEvent(session), Cancellable {
override var isCancelled = false
}

View file

@ -4,6 +4,9 @@ import space.blokk.events.Cancellable
import space.blokk.net.Session
import space.blokk.net.protocols.OutgoingPacket
/**
* Emitted when a packet is going to be sent. **You should only listen to this event when there is no other option.**
*/
class PacketSendEvent(session: Session, var packet: OutgoingPacket) : SessionEvent(session), Cancellable {
override var isCancelled = false
}

View file

@ -4,6 +4,9 @@ import space.blokk.events.Cancellable
import space.blokk.net.Session
import space.blokk.net.protocols.status.ResponsePacket
/**
* Emitted when a client requests general info about the server, most likely to show it in the server list.
*/
class ServerListInfoRequestEvent(
session: Session,
var response: ResponsePacket

View file

@ -1,6 +1,6 @@
package space.blokk.net.protocols
abstract class Protocol internal constructor(val name: String, packets: Set<PacketCompanion<*>>) {
abstract class Protocol internal constructor(val name: String, vararg packets: PacketCompanion<*>) {
val incomingPackets = packets.filterIsInstance<IncomingPacketCompanion<*>>()
val incomingPacketsByID = incomingPackets.mapToIDMap()
val outgoingPackets = packets.filterIsInstance<OutgoingPacketCompanion<*>>()

View file

@ -2,4 +2,4 @@ package space.blokk.net.protocols.handshaking
import space.blokk.net.protocols.Protocol
object HandshakingProtocol : Protocol("HANDSHAKING", setOf(HandshakePacket))
object HandshakingProtocol : Protocol("HANDSHAKING", HandshakePacket)

View file

@ -1,6 +1,7 @@
package space.blokk.net.protocols.login
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
@ -16,11 +17,33 @@ import space.blokk.net.protocols.OutgoingPacketCompanion
data class LoginPluginRequestPacket(
val messageID: Int,
val channel: String,
val data: ByteBuf
val data: ByteArray
) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<LoginPluginRequestPacket>(0x04, LoginPluginRequestPacket::class)
override fun encode(dst: ByteBuf) {
dst.writeVarInt(messageID)
dst.writeString(channel)
dst.writeBytes(data)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LoginPluginRequestPacket
if (messageID != other.messageID) return false
if (channel != other.channel) return false
if (!data.contentEquals(other.data)) return false
return true
}
override fun hashCode(): Int {
var result = messageID
result = 31 * result + channel.hashCode()
result = 31 * result + data.contentHashCode()
return result
}
}

View file

@ -3,14 +3,13 @@ package space.blokk.net.protocols.login
import space.blokk.net.protocols.Protocol
object LoginProtocol : Protocol(
"LOGIN", setOf(
DisconnectPacket,
LoginStartPacket,
EncryptionRequestPacket,
EncryptionResponsePacket,
SetCompressionPacket,
LoginSuccessPacket,
LoginPluginRequestPacket,
LoginPluginResponsePacket
)
"LOGIN",
DisconnectPacket,
LoginStartPacket,
EncryptionRequestPacket,
EncryptionResponsePacket,
SetCompressionPacket,
LoginSuccessPacket,
LoginPluginRequestPacket,
LoginPluginResponsePacket
)

View file

@ -0,0 +1,67 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.PacketDecodingException
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
import space.blokk.players.ChatMode
import space.blokk.players.DisplayedSkinParts
import space.blokk.players.MainHand
import space.blokk.utils.checkBit
/**
* Sent by the client when the player joins or the settings changed.
*/
data class ClientSettingsPacket(
val locale: String,
val viewDistance: Byte,
val chatMode: ChatMode,
val chatColorsEnabled: Boolean,
val displayedSkinParts: DisplayedSkinParts,
val mainHand: MainHand
) : IncomingPacket() {
companion object : IncomingPacketCompanion<ClientSettingsPacket>(0x05, ClientSettingsPacket::class) {
override fun decode(msg: ByteBuf): ClientSettingsPacket {
return with(MinecraftDataTypes) {
val locale = msg.readString()
val viewDistance = msg.readByte()
val chatMode = when (msg.readVarInt()) {
0 -> ChatMode.ENABLED
1 -> ChatMode.COMMANDS_ONLY
2 -> ChatMode.HIDDEN
else -> throw PacketDecodingException()
}
val chatColorsEnabled = msg.readBoolean()
val skinFlags = msg.readByte()
val displayedSkinParts = DisplayedSkinParts(
cape = skinFlags.checkBit(0),
jacket = skinFlags.checkBit(1),
leftSleeve = skinFlags.checkBit(2),
rightSleeve = skinFlags.checkBit(3),
leftPantsLeg = skinFlags.checkBit(4),
rightPantsLeg = skinFlags.checkBit(5),
hat = skinFlags.checkBit(6)
)
val mainHand = when (msg.readVarInt()) {
0 -> MainHand.LEFT
1 -> MainHand.RIGHT
else -> throw PacketDecodingException()
}
ClientSettingsPacket(
locale,
viewDistance,
chatMode,
chatColorsEnabled,
displayedSkinParts,
mainHand
)
}
}
}
}

View file

@ -0,0 +1,46 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
/**
* Can be used by client-side mods to communicate with a potentially modded server.
* Minecraft itself [also uses this for some things](https://wiki.vg/Plugin_channels#Channels_internal_to_Minecraft).
*
* @param channel Channel in which the message was sent.
* @param data Data which was sent.
*/
data class IncomingPluginMessagePacket(val channel: String, val data: ByteArray) : IncomingPacket() {
companion object : IncomingPacketCompanion<IncomingPluginMessagePacket>(0x0B, IncomingPluginMessagePacket::class) {
override fun decode(msg: ByteBuf): IncomingPluginMessagePacket {
return with(MinecraftDataTypes) {
val channel = msg.readString()
// TODO: Test this
val data = ByteArray(msg.readableBytes()).also { msg.readBytes(it) }
IncomingPluginMessagePacket(channel, data)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as IncomingPluginMessagePacket
if (channel != other.channel) return false
if (!data.contentEquals(other.data)) return false
return true
}
override fun hashCode(): Int {
var result = channel.hashCode()
result = 31 * result + data.contentHashCode()
return result
}
}

View file

@ -0,0 +1,81 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.GameMode
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.MinecraftDataTypes.writeVarInt
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.worlds.WorldDimension
import space.blokk.worlds.WorldType
/**
* Sent by the server after the client logged in.
*
* @param entityID ID of the player entity.
* @param gameMode Game mode of the player.
* @param hardcore Whether the player is in hardcore mode.
* @param worldDimension Dimension of the world the player joins.
* @param worldSeedHash First 8 bytes of the SHA-256 hash of the world's seed.
* @param worldType Type of the world the player joins.
* @param viewDistance Maximum view distance.
* @param reducedDebugInfo Whether the debug screen shows only reduced info.
* @param respawnScreenEnabled Whether the respawn screen is shown when the player dies.
*/
data class JoinGamePacket(
val entityID: Int,
val gameMode: GameMode,
val hardcore: Boolean,
val worldDimension: WorldDimension,
val worldSeedHash: Long,
val worldType: WorldType,
val viewDistance: Byte,
val reducedDebugInfo: Boolean,
val respawnScreenEnabled: Boolean
) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<JoinGamePacket>(0x26, JoinGamePacket::class)
init {
if (viewDistance < 2 || viewDistance > 32) throw IllegalArgumentException("viewDistance must be between 2 and 32.")
}
override fun encode(dst: ByteBuf) {
dst.writeInt(entityID)
var gameMode = when (gameMode) {
GameMode.SURVIVAL -> 0
GameMode.CREATIVE -> 1
GameMode.ADVENTURE -> 2
GameMode.SPECTATOR -> 3
}
if (hardcore) gameMode = gameMode and 0x8
dst.writeByte(gameMode)
dst.writeInt(
when (worldDimension) {
WorldDimension.NETHER -> -1
WorldDimension.OVERWORLD -> 0
WorldDimension.END -> 1
}
)
dst.writeLong(worldSeedHash)
dst.writeByte(0x00) // max players; not used anymore
dst.writeString(
when (worldType) {
WorldType.DEFAULT -> "default"
WorldType.FLAT -> "flat"
WorldType.LARGE_BIOMES -> "largeBiomes"
WorldType.AMPLIFIED -> "amplified"
WorldType.CUSTOMIZED -> "customized"
WorldType.BUFFET -> "buffet"
}
)
dst.writeVarInt(viewDistance.toInt())
dst.writeBoolean(reducedDebugInfo)
dst.writeBoolean(respawnScreenEnabled)
}
}

View file

@ -0,0 +1,22 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
/**
* Can be used by mods or plugins to communicate with a potentially modded client.
* Minecraft itself [also uses this for some things](https://wiki.vg/Plugin_channels#Channels_internal_to_Minecraft).
*
* @param channel Channel in which the message should be send.
* @param data Data which should be send. The ByteBuf needs to be released by the caller.
*/
data class OutgoingPluginMessagePacket(val channel: String, val data: ByteBuf) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<OutgoingPluginMessagePacket>(0x19, OutgoingPluginMessagePacket::class)
override fun encode(dst: ByteBuf) {
dst.writeString(channel)
dst.writeBytes(data)
}
}

View file

@ -2,4 +2,12 @@ package space.blokk.net.protocols.play
import space.blokk.net.protocols.Protocol
object PlayProtocol : Protocol("PLAY", setOf())
object PlayProtocol : Protocol(
"PLAY",
JoinGamePacket,
ClientSettingsPacket,
IncomingPluginMessagePacket,
OutgoingPluginMessagePacket,
PlayerAbilitiesPacket,
ServerDifficultyPacket
)

View file

@ -0,0 +1,38 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
/**
* Tells the client some things about the player.
*
* @param invulnerable Whether the player is invulnerable.
* @param flying Whether the player is currently flying.
* @param flyingAllowed Whether the player is allowed to fly.
* @param instantlyBreakBlocks Whether can break blocks instantly like in creative mode.
* @param flyingSpeed The player's flying speed. The default value is `0.5`
* @param fieldOfView The player's field of view modifier. The default value is `0.1`.
*/
data class PlayerAbilitiesPacket(
val invulnerable: Boolean,
val flying: Boolean,
val flyingAllowed: Boolean,
val instantlyBreakBlocks: Boolean,
val flyingSpeed: Float = 0.5f,
val fieldOfView: Float = 0.1f
) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<PlayerAbilitiesPacket>(0x32, PlayerAbilitiesPacket::class)
override fun encode(dst: ByteBuf) {
var flags = 0
if (invulnerable) flags = flags and 0x01
if (flying) flags = flags and 0x02
if (flyingAllowed) flags = flags and 0x04
if (instantlyBreakBlocks) flags = flags and 0x08
dst.writeByte(flags)
dst.writeFloat(flyingSpeed)
dst.writeFloat(fieldOfView)
}
}

View file

@ -0,0 +1,26 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.worlds.WorldDifficulty
/**
* Sets the difficulty shown in the pause menu.
*/
data class ServerDifficultyPacket(val difficulty: WorldDifficulty, val locked: Boolean) : OutgoingPacket() {
companion object : OutgoingPacketCompanion<ServerDifficultyPacket>(0x0E, ServerDifficultyPacket::class)
override fun encode(dst: ByteBuf) {
dst.writeByte(
when (difficulty) {
WorldDifficulty.PEACEFUL -> 0
WorldDifficulty.EASY -> 1
WorldDifficulty.NORMAL -> 2
WorldDifficulty.HARD -> 3
}
)
dst.writeBoolean(locked)
}
}

View file

@ -2,4 +2,10 @@ package space.blokk.net.protocols.status
import space.blokk.net.protocols.Protocol
object StatusProtocol : Protocol("STATUS", setOf(RequestPacket, ResponsePacket, PingPacket, PongPacket))
object StatusProtocol : Protocol(
"STATUS",
RequestPacket,
ResponsePacket,
PingPacket,
PongPacket
)

View file

@ -0,0 +1,7 @@
package space.blokk.players
enum class ChatMode {
ENABLED,
COMMANDS_ONLY,
HIDDEN
}

View file

@ -0,0 +1,11 @@
package space.blokk.players
data class DisplayedSkinParts(
val cape: Boolean,
val jacket: Boolean,
val leftSleeve: Boolean,
val rightSleeve: Boolean,
val leftPantsLeg: Boolean,
val rightPantsLeg: Boolean,
val hat: Boolean
)

View file

@ -0,0 +1,6 @@
package space.blokk.players
enum class MainHand {
LEFT,
RIGHT
}

View file

@ -0,0 +1,9 @@
package space.blokk.utils
import kotlin.experimental.and
fun Byte.checkBit(bitIndex: Int): Boolean {
val flag = (0x1 shl bitIndex).toByte()
return (this and flag) == flag
}

View file

@ -0,0 +1,8 @@
package space.blokk.worlds
enum class WorldDifficulty {
PEACEFUL,
EASY,
NORMAL,
HARD
}

View file

@ -0,0 +1,7 @@
package space.blokk.worlds
enum class WorldDimension {
OVERWORLD,
NETHER,
END
}

View file

@ -0,0 +1,10 @@
package space.blokk.worlds
enum class WorldType {
DEFAULT,
FLAT,
LARGE_BIOMES,
AMPLIFIED,
CUSTOMIZED,
BUFFET
}

View file

@ -81,7 +81,7 @@ class BlokkServer internal constructor() : Server {
}
fun start() {
logger info "Starting BlokkServer (${if (VERSION == "development") VERSION else "v$VERSION"})"
logger info "Starting BlokkServer ($VERSION_WITH_V)"
logger trace "Configuration: $config"
socketServer.bind()
@ -91,6 +91,7 @@ class BlokkServer internal constructor() : Server {
companion object {
lateinit var instance: BlokkServer; private set
val VERSION = BlokkServer::class.java.`package`.implementationVersion ?: "development"
val VERSION_WITH_V = if (VERSION == "development") VERSION else "v$VERSION"
@JvmStatic
fun main(args: Array<String>) {

View file

@ -1,5 +1,6 @@
package space.blokk.net
import io.netty.buffer.ByteBuf
import io.netty.channel.Channel
import kotlinx.coroutines.*
import space.blokk.BlokkServer
@ -14,6 +15,8 @@ import space.blokk.net.protocols.Protocol
import space.blokk.net.protocols.handshaking.HandshakingProtocol
import space.blokk.net.protocols.login.DisconnectPacket
import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.play.OutgoingPluginMessagePacket
import space.blokk.net.protocols.play.PlayProtocol
import space.blokk.server.events.SessionInitializedEvent
import space.blokk.utils.awaitSuspending
import java.net.InetAddress
@ -35,11 +38,14 @@ class BlokkSession(private val channel: Channel) : Session {
override val scope = CoroutineScope(Dispatchers.Unconfined + CoroutineName(identifier))
override val eventBus = EventBus(SessionEvent::class, scope)
override var brand: String? = null; internal set
var loginState: LoginState = LoginState.NotStarted
private var active: Boolean = true
private var disconnectReason: String? = null
val joinProcedure = JoinProcedure(this)
init {
eventBus.register(object : Listener {
@EventHandler(priority = EventPriority.INTERNAL)
@ -108,13 +114,20 @@ class BlokkSession(private val channel: Channel) : Session {
}
}
override suspend fun sendPluginMessage(channel: String, data: ByteBuf) {
if (this.currentProtocol != PlayProtocol) throw IllegalStateException("The session is not using the PLAY protocol")
send(OutgoingPluginMessagePacket(channel, data))
data.release()
}
sealed class LoginState(val name: String) {
object NotStarted: LoginState("NOT_STARTED")
object NotStarted : LoginState("NOT_STARTED")
class WaitingForVerification(val username: String, val verifyToken: ByteArray): LoginState("WAITING_FOR_VERIFICATION")
class WaitingForVerification(val username: String, val verifyToken: ByteArray) :
LoginState("WAITING_FOR_VERIFICATION")
class Encrypted(val username: String): LoginState("ENCRYPTED")
class Encrypted(val username: String) : LoginState("ENCRYPTED")
class Success(val username: String, val uuid: UUID): LoginState("SUCCESS")
class Success(val username: String, val uuid: UUID) : LoginState("SUCCESS")
}
}

View file

@ -6,6 +6,8 @@ import space.blokk.net.protocols.handshaking.HandshakingProtocol
import space.blokk.net.protocols.handshaking.HandshakingProtocolHandler
import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.login.LoginProtocolHandler
import space.blokk.net.protocols.play.PlayProtocol
import space.blokk.net.protocols.play.PlayProtocolHandler
import space.blokk.net.protocols.status.StatusProtocol
import space.blokk.net.protocols.status.StatusProtocolHandler
@ -36,6 +38,7 @@ object SessionPacketReceivedEventHandler {
HandshakingProtocol -> HandshakingProtocolHandler
StatusProtocol -> StatusProtocolHandler
LoginProtocol -> LoginProtocolHandler
PlayProtocol -> PlayProtocolHandler
else -> return
}

View file

@ -0,0 +1,131 @@
package space.blokk.net
import io.netty.buffer.Unpooled
import space.blokk.BlokkServer
import space.blokk.GameMode
import space.blokk.chat.TextComponent
import space.blokk.net.MinecraftDataTypes.writeString
import space.blokk.net.protocols.login.EncryptionRequestPacket
import space.blokk.net.protocols.login.EncryptionResponsePacket
import space.blokk.net.protocols.login.LoginStartPacket
import space.blokk.net.protocols.login.LoginSuccessPacket
import space.blokk.net.protocols.play.JoinGamePacket
import space.blokk.net.protocols.play.PlayProtocol
import space.blokk.net.protocols.play.PlayerAbilitiesPacket
import space.blokk.net.protocols.play.ServerDifficultyPacket
import space.blokk.utils.AuthenticationHelper
import space.blokk.utils.EncryptionUtils
import space.blokk.worlds.WorldDifficulty
import space.blokk.worlds.WorldDimension
import space.blokk.worlds.WorldType
import java.security.MessageDigest
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
class JoinProcedure(val session: BlokkSession) {
suspend fun start(packet: LoginStartPacket) {
if (session.loginState !is BlokkSession.LoginState.NotStarted)
throw IllegalStateException("loginState is not NotStarted")
if (BlokkServer.instance.config.authenticateAndEncrypt) {
val verifyToken = EncryptionUtils.generateVerifyToken()
session.loginState = BlokkSession.LoginState.WaitingForVerification(packet.username, verifyToken)
session.send(EncryptionRequestPacket(BlokkServer.instance.x509EncodedPublicKey, verifyToken))
} else {
session.send(
LoginSuccessPacket(
UUID.nameUUIDFromBytes("OfflinePlayer:${packet.username}".toByteArray()),
packet.username
)
)
afterLogin()
}
}
suspend fun onEncryptionResponse(packet: EncryptionResponsePacket) {
var loginState = session.loginState
if (loginState !is BlokkSession.LoginState.WaitingForVerification)
throw IllegalStateException("loginState is not WaitingForVerification")
val cipher = Cipher.getInstance("RSA")
cipher.init(Cipher.DECRYPT_MODE, BlokkServer.instance.keyPair.private)
// Decrypt verify token
val verifyToken: ByteArray = cipher.doFinal(packet.verifyToken)
if (!verifyToken.contentEquals((session.loginState as BlokkSession.LoginState.WaitingForVerification).verifyToken)) {
session.disconnect(TextComponent of "Encryption failed.")
return
}
// Decrypt shared secret
val sharedSecretKey = SecretKeySpec(cipher.doFinal(packet.sharedSecret), "AES")
loginState = BlokkSession.LoginState.Encrypted(loginState.username)
session.loginState = loginState
session.enableEncryption(sharedSecretKey)
val hash = MessageDigest.getInstance("SHA-1")
hash.update(sharedSecretKey.encoded)
hash.update(BlokkServer.instance.x509EncodedPublicKey)
val hashString = EncryptionUtils.toMinecraftStyleSha1String(hash.digest())
val result = AuthenticationHelper.authenticate(hashString, loginState.username)
loginState = BlokkSession.LoginState.Success(result.username, result.uuid)
session.loginState = loginState
session.send(LoginSuccessPacket(result.uuid, result.username))
afterLogin()
}
private suspend fun afterLogin() {
session.currentProtocol = PlayProtocol
// TODO: Use real data
session.send(
JoinGamePacket(
0,
GameMode.CREATIVE,
false,
WorldDimension.OVERWORLD,
12,
WorldType.DEFAULT,
32,
reducedDebugInfo = false,
respawnScreenEnabled = true
)
)
session.sendPluginMessage(
"minecraft:brand",
Unpooled.buffer().writeString("Blokk ${BlokkServer.VERSION_WITH_V}")
)
// TODO: Use real data
session.send(
PlayerAbilitiesPacket(
invulnerable = false,
flying = false,
flyingAllowed = true,
instantlyBreakBlocks = true,
flyingSpeed = 0.2f,
fieldOfView = 0.2f
)
)
joinWorld()
}
private suspend fun joinWorld() {
session.send(
ServerDifficultyPacket(
WorldDifficulty.NORMAL,
true
)
)
}
}

View file

@ -1,52 +1,14 @@
package space.blokk.net.protocols.login
import space.blokk.BlokkServer
import space.blokk.chat.TextComponent
import space.blokk.net.BlokkSession
import space.blokk.net.PacketReceivedEventHandler
import space.blokk.net.protocols.play.PlayProtocol
import space.blokk.utils.AuthenticationHelper
import space.blokk.utils.toMinecraftStyleSha1String
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
object EncryptionResponsePacketHandler : PacketReceivedEventHandler<EncryptionResponsePacket>() {
override suspend fun handle(session: BlokkSession, packet: EncryptionResponsePacket) {
var loginState = session.loginState
if (loginState !is BlokkSession.LoginState.WaitingForVerification) {
try {
session.joinProcedure.onEncryptionResponse(packet)
} catch (e: IllegalStateException) {
session.failBecauseOfClient("Client sent EncryptionResponsePacket although loginState is ${session.loginState.name}")
return
}
val cipher = Cipher.getInstance("RSA")
cipher.init(Cipher.DECRYPT_MODE, BlokkServer.instance.keyPair.private)
// Decrypt verify token
val verifyToken: ByteArray = cipher.doFinal(packet.verifyToken)
if (!verifyToken.contentEquals(loginState.verifyToken)) {
session.disconnect(TextComponent of "Encryption failed.")
return
}
// Decrypt shared secret
val sharedSecretKey = SecretKeySpec(cipher.doFinal(packet.sharedSecret), "AES")
loginState = BlokkSession.LoginState.Encrypted(loginState.username)
session.loginState = loginState
session.enableEncryption(sharedSecretKey)
val hash = MessageDigest.getInstance("SHA-1")
hash.update(sharedSecretKey.encoded)
hash.update(BlokkServer.instance.x509EncodedPublicKey)
val hashString = toMinecraftStyleSha1String(hash.digest())
val result = AuthenticationHelper.authenticate(hashString, loginState.username)
loginState = BlokkSession.LoginState.Success(result.username, result.uuid)
session.loginState = loginState
session.send(LoginSuccessPacket(result.uuid, result.username))
session.currentProtocol = PlayProtocol
}
}

View file

@ -1,33 +1,14 @@
package space.blokk.net.protocols.login
import space.blokk.BlokkServer
import space.blokk.net.BlokkSession
import space.blokk.net.PacketReceivedEventHandler
import space.blokk.net.protocols.play.PlayProtocol
import space.blokk.utils.EncryptionUtils
import java.util.*
object LoginStartPacketHandler : PacketReceivedEventHandler<LoginStartPacket>() {
override suspend fun handle(session: BlokkSession, packet: LoginStartPacket) {
if (session.loginState != BlokkSession.LoginState.NotStarted) {
try {
session.joinProcedure.start(packet)
} catch (e: IllegalStateException) {
session.failBecauseOfClient("Client sent LoginStartPacket although loginState is ${session.loginState.name}")
return
}
if (BlokkServer.instance.config.authenticateAndEncrypt) {
val verifyToken = EncryptionUtils.generateVerifyToken()
session.loginState = BlokkSession.LoginState.WaitingForVerification(packet.username, verifyToken)
session.send(EncryptionRequestPacket(BlokkServer.instance.x509EncodedPublicKey, verifyToken))
} else {
session.send(
LoginSuccessPacket(
UUID.nameUUIDFromBytes("OfflinePlayer:${packet.username}".toByteArray()),
packet.username
)
)
session.currentProtocol = PlayProtocol
}
}
}

View file

@ -0,0 +1,10 @@
package space.blokk.net.protocols.play
import space.blokk.net.BlokkSession
import space.blokk.net.PacketReceivedEventHandler
object ClientSettingsPacketHandler : PacketReceivedEventHandler<ClientSettingsPacket>() {
override suspend fun handle(session: BlokkSession, packet: ClientSettingsPacket) {
// TODO: Save the settings
}
}

View file

@ -0,0 +1,24 @@
package space.blokk.net.protocols.play
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import space.blokk.net.BlokkSession
import space.blokk.net.MinecraftDataTypes.readString
import space.blokk.net.PacketReceivedEventHandler
import space.blokk.net.events.ClientBrandReceivedEvent
object IncomingPluginMessagePacketHandler : PacketReceivedEventHandler<IncomingPluginMessagePacket>() {
override suspend fun handle(session: BlokkSession, packet: IncomingPluginMessagePacket) {
if (packet.channel == "minecraft:brand") {
if (session.brand != null) session.failBecauseOfClient("The client send his brand string more than once.")
else {
val buffer: ByteBuf = Unpooled.copiedBuffer(packet.data)
val brand = buffer.readString()
buffer.release()
session.brand = brand
session.emit(ClientBrandReceivedEvent(session, brand))
}
}
}
}

View file

@ -0,0 +1,11 @@
package space.blokk.net.protocols.play
import space.blokk.net.ProtocolPacketReceivedEventHandler
// NOTE: PacketReceivedEventHandler.of<T> MUST have T specified correctly, otherwise the code breaks at runtime
object PlayProtocolHandler : ProtocolPacketReceivedEventHandler(
mapOf(
ClientSettingsPacket to ClientSettingsPacketHandler,
IncomingPluginMessagePacket to IncomingPluginMessagePacketHandler
)
)

View file

@ -1,10 +1,10 @@
package space.blokk.utils
import java.math.BigInteger
import java.security.*
import java.security.spec.X509EncodedKeySpec
import kotlin.random.Random
object EncryptionUtils {
fun generateKeyPair(): KeyPair {
try {
@ -25,4 +25,6 @@ object EncryptionUtils {
}
fun generateVerifyToken() = ByteArray(4).also { Random.Default.nextBytes(it) }
fun toMinecraftStyleSha1String(value: ByteArray): String = BigInteger(value).toString(16)
}

View file

@ -1,5 +0,0 @@
package space.blokk.utils
import java.math.BigInteger
fun toMinecraftStyleSha1String(value: ByteArray): String = BigInteger(value).toString(16)