Dieser Commit ist enthalten in:
Chaoscaot 2023-11-03 20:30:30 +01:00
Ursprung a81a7e758f
Commit dfc3fd0b3b
14 geänderte Dateien mit 553 neuen und 17 gelöschten Zeilen

3
.gitignore vendored
Datei anzeigen

@ -33,4 +33,5 @@ out/
/.nb-gradle/
### VS Code ###
.vscode/
.vscode/
/config.json

@ -1 +1 @@
Subproject commit 66fc131803d8fad84dbc51e60999301e4a162190
Subproject commit fa049455956e40efd126082c1eef470698263abb

Datei anzeigen

@ -28,13 +28,17 @@ dependencies {
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
implementation("io.ktor:ktor-server-cors-jvm:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
implementation("io.ktor:ktor-server-jetty-jvm:$ktor_version")
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version")
implementation("io.ktor:ktor-server-request-validation:$ktor_version")
implementation("io.ktor:ktor-server-auth:$ktor_version")
implementation("io.ktor:ktor-server-auth-jvm:$ktor_version")
implementation("io.ktor:ktor-server-auth-ldap-jvm:$ktor_version")
implementation("io.ktor:ktor-client-core-jvm:$ktor_version")
implementation("io.ktor:ktor-client-java:$ktor_version")
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-client-auth:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
implementation("com.mysql:mysql-connector-j:8.1.0")
implementation(project(":CommonCore"))

0
gradlew vendored Ausführbare Datei → Normale Datei
Datei anzeigen

Datei anzeigen

@ -24,15 +24,25 @@ import io.ktor.server.application.*
import io.ktor.server.engine.*
import de.steamwar.routes.configureRoutes
import de.steamwar.sql.SchematicType
import io.ktor.server.jetty.*
import io.ktor.server.netty.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.File
@Serializable
data class ResponseError(val error: String)
@Serializable
data class Config(val giteaToken: String)
@OptIn(ExperimentalSerializationApi::class)
val config = Json.decodeFromStream<Config>(File("config.json").inputStream())
fun main() {
SchematicType.Normal.name().length
embeddedServer(Jetty, port = 1337, host = "127.0.0.1", module = Application::module)
embeddedServer(Netty, port = 1337, host = "127.0.0.1", module = Application::module)
.start(wait = true)
}

Datei anzeigen

@ -58,7 +58,7 @@ val SWPermissionCheck = createRouteScopedPlugin("SWAuth", ::SWAuthConfig) {
val token = call.principal<SWAuthPrincipal>()
if (token == null) {
call.respond(HttpStatusCode.Unauthorized, "Invalid token")
call.respond(HttpStatusCode.Unauthorized)
}
}
@ -68,16 +68,18 @@ val SWPermissionCheck = createRouteScopedPlugin("SWAuth", ::SWAuthConfig) {
val token = call.principal<SWAuthPrincipal>()
if (token == null) {
call.respond(HttpStatusCode.Unauthorized, "Invalid token")
call.respond(HttpStatusCode.Unauthorized)
return@on
}
if (!token.user.hasPerm(permission)) {
call.respond(HttpStatusCode.Forbidden, "Insufficient permissions")
call.respond(HttpStatusCode.Forbidden)
return@on
}
if (!token.userCheck(call.request)) {
call.respond(HttpStatusCode.Forbidden, "Insufficient permissions")
call.respond(HttpStatusCode.Forbidden)
return@on
}
}
}

Datei anzeigen

@ -25,6 +25,7 @@ import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.application.*
import io.ktor.server.application.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.contentnegotiation.*
@ -49,7 +50,7 @@ fun Application.configurePlugins() {
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowHeader("x-sw-auth")
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.AccessControlAllowOrigin)
allowHeader(HttpHeaders.ContentType)
anyHost()
@ -58,6 +59,13 @@ fun Application.configurePlugins() {
install(RateLimit) {
global {
rateLimiter(limit = 60, refillPeriod = 60.seconds)
requestWeight { applicationCall, _ ->
if(applicationCall.request.local.remoteAddress == "127.0.0.1") {
0
} else {
1
}
}
}
}
authentication {
@ -81,9 +89,9 @@ fun Application.configurePlugins() {
format {
val verified = it.principal<SWAuthPrincipal>()
if (verified != null) {
"User: ${verified.token.owner.userName}, Token: ${verified.token.name}, ${it.request.httpMethod.value} ${it.request.uri}"
"User: ${verified.token.owner.userName}, Token: ${verified.token.name}, ${it.request.httpMethod.value} ${it.request.uri}, Response: ${it.response.status()?.value}"
} else {
"Unauthenticated Request: ${it.request.httpMethod.value} ${it.request.uri}"
"Unauthenticated Request: ${it.request.httpMethod.value} ${it.request.uri}, Response: ${it.response.status()?.value}"
}
}
}

Datei anzeigen

@ -23,6 +23,5 @@ import de.steamwar.sql.SteamwarUser
import io.ktor.server.request.*
fun ApplicationRequest.getUser(key: String = "id"): SteamwarUser? {
SteamwarUser.clear()
return SteamwarUser.get(call.parameters[key]?.toIntOrNull() ?: return null)
}

Datei anzeigen

@ -23,7 +23,9 @@ import de.steamwar.ResponseError
import de.steamwar.data.Groups
import de.steamwar.sql.SchematicType
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.UserPerm
import de.steamwar.sql.loadSchematicTypes
import de.steamwar.util.fetchData
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
@ -31,17 +33,21 @@ import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import org.bspfsystems.yamlconfiguration.file.YamlConfiguration
import java.io.File
import java.net.InetSocketAddress
@Serializable
data class ResponseSchematicType(val name: String, val db: String)
@Serializable
data class ResponseUser(val id: Int, val name: String, val uuid: String) {
constructor(user: SteamwarUser) : this(user.id, user.userName, user.uuid.toString())
data class ResponseUser(val id: Int, val name: String, val uuid: String, val prefix: String) {
constructor(user: SteamwarUser) : this(user.id, user.userName, user.uuid.toString(), user.prefix().chatPrefix)
}
fun Route.configureDataRoutes() {
route("/data") {
get {
call.respondText("Hello World!")
}
get("/schematicTypes") {
val types = mutableListOf<SchematicType>()
loadSchematicTypes(types, mutableMapOf())
@ -69,11 +75,21 @@ fun Route.configureDataRoutes() {
get("/users") {
call.respond(SteamwarUser.getAll().map { ResponseUser(it) })
}
get {
call.respondText("Hello World!")
get("/team") {
call.respond(SteamwarUser.getAll().filter { !it.hasPerm(UserPerm.PREFIX_NONE) && !it.hasPerm(UserPerm.PREFIX_YOUTUBER) }.sortedBy { it.prefix().ordinal }.reversed().map { ResponseUser(it) })
}
get("/groups") {
call.respond(Groups.getAllGroups())
}
get("/server") {
try {
val server = fetchData(InetSocketAddress("steamwar.de", 25565), 100)
call.respond(server)
} catch (e: Exception) {
e.printStackTrace()
call.respond(HttpStatusCode.InternalServerError, ResponseError(e.message ?: "Unknown error"))
return@get
}
}
}
}

Datei anzeigen

@ -0,0 +1,264 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2023 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
package de.steamwar.routes
import de.steamwar.config
import de.steamwar.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.UserPerm
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.java.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.reflect.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
import java.util.Base64
val pathPageIdMap = mutableMapOf<String, Int>()
var pageId = 1
@Serializable
data class Identity(val name: String, val email: String)
@Serializable
data class PageResponseList(
val path: String,
val name: String,
val sha: String,
val downloadUrl: String,
val id: Int
) {
constructor(res: JsonObject, id: Int) : this(
res["path"]?.jsonPrimitive?.content!!,
res["name"]?.jsonPrimitive?.content!!,
res["sha"]?.jsonPrimitive?.content!!,
res["download_url"]?.jsonPrimitive?.content!!,
id
)
}
@Serializable
data class PageResponse(
val path: String,
val name: String,
val sha: String,
val downloadUrl: String,
val content: String,
val size: Int,
val id: Int,
) {
constructor(res: JsonObject, id: Int) : this(
res["path"]?.jsonPrimitive?.content!!,
res["name"]?.jsonPrimitive?.content!!,
res["sha"]?.jsonPrimitive?.content!!,
res["download_url"]?.jsonPrimitive?.content!!,
res["content"]?.jsonPrimitive?.content!!,
res["size"]?.jsonPrimitive?.int!!,
id
)
}
@Serializable
data class CreatePageRequest(val path: String)
@Serializable
data class CreateBranchRequest(val branch: String)
@Serializable
data class UpdatePageRequest(val content: String, val sha: String, val message: String)
@Serializable
data class MergeBranchRequest(val branch: String, val message: String)
@Serializable
data class DeletePageRequest(val sha: String, val message: String)
fun Route.configurePage() {
val client = HttpClient(Java) {
install(ContentNegotiation) {
json()
}
defaultRequest {
url("https://steamwar.de/devlabs/api/v1/")
header("Authorization", "token " + config.giteaToken)
}
}
route("page") {
install(SWPermissionCheck) {
permission = UserPerm.MODERATION
}
get {
val branch = call.request.queryParameters["branch"] ?: "master"
val filesToCheck = mutableListOf("src/content")
val files = mutableListOf<PageResponseList>()
while (filesToCheck.isNotEmpty()) {
val path = filesToCheck.removeAt(0)
val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch")
val fileJson = Json.parseToJsonElement(res.bodyAsText())
if (fileJson is JsonArray) {
fileJson.forEach {
val obj = it.jsonObject
if (obj["type"]?.jsonPrimitive?.content == "dir") {
filesToCheck.add(obj["path"]?.jsonPrimitive?.content!!)
} else if (obj["type"]?.jsonPrimitive?.content == "file" && obj["name"]?.jsonPrimitive?.content?.endsWith(".md") == true) {
files.add(PageResponseList(obj, pathPageIdMap.computeIfAbsent(obj["path"]?.jsonPrimitive?.content!!) { pageId++ }))
}
}
} else {
files.add(PageResponseList(fileJson.jsonObject, pathPageIdMap.computeIfAbsent(fileJson.jsonObject["path"]?.jsonPrimitive?.content!!) { pageId++ }))
}
}
call.respond(files)
}
get("branch") {
val res = client.get("repos/SteamWar/Website/branches")
call.respond(res.status, Json.parseToJsonElement(res.bodyAsText()).jsonArray.map { it.jsonObject["name"]?.jsonPrimitive?.content!! })
}
post("branch") {
@Serializable
data class CreateGiteaBranchRequest(val new_branch_name: String, val old_branch_name: String)
val branch = call.receive<CreateBranchRequest>().branch
val res = client.post("repos/SteamWar/Website/branches") {
contentType(ContentType.Application.Json)
setBody(CreateGiteaBranchRequest(branch, "master"))
}
call.respond(res.status)
}
post("branch/merge") {
@Serializable
data class CreateGiteaMergeRequest(val base: String, val head: String, val title: String)
val data = call.receive<MergeBranchRequest>()
val createRes = client.post("repos/SteamWar/Website/pulls") {
contentType(ContentType.Application.Json)
setBody(CreateGiteaMergeRequest("master", data.branch, data.message))
}
val id = Json.parseToJsonElement(createRes.bodyAsText()).jsonObject["number"]?.jsonPrimitive?.int!!
@Serializable
data class MergeGiteaMergeRequest(val Do: String)
val res = client.post("repos/SteamWar/Website/pulls/$id/merge") {
contentType(ContentType.Application.Json)
setBody(MergeGiteaMergeRequest("merge"))
}
call.respond(res.status)
}
delete("branch") {
val branch = call.receive<CreateBranchRequest>().branch
val res = client.delete("repos/SteamWar/Website/branches/$branch")
call.respond(res.status)
}
post {
@Serializable
data class CreateGiteaPageRequest(val message: String, val content: String, val branch: String, val author: Identity)
val path = call.receive<CreatePageRequest>().path
if(path.startsWith("src/content/")) {
call.respond(HttpStatusCode.BadRequest, "Invalid path")
return@post
}
val res = client.post("repos/SteamWar/Website/contents/src/content/$path") {
contentType(ContentType.Application.Json)
setBody(CreateGiteaPageRequest(
"Create page $path",
Base64.getEncoder().encodeToString("""
---
title: [Enter Title]
description: [Enter Description]
slug: [Enter Slug]
---
# $path
""".trimIndent().toByteArray()),
call.request.queryParameters["branch"] ?: "master",
Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de"
)))
}
call.respond(res.status)
}
get("{id}") {
val id = call.parameters["id"]?.toIntOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
val path = pathPageIdMap.entries.find { it.value == id }?.key ?: return@get call.respond(HttpStatusCode.NotFound, "Page not found")
val branch = call.request.queryParameters["branch"] ?: "master"
val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch")
val fileJson = Json.parseToJsonElement(res.bodyAsText())
if (fileJson is JsonArray) {
return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
}
val file = PageResponse(fileJson.jsonObject, id)
call.respond(file)
}
delete("{id}") {
val data = call.receive<DeletePageRequest>()
val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@delete call.respond(HttpStatusCode.NotFound, "Page not found")
val branch = call.request.queryParameters["branch"] ?: "master"
@Serializable
data class DeleteGiteaPageRequest(val sha: String, val message: String, val branch: String, val author: Identity)
val res = client.delete("repos/SteamWar/Website/contents/$path") {
contentType(ContentType.Application.Json)
setBody(DeleteGiteaPageRequest(data.sha, data.message, branch, Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de")))
}
call.respond(res.status)
}
put("{id}") {
@Serializable
data class UpdateGiteaPageRequest(val content: String, val sha: String, val message: String, val branch: String, val author: Identity)
val data = call.receive<UpdatePageRequest>()
val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@put call.respond(HttpStatusCode.NotFound, "Page not found")
val res = client.put("repos/SteamWar/Website/contents/$path") {
contentType(ContentType.Application.Json)
setBody(UpdateGiteaPageRequest(data.content, data.sha, data.message, (call.request.queryParameters["branch"] ?: "master"), Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de")))
}
call.respond(res.status)
}
}
}

Datei anzeigen

@ -31,6 +31,8 @@ fun Application.configureRoutes() {
configureEventFightRoutes()
configureModRoutes()
configureUserPerms()
configureStats()
configurePage()
}
}
}

Datei anzeigen

@ -0,0 +1,69 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2023 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.routes
import de.steamwar.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.plugins.getUser
import de.steamwar.sql.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
@Serializable
data class UserStats(val eventFightParticipation: Int, val eventParticipation: Int, val acceptedSchematics: Int) {
constructor(user: SteamwarUser): this(
getEventFightParticipation(user) ?: 0,
getEventParticipation(user) ?: 0,
getAcceptedSchematics(user) ?: 0
)
}
fun Route.configureStats() {
route("/stats") {
route("/user/{id}") {
install(SWPermissionCheck) {
userCheck {
val user = it.call.request.getUser()
val auth = it.call.principal<SWAuthPrincipal>()
if (user == null || auth == null) {
return@userCheck false
}
return@userCheck user.id == auth.user.id || auth.user.hasPerm(UserPerm.MODERATION)
}
}
get {
val user = call.request.getUser()
if (user == null) {
call.respond(HttpStatusCode.NotFound, "User not found")
return@get
}
call.respond(UserStats(user))
}
}
}
}

Datei anzeigen

@ -0,0 +1,43 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2023 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql
import de.steamwar.sql.internal.Statement
import de.steamwar.sql.internal.Statement.ResultSetUser
private val getNum: ResultSetUser<Int?> = ResultSetUser { res ->
if (res.next()) {
res.getInt("num")
} else {
null
}
}
private val eventFightParticipation = Statement("SELECT FightPlayer.UserID, COUNT(UserID) as num FROM FightPlayer INNER JOIN Fight on FightPlayer.FightID = Fight.FightID WHERE Fight.Server LIKE '%vs%' AND FightPlayer.UserID = ? GROUP BY FightPlayer.UserID")
fun getEventFightParticipation(user: SteamwarUser): Int? = eventFightParticipation.select(getNum, user.id)
private val eventParticipation = Statement("SELECT FightPlayer.UserID, COUNT(DISTINCT EventID) as num FROM FightPlayer INNER JOIN core.Fight F on FightPlayer.FightID = F.FightID INNER JOIN core.EventFight EF on F.FightID = EF.Fight WHERE F.FightID = FightPlayer.FightID AND FightPlayer.FightID = EF.Fight AND F.Server LIKE '%vs%' AND FightPlayer.UserID = ? GROUP BY FightPlayer.UserID")
fun getEventParticipation(user: SteamwarUser): Int? = eventParticipation.select(getNum, user.id)
private val acceptedSchematics = Statement("SELECT NodeOwner, COUNT(DISTINCT NodeId) AS num FROM SchematicNode WHERE NodeType != 'normal' AND NodeType IS NOT NULL AND NodeType NOT LIKE 'c%' AND NodeOwner = ?")
fun getAcceptedSchematics(user: SteamwarUser): Int? = acceptedSchematics.select(getNum, user.id)

Datei anzeigen

@ -0,0 +1,118 @@
package de.steamwar.util
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import java.io.*
import java.net.InetSocketAddress
import java.net.Socket
/**
*
* @author zh32 <zh32 at zh32.de>
</zh32> */
private fun readVarInt(`in`: DataInputStream): Int {
var i = 0
var j = 0
while (true) {
val k = `in`.readByte().toInt()
i = i or (k and 0x7F shl j++ * 7)
if (j > 5) throw RuntimeException("VarInt too big")
if (k and 0x80 != 128) break
}
return i
}
private fun writeVarInt(out: DataOutputStream, paramInt: Int) {
var paramInt = paramInt
while (true) {
if (paramInt and -0x80 == 0) {
out.writeByte(paramInt)
return
}
out.writeByte(paramInt and 0x7F or 0x80)
paramInt = paramInt ushr 7
}
}
private val JSON = Json {
ignoreUnknownKeys = true
}
fun fetchData(address: InetSocketAddress, timeout: Int = 7000): StatusResponse {
val socket = Socket()
socket.setSoTimeout(timeout)
socket.connect(address, timeout)
val outputStream = socket.getOutputStream()
val dataOutputStream = DataOutputStream(outputStream)
val inputStream = socket.getInputStream()
val inputStreamReader = InputStreamReader(inputStream)
val b = ByteArrayOutputStream()
val handshake = DataOutputStream(b)
handshake.writeByte(0x00) //packet id for handshake
writeVarInt(handshake, 4) //protocol version
writeVarInt(handshake, address.hostString.length) //host length
handshake.writeBytes(address.hostString) //host string
handshake.writeShort(address.port) //port
writeVarInt(handshake, 1) //state (1 for handshake)
writeVarInt(dataOutputStream, b.size()) //prepend size
dataOutputStream.write(b.toByteArray()) //write handshake packet
dataOutputStream.writeByte(0x01) //size is only 1
dataOutputStream.writeByte(0x00) //packet id for ping
val dataInputStream = DataInputStream(inputStream)
val size = readVarInt(dataInputStream) //size of packet
var id = readVarInt(dataInputStream) //packet id
if (id == -1) {
throw IOException("Premature end of stream.")
}
if (id != 0x00) { //we want a status response
throw IOException("Invalid packetID")
}
val length = readVarInt(dataInputStream) //length of json string
if (length == -1) {
throw IOException("Premature end of stream.")
}
if (length == 0) {
throw IOException("Invalid string length.")
}
val `in` = ByteArray(length)
dataInputStream.readFully(`in`) //read json string
val json = String(`in`)
val now = System.currentTimeMillis()
dataOutputStream.writeByte(0x09) //size of packet
dataOutputStream.writeByte(0x01) //0x01 for ping
dataOutputStream.writeLong(now) //time!?
readVarInt(dataInputStream)
id = readVarInt(dataInputStream)
if (id == -1) {
throw IOException("Premature end of stream.")
}
if (id != 0x01) {
throw IOException("Invalid packetID")
}
val pingtime = dataInputStream.readLong() //read response
val response: StatusResponse = JSON.decodeFromString(json)
response.time = (now - pingtime).toInt()
dataOutputStream.close()
outputStream.close()
inputStreamReader.close()
inputStream.close()
socket.close()
return response
}
@Serializable
data class StatusResponse(val description: JsonElement, val players: Players, val version: Version, val favicon: String) {
@Transient
var time = 0
}
@Serializable
data class Players(val max: Int, val online: Int, val sample: List<Player> = emptyList())
@Serializable
data class Player(val name: String, val id: String)
@Serializable
data class Version(val name: String, val protocol: Int)