/* * This file is a part of the SteamWar software. * * Copyright (C) 2025 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 . */ package de.steamwar.routes import de.steamwar.ResponseError import de.steamwar.plugins.SWAuthPrincipal import de.steamwar.plugins.SWPermissionCheck import de.steamwar.sql.* import de.steamwar.sql.NodeData.SchematicFormat import dev.dewy.nbt.Nbt import dev.dewy.nbt.tags.collection.CompoundTag import io.ktor.http.* 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 kotlinx.serialization.Serializable import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.DataInputStream import java.io.InputStream import java.security.MessageDigest import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* import java.util.zip.GZIPInputStream @Serializable data class ResponseSchematic(val name: String, val id: Int, val type: String?, val owner: Int, val item: String, val lastUpdate: Long, val rank: Int, val replaceColor: Boolean, val allowReplay: Boolean) { constructor(node: SchematicNode) : this(node.name, node.getId(), node.schemtype?.name(), node.owner, node.item, node.lastUpdate.time, node.rank, node.replaceColor(), node.allowReplay()) } @Serializable data class ResponseSchematicLong(val members: List, val path: String, val schem: ResponseSchematic) { constructor(node: SchematicNode, path: String): this(NodeMember.getNodeMembers(node.getId()).map { ResponseUser.get(it.member) }, path, ResponseSchematic(node)) } @Serializable data class ResponseSchematicList(val breadcrumbs: List, val schematics: List, val players: Map) { constructor(schematics: List, breadcrumbs: List) : this(breadcrumbs, schematics, schematics.map { it.owner }.distinct().map { ResponseUser.get(it) }.associateBy { it.uuid }) } @Serializable data class ResponseBreadcrumb(val name: String, val id: Int) fun generateCode(): String { val md = MessageDigest.getInstance("SHA-256") val random = ByteArray(64).map { (0..255).random().toByte() }.toByteArray() val code = md.digest(random) return code.joinToString("") { "%02x".format(it) } } @Serializable data class SchematicCode(val id: Int, val code: String, val expires: Long) @Serializable data class UploadSchematic(val name: String, val content: String) val nbt = Nbt() fun Route.configureSchematic() { route("/download/{code}") { get { val node = call.receiveSchematic() ?: return@get val user = call.principal()?.user if(user != null && !node.accessibleByUser(user)) { call.respond(HttpStatusCode.Forbidden) SWException.log("User ${user.userName} tried to download schematic ${node.name} without permission", user.id.toString()) return@get } val data = NodeData.getLatest(node) ?: run { call.respond(HttpStatusCode.InternalServerError) return@get } call.response.header("Content-Disposition", "attachment; filename=\"${node.name}${data.nodeFormat.fileEnding}\"") call.respondBytes(data.schemData(false).readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK) } get("/info") { val node = call.receiveSchematic() ?: return@get call.respond(ResponseSchematic(node)) } } route("/schem") { install(SWPermissionCheck) post { val file = call.receive() val schemName = file.name.substringBeforeLast(".") if (SchematicNode.invalidSchemName(arrayOf(schemName))) { call.respond(HttpStatusCode.BadRequest, ResponseError( error = "INVALID_NAME" )) return@post } val schemType = file.name.substringAfterLast(".") if (schemType != "schem" && schemType != "schematic") { call.respond(HttpStatusCode.BadRequest, ResponseError( error = "INVALID_SUFFIX" )) return@post } val user = call.principal()!!.user var node = SchematicNode.getSchematicNode(user.getId(), schemName, null as Int?) if (node == null) { node = SchematicNode.createSchematic(user.getId(), schemName, null) } try { val content = Base64.getDecoder().decode(file.content) var schem = nbt.fromStream(DataInputStream(BufferedInputStream(GZIPInputStream(content.inputStream())))) if (schem.size() == 1) schem = schem.first() as CompoundTag val version = schem.let { if (it.contains("Materials")) return@let SchematicFormat.MCEDIT else if (it.contains("Blocks")) return@let SchematicFormat.SPONGE_V3 else return@let SchematicFormat.SPONGE_V2 } if (version == SchematicFormat.SPONGE_V3) { try { val fawe = schem.getCompound("Metadata") .getCompound("WorldEdit") .getString("Version") .value if (fawe.equals("2.12.3-SNAPSHOT")) { SWException.log("Schematic with Bugged Version Uploaded", """ Schematic=$schemName User=${user.userName} Id=${user.id} """.trimIndent()) } } catch (_: Exception) {} } NodeData.saveFromStream(node, content.inputStream(), version) call.respond(ResponseSchematic(node)) } catch (e: Exception) { call.respond(HttpStatusCode.BadRequest, ResponseError( error = e.message ?: "GENERIC", code = "UPLOAD_ERROR" )) } } } } suspend fun ApplicationCall.receiveSchematic(fieldName: String = "code", delete: Boolean = false): SchematicNode? { val code = parameters[fieldName] ?: run { respond(HttpStatusCode.BadRequest) return null } val dl = NodeDownload.get(code) ?: run { respond(HttpStatusCode.NotFound) return null } if(dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) { respond(HttpStatusCode.Gone) return null } if (delete) { dl.delete() } val node = SchematicNode.getSchematicNode(dl.nodeId) ?: run { respond(HttpStatusCode.NotFound) return null } return node }