From 7a740b35dd112e9e04c187686a8d5d034e6bc9e6 Mon Sep 17 00:00:00 2001 From: MiniDigger | Martin Date: Thu, 12 Oct 2023 17:05:54 +0200 Subject: [PATCH] feat: add initial server list ping implementation --- build.gradle.kts | 2 + gradle/libs.versions.toml | 2 + .../kyori/adventure/webui/jvm/Application.kt | 34 ++-- .../webui/jvm/minimessage/SocketTest.kt | 164 ++++++++++++++++++ 4 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 src/jvmMain/kotlin/net/kyori/adventure/webui/jvm/minimessage/SocketTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index b111cf5..d6a392e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -91,9 +91,11 @@ kotlin { dependencies { implementation(libs.bundles.ktor.server) implementation(libs.bundles.ktor.client) + implementation(libs.ktor.network) implementation(libs.adventure.minimessage) implementation(libs.adventure.text.serializer.gson) + implementation(libs.adventure.text.serializer.legacy) implementation(libs.cache4k) implementation(libs.logback.classic) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 419fd4a..226efd5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ spotless = { id = "com.diffplug.spotless", version = "6.25.0" } [libraries] adventure-minimessage = { group = "net.kyori", name = "adventure-text-minimessage", version.ref = "adventure" } adventure-text-serializer-gson = { group = "net.kyori", name = "adventure-text-serializer-gson", version.ref = "adventure" } +adventure-text-serializer-legacy = { group = "net.kyori", name = "adventure-text-serializer-legacy", version.ref = "adventure" } cache4k = { group = "io.github.reactivecircus.cache4k", name = "cache4k", version = "0.13.0" } kotlinx-html = { group = "org.jetbrains.kotlinx", name = "kotlinx-html", version = "0.8.0" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.7.3" } @@ -24,6 +25,7 @@ ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.r ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" } ktor-server-caching-headers = { group = "io.ktor", name = "ktor-server-caching-headers", version.ref = "ktor" } ktor-server-compression = { group = "io.ktor", name = "ktor-server-compression", version.ref = "ktor" } +ktor-network = { group = "io.ktor", name = "ktor-network", version.ref = "ktor" } logback-classic = { group = "ch.qos.logback", name = "logback-classic", version = "1.5.8" } zKtlint = { group = "com.pinterest", name = "ktlint", version.ref = "ktlint"} diff --git a/src/jvmMain/kotlin/net/kyori/adventure/webui/jvm/Application.kt b/src/jvmMain/kotlin/net/kyori/adventure/webui/jvm/Application.kt index 17bbbe1..4ebea5b 100644 --- a/src/jvmMain/kotlin/net/kyori/adventure/webui/jvm/Application.kt +++ b/src/jvmMain/kotlin/net/kyori/adventure/webui/jvm/Application.kt @@ -1,20 +1,21 @@ package net.kyori.adventure.webui.jvm -import io.ktor.http.CacheControl -import io.ktor.http.ContentType -import io.ktor.http.content.CachingOptions -import io.ktor.server.application.Application -import io.ktor.server.application.install -import io.ktor.server.application.log -import io.ktor.server.plugins.cachingheaders.CachingHeaders -import io.ktor.server.plugins.compression.Compression -import io.ktor.server.plugins.compression.deflate -import io.ktor.server.plugins.compression.gzip -import io.ktor.server.routing.routing -import io.ktor.server.websocket.WebSockets -import io.ktor.server.websocket.pingPeriod -import io.ktor.server.websocket.timeout -import io.ktor.websocket.WebSocketDeflateExtension +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.network.selector.* +import io.ktor.network.sockets.* +import io.ktor.server.application.* +import io.ktor.server.plugins.cachingheaders.* +import io.ktor.server.plugins.compression.* +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import io.ktor.utils.io.* +import io.ktor.websocket.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.webui.jvm.minimessage.SocketTest +import okhttp3.internal.and import java.time.Duration public fun Application.main() { @@ -48,8 +49,11 @@ public fun Application.main() { trace { route -> this@main.log.debug(route.buildText()) } } } + + SocketTest().main() } + /** Reads a string value from the `config` block in `application.conf`. */ public fun Application.getConfigString(key: String): String = environment.config.property("ktor.config.$key").getString() diff --git a/src/jvmMain/kotlin/net/kyori/adventure/webui/jvm/minimessage/SocketTest.kt b/src/jvmMain/kotlin/net/kyori/adventure/webui/jvm/minimessage/SocketTest.kt new file mode 100644 index 0000000..a54e898 --- /dev/null +++ b/src/jvmMain/kotlin/net/kyori/adventure/webui/jvm/minimessage/SocketTest.kt @@ -0,0 +1,164 @@ +package net.kyori.adventure.webui.jvm.minimessage + +import io.ktor.network.selector.* +import io.ktor.network.sockets.* +import io.ktor.utils.io.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer +import okhttp3.internal.and +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import kotlin.text.Charsets.UTF_8 + +public class SocketTest { + + public fun main() { + // TODO + // 1. make this non blocking somehow, idk how kotlin works + // 2. add api/ui to store into some cache + // 3. parse server address to get stuff from the cache and return that in the status response + runBlocking { + val serverSocket = aSocket(SelectorManager(Dispatchers.IO)).tcp().bind("127.0.0.1", 9002) + println("Server is listening at ${serverSocket.localAddress}") + while (true) { + val socket = serverSocket.accept() + println("Accepted ${socket.remoteAddress}") + launch { + try { + val receiveChannel = socket.openReadChannel() + val sendChannel = socket.openWriteChannel(autoFlush = true) + + // handshake + val handshakePacket = receiveChannel.readMcPacket() + val protocolVersion = handshakePacket.readVarInt() + val serverAddress = handshakePacket.readUtf8String() + val serverPort = handshakePacket.readShort() + val nextState = handshakePacket.readVarInt() + + if (nextState != 1) { + // send kick + sendChannel.writeMcPacket(0) { + it.writeString( + GsonComponentSerializer.gson() + .serialize(MiniMessage.miniMessage().deserialize("You cant join here!")) + ) + } + } else { + // send status response + sendChannel.writeMcPacket(0) { + it.writeString( + """{ + "version": { + "name": "${ + LegacyComponentSerializer.legacySection() + .serialize(MiniMessage.miniMessage().deserialize("MiniMessage")) + }", + "protocol": 762 + }, + "description": ${ + GsonComponentSerializer.gson().serialize( + MiniMessage.miniMessage().deserialize("MiniMessage is cool!") + ) + } + }""".trimIndent() + ) + } + } + + sendChannel.close() + } catch (e: Exception) { + println(e) + } + + socket.close() + return@launch + } + } + } + } +} + +public suspend fun ByteWriteChannel.writeMcPacket(packetId: Int, consumer: (packet: DataOutputStream) -> Unit) { + val stream = ByteArrayOutputStream() + val packet = DataOutputStream(stream) + + consumer.invoke(packet) + + val data = stream.toByteArray() + writeVarInt(data.size + 1) + writeVarInt(packetId) + writeFully(data) +} + +public fun DataOutputStream.writeString(string: String) { + val bytes = string.toByteArray(UTF_8) + writeVarInt(bytes.size) + write(bytes) +} + +public fun DataOutputStream.writeVarInt(int: Int) { + var value = int + while (true) { + if ((value and 0x7F.inv()) == 0) { + writeByte(value) + return + } + + writeByte((value and 0x7F) or 0x80) + + value = value ushr 7 + } +} + +public suspend fun ByteWriteChannel.writeVarInt(int: Int) { + var value = int + while (true) { + if ((value and 0x7F.inv()) == 0) { + writeByte(value) + return + } + + writeByte((value and 0x7F) or 0x80) + + value = value ushr 7 + } +} + +public suspend fun ByteReadChannel.readMcPacket(): ByteReadChannel { + val length = readVarInt() + val packetId = readVarInt() + val data = ByteArray(length) + readFully(data, 0, length) + return ByteReadChannel(data) +} + +public suspend fun ByteReadChannel.readVarInt(): Int { + var value = 0 + var position = 0 + var currentByte: Byte + + while (true) { + currentByte = readByte() + value = value or ((currentByte and 0x7F) shl position) + + if ((currentByte and 0x80) == 0) break + + position += 7 + + if (position >= 32) throw RuntimeException("VarInt is too big") + } + + return value +} + +public suspend fun ByteReadChannel.readUtf8String(): String { + val length = readVarInt() + val data = ByteArray(length) + readFully(data, 0, length) + return String(data) +}