From fd7fe8c305860b1e30a4bf265734d0d3abd8711e Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 18 Aug 2024 11:15:54 +0200 Subject: [PATCH 1/9] Add Backend to Monorepo --- CommonCore/SQL/src/de/steamwar/sql/Event.java | 28 +- .../SQL/src/de/steamwar/sql/EventFight.java | 46 +++- .../SQL/src/de/steamwar/sql/NodeDownload.java | 17 ++ .../SQL/src/de/steamwar/sql/NodeMember.java | 4 + .../SQL/src/de/steamwar/sql/Referee.java | 12 + .../src/de/steamwar/sql/SchematicNode.java | 15 +- .../src/de/steamwar/sql/SchematicType.java | 2 +- .../SQL/src/de/steamwar/sql/SteamwarUser.java | 5 + .../SQL/src/de/steamwar/sql/UserPerm.java | 15 +- .../src/de/steamwar/sql/internal/Table.java | 6 +- WebsiteBackend/build.gradle.kts | 61 +++++ WebsiteBackend/src/de/steamwar/Application.kt | 61 +++++ .../src/de/steamwar/bungee/Bungee.kt | 56 ++++ WebsiteBackend/src/de/steamwar/data/Groups.kt | 86 ++++++ WebsiteBackend/src/de/steamwar/data/Paths.kt | 30 +++ .../src/de/steamwar/data/SkinCache.kt | 110 ++++++++ .../src/de/steamwar/plugins/Auth.kt | 104 ++++++++ .../src/de/steamwar/plugins/Plugins.kt | 78 ++++++ .../src/de/steamwar/plugins/SteamWar.kt | 27 ++ WebsiteBackend/src/de/steamwar/routes/Auth.kt | 168 ++++++++++++ WebsiteBackend/src/de/steamwar/routes/Data.kt | 150 +++++++++++ .../src/de/steamwar/routes/EventFights.kt | 171 ++++++++++++ .../src/de/steamwar/routes/Events.kt | 252 ++++++++++++++++++ WebsiteBackend/src/de/steamwar/routes/Page.kt | 251 +++++++++++++++++ .../src/de/steamwar/routes/Routes.kt | 40 +++ .../src/de/steamwar/routes/Schematic.kt | 229 ++++++++++++++++ .../src/de/steamwar/routes/Stats.kt | 87 ++++++ .../src/de/steamwar/routes/Teams.kt | 33 +++ WebsiteBackend/src/de/steamwar/routes/User.kt | 54 ++++ .../src/de/steamwar/routes/UserPerms.kt | 154 +++++++++++ .../src/de/steamwar/sql/SQLConfigImpl.kt | 29 ++ .../src/de/steamwar/sql/SQLWrapperImpl.kt | 68 +++++ WebsiteBackend/src/de/steamwar/sql/Stats.kt | 67 +++++ .../src/de/steamwar/util/ServerPing.kt | 137 ++++++++++ WebsiteBackend/src/logback.xml | 48 ++++ settings.gradle.kts | 24 ++ steamwarci.yml | 4 +- 37 files changed, 2703 insertions(+), 26 deletions(-) create mode 100644 WebsiteBackend/build.gradle.kts create mode 100644 WebsiteBackend/src/de/steamwar/Application.kt create mode 100644 WebsiteBackend/src/de/steamwar/bungee/Bungee.kt create mode 100644 WebsiteBackend/src/de/steamwar/data/Groups.kt create mode 100644 WebsiteBackend/src/de/steamwar/data/Paths.kt create mode 100644 WebsiteBackend/src/de/steamwar/data/SkinCache.kt create mode 100644 WebsiteBackend/src/de/steamwar/plugins/Auth.kt create mode 100644 WebsiteBackend/src/de/steamwar/plugins/Plugins.kt create mode 100644 WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/Auth.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/Data.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/EventFights.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/Events.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/Page.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/Routes.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/Schematic.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/Stats.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/Teams.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/User.kt create mode 100644 WebsiteBackend/src/de/steamwar/routes/UserPerms.kt create mode 100644 WebsiteBackend/src/de/steamwar/sql/SQLConfigImpl.kt create mode 100644 WebsiteBackend/src/de/steamwar/sql/SQLWrapperImpl.kt create mode 100644 WebsiteBackend/src/de/steamwar/sql/Stats.kt create mode 100644 WebsiteBackend/src/de/steamwar/util/ServerPing.kt create mode 100644 WebsiteBackend/src/logback.xml diff --git a/CommonCore/SQL/src/de/steamwar/sql/Event.java b/CommonCore/SQL/src/de/steamwar/sql/Event.java index 31c220ac..a92cc9f2 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/Event.java +++ b/CommonCore/SQL/src/de/steamwar/sql/Event.java @@ -21,6 +21,7 @@ package de.steamwar.sql; import de.steamwar.sql.internal.Field; import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Statement; import de.steamwar.sql.internal.Table; import lombok.AllArgsConstructor; import lombok.Getter; @@ -42,6 +43,11 @@ public class Event { private static final SelectStatement byId = table.select(Table.PRIMARY); private static final SelectStatement byName = table.select("eventName"); private static final SelectStatement byComing = new SelectStatement<>(table, "SELECT * FROM Event WHERE Start > now()"); + private static final SelectStatement all = new SelectStatement<>(table, "SELECT * FROM Event"); + + private static final Statement create = table.insertFields(true, "eventName", "deadline", "start", "end", "maximumTeamMembers", "publicSchemsOnly"); + private static final Statement update = table.update(Table.PRIMARY, "eventName", "deadline", "start", "end", "schemType", "maximumTeamMembers", "publicSchemsOnly"); + private static final Statement delete = table.delete(Table.PRIMARY); private static Event current = null; @@ -53,6 +59,14 @@ public class Event { return current; } + public static List getAll(){ + return all.listSelect(); + } + + public static Event create(String eventName, Timestamp start, Timestamp end){ + return get(create.insertGetKey(eventName, start, start, end, 5, false, false)); + } + public static Event get(int eventID){ return byId.select(eventID); } @@ -87,16 +101,10 @@ public class Event { private final SchematicType schemType; @Field private final boolean publicSchemsOnly; - @Deprecated - @Field - private final boolean spectateSystem; public boolean publicSchemsOnly() { return publicSchemsOnly; } - public boolean spectateSystem(){ - return spectateSystem; - } public SchematicType getSchematicType() { return schemType; @@ -106,4 +114,12 @@ public class Event { Instant now = Instant.now(); return now.isAfter(start.toInstant()) && now.isBefore(end.toInstant()); } + + public void update(String eventName, Timestamp deadline, Timestamp start, Timestamp end, SchematicType schemType, int maximumTeamMembers, boolean publicSchemsOnly) { + update.update(eventName, deadline, start, end, schemType, maximumTeamMembers, publicSchemsOnly, eventID); + } + + public void delete() { + delete.update(eventID); + } } diff --git a/CommonCore/SQL/src/de/steamwar/sql/EventFight.java b/CommonCore/SQL/src/de/steamwar/sql/EventFight.java index 623e383a..cd7352b3 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/EventFight.java +++ b/CommonCore/SQL/src/de/steamwar/sql/EventFight.java @@ -25,6 +25,7 @@ import de.steamwar.sql.internal.Statement; import de.steamwar.sql.internal.Table; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.Setter; import java.sql.Timestamp; import java.util.*; @@ -42,6 +43,11 @@ public class EventFight implements Comparable { private static final Statement setResult = table.update(Table.PRIMARY, "Ergebnis"); private static final Statement setFight = table.update(Table.PRIMARY, "Fight"); + private static final Statement create = table.insertAll(true); + private static final Statement update = table.update(Table.PRIMARY, "startTime", "spielModus", "map", "teamBlue", "teamRed", "spectatePort"); + private static final Statement delete = table.delete(Table.PRIMARY); + + @Getter private static final Queue fights = new PriorityQueue<>(); public static EventFight get(int fightID) { @@ -57,8 +63,8 @@ public class EventFight implements Comparable { return event.listSelect(eventID); } - public static Queue getFights() { - return fights; + public static EventFight create(int event, Timestamp from, String spielmodus, String map, int blueTeam, int redTeam, Integer spectatePort) { + return get(create.insertGetKey(event, from, spielmodus, map, blueTeam, redTeam, spectatePort)); } @Getter @@ -68,27 +74,29 @@ public class EventFight implements Comparable { @Field(keys = {Table.PRIMARY}, autoincrement = true) private final int fightID; @Getter + @Setter @Field private Timestamp startTime; @Getter + @Setter @Field - private final String spielmodus; + private String spielmodus; @Getter + @Setter @Field - private final String map; + private String map; @Getter + @Setter @Field - private final int teamBlue; + private int teamBlue; @Getter + @Setter @Field - private final int teamRed; + private int teamRed; @Getter - @Field - @Deprecated - private final int kampfleiter; - @Getter - @Field - private final int spectatePort; + @Setter + @Field(nullable = true) + private int spectatePort; @Getter @Field(def = "0") private int ergebnis; @@ -133,4 +141,18 @@ public class EventFight implements Comparable { public int compareTo(EventFight o) { return startTime.compareTo(o.startTime); } + + public void update(Timestamp startTime, String spielmodus, String map, int teamBlue, int teamRed, Integer spectatePort) { + update.update(startTime, spielmodus, map, teamBlue, teamRed, spectatePort, fightID); + this.startTime = startTime; + this.spielmodus = spielmodus; + this.map = map; + this.teamBlue = teamBlue; + this.teamRed = teamRed; + this.spectatePort = spectatePort; + } + + public void delete() { + delete.update(fightID); + } } diff --git a/CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java b/CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java index 666cf681..018d95d7 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java +++ b/CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java @@ -20,9 +20,11 @@ package de.steamwar.sql; import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; import de.steamwar.sql.internal.Statement; import de.steamwar.sql.internal.Table; import lombok.AllArgsConstructor; +import lombok.Getter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -38,11 +40,21 @@ public class NodeDownload { private static final Table table = new Table<>(NodeDownload.class); private static final Statement insert = table.insertFields("NodeId", "Link"); + private static final SelectStatement select = table.select("link"); + + private static final Statement delete = table.delete(Table.PRIMARY); + + public static NodeDownload get(String link) { + return select.select(link); + } + + @Getter @Field(keys = {Table.PRIMARY}) private final int nodeId; @Field private final String link; @Field(def = "CURRENT_TIMESTAMP") + @Getter private final Timestamp timestamp; public static String getLink(SchematicNode schem){ @@ -60,10 +72,15 @@ public class NodeDownload { insert.update(schem.getId(), hash); return LINK_BASE + hash; } + public static String base16encode(byte[] byteArray) { StringBuilder hexBuffer = new StringBuilder(byteArray.length * 2); for (byte b : byteArray) hexBuffer.append(HEX[(b >>> 4) & 0xF]).append(HEX[b & 0xF]); return hexBuffer.toString(); } + + public void delete() { + delete.update(nodeId); + } } diff --git a/CommonCore/SQL/src/de/steamwar/sql/NodeMember.java b/CommonCore/SQL/src/de/steamwar/sql/NodeMember.java index e25764ec..64844e94 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/NodeMember.java +++ b/CommonCore/SQL/src/de/steamwar/sql/NodeMember.java @@ -72,6 +72,10 @@ public class NodeMember { return new NodeMember(node, member, null); } + public static NodeMember getNodeMember(int node, SteamwarUser member) { + return getNodeMember(node, member.getId()); + } + public static NodeMember getNodeMember(int node, int member) { return getNodeMember.select(node, member); } diff --git a/CommonCore/SQL/src/de/steamwar/sql/Referee.java b/CommonCore/SQL/src/de/steamwar/sql/Referee.java index 281ad302..e22faf7e 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/Referee.java +++ b/CommonCore/SQL/src/de/steamwar/sql/Referee.java @@ -21,6 +21,7 @@ package de.steamwar.sql; import de.steamwar.sql.internal.Field; import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Statement; import de.steamwar.sql.internal.Table; import lombok.AllArgsConstructor; @@ -33,6 +34,17 @@ public class Referee { private static final Table table = new Table<>(Referee.class); private static final SelectStatement byEvent = table.selectFields("eventID"); + private static final Statement insert = table.insertAll(); + private static final Statement delete = table.delete("eventReferee"); + + public static void add(int eventID, int userID) { + insert.update(eventID, userID); + } + + public static void remove(int eventID, int userID) { + delete.update(eventID, userID); + } + public static Set get(int eventID) { return byEvent.listSelect(eventID).stream().map(referee -> referee.userID).collect(Collectors.toSet()); } diff --git a/CommonCore/SQL/src/de/steamwar/sql/SchematicNode.java b/CommonCore/SQL/src/de/steamwar/sql/SchematicNode.java index b432a709..3df740da 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SchematicNode.java +++ b/CommonCore/SQL/src/de/steamwar/sql/SchematicNode.java @@ -443,7 +443,7 @@ public class SchematicNode { return SchemElo.getElo(this, season); } - public boolean accessibleByUser(int user) { + public boolean accessibleByUser(SteamwarUser user) { return NodeMember.getNodeMember(nodeId, user) != null; } @@ -506,6 +506,19 @@ public class SchematicNode { return builder.toString(); } + public List> generateBreadcrumbsMap(SteamwarUser user) { + List> map = new ArrayList<>(); + Optional currentNode = Optional.of(this); + if(currentNode.map(SchematicNode::isDir).orElse(false)) { + map.add(new AbstractMap.SimpleEntry<>(getName(), getId())); + } + while (currentNode.isPresent()) { + currentNode = currentNode.flatMap(schematicNode -> Optional.ofNullable(NodeMember.getNodeMember(schematicNode.getId(), effectiveOwner)).map(NodeMember::getParent).orElse(schematicNode.getOptionalParent())).map(SchematicNode::getSchematicNode); + currentNode.ifPresent(node -> map.add(0, new AbstractMap.SimpleEntry<>(node.getName(), node.getId()))); + } + return map; + } + private static final List FORBIDDEN_NAMES = Collections.unmodifiableList(Arrays.asList("public")); public static boolean invalidSchemName(String[] layers) { for (String layer : layers) { diff --git a/CommonCore/SQL/src/de/steamwar/sql/SchematicType.java b/CommonCore/SQL/src/de/steamwar/sql/SchematicType.java index 0ee483a8..62b047c8 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SchematicType.java +++ b/CommonCore/SQL/src/de/steamwar/sql/SchematicType.java @@ -108,7 +108,7 @@ public class SchematicType { return name.toLowerCase(); } - public static SchematicType fromDB(String input){ + public static SchematicType fromDB(String input) { return fromDB.get(input.toLowerCase()); } diff --git a/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.java b/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.java index b505ab1f..8aefbd49 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.java +++ b/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.java @@ -62,6 +62,7 @@ public class SteamwarUser { private static final SelectStatement byDiscord = table.selectFields("DiscordId"); private static final SelectStatement byTeam = table.selectFields("Team"); private static final SelectStatement getUsersWithPerm = new SelectStatement<>(table, "SELECT S.* FROM UserData S JOIN UserPerm P ON S.id = P.User WHERE P.Perm = ?"); + private static final SelectStatement getAll = new SelectStatement(table, "SELECT * FROM UserData"); private static final Statement updateName = table.update(Table.PRIMARY, "UserName"); private static final Statement updatePassword = table.update(Table.PRIMARY, "Password"); @@ -370,4 +371,8 @@ public class SteamwarUser { permissions = UserPerm.getPerms(id); prefix = permissions.stream().filter(UserPerm.prefixes::containsKey).findAny().map(UserPerm.prefixes::get).orElse(UserPerm.emptyPrefix); } + + public static List getAll() { + return null; + } } diff --git a/CommonCore/SQL/src/de/steamwar/sql/UserPerm.java b/CommonCore/SQL/src/de/steamwar/sql/UserPerm.java index 0aca0319..a05fe2ad 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/UserPerm.java +++ b/CommonCore/SQL/src/de/steamwar/sql/UserPerm.java @@ -19,10 +19,7 @@ package de.steamwar.sql; -import de.steamwar.sql.internal.Field; -import de.steamwar.sql.internal.SelectStatement; -import de.steamwar.sql.internal.SqlTypeMapper; -import de.steamwar.sql.internal.Table; +import de.steamwar.sql.internal.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -68,11 +65,21 @@ public enum UserPerm { private static final Table table = new Table<>(UserPermTable.class, "UserPerm"); private static final SelectStatement getPerms = table.selectFields("user"); + private static final Statement addPerm = table.insertFields("user", "perm"); + private static final Statement removePerm = table.delete(Table.PRIMARY); public static Set getPerms(int user) { return getPerms.listSelect(user).stream().map(up -> up.perm).collect(Collectors.toSet()); } + public static void addPerm(SteamwarUser user, UserPerm perm) { + addPerm.update(user, perm); + } + + public static void removePerm(SteamwarUser user, UserPerm perm) { + removePerm.update(user, perm); + } + @Getter @AllArgsConstructor public static class Prefix { diff --git a/CommonCore/SQL/src/de/steamwar/sql/internal/Table.java b/CommonCore/SQL/src/de/steamwar/sql/internal/Table.java index ddb5adc7..e052bf26 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/internal/Table.java +++ b/CommonCore/SQL/src/de/steamwar/sql/internal/Table.java @@ -83,7 +83,11 @@ public class Table { } public Statement insertAll() { - return insertFields(false, Arrays.stream(fields).map(f -> f.identifier).toArray(String[]::new)); + return insertAll(false); + } + + public Statement insertAll(boolean returnGeneratedKeys) { + return insertFields(returnGeneratedKeys, Arrays.stream(fields).map(f -> f.identifier).toArray(String[]::new)); } public Statement insertFields(String... fields) { diff --git a/WebsiteBackend/build.gradle.kts b/WebsiteBackend/build.gradle.kts new file mode 100644 index 00000000..cd077973 --- /dev/null +++ b/WebsiteBackend/build.gradle.kts @@ -0,0 +1,61 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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 . + */ + +plugins { + steamwar.kotlin + id("io.ktor.plugin") version "2.3.12" + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.10" + application +} + +application { + mainClass.set("de.steamwar.ApplicationKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +ktor { + fatJar { + archiveFileName.set("api.jar") + } +} + +dependencies { + implementation(libs.logback) + implementation(libs.ktor) + implementation(libs.ktorContentNegotiation) + implementation(libs.ktorCors) + implementation(libs.ktorSerialization) + implementation(libs.ktorNetty) + implementation(libs.ktorHost) + implementation(libs.ktorRequestValidation) + implementation(libs.ktorAuth) + implementation(libs.ktorAuthJvm) + implementation(libs.ktorAuthLdap) + implementation(libs.ktorClientCore) + implementation(libs.ktorClientJava) + implementation(libs.ktorClientContentNegotiation) + implementation(libs.ktorClientAuth) + implementation(libs.mysql) + implementation(project(":CommonCore")) + implementation(libs.yamlconfig) + implementation(libs.kotlinxSerializationCbor) + implementation(libs.ktorRateLimit) +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/Application.kt b/WebsiteBackend/src/de/steamwar/Application.kt new file mode 100644 index 00000000..1b653a60 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/Application.kt @@ -0,0 +1,61 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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 + +import de.steamwar.plugins.configurePlugins +import de.steamwar.routes.ResponseUser +import de.steamwar.routes.SchematicCode +import io.ktor.server.application.* +import io.ktor.server.engine.* +import de.steamwar.routes.configureRoutes +import de.steamwar.sql.SchematicType +import de.steamwar.sql.SteamwarUser +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, val code: String = error) + +@Serializable +data class Config(val giteaToken: String) + +@OptIn(ExperimentalSerializationApi::class) +val config = Json.decodeFromStream(File("config.json").inputStream()) + +fun main() { + SchematicType.Normal.name().length + Thread { + while (true) { + Thread.sleep(1000 * 10) + ResponseUser.clearCache() + } + }.start() + embeddedServer(Netty, port = 1337, host = "127.0.0.1", module = Application::module) + .start(wait = true) +} + +fun Application.module() { + configurePlugins() + configureRoutes() +} diff --git a/WebsiteBackend/src/de/steamwar/bungee/Bungee.kt b/WebsiteBackend/src/de/steamwar/bungee/Bungee.kt new file mode 100644 index 00000000..547aa2cf --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/bungee/Bungee.kt @@ -0,0 +1,56 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.bungee + +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.net.Socket + +object Bungee { + var socket: Socket? = null + var output: ObjectOutputStream? = null + var thread: Thread? = null + + fun run() { + val input = ObjectInputStream(socket!!.getInputStream()) + + while (socket != null && !socket!!.isClosed) { + if (input.available() <= 0) { + Thread.sleep(50) + continue + } + } + + socket = null + thread = null + output = null + } + + inline fun connect(func: (Socket) -> Unit) { + if(socket == null || socket!!.isClosed) { + socket = Socket("localhost", 7546) + output = ObjectOutputStream(socket!!.getOutputStream()) + thread = Thread(Bungee::run) + thread!!.start() + } + + func(socket!!) + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/data/Groups.kt b/WebsiteBackend/src/de/steamwar/data/Groups.kt new file mode 100644 index 00000000..ae895bfc --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/data/Groups.kt @@ -0,0 +1,86 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.data + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray + +@Serializable +data class GroupsData(val groups: MutableList) + +@Serializable +data class GroupData(val name: String, val fights: MutableList) + +@OptIn(ExperimentalSerializationApi::class) +class Groups { + companion object { + private var groups: GroupsData = if (kGroupsFile.exists()) { + Cbor.decodeFromByteArray(kGroupsFile.readBytes()) + } else { + kGroupsFile.createNewFile() + kGroupsFile.writeBytes(Cbor.encodeToByteArray(GroupsData(mutableListOf()))) + + GroupsData(mutableListOf()) + } + + fun getGroup(name: String): GroupData? { + return groups.groups.find { it.name == name } + } + + fun getGroup(fight: Int): GroupData? { + return groups.groups.find { it.fights.contains(fight) } + } + + fun getOrCreateGroup(name: String): GroupData { + val group = getGroup(name) + if (group != null) { + return group + } + val newGroup = GroupData(name, mutableListOf()) + groups.groups.add(newGroup) + return newGroup + } + + fun resetGroup(fight: Int, save: Boolean = false) { + val oldGroup = getGroup(fight) + oldGroup?.fights?.remove(fight) + if(oldGroup?.fights?.isEmpty() == true) { + groups.groups.remove(oldGroup) + } + if(save) { + kGroupsFile.writeBytes(Cbor.encodeToByteArray(groups)) + } + } + + fun setGroup(fight: Int, group: String) { + resetGroup(fight) + val newGroup = getOrCreateGroup(group) + newGroup.fights.add(fight) + kGroupsFile.writeBytes(Cbor.encodeToByteArray(groups)) + } + + fun getAllGroups(): List { + return groups.groups.map { it.name } + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/data/Paths.kt b/WebsiteBackend/src/de/steamwar/data/Paths.kt new file mode 100644 index 00000000..145f958f --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/data/Paths.kt @@ -0,0 +1,30 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.data + +import java.io.File + +const val kDataFolder: String = "data" + +const val kGroupsName: String = "groups.cbor" +val kGroupsFile: File = File(kDataFolder, kGroupsName) + +const val kRelationsName = "relations.cbor" +val kRelationsFile: File = File(kDataFolder, kRelationsName) \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/data/SkinCache.kt b/WebsiteBackend/src/de/steamwar/data/SkinCache.kt new file mode 100644 index 00000000..6a6ff935 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/data/SkinCache.kt @@ -0,0 +1,110 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.data + +import io.ktor.client.* +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.serialization.kotlinx.json.* +import io.ktor.utils.io.jvm.javaio.* +import kotlinx.coroutines.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray +import java.io.File +import java.time.Instant +import java.time.temporal.ChronoUnit + +const val kCacheFolder: String = "skins" +val kCacheFolderFile: File = File(kCacheFolder) +const val kCacheConfigName: String = "cache.cbor" +val kCacheConfigFile: File = File(kCacheFolder, kCacheConfigName) + +@Serializable +data class CacheConfig(val lastUpdate: MutableMap) { + @OptIn(ExperimentalSerializationApi::class) + companion object { + private var config: CacheConfig = if (kCacheConfigFile.exists()) { + kCacheConfigFile.inputStream().use { + Cbor.decodeFromByteArray(it.readBytes()) + } + } else { + kCacheConfigFile.createNewFile() + kCacheConfigFile.outputStream().use { + it.write(Cbor.encodeToByteArray(CacheConfig(mutableMapOf()))) + } + + CacheConfig(mutableMapOf()) + } + + private fun save() { + kCacheConfigFile.outputStream().use { + it.write(Cbor.encodeToByteArray(config)) + } + } + + fun update(uuid: String) { + config.lastUpdate[uuid] = Instant.now().toEpochMilli() + save() + } + + fun isOutdated(uuid: String): Boolean { + return config.lastUpdate[uuid]?.let { + it < Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli() + } ?: true + } + } +} + +val client = HttpClient(Java) { + install(ContentNegotiation) { + json() + } + defaultRequest { + header("User-Agent", "SteamWar/1.0") + } +} + +suspend fun getCachedSkin(uuid: String): Pair { + val file = File(kCacheFolderFile, "$uuid.webp") + if (file.exists()) { + if (CacheConfig.isOutdated(uuid)) { + val skin = client.get("https://visage.surgeplay.com/bust/150/$uuid") + skin.bodyAsChannel().copyTo(file.outputStream()) + + CacheConfig.update(uuid) + return file to false + } + return file to true + } + + withContext(Dispatchers.IO) { + file.createNewFile() + } + val skin = client.get("https://visage.surgeplay.com/bust/150/$uuid") + skin.bodyAsChannel().copyTo(file.outputStream()) + CacheConfig.update(uuid) + return file to false +} diff --git a/WebsiteBackend/src/de/steamwar/plugins/Auth.kt b/WebsiteBackend/src/de/steamwar/plugins/Auth.kt new file mode 100644 index 00000000..40006aa5 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/plugins/Auth.kt @@ -0,0 +1,104 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.plugins + +import de.steamwar.sql.SWException +import de.steamwar.sql.SteamwarUser +import de.steamwar.sql.Token +import de.steamwar.sql.UserPerm +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.application.hooks.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* + + +data class SWAuthPrincipal(val token: Token, val user: SteamwarUser) : Principal + +class SWAuthConfig { + var permission: UserPerm? = null + var allowedMethods = mutableListOf() + var userCheck: SWAuthPrincipal.(ApplicationRequest) -> Boolean = { true } + var mustAuth: Boolean = false + + fun allowMethod(method: HttpMethod) { + allowedMethods.add(method) + } + + fun allowMethods(methods: List) { + allowedMethods.addAll(methods) + } + + fun userCheck(check: SWAuthPrincipal.(ApplicationRequest) -> Boolean) { + userCheck = check + } +} + +val SWPermissionCheck = createRouteScopedPlugin("SWAuth", ::SWAuthConfig) { + pluginConfig.apply { + on(AuthenticationChecked) { call -> + if (call.request.httpMethod in allowedMethods) { + if(mustAuth) { + val token = call.principal() + + if (token == null) { + call.respond(HttpStatusCode.Unauthorized) + } + } + + return@on + } + + val token = call.principal() + + if (token == null) { + call.respond(HttpStatusCode.Unauthorized) + return@on + } + + if (permission != null && !token.user.hasPerm(permission)) { + call.respond(HttpStatusCode.Forbidden) + return@on + } + + if (!token.userCheck(call.request)) { + call.respond(HttpStatusCode.Forbidden) + return@on + } + } + } +} + +val ErrorLogger = createApplicationPlugin("SWLogger") { + on(CallFailed) { call, cause -> + val msg = """ +{ +URI: ${call.request.uri} +Method: ${call.request.httpMethod.value} +Headers: ${call.request.headers.entries().joinToString("\n ") { "${it.key}: ${it.value}" }} + +Message: ${cause.message} +} + """ + + call.response.headers.append("X-Caught", "1") + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt b/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt new file mode 100644 index 00000000..b8780f64 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt @@ -0,0 +1,78 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.plugins + +import de.steamwar.sql.Token +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.plugins.ratelimit.* +import kotlinx.serialization.json.Json +import kotlin.time.Duration.Companion.seconds + +fun Application.configurePlugins() { + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowHeader(HttpHeaders.Authorization) + allowHeader(HttpHeaders.AccessControlAllowOrigin) + allowHeader(HttpHeaders.ContentType) + anyHost() + allowXHttpMethodOverride() + } + install(RateLimit) { + global { + rateLimiter(limit = 60, refillPeriod = 60.seconds) + requestKey { + it.request.headers["X-Forwarded-For"] ?: it.request.local.remoteHost + } + requestWeight { applicationCall, _ -> + if(applicationCall.request.headers["X-Forwarded-For"] != null) { + 0 + } else { + 1 + } + } + } + } + authentication { + bearer("sw-auth") { + realm = "SteamWar API" + authenticate { call -> + val token = Token.getTokenByCode(call.token) + if (token == null) { + null + } else { + SWAuthPrincipal(token, token.owner) + } + } + } + } + install(ContentNegotiation) { + json(Json) + } + install(ErrorLogger) +} diff --git a/WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt b/WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt new file mode 100644 index 00000000..bcac22bc --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt @@ -0,0 +1,27 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.plugins + +import de.steamwar.sql.SteamwarUser +import io.ktor.server.request.* + +fun ApplicationRequest.getUser(key: String = "id"): SteamwarUser? { + return SteamwarUser.get(call.parameters[key]?.toIntOrNull() ?: return null) +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/Auth.kt new file mode 100644 index 00000000..d230c4f4 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/Auth.kt @@ -0,0 +1,168 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.SteamwarUser +import de.steamwar.sql.Token +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.time.format.DateTimeFormatter +import java.time.LocalDateTime + +@Serializable +data class AuthLoginRequest(val username: String, val password: String) + +@Serializable +data class AuthTokenResponse(val token: String) + +@Serializable +data class ResponseToken(val id: Int, val name: String, val created: String) { + constructor(token: Token) : this(token.id, token.name, token.created.toLocalDateTime().toString()) +} + +@Serializable +data class CreateTokenRequest(val name: String, val password: String) + +fun Route.configureAuthRoutes() { + route("/auth") { + post("/login") { + if (call.principal() != null) { + call.respond(HttpStatusCode.Forbidden, ResponseError("Already logged in", "already_logged_in")) + return@post + } + + val request = call.receive() + + val user = SteamwarUser.get(request.username) + + if (user == null) { + call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid")) + return@post + } + + if (!user.verifyPassword(request.password)) { + call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid")) + return@post + } + + val code = Token.createToken("Website: ${DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now())}", user) + call.respond(AuthTokenResponse(code)) + } + route("/tokens") { + install(SWPermissionCheck) { + mustAuth = true + } + + get { + val auth = call.principal() + + if(auth == null) { + call.respond(HttpStatusCode.InternalServerError) + return@get + } + + call.respond(Token.listUser(auth.user).map { ResponseToken(it) }) + } + + post { + val auth = call.principal() + + if(auth == null) { + call.respond(HttpStatusCode.InternalServerError) + return@post + } + + val request = call.receive() + + if(request.name.length > 32) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Name too long", "name_too_long")) + return@post + } + + if(request.name.length < 3) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Name too short", "name_too_short")) + return@post + } + + if(!auth.user.verifyPassword(request.password)) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid password", "invalid_password")) + return@post + } + + val token = Token.createToken(request.name, auth.user) + + call.respond(AuthTokenResponse(token)) + } + + route("/{id}") { + delete { + val auth = call.principal() + + if(auth == null) { + call.respond(HttpStatusCode.InternalServerError) + return@delete + } + + val id = call.parameters["id"]?.toIntOrNull() + + if(id == null) { + call.respond(HttpStatusCode.BadRequest) + return@delete + } + + val token = Token.get(id) + + if(token == null) { + call.respond(HttpStatusCode.NotFound) + return@delete + } + + if(token.owner != auth.user) { + call.respond(HttpStatusCode.Forbidden) + return@delete + } + + token.delete() + call.respond(HttpStatusCode.OK) + } + } + + post("/logout") { + val auth = call.principal() + + if(auth == null) { + call.respond(HttpStatusCode.InternalServerError) + return@post + } + + auth.token.delete() + call.respond(HttpStatusCode.OK) + } + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Data.kt b/WebsiteBackend/src/de/steamwar/routes/Data.kt new file mode 100644 index 00000000..d05ffc90 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/Data.kt @@ -0,0 +1,150 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.data.Groups +import de.steamwar.data.getCachedSkin +import de.steamwar.plugins.SWAuthPrincipal +import de.steamwar.plugins.SWPermissionCheck +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.auth.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable +import org.bspfsystems.yamlconfiguration.file.YamlConfiguration +import java.io.File +import java.net.InetSocketAddress +import java.util.UUID + +@Serializable +data class ResponseSchematicType(val name: String, val db: String) + +@Serializable +data class ResponseUser(val id: Int, val name: String, val uuid: String, val prefix: String, val perms: List) { + constructor(user: SteamwarUser) : this(user.id, user.userName, user.uuid.toString(), user.prefix().chatPrefix, user.perms().map { it.name }) { + synchronized(cache) { + cache[id] = this + } + } + + companion object { + private val cache = mutableMapOf() + + fun get(id: Int): ResponseUser { + synchronized(cache) { + return cache[id] ?: ResponseUser(SteamwarUser.get(id)).also { cache[id] = it } + } + } + + fun clearCache() { + synchronized(cache) { + cache.clear() + } + } + } +} + +fun Route.configureDataRoutes() { + route("/data") { + get { + call.respondText("Hello World!") + } + get("/schematicTypes") { + val types = mutableListOf() + loadSchematicTypes(types, mutableMapOf()) + call.respond(types.filter { !it.check() }.map { ResponseSchematicType(it.name(), it.toDB()) }) + } + get("/gamemodes") { + call.respond( + File("/configs/GameModes/").listFiles()!! + .filter { it.name.endsWith(".yml") && !it.name.endsWith(".kits.yml") } + .map { it.nameWithoutExtension }) + } + get("/gamemodes/{gamemode}/maps") { + val gamemode = call.parameters["gamemode"] + if (gamemode == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid gamemode")) + return@get + } + val file = File("/configs/GameModes/$gamemode.yml") + if (!file.exists()) { + call.respond(HttpStatusCode.NotFound, ResponseError("Gamemode not found")) + return@get + } + call.respond(YamlConfiguration.loadConfiguration(file).getStringList("Server.Maps")) + } + get("/users") { + call.respond(SteamwarUser.getAll().map { ResponseUser(it) }) + } + get("/team") { + call.respond( + listOf(UserPerm.PREFIX_ADMIN, UserPerm.PREFIX_DEVELOPER, UserPerm.PREFIX_MODERATOR, UserPerm.PREFIX_SUPPORTER, UserPerm.PREFIX_BUILDER) + .associateWith { SteamwarUser.getUsersWithPerm(it) } + .mapKeys { UserPerm.prefixes[it.key]!!.chatPrefix } + .mapValues { it.value.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 + } + } + get("/skin/{uuid}") { + val uuid = call.parameters["uuid"] + if (uuid == null || catchException { UUID.fromString(uuid) } == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid UUID")) + return@get + } + + val skin = getCachedSkin(uuid) + call.response.header("X-Cache", if (skin.second) "HIT" else "MISS") + call.response.header("Cache-Control", "public, max-age=604800") + call.respondFile(skin.first) + } + + route("/me") { + install(SWPermissionCheck) + get { + call.respond(ResponseUser(call.principal()!!.user)) + } + } + } +} + +inline fun catchException(yield: () -> T): T? = try { + yield() +} catch (e: Exception) { + null +} diff --git a/WebsiteBackend/src/de/steamwar/routes/EventFights.kt b/WebsiteBackend/src/de/steamwar/routes/EventFights.kt new file mode 100644 index 00000000..8112d87c --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/EventFights.kt @@ -0,0 +1,171 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.data.Groups +import de.steamwar.plugins.SWPermissionCheck +import de.steamwar.sql.EventFight +import de.steamwar.sql.SteamwarUser +import de.steamwar.sql.Team +import de.steamwar.sql.UserPerm +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable +import java.sql.Timestamp +import java.time.Instant + +@Serializable +data class ResponseEventFight( + val id: Int, + val spielmodus: String, + val map: String, + val blueTeam: ResponseTeam, + val redTeam: ResponseTeam, + val start: Long, + val ergebnis: Int, + val spectatePort: Int?, + val group: String? +) { + constructor(eventFight: EventFight) : this( + eventFight.fightID, + eventFight.spielmodus, + eventFight.map, + ResponseTeam(Team.get(eventFight.teamBlue)), + ResponseTeam(Team.get(eventFight.teamRed)), + eventFight.startTime.time, + eventFight.ergebnis, + eventFight.spectatePort, + Groups.getGroup(eventFight.fightID)?.name + ) +} + +@Serializable +data class ResponseTeam(val id: Int, val name: String, val kuerzel: String, val color: String) { + constructor(team: Team) : this(team.teamId, team.teamName, team.teamKuerzel, team.teamColor) +} + +@Serializable +data class UpdateEventFight( + val blueTeam: Int? = null, + val redTeam: Int? = null, + val start: Long? = null, + val spielmodus: String? = null, + val map: String? = null, + val group: String? = null, + val spectatePort: Int? = null +) + +@Serializable +data class CreateEventFight( + val event: Int, + val spielmodus: String, + val map: String, + val blueTeam: Int, + val redTeam: Int, + val start: Long, + val spectatePort: Int? = null, + val group: String? = null +) + +fun Route.configureEventFightRoutes() { + route("/fights") { + install(SWPermissionCheck) { + allowMethod(HttpMethod.Get) + permission = UserPerm.MODERATION + } + post { + val fight = call.receiveNullable() + if (fight == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) + return@post + } + val eventFight = EventFight.create( + fight.event, + Timestamp.from(Instant.ofEpochMilli(fight.start)), + fight.spielmodus, + fight.map, + fight.blueTeam, + fight.redTeam, + fight.spectatePort + ) + if (fight.group != null) { + if (fight.group != "null") { + Groups.setGroup(eventFight.fightID, fight.group) + } + } + call.respond(HttpStatusCode.Created, ResponseEventFight(eventFight)) + } + route("/{fight}") { + put { + val fightId = call.parameters["fight"]?.toIntOrNull() + if (fightId == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID")) + return@put + } + val fight = EventFight.get(fightId) + if (fight == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Fight not found")) + return@put + } + val updateFight = call.receiveNullable() + if (updateFight == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) + return@put + } + + val teamBlue = updateFight.blueTeam ?: fight.teamBlue + val teamRed = updateFight.redTeam ?: fight.teamRed + val start = updateFight.start?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: fight.startTime + val spielmodus = updateFight.spielmodus ?: fight.spielmodus + val map = updateFight.map ?: fight.map + val spectatePort = updateFight.spectatePort ?: fight.spectatePort + + if (updateFight.group != null) { + if (updateFight.group == "null") { + Groups.resetGroup(fightId, true) + } else { + Groups.setGroup(fightId, updateFight.group) + } + } + fight.update(start, spielmodus, map, teamBlue, teamRed, spectatePort) + call.respond(HttpStatusCode.OK, ResponseEventFight(fight)) + } + delete { + val fightId = call.parameters["fight"]?.toIntOrNull() + if (fightId == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID")) + return@delete + } + val fight = EventFight.get(fightId) + if (fight == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Fight not found")) + return@delete + } + fight.delete() + call.respond(HttpStatusCode.OK) + } + + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Events.kt b/WebsiteBackend/src/de/steamwar/routes/Events.kt new file mode 100644 index 00000000..987479c2 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/Events.kt @@ -0,0 +1,252 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.data.Groups +import de.steamwar.plugins.SWPermissionCheck +import de.steamwar.sql.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable +import java.lang.StringBuilder +import java.sql.Timestamp +import java.time.Instant + +@Serializable +data class ShortEvent(val id: Int, val name: String, val start: Long, val end: Long) { + constructor(event: Event) : this(event.eventID, event.eventName, event.start.time, event.end.time) +} + +@Serializable +data class ResponseEvent( + val id: Int, + val name: String, + val deadline: Long, + val start: Long, + val end: Long, + val maxTeamMembers: Int, + val schemType: String?, + val publicSchemsOnly: Boolean, + val referees: List, +) { + constructor(event: Event) : this( + event.eventID, + event.eventName, + event.deadline.time, + event.start.time, + event.end.time, + event.maximumTeamMembers, + event.schematicType?.toDB(), + event.publicSchemsOnly(), + Referee.get(event.eventID).map { ResponseUser(SteamwarUser.get(it)) } + ) +} + +@Serializable +data class ExtendedResponseEvent( + val event: ResponseEvent, + val teams: List, + val fights: List +) + +@Serializable +data class CreateEvent(val name: String, val start: Long, val end: Long) + +@Serializable +data class UpdateEvent( + val name: String? = null, + val deadline: Long? = null, + val start: Long? = null, + val end: Long? = null, + val maxTeamMembers: Int? = null, + val schemType: String? = null, + val publicSchemsOnly: Boolean? = null, + val addReferee: Set? = null, + val removeReferee: Set? = null, +) + +fun Route.configureEventsRoute() { + route("/events") { + install(SWPermissionCheck) { + allowMethod(HttpMethod.Get) + permission = UserPerm.MODERATION + } + get { + call.respond(Event.getAll().map { ShortEvent(it) }) + } + post { + val createEvent = call.receiveNullable() + if (createEvent == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) + return@post + } + val event = Event.create( + createEvent.name, + Timestamp.from(Instant.ofEpochMilli(createEvent.start)), + Timestamp.from(Instant.ofEpochMilli(createEvent.end)) + ) + call.respond(HttpStatusCode.Created, ResponseEvent(event)) + } + route("/{id}") { + get { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@get + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@get + } + call.respond( + ExtendedResponseEvent( + ResponseEvent(event), + TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) }, + EventFight.getEvent(event.eventID).map { ResponseEventFight(it) }) + ) + } + get("/teams") { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@get + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@get + } + call.respond(TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) }) + } + get("/fights") { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@get + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@get + } + call.respond(EventFight.getEvent(event.eventID).map { ResponseEventFight(it) }) + } + get("/csv") { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@get + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@get + } + + val fights = EventFight.getEvent(event.eventID) + val csv = StringBuilder(); + csv.append(arrayOf("Start", "BlueTeam", "RedTeam", "WinnerTeam", "Group").joinToString(",")) + fights.forEach { + csv.appendLine() + val blue = Team.get(it.teamBlue) + val red = Team.get(it.teamRed) + val winner = when(it.ergebnis) { + 1 -> blue.teamName + 2 -> red.teamName + 3 -> "Tie" + else -> "Unknown" + } + csv.append( + arrayOf( + it.startTime.toString(), + Team.get(it.teamBlue).teamName, + Team.get(it.teamRed).teamName, + winner, + Groups.getGroup(it.fightID)?.name ?: "Ungrouped" + ).joinToString(",") + ) + } + call.response.header("Content-Disposition", "attachment; filename=\"${event.eventName}.csv\"") + call.response.header("Content-Type", "text/csv") + call.response.header("Content-Transfer-Encoding", "binary") + call.response.header("Pragma", "no-cache") + call.respondText(csv.toString()) + } + put { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@put + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@put + } + val updateEvent = call.receiveNullable() + if (updateEvent == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) + return@put + } + val eventName = updateEvent.name ?: event.eventName + val deadline = updateEvent.deadline?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.deadline + val start = updateEvent.start?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.start + val end = updateEvent.end?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.end + val maxTeamMembers = updateEvent.maxTeamMembers ?: event.maximumTeamMembers + + val schemType = if (updateEvent.schemType == "null") null else updateEvent.schemType?.let { SchematicType.fromDB(it) } ?: event.schematicType + val publicSchemsOnly = updateEvent.publicSchemsOnly ?: event.publicSchemsOnly() + + if (updateEvent.addReferee != null) { + updateEvent.addReferee.forEach { + Referee.add(event.eventID, it) + } + } + + if (updateEvent.removeReferee != null) { + updateEvent.removeReferee.forEach { + Referee.remove(event.eventID, it) + } + } + event.update(eventName, deadline, start, end, schemType, maxTeamMembers, publicSchemsOnly) + call.respond(ResponseEvent(event)) + } + delete { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) + return@delete + } + val event = Event.get(id) + if (event == null) { + call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return@delete + } + event.delete() + call.respond(HttpStatusCode.NoContent) + } + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Page.kt b/WebsiteBackend/src/de/steamwar/routes/Page.kt new file mode 100644 index 00000000..9b28ddd5 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/Page.kt @@ -0,0 +1,251 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.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() +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, val slug: String?, val title: 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() + + 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 || obj["name"]?.jsonPrimitive?.content?.endsWith(".json") == 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().branch + val res = client.post("repos/SteamWar/Website/branches") { + contentType(ContentType.Application.Json) + setBody(CreateGiteaBranchRequest(branch, "master")) + } + + + @Serializable + data class CreateGiteaMergeRequest(val base: String, val head: String, val title: String) + + client.post("repos/SteamWar/Website/pulls") { + contentType(ContentType.Application.Json) + setBody(CreateGiteaMergeRequest("master", branch, "Merge branch $branch")) + } + + call.respond(res.status) + } + delete("branch") { + val branch = call.receive().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 req = call.receive() + if(req.path.startsWith("src/content/")) { + call.respond(HttpStatusCode.BadRequest, "Invalid path") + return@post + } + val res = client.post("repos/SteamWar/Website/contents/src/content/${req.path}") { + contentType(ContentType.Application.Json) + setBody(CreateGiteaPageRequest( + "Create page ${req.path}", + Base64.getEncoder().encodeToString(""" + --- + title: ${req.title ?: "[Enter Title]"} + description: [Enter Description] + slug: ${req.slug ?: "[Enter Slug]"} + --- + + # ${req.path} + """.trimIndent().toByteArray()), + call.request.queryParameters["branch"] ?: "master", + Identity(call.principal()!!.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() + + 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()!!.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() + 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()!!.user.userName, "admin-tool@steamwar.de"))) + } + + call.respond(res.status) + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Routes.kt b/WebsiteBackend/src/de/steamwar/routes/Routes.kt new file mode 100644 index 00000000..85471d14 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/Routes.kt @@ -0,0 +1,40 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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 io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.routing.* + +fun Application.configureRoutes() { + routing { + authenticate("sw-auth", optional = true) { + configureEventsRoute() + configureDataRoutes() + configureEventFightRoutes() + configureUserPerms() + configureStats() + configurePage() + configureSchematic() + configureAuthRoutes() + configureUser() + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Schematic.kt b/WebsiteBackend/src/de/steamwar/routes/Schematic.kt new file mode 100644 index 00000000..ef2900f5 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/Schematic.kt @@ -0,0 +1,229 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.plugins.SWAuthPrincipal +import de.steamwar.plugins.SWPermissionCheck +import de.steamwar.sql.NodeData +import de.steamwar.sql.NodeDownload +import de.steamwar.sql.NodeMember +import de.steamwar.sql.SWException +import de.steamwar.sql.SchematicNode +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.security.MessageDigest +import java.time.Duration +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* + +@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.id, 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.id).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.id }) +} + +@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) + +fun Route.configureSchematic() { + route("/download/{code}") { + get { + val code = call.parameters["code"] ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + val dl = NodeDownload.get(code) ?: run { + call.respond(HttpStatusCode.NotFound) + return@get + } + + dl.delete() + + if(dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) { + call.respond(HttpStatusCode.Gone) + return@get + } + + val node = SchematicNode.getSchematicNode(dl.nodeId) ?: run { + call.respond(HttpStatusCode.NotFound) + 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.get(node) ?: run { + call.respond(HttpStatusCode.InternalServerError) + return@get + } + + call.response.header("Content-Disposition", "attachment; filename=\"${node.name}.${if (data.nodeFormat) "schem" else "schematic"}\"") + call.respondBytes(data.schemData().readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK) + } + get("/info") { + val code = call.parameters["code"] ?: run { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + val dl = NodeDownload.get(code) ?: run { + call.respond(HttpStatusCode.NotFound) + return@get + } + + val node = SchematicNode.getSchematicNode(dl.nodeId) ?: run { + call.respond(HttpStatusCode.NotFound) + return@get + } + + call.respond(ResponseSchematic(node)) + } + } + route("/schem") { + install(SWPermissionCheck) + get { + val user = call.principal()!!.user + call.respond(ResponseSchematicList(SchematicNode.list(user, null).filter { it.name != "//copy" }.sortedWith { o1, o2 -> + if (o1.isDir || o2.isDir) { + o2.isDir.compareTo(o1.isDir) + } else { + o1.name.compareTo(o2.name) + } + }.map { ResponseSchematic(it) }, listOf())) + } + + post { + val file = call.receive() + val schemName = file.name.substringBeforeLast(".") + val schemType = file.name.substringAfterLast(".") + + if (schemType != "schem" && schemType != "schematic") { + call.respond(HttpStatusCode.BadRequest) + return@post + } + + val user = call.principal()!!.user + + val content = Base64.getDecoder().decode(file.content) + var node = SchematicNode.getSchematicNode(user.id, schemName, 0) + if (node == null) { + node = SchematicNode.createSchematic(user.id, schemName, 0) + } + + val data = NodeData(node.id, false) + data.saveFromStream(content.inputStream(), schemType == "schem") + + call.respond(ResponseSchematic(node)) + } + + route("/{id}") { + get { + val user = call.principal()!!.user + val parentId = call.parameters["id"]?.toIntOrNull() + if(parentId == null) { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + val parent = SchematicNode.getSchematicNode(parentId) + + if(parent == null) { + call.respond(HttpStatusCode.NotFound) + return@get + } + + if(!parent.accessibleByUser(user)) { + call.respond(HttpStatusCode.Forbidden) + return@get + } + + call.respond(ResponseSchematicLong(parent, parent.generateBreadcrumbs(user))) + } + + get("/list") { + val user = call.principal()!!.user + val parentId = call.parameters["id"]?.toIntOrNull() + if(parentId == null) { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + val parent = SchematicNode.getSchematicNode(parentId) + + if(parent == null) { + call.respond(HttpStatusCode.NotFound) + return@get + } + + if(!parent.isDir) { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + if(!parent.accessibleByUser(user)) { + call.respond(HttpStatusCode.Forbidden) + return@get + } + + call.respond(ResponseSchematicList(SchematicNode.list(user, parent.id).filter { it.name != "//copy" }.sortedWith { o1, o2 -> + if (o1.isDir || o2.isDir) { + o2.isDir.compareTo(o1.isDir) + } else { + o1.name.compareTo(o2.name) + } + }.map { ResponseSchematic(it) }, parent.generateBreadcrumbsMap(user).map { ResponseBreadcrumb(it.key, it.value) })) + } + } + } +} diff --git a/WebsiteBackend/src/de/steamwar/routes/Stats.kt b/WebsiteBackend/src/de/steamwar/routes/Stats.kt new file mode 100644 index 00000000..de3ee2e3 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/Stats.kt @@ -0,0 +1,87 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.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, val fights: Int, val playtime: Double) { + constructor(user: SteamwarUser): this( + getEventFightParticipation(user) ?: 0, + getEventParticipation(user) ?: 0, + getAcceptedSchematics(user) ?: 0, + getFightCount(user) ?: 0, + user.onlinetime / 3600.0 + ) +} + +fun Route.configureStats() { + route("/stats") { + get("/ranked/{gamemode}") { + val gamemode = call.parameters["gamemode"] ?: return@get call.respond(HttpStatusCode.NotFound) + + @Serializable + data class RankedUser(val name: String, val elo: Int) + + call.respond(getRankedList(gamemode).map { RankedUser(it.first, it.second) }) + } + get("/fights") { + val list = getFightList() + + @Serializable + data class Fight(val date: String, val gamemode: String, val count: Int) + + call.respond(list.map { Fight(it.first, it.second, it.third) }) + } + route("/user/{id}") { + install(SWPermissionCheck) { + userCheck { + val user = it.call.request.getUser() + val auth = it.call.principal() + + 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)) + } + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Teams.kt b/WebsiteBackend/src/de/steamwar/routes/Teams.kt new file mode 100644 index 00000000..b6c5f436 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/Teams.kt @@ -0,0 +1,33 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.sql.Team +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.configureTeamRoutes() { + route("/team") { + get { + call.respond(Team.getAll().map { ResponseTeam(it) }) + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/User.kt b/WebsiteBackend/src/de/steamwar/routes/User.kt new file mode 100644 index 00000000..9571a90e --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/User.kt @@ -0,0 +1,54 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.plugins.SWPermissionCheck +import de.steamwar.plugins.getUser +import de.steamwar.sql.UserPerm +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.configureUser() { + route("/user") { + route("/{id}") { + route("/admin") { + install(SWPermissionCheck) { + permission = UserPerm.ADMINISTRATION + } + + put("/password") { + val user = call.request.getUser() ?: return@put call.respond(HttpStatusCode.NotFound) + val password = call.receiveText() + + if (password.isEmpty()) { + call.respond(HttpStatusCode.BadRequest, "Password too short") + return@put + } + + user.setPassword(password) + call.respond(HttpStatusCode.OK) + } + } + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt b/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt new file mode 100644 index 00000000..dd6244e2 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt @@ -0,0 +1,154 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.plugins.SWPermissionCheck +import de.steamwar.plugins.getUser +import de.steamwar.sql.SteamwarUser +import de.steamwar.sql.UserPerm +import de.steamwar.sql.UserPerm.Prefix +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable + +@Serializable +data class RespondPrefix(val name: String, val colorCode: String, val chatPrefix: String) + +@Serializable +data class RespondUserPerms(val prefixes: Map, val perms: List) + +@Serializable +data class RespondUserPermsPrefix(val prefix: RespondPrefix, val perms: List) + +fun Route.configureUserPerms() { + route("/perms") { + install(SWPermissionCheck) { + allowMethod(HttpMethod.Get) + permission = UserPerm.MODERATION + } + get { + val perms = mutableListOf() + val prefixes = mutableMapOf() + UserPerm.entries.forEach { + if (it.name.startsWith("PREFIX_")) { + val prefix = UserPerm.prefixes[it]!! + prefixes[it.name] = RespondPrefix(it.name, prefix.colorCode, prefix.chatPrefix) + } else { + perms.add(it.name) + } + } + + call.respond(RespondUserPerms(prefixes, perms)) + } + route("/user/{id}") { + install(SWPermissionCheck) { + allowMethod(HttpMethod.Get) + permission = UserPerm.MODERATION + mustAuth = true + } + get { + val user = call.request.getUser() + if (user == null) { + call.respond(HttpStatusCode.BadRequest) + return@get + } + val perms = mutableListOf() + var prefix = UserPerm.PREFIX_NONE + user.perms().forEach { + if (it.name.startsWith("PREFIX_")) { + prefix = it + } else { + perms.add(it.name) + } + } + + val prefixs = UserPerm.prefixes[prefix]!! + + call.respond(RespondUserPermsPrefix(RespondPrefix(prefix.name, prefixs.colorCode, prefixs.chatPrefix), perms)) + } + put("/prefix/{prefix}") { + val user = call.request.getUser() + if (user == null) { + call.respond(HttpStatusCode.BadRequest) + return@put + } + + val prefix = call.parameters["prefix"] + if (prefix == null || UserPerm.values().find { it.name == prefix } == null) { + call.respond(HttpStatusCode.BadRequest) + return@put + } + user.perms().filter { it.name.startsWith("PREFIX_") }.forEach { + UserPerm.removePerm(user, it) + } + + UserPerm.addPerm(user, UserPerm.values().find { it.name == prefix }!!) + call.respond(HttpStatusCode.Accepted) + } + put("/{perm}") { + val user = call.request.getUser() + if (user == null) { + call.respond(HttpStatusCode.BadRequest) + return@put + } + + + val perm = call.parameters["perm"] + val permission = UserPerm.values().find { it.name == perm } + if (perm == null || perm.startsWith("PREFIX_") || permission == null) { + call.respond(HttpStatusCode.BadRequest) + return@put + } + + if (!user.hasPerm(permission)) { + UserPerm.addPerm(user, permission) + call.respond(HttpStatusCode.Accepted) + return@put + } + call.respond(HttpStatusCode.NoContent) + } + delete("/{perm}") { + val user = call.request.getUser() + if (user == null) { + call.respond(HttpStatusCode.BadRequest) + return@delete + } + + val perm = call.parameters["perm"] + val permission = UserPerm.values().find { it.name == perm } + if (perm == null || perm.startsWith("PREFIX_") || permission == null) { + call.respond(HttpStatusCode.BadRequest) + return@delete + } + + + + if (user.hasPerm(permission)) { + UserPerm.removePerm(user, permission) + call.respond(HttpStatusCode.Accepted) + return@delete + } + call.respond(HttpStatusCode.NoContent) + } + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/sql/SQLConfigImpl.kt b/WebsiteBackend/src/de/steamwar/sql/SQLConfigImpl.kt new file mode 100644 index 00000000..551ed07e --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/sql/SQLConfigImpl.kt @@ -0,0 +1,29 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.sql + +import de.steamwar.sql.internal.SQLConfig +import java.util.logging.Logger + +class SQLConfigImpl: SQLConfig { + override fun getLogger(): Logger = Logger.getGlobal() + + override fun maxConnections(): Int = 1 +} diff --git a/WebsiteBackend/src/de/steamwar/sql/SQLWrapperImpl.kt b/WebsiteBackend/src/de/steamwar/sql/SQLWrapperImpl.kt new file mode 100644 index 00000000..4f2c90e3 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/sql/SQLWrapperImpl.kt @@ -0,0 +1,68 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.sql + +import org.bspfsystems.yamlconfiguration.file.YamlConfiguration +import java.io.File +import java.util.* +import java.util.stream.Collectors + +fun loadSchematicTypes(tmpTypes: MutableList?, tmpFromDB: MutableMap?) { + val folder = File("/configs/GameModes") + if (folder.exists()) { + for (configFile in Arrays.stream(folder.listFiles { _, name -> name.endsWith(".yml") && !name.endsWith(".kits.yml") }) + .sorted().collect(Collectors.toList())) { + val config: YamlConfiguration = YamlConfiguration.loadConfiguration(configFile) + if (!config.isConfigurationSection("Schematic")) continue + val type: String = config.getString("Schematic.Type")!! + val shortcut = config.getString("Schematic.Shortcut") + if (shortcut == null) { + println("No shortcut for $type") + continue + } + if (tmpFromDB!!.containsKey(type.lowercase(Locale.getDefault()))) continue + var checktype: SchematicType? = null + val material: String = config.getString("Schematic.Material", "STONE_BUTTON")!! + if (!config.getStringList("CheckQuestions").isEmpty()) { + checktype = SchematicType("C$type", "C$shortcut", SchematicType.Type.CHECK_TYPE, null, material, false) + tmpTypes!!.add(checktype) + tmpFromDB[checktype.toDB()] = checktype + } + val current = SchematicType( + type, + shortcut, + if (config.isConfigurationSection("Server")) SchematicType.Type.FIGHT_TYPE else SchematicType.Type.NORMAL, + checktype, + material, + false + ) + tmpTypes!!.add(current) + tmpFromDB[type.lowercase(Locale.getDefault())] = current + } + } +} + +class SQLWrapperImpl: SQLWrapper { + override fun loadSchemTypes(tmpTypes: MutableList?, tmpFromDB: MutableMap?) = loadSchematicTypes(tmpTypes, tmpFromDB) + + override fun additionalExceptionMetadata(builder: StringBuilder) { + builder.append("\n\nWebsiteApi") + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/sql/Stats.kt b/WebsiteBackend/src/de/steamwar/sql/Stats.kt new file mode 100644 index 00000000..d8beb867 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/sql/Stats.kt @@ -0,0 +1,67 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.sql + +import de.steamwar.sql.internal.Statement +import de.steamwar.sql.internal.Statement.ResultSetUser + +private val getNum: ResultSetUser = 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) + +private val fightCount = Statement("SELECT COUNT(*) AS num FROM FightPlayer WHERE UserID = ?") + +fun getFightCount(user: SteamwarUser): Int? = fightCount.select(getNum, user.id) + +private val rankedList = Statement("SELECT UserName, Elo FROM UserData, UserElo WHERE UserID = id AND GameMode = ? AND Season = ? ORDER BY Elo DESC") + +fun getRankedList(gamemode: String): List> = rankedList.select({ res -> + val list = mutableListOf>() + while (res.next()) { + list.add(res.getString("UserName") to res.getInt("Elo")) + } + list +}, gamemode, Season.getSeason()) + +private val fightList = Statement("SELECT DATE(StartTime) AS Datum, GameMode AS Modus, COUNT(*) AS Anzahl FROM Fight WHERE DATE(StartTime) >= DATE(NOW()) - INTERVAL 1 WEEK GROUP BY Datum, GameMode ORDER BY Datum ASC") + +fun getFightList(): List> = fightList.select({ res -> + val list = mutableListOf>() + while (res.next()) { + list.add(Triple(res.getString("Datum"), res.getString("Modus"), res.getInt("Anzahl"))) + } + list +}) diff --git a/WebsiteBackend/src/de/steamwar/util/ServerPing.kt b/WebsiteBackend/src/de/steamwar/util/ServerPing.kt new file mode 100644 index 00000000..fb2b9a6d --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/util/ServerPing.kt @@ -0,0 +1,137 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.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 + */ +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 paramInts = paramInt + while (true) { + if (paramInts and -0x80 == 0) { + out.writeByte(paramInts) + return + } + out.writeByte(paramInts and 0x7F or 0x80) + paramInts = paramInts 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) + 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 = emptyList()) + +@Serializable +data class Player(val name: String, val id: String) + +@Serializable +data class Version(val name: String, val protocol: Int) \ No newline at end of file diff --git a/WebsiteBackend/src/logback.xml b/WebsiteBackend/src/logback.xml new file mode 100644 index 00000000..287d6348 --- /dev/null +++ b/WebsiteBackend/src/logback.xml @@ -0,0 +1,48 @@ + + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + logs/${LOG_FILE}.log + + + logs/${LOG_FILE}.%d{yyyy-MM-dd}.gz + + + 30 + 1GB + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index ad769fd8..7c34b9b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -140,6 +140,29 @@ dependencyResolutionManagement { library("velocityapi", "com.velocitypowered:velocity-api:3.3.0-SNAPSHOT") library("apolloapi", "com.lunarclient:apollo-api:1.1.0") library("apollocommon", "com.lunarclient:apollo-common:1.1.0") + + library("logback", "ch.qos.logback:logback-classic:1.5.6") + + val ktorVersion = "2.3.12" + + library("ktor", "io.ktor:ktor-server-core-jvm:$ktorVersion") + library("ktorContentNegotiation", "io.ktor:ktor-server-content-negotiation-jvm:$ktorVersion") + library("ktorCors", "io.ktor:ktor-server-cors-jvm:$ktorVersion") + library("ktorSerialization", "io.ktor:ktor-serialization-kotlinx-json-jvm:$ktorVersion") + library("ktorNetty", "io.ktor:ktor-server-netty-jvm:$ktorVersion") + library("ktorHost", "io.ktor:ktor-server-host-common-jvm:$ktorVersion") + library("ktorRequestValidation", "io.ktor:ktor-server-request-validation:$ktorVersion") + library("ktorAuth", "io.ktor:ktor-server-auth:$ktorVersion") + library("ktorAuthJvm", "io.ktor:ktor-server-auth-jvm:$ktorVersion") + library("ktorAuthLdap", "io.ktor:ktor-server-auth-ldap-jvm:$ktorVersion") + library("ktorClientCore", "io.ktor:ktor-client-core-jvm:$ktorVersion") + library("ktorClientJava", "io.ktor:ktor-client-java:$ktorVersion") + library("ktorClientContentNegotiation", "io.ktor:ktor-client-content-negotiation:$ktorVersion") + library("ktorClientAuth", "io.ktor:ktor-client-auth:$ktorVersion") + + library("yamlconfig", "org.bspfsystems:yamlconfiguration:1.3.0") + library("kotlinxSerializationCbor", "org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.4.1") + library("ktorRateLimit", "io.ktor:ktor-server-rate-limit:$ktorVersion") } } } @@ -218,3 +241,4 @@ include( ) include("TNTLeague") +include("WebsiteBackend") diff --git a/steamwarci.yml b/steamwarci.yml index c2769abd..8fbd084b 100644 --- a/steamwarci.yml +++ b/steamwarci.yml @@ -27,4 +27,6 @@ artifacts: "/binarys/PersistentVelocityCore.jar": "VelocityCore/Persistent/build/libs/Persistent.jar" "/binarys/VelocityCore.jar": "VelocityCore/build/libs/VelocityCore-all.jar" - "/binarys/deployarena.py": "VelocityCore/deployarena.py" \ No newline at end of file + "/binarys/deployarena.py": "VelocityCore/deployarena.py" + + "/binarys/website-api.jar": "WebsiteBackend/build/libs/api.jar" \ No newline at end of file From f69f7ae294a26bd12d8abf8a5870d0bf757439c3 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 18 Aug 2024 11:21:39 +0200 Subject: [PATCH 2/9] Fix Build --- WebsiteBackend/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WebsiteBackend/build.gradle.kts b/WebsiteBackend/build.gradle.kts index cd077973..321dc310 100644 --- a/WebsiteBackend/build.gradle.kts +++ b/WebsiteBackend/build.gradle.kts @@ -37,6 +37,10 @@ ktor { } } +tasks.build { + finalizedBy(tasks.buildFatJar) +} + dependencies { implementation(libs.logback) implementation(libs.ktor) From 6d648b9a719ded0a0c14c4476595f6d28c97224e Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 18 Aug 2024 13:03:24 +0200 Subject: [PATCH 3/9] Fixes --- .../SQL/src/de/steamwar/sql/SteamwarUser.java | 2 +- .../SQL/src/de/steamwar/sql/UserPerm.java | 2 +- WebsiteBackend/build.gradle.kts | 9 -- WebsiteBackend/src/de/steamwar/Application.kt | 1 - .../src/de/steamwar/bungee/Bungee.kt | 56 ------------ .../src/de/steamwar/data/SkinCache.kt | 4 +- .../src/de/steamwar/plugins/Auth.kt | 1 + .../src/de/steamwar/plugins/SteamWar.kt | 4 +- WebsiteBackend/src/de/steamwar/routes/Data.kt | 86 ++++++++++--------- .../src/de/steamwar/routes/Schematic.kt | 12 +-- steamwarci.yml | 2 +- 11 files changed, 61 insertions(+), 118 deletions(-) delete mode 100644 WebsiteBackend/src/de/steamwar/bungee/Bungee.kt diff --git a/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.java b/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.java index 8aefbd49..96974c61 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.java +++ b/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.java @@ -373,6 +373,6 @@ public class SteamwarUser { } public static List getAll() { - return null; + return getAll.listSelect(); } } diff --git a/CommonCore/SQL/src/de/steamwar/sql/UserPerm.java b/CommonCore/SQL/src/de/steamwar/sql/UserPerm.java index a05fe2ad..ed842d54 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/UserPerm.java +++ b/CommonCore/SQL/src/de/steamwar/sql/UserPerm.java @@ -65,7 +65,7 @@ public enum UserPerm { private static final Table table = new Table<>(UserPermTable.class, "UserPerm"); private static final SelectStatement getPerms = table.selectFields("user"); - private static final Statement addPerm = table.insertFields("user", "perm"); + private static final Statement addPerm = table.insertAll(); private static final Statement removePerm = table.delete(Table.PRIMARY); public static Set getPerms(int user) { diff --git a/WebsiteBackend/build.gradle.kts b/WebsiteBackend/build.gradle.kts index 321dc310..2cf527a7 100644 --- a/WebsiteBackend/build.gradle.kts +++ b/WebsiteBackend/build.gradle.kts @@ -26,15 +26,6 @@ plugins { application { mainClass.set("de.steamwar.ApplicationKt") - - val isDevelopment: Boolean = project.ext.has("development") - applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") -} - -ktor { - fatJar { - archiveFileName.set("api.jar") - } } tasks.build { diff --git a/WebsiteBackend/src/de/steamwar/Application.kt b/WebsiteBackend/src/de/steamwar/Application.kt index 1b653a60..23fc3066 100644 --- a/WebsiteBackend/src/de/steamwar/Application.kt +++ b/WebsiteBackend/src/de/steamwar/Application.kt @@ -44,7 +44,6 @@ data class Config(val giteaToken: String) val config = Json.decodeFromStream(File("config.json").inputStream()) fun main() { - SchematicType.Normal.name().length Thread { while (true) { Thread.sleep(1000 * 10) diff --git a/WebsiteBackend/src/de/steamwar/bungee/Bungee.kt b/WebsiteBackend/src/de/steamwar/bungee/Bungee.kt deleted file mode 100644 index 547aa2cf..00000000 --- a/WebsiteBackend/src/de/steamwar/bungee/Bungee.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2024 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.bungee - -import java.io.ObjectInputStream -import java.io.ObjectOutputStream -import java.net.Socket - -object Bungee { - var socket: Socket? = null - var output: ObjectOutputStream? = null - var thread: Thread? = null - - fun run() { - val input = ObjectInputStream(socket!!.getInputStream()) - - while (socket != null && !socket!!.isClosed) { - if (input.available() <= 0) { - Thread.sleep(50) - continue - } - } - - socket = null - thread = null - output = null - } - - inline fun connect(func: (Socket) -> Unit) { - if(socket == null || socket!!.isClosed) { - socket = Socket("localhost", 7546) - output = ObjectOutputStream(socket!!.getOutputStream()) - thread = Thread(Bungee::run) - thread!!.start() - } - - func(socket!!) - } -} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/data/SkinCache.kt b/WebsiteBackend/src/de/steamwar/data/SkinCache.kt index 6a6ff935..a8b056eb 100644 --- a/WebsiteBackend/src/de/steamwar/data/SkinCache.kt +++ b/WebsiteBackend/src/de/steamwar/data/SkinCache.kt @@ -91,7 +91,7 @@ suspend fun getCachedSkin(uuid: String): Pair { val file = File(kCacheFolderFile, "$uuid.webp") if (file.exists()) { if (CacheConfig.isOutdated(uuid)) { - val skin = client.get("https://visage.surgeplay.com/bust/150/$uuid") + val skin = client.get("https://vzge.me/bust/150/$uuid") skin.bodyAsChannel().copyTo(file.outputStream()) CacheConfig.update(uuid) @@ -103,7 +103,7 @@ suspend fun getCachedSkin(uuid: String): Pair { withContext(Dispatchers.IO) { file.createNewFile() } - val skin = client.get("https://visage.surgeplay.com/bust/150/$uuid") + val skin = client.get("https://vzge.me/bust/150/$uuid") skin.bodyAsChannel().copyTo(file.outputStream()) CacheConfig.update(uuid) return file to false diff --git a/WebsiteBackend/src/de/steamwar/plugins/Auth.kt b/WebsiteBackend/src/de/steamwar/plugins/Auth.kt index 40006aa5..bf26f6f4 100644 --- a/WebsiteBackend/src/de/steamwar/plugins/Auth.kt +++ b/WebsiteBackend/src/de/steamwar/plugins/Auth.kt @@ -99,6 +99,7 @@ Message: ${cause.message} } """ + SWException.log(msg, cause.stackTraceToString()) call.response.headers.append("X-Caught", "1") } } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt b/WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt index bcac22bc..db96a700 100644 --- a/WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt +++ b/WebsiteBackend/src/de/steamwar/plugins/SteamWar.kt @@ -19,9 +19,11 @@ package de.steamwar.plugins +import de.steamwar.routes.catchException import de.steamwar.sql.SteamwarUser import io.ktor.server.request.* +import java.util.UUID fun ApplicationRequest.getUser(key: String = "id"): SteamwarUser? { - return SteamwarUser.get(call.parameters[key]?.toIntOrNull() ?: return null) + return SteamwarUser.get(call.parameters[key]?.let { catchException { UUID.fromString(it) } } ?: return null) } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Data.kt b/WebsiteBackend/src/de/steamwar/routes/Data.kt index d05ffc90..2e055d40 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Data.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Data.kt @@ -44,10 +44,10 @@ import java.util.UUID data class ResponseSchematicType(val name: String, val db: String) @Serializable -data class ResponseUser(val id: Int, val name: String, val uuid: String, val prefix: String, val perms: List) { - constructor(user: SteamwarUser) : this(user.id, user.userName, user.uuid.toString(), user.prefix().chatPrefix, user.perms().map { it.name }) { +data class ResponseUser(val name: String, val uuid: String, val prefix: String, val perms: List) { + constructor(user: SteamwarUser) : this(user.userName, user.uuid.toString(), user.prefix().chatPrefix, user.perms().map { it.name }) { synchronized(cache) { - cache[id] = this + cache[user.id] = this } } @@ -73,32 +73,51 @@ fun Route.configureDataRoutes() { get { call.respondText("Hello World!") } - get("/schematicTypes") { - val types = mutableListOf() - loadSchematicTypes(types, mutableMapOf()) - call.respond(types.filter { !it.check() }.map { ResponseSchematicType(it.name(), it.toDB()) }) - } - get("/gamemodes") { - call.respond( - File("/configs/GameModes/").listFiles()!! - .filter { it.name.endsWith(".yml") && !it.name.endsWith(".kits.yml") } - .map { it.nameWithoutExtension }) - } - get("/gamemodes/{gamemode}/maps") { - val gamemode = call.parameters["gamemode"] - if (gamemode == null) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid gamemode")) - return@get + route("/admin") { + install(SWPermissionCheck) { + mustAuth = true + permission = UserPerm.PREFIX_MODERATOR } - val file = File("/configs/GameModes/$gamemode.yml") - if (!file.exists()) { - call.respond(HttpStatusCode.NotFound, ResponseError("Gamemode not found")) - return@get + get("/users") { + call.respond(SteamwarUser.getAll().map { ResponseUser(it) }) + } + get("/schematicTypes") { + val types = mutableListOf() + loadSchematicTypes(types, mutableMapOf()) + call.respond(types.filter { !it.check() }.map { ResponseSchematicType(it.name(), it.toDB()) }) + } + get("/gamemodes") { + call.respond( + File("/configs/GameModes/").listFiles()!! + .filter { it.name.endsWith(".yml") && !it.name.endsWith(".kits.yml") } + .map { it.nameWithoutExtension }) + } + get("/gamemodes/{gamemode}/maps") { + val gamemode = call.parameters["gamemode"] + if (gamemode == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid gamemode")) + return@get + } + val file = File("/configs/GameModes/$gamemode.yml") + if (!file.exists()) { + call.respond(HttpStatusCode.NotFound, ResponseError("Gamemode not found")) + return@get + } + call.respond(YamlConfiguration.loadConfiguration(file).getStringList("Server.Maps")) + } + 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 + } } - call.respond(YamlConfiguration.loadConfiguration(file).getStringList("Server.Maps")) - } - get("/users") { - call.respond(SteamwarUser.getAll().map { ResponseUser(it) }) } get("/team") { call.respond( @@ -108,19 +127,6 @@ fun Route.configureDataRoutes() { .mapValues { it.value.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 - } - } get("/skin/{uuid}") { val uuid = call.parameters["uuid"] if (uuid == null || catchException { UUID.fromString(uuid) } == null) { diff --git a/WebsiteBackend/src/de/steamwar/routes/Schematic.kt b/WebsiteBackend/src/de/steamwar/routes/Schematic.kt index ef2900f5..3e21d374 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Schematic.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Schematic.kt @@ -50,8 +50,8 @@ data class ResponseSchematicLong(val members: List, val path: Stri } @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.id }) +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 @@ -132,7 +132,7 @@ fun Route.configureSchematic() { } route("/schem") { install(SWPermissionCheck) - get { + /*get { val user = call.principal()!!.user call.respond(ResponseSchematicList(SchematicNode.list(user, null).filter { it.name != "//copy" }.sortedWith { o1, o2 -> if (o1.isDir || o2.isDir) { @@ -141,7 +141,7 @@ fun Route.configureSchematic() { o1.name.compareTo(o2.name) } }.map { ResponseSchematic(it) }, listOf())) - } + }*/ post { val file = call.receive() @@ -166,7 +166,7 @@ fun Route.configureSchematic() { call.respond(ResponseSchematic(node)) } - + /* route("/{id}") { get { val user = call.principal()!!.user @@ -224,6 +224,6 @@ fun Route.configureSchematic() { } }.map { ResponseSchematic(it) }, parent.generateBreadcrumbsMap(user).map { ResponseBreadcrumb(it.key, it.value) })) } - } + }*/ } } diff --git a/steamwarci.yml b/steamwarci.yml index 8fbd084b..19b1643c 100644 --- a/steamwarci.yml +++ b/steamwarci.yml @@ -29,4 +29,4 @@ artifacts: "/binarys/VelocityCore.jar": "VelocityCore/build/libs/VelocityCore-all.jar" "/binarys/deployarena.py": "VelocityCore/deployarena.py" - "/binarys/website-api.jar": "WebsiteBackend/build/libs/api.jar" \ No newline at end of file + "/binarys/website-api.jar": "WebsiteBackend/build/libs/WebsiteBackend-all.jar" \ No newline at end of file From 42254a5133da6b277d15235a65433b594481ed8f Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 18 Aug 2024 13:09:19 +0200 Subject: [PATCH 4/9] Fixes --- WebsiteBackend/src/de/steamwar/routes/Data.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/WebsiteBackend/src/de/steamwar/routes/Data.kt b/WebsiteBackend/src/de/steamwar/routes/Data.kt index 2e055d40..fcf13b34 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Data.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Data.kt @@ -108,15 +108,15 @@ fun Route.configureDataRoutes() { 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 - } + } + 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 } } get("/team") { From c11eaaee45d039cce8d7c2f52c9e20f626bd3d14 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Tue, 27 Aug 2024 21:34:18 +0200 Subject: [PATCH 5/9] Fixes... --- .../SQL/src/de/steamwar/sql/EventFight.java | 2 +- .../steamwar/velocitycore/EventStarter.java | 2 +- .../src/de/steamwar/plugins/Auth.kt | 23 +++ WebsiteBackend/src/de/steamwar/routes/Auth.kt | 74 ---------- WebsiteBackend/src/de/steamwar/routes/Data.kt | 3 - .../src/de/steamwar/routes/EventFights.kt | 41 +++--- .../src/de/steamwar/routes/Events.kt | 39 +++-- .../src/de/steamwar/routes/Routes.kt | 1 - .../src/de/steamwar/routes/Schematic.kt | 134 ++++-------------- .../src/de/steamwar/routes/Stats.kt | 19 +-- .../src/de/steamwar/routes/Teams.kt | 33 ----- WebsiteBackend/src/de/steamwar/routes/User.kt | 54 ------- .../src/de/steamwar/routes/UserPerms.kt | 66 +++------ 13 files changed, 118 insertions(+), 373 deletions(-) delete mode 100644 WebsiteBackend/src/de/steamwar/routes/Teams.kt delete mode 100644 WebsiteBackend/src/de/steamwar/routes/User.kt diff --git a/CommonCore/SQL/src/de/steamwar/sql/EventFight.java b/CommonCore/SQL/src/de/steamwar/sql/EventFight.java index cd7352b3..72497525 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/EventFight.java +++ b/CommonCore/SQL/src/de/steamwar/sql/EventFight.java @@ -96,7 +96,7 @@ public class EventFight implements Comparable { @Getter @Setter @Field(nullable = true) - private int spectatePort; + private Integer spectatePort; @Getter @Field(def = "0") private int ergebnis; diff --git a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java index 17d4f537..15c16b64 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java @@ -62,7 +62,7 @@ public class EventStarter { //Don't start EventServer if not the event bungee String command; - if(VelocityCore.get().getConfig().isEventmode() || next.getSpectatePort() == 0) { + if(VelocityCore.get().getConfig().isEventmode() || next.getSpectatePort() == null) { ServerStarter starter = new ServerStarter().event(next); starter.callback(subserver -> { diff --git a/WebsiteBackend/src/de/steamwar/plugins/Auth.kt b/WebsiteBackend/src/de/steamwar/plugins/Auth.kt index bf26f6f4..6d90339a 100644 --- a/WebsiteBackend/src/de/steamwar/plugins/Auth.kt +++ b/WebsiteBackend/src/de/steamwar/plugins/Auth.kt @@ -102,4 +102,27 @@ Message: ${cause.message} SWException.log(msg, cause.stackTraceToString()) call.response.headers.append("X-Caught", "1") } + + onCallRespond { call -> + if (call.response.status()?.isSuccess() == true) { + return@onCallRespond + } + + val msg = """ + URI: ${call.request.uri} + Method: ${call.request.httpMethod.value} + Response: ${call.response.status()?.value} + IP: ${call.request.headers["X-Forwarded-For"] ?: call.request.local.remoteHost} + UserAgent: ${call.request.headers["User-Agent"]} + """.trimIndent() + + val stack = """ + Headers: + ${call.request.headers.entries().joinToString("\n ") { "${it.key}: ${it.value}" }} + Body: + ${call.request.receiveChannel()} + """.trimIndent() + + SWException.log(msg, stack) + } } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/Auth.kt index d230c4f4..52238c32 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Auth.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Auth.kt @@ -78,80 +78,6 @@ fun Route.configureAuthRoutes() { mustAuth = true } - get { - val auth = call.principal() - - if(auth == null) { - call.respond(HttpStatusCode.InternalServerError) - return@get - } - - call.respond(Token.listUser(auth.user).map { ResponseToken(it) }) - } - - post { - val auth = call.principal() - - if(auth == null) { - call.respond(HttpStatusCode.InternalServerError) - return@post - } - - val request = call.receive() - - if(request.name.length > 32) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Name too long", "name_too_long")) - return@post - } - - if(request.name.length < 3) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Name too short", "name_too_short")) - return@post - } - - if(!auth.user.verifyPassword(request.password)) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid password", "invalid_password")) - return@post - } - - val token = Token.createToken(request.name, auth.user) - - call.respond(AuthTokenResponse(token)) - } - - route("/{id}") { - delete { - val auth = call.principal() - - if(auth == null) { - call.respond(HttpStatusCode.InternalServerError) - return@delete - } - - val id = call.parameters["id"]?.toIntOrNull() - - if(id == null) { - call.respond(HttpStatusCode.BadRequest) - return@delete - } - - val token = Token.get(id) - - if(token == null) { - call.respond(HttpStatusCode.NotFound) - return@delete - } - - if(token.owner != auth.user) { - call.respond(HttpStatusCode.Forbidden) - return@delete - } - - token.delete() - call.respond(HttpStatusCode.OK) - } - } - post("/logout") { val auth = call.principal() diff --git a/WebsiteBackend/src/de/steamwar/routes/Data.kt b/WebsiteBackend/src/de/steamwar/routes/Data.kt index fcf13b34..64039974 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Data.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Data.kt @@ -70,9 +70,6 @@ data class ResponseUser(val name: String, val uuid: String, val prefix: String, fun Route.configureDataRoutes() { route("/data") { - get { - call.respondText("Hello World!") - } route("/admin") { install(SWPermissionCheck) { mustAuth = true diff --git a/WebsiteBackend/src/de/steamwar/routes/EventFights.kt b/WebsiteBackend/src/de/steamwar/routes/EventFights.kt index 8112d87c..e95ea89d 100644 --- a/WebsiteBackend/src/de/steamwar/routes/EventFights.kt +++ b/WebsiteBackend/src/de/steamwar/routes/EventFights.kt @@ -118,16 +118,7 @@ fun Route.configureEventFightRoutes() { } route("/{fight}") { put { - val fightId = call.parameters["fight"]?.toIntOrNull() - if (fightId == null) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID")) - return@put - } - val fight = EventFight.get(fightId) - if (fight == null) { - call.respond(HttpStatusCode.NotFound, ResponseError("Fight not found")) - return@put - } + val fight = call.receiveFight() ?: return@put val updateFight = call.receiveNullable() if (updateFight == null) { call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) @@ -143,29 +134,35 @@ fun Route.configureEventFightRoutes() { if (updateFight.group != null) { if (updateFight.group == "null") { - Groups.resetGroup(fightId, true) + Groups.resetGroup(fight.fightID, true) } else { - Groups.setGroup(fightId, updateFight.group) + Groups.setGroup(fight.fightID, updateFight.group) } } fight.update(start, spielmodus, map, teamBlue, teamRed, spectatePort) call.respond(HttpStatusCode.OK, ResponseEventFight(fight)) } delete { - val fightId = call.parameters["fight"]?.toIntOrNull() - if (fightId == null) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID")) - return@delete - } - val fight = EventFight.get(fightId) - if (fight == null) { - call.respond(HttpStatusCode.NotFound, ResponseError("Fight not found")) - return@delete - } + val fight = call.receiveFight() ?: return@delete fight.delete() call.respond(HttpStatusCode.OK) } } } +} + +suspend fun ApplicationCall.receiveFight(fieldName: String = "fight"): EventFight? { + val fightId = parameters[fieldName]?.toIntOrNull() + if (fightId == null) { + respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID")) + return null + } + + val fight = EventFight.get(fightId) + if (fight == null) { + respond(HttpStatusCode.NotFound, ResponseError("Fight not found")) + return null + } + return fight } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Events.kt b/WebsiteBackend/src/de/steamwar/routes/Events.kt index 987479c2..e351f3e4 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Events.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Events.kt @@ -154,16 +154,7 @@ fun Route.configureEventsRoute() { call.respond(EventFight.getEvent(event.eventID).map { ResponseEventFight(it) }) } get("/csv") { - val id = call.parameters["id"]?.toIntOrNull() - if (id == null) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) - return@get - } - val event = Event.get(id) - if (event == null) { - call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) - return@get - } + val event = call.receiveEvent() ?: return@get val fights = EventFight.getEvent(event.eventID) val csv = StringBuilder(); @@ -195,16 +186,8 @@ fun Route.configureEventsRoute() { call.respondText(csv.toString()) } put { - val id = call.parameters["id"]?.toIntOrNull() - if (id == null) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID")) - return@put - } - val event = Event.get(id) - if (event == null) { - call.respond(HttpStatusCode.NotFound, ResponseError("Event not found")) - return@put - } + val event = call.receiveEvent() ?: return@put + val updateEvent = call.receiveNullable() if (updateEvent == null) { call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body")) @@ -249,4 +232,20 @@ fun Route.configureEventsRoute() { } } } +} + +suspend fun ApplicationCall.receiveEvent(fieldName: String = "event"): Event? { + val eventId = parameters[fieldName]?.toIntOrNull() + if (eventId == null) { + respond(HttpStatusCode.BadRequest, ResponseError("Invalid event ID")) + return null + } + + val event = Event.get(eventId) + if (event == null) { + respond(HttpStatusCode.NotFound, ResponseError("Event not found")) + return null + } + + return event } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Routes.kt b/WebsiteBackend/src/de/steamwar/routes/Routes.kt index 85471d14..041d4b31 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Routes.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Routes.kt @@ -34,7 +34,6 @@ fun Application.configureRoutes() { configurePage() configureSchematic() configureAuthRoutes() - configureUser() } } } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Schematic.kt b/WebsiteBackend/src/de/steamwar/routes/Schematic.kt index 3e21d374..b873b97d 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Schematic.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Schematic.kt @@ -74,27 +74,7 @@ data class UploadSchematic(val name: String, val content: String) fun Route.configureSchematic() { route("/download/{code}") { get { - val code = call.parameters["code"] ?: run { - call.respond(HttpStatusCode.BadRequest) - return@get - } - - val dl = NodeDownload.get(code) ?: run { - call.respond(HttpStatusCode.NotFound) - return@get - } - - dl.delete() - - if(dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) { - call.respond(HttpStatusCode.Gone) - return@get - } - - val node = SchematicNode.getSchematicNode(dl.nodeId) ?: run { - call.respond(HttpStatusCode.NotFound) - return@get - } + val node = call.receiveSchematic() ?: return@get val user = call.principal()?.user if(user != null && !node.accessibleByUser(user)) { @@ -112,36 +92,13 @@ fun Route.configureSchematic() { call.respondBytes(data.schemData().readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK) } get("/info") { - val code = call.parameters["code"] ?: run { - call.respond(HttpStatusCode.BadRequest) - return@get - } - - val dl = NodeDownload.get(code) ?: run { - call.respond(HttpStatusCode.NotFound) - return@get - } - - val node = SchematicNode.getSchematicNode(dl.nodeId) ?: run { - call.respond(HttpStatusCode.NotFound) - return@get - } + val node = call.receiveSchematic() ?: return@get call.respond(ResponseSchematic(node)) } } route("/schem") { install(SWPermissionCheck) - /*get { - val user = call.principal()!!.user - call.respond(ResponseSchematicList(SchematicNode.list(user, null).filter { it.name != "//copy" }.sortedWith { o1, o2 -> - if (o1.isDir || o2.isDir) { - o2.isDir.compareTo(o1.isDir) - } else { - o1.name.compareTo(o2.name) - } - }.map { ResponseSchematic(it) }, listOf())) - }*/ post { val file = call.receive() @@ -166,64 +123,33 @@ fun Route.configureSchematic() { call.respond(ResponseSchematic(node)) } - /* - route("/{id}") { - get { - val user = call.principal()!!.user - val parentId = call.parameters["id"]?.toIntOrNull() - if(parentId == null) { - call.respond(HttpStatusCode.BadRequest) - return@get - } - - val parent = SchematicNode.getSchematicNode(parentId) - - if(parent == null) { - call.respond(HttpStatusCode.NotFound) - return@get - } - - if(!parent.accessibleByUser(user)) { - call.respond(HttpStatusCode.Forbidden) - return@get - } - - call.respond(ResponseSchematicLong(parent, parent.generateBreadcrumbs(user))) - } - - get("/list") { - val user = call.principal()!!.user - val parentId = call.parameters["id"]?.toIntOrNull() - if(parentId == null) { - call.respond(HttpStatusCode.BadRequest) - return@get - } - - val parent = SchematicNode.getSchematicNode(parentId) - - if(parent == null) { - call.respond(HttpStatusCode.NotFound) - return@get - } - - if(!parent.isDir) { - call.respond(HttpStatusCode.BadRequest) - return@get - } - - if(!parent.accessibleByUser(user)) { - call.respond(HttpStatusCode.Forbidden) - return@get - } - - call.respond(ResponseSchematicList(SchematicNode.list(user, parent.id).filter { it.name != "//copy" }.sortedWith { o1, o2 -> - if (o1.isDir || o2.isDir) { - o2.isDir.compareTo(o1.isDir) - } else { - o1.name.compareTo(o2.name) - } - }.map { ResponseSchematic(it) }, parent.generateBreadcrumbsMap(user).map { ResponseBreadcrumb(it.key, it.value) })) - } - }*/ } } + +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 +} diff --git a/WebsiteBackend/src/de/steamwar/routes/Stats.kt b/WebsiteBackend/src/de/steamwar/routes/Stats.kt index de3ee2e3..0c4e571b 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Stats.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Stats.kt @@ -59,28 +59,17 @@ fun Route.configureStats() { call.respond(list.map { Fight(it.first, it.second, it.third) }) } - route("/user/{id}") { - install(SWPermissionCheck) { - userCheck { - val user = it.call.request.getUser() - val auth = it.call.principal() - - if (user == null || auth == null) { - return@userCheck false - } - - return@userCheck user.id == auth.user.id || auth.user.hasPerm(UserPerm.MODERATION) - } - } + route("/user") { + install(SWPermissionCheck) get { - val user = call.request.getUser() + val user = call.authentication.principal() if (user == null) { call.respond(HttpStatusCode.NotFound, "User not found") return@get } - call.respond(UserStats(user)) + call.respond(UserStats(user.user)) } } } diff --git a/WebsiteBackend/src/de/steamwar/routes/Teams.kt b/WebsiteBackend/src/de/steamwar/routes/Teams.kt deleted file mode 100644 index b6c5f436..00000000 --- a/WebsiteBackend/src/de/steamwar/routes/Teams.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2024 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.sql.Team -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun Route.configureTeamRoutes() { - route("/team") { - get { - call.respond(Team.getAll().map { ResponseTeam(it) }) - } - } -} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/User.kt b/WebsiteBackend/src/de/steamwar/routes/User.kt deleted file mode 100644 index 9571a90e..00000000 --- a/WebsiteBackend/src/de/steamwar/routes/User.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2024 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.plugins.SWPermissionCheck -import de.steamwar.plugins.getUser -import de.steamwar.sql.UserPerm -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun Route.configureUser() { - route("/user") { - route("/{id}") { - route("/admin") { - install(SWPermissionCheck) { - permission = UserPerm.ADMINISTRATION - } - - put("/password") { - val user = call.request.getUser() ?: return@put call.respond(HttpStatusCode.NotFound) - val password = call.receiveText() - - if (password.isEmpty()) { - call.respond(HttpStatusCode.BadRequest, "Password too short") - return@put - } - - user.setPassword(password) - call.respond(HttpStatusCode.OK) - } - } - } - } -} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt b/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt index dd6244e2..2befc916 100644 --- a/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt +++ b/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt @@ -23,7 +23,6 @@ import de.steamwar.plugins.SWPermissionCheck import de.steamwar.plugins.getUser import de.steamwar.sql.SteamwarUser import de.steamwar.sql.UserPerm -import de.steamwar.sql.UserPerm.Prefix import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.response.* @@ -42,7 +41,6 @@ data class RespondUserPermsPrefix(val prefix: RespondPrefix, val perms: List? { + val user = request.getUser() + if (user == null) { + respond(HttpStatusCode.BadRequest) + return null + } + + val perm = parameters[fieldName] + val permission = UserPerm.entries.find { it.name == perm } + if (perm == null || perm.startsWith("PREFIX_") == isPrefix || permission == null) { + respond(HttpStatusCode.BadRequest) + return null + } + + return user to permission } \ No newline at end of file From edbc43abf16d68577f3ea9fb98d4e9351f7fea66 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Mon, 18 Nov 2024 18:07:53 +0100 Subject: [PATCH 6/9] Hotfix: ModUtils Autoban --- VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java b/VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java index b6e68cb9..c1cf7bad 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java @@ -20,6 +20,7 @@ package de.steamwar.velocitycore.mods; import com.velocitypowered.api.proxy.Player; +import de.steamwar.sql.Punishment; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.commands.PunishmentCommand; import de.steamwar.messages.Chatter; @@ -82,6 +83,7 @@ public class ModUtils { } if(max == ModType.RED) { + user.punish(Punishment.PunishmentType.Ban, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), message, -1, false); PunishmentCommand.ban(user, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), message, SteamwarUser.get(-1), false); VelocityCore.getLogger().log(Level.SEVERE, "%s %s wurde automatisch wegen der Mods %s gebannt.".formatted(user.getUserName(), user.getId(), modList)); } From 7437f87d22e6d2427b76b42567902a0899c5b9e4 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sat, 23 Nov 2024 15:36:34 +0100 Subject: [PATCH 7/9] Fixes --- .../src/de/steamwar/scoreboard/SWScoreboard21.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SpigotCore/SpigotCore_21/src/de/steamwar/scoreboard/SWScoreboard21.java b/SpigotCore/SpigotCore_21/src/de/steamwar/scoreboard/SWScoreboard21.java index 0834e166..f3c5f727 100644 --- a/SpigotCore/SpigotCore_21/src/de/steamwar/scoreboard/SWScoreboard21.java +++ b/SpigotCore/SpigotCore_21/src/de/steamwar/scoreboard/SWScoreboard21.java @@ -40,7 +40,7 @@ public class SWScoreboard21 implements SWScoreboard.ISWScoreboard { for(Map.Entry scoreboard : playerBoards.entrySet()) { render(scoreboard.getKey(), scoreboard.getValue()); } - }, 5, 10); + }, 10, 5); } private static void render(Player player, ScoreboardCallback callback) { From 97e4ccb07607d719a5b8e0005bf0bb89f0e390aa Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sat, 23 Nov 2024 15:43:05 +0100 Subject: [PATCH 8/9] Fixes --- .../features/world/BauScoreboard.java | 2 +- .../fightsystem/listener/FightScoreboard.java | 6 +++--- .../steamwar/misslewars/FightScoreboard.java | 4 ++-- .../steamwar/scoreboard/SWScoreboard21.java | 2 +- .../de/steamwar/scoreboard/SWScoreboard8.java | 2 +- .../de/steamwar/scoreboard/SWScoreboard.java | 20 ++++--------------- .../tntleague/events/IngameListener.kt | 2 +- .../steamwar/tntleague/game/TNTLeagueGame.kt | 4 ++-- 8 files changed, 15 insertions(+), 27 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java index 66435414..5bd1e06d 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java @@ -56,7 +56,7 @@ public class BauScoreboard implements Listener { public void handlePlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - SWScoreboard.createScoreboard(player, new ScoreboardCallback() { + SWScoreboard.impl.createScoreboard(player, new ScoreboardCallback() { @Override public HashMap getData() { Region region = Region.getRegion(player.getLocation()); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/FightScoreboard.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/FightScoreboard.java index 63ca402f..7de1f306 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/FightScoreboard.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/FightScoreboard.java @@ -60,17 +60,17 @@ public class FightScoreboard implements Listener, ScoreboardCallback { private FightScoreboard(){ new StateDependentListener(ArenaMode.Replay, FightState.All, this); - Bukkit.getOnlinePlayers().forEach(player -> SWScoreboard.createScoreboard(player, this)); + Bukkit.getOnlinePlayers().forEach(player -> SWScoreboard.impl.createScoreboard(player, this)); } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { - SWScoreboard.createScoreboard(event.getPlayer(), this); + SWScoreboard.impl.createScoreboard(event.getPlayer(), this); } @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { - SWScoreboard.removeScoreboard(event.getPlayer()); + SWScoreboard.impl.removeScoreboard(event.getPlayer()); } public void setTitle(String t) { diff --git a/MissileWars/src/de/steamwar/misslewars/FightScoreboard.java b/MissileWars/src/de/steamwar/misslewars/FightScoreboard.java index 8b16b403..ca217fe2 100644 --- a/MissileWars/src/de/steamwar/misslewars/FightScoreboard.java +++ b/MissileWars/src/de/steamwar/misslewars/FightScoreboard.java @@ -53,7 +53,7 @@ class FightScoreboard implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { - SWScoreboard.createScoreboard(event.getPlayer(), new ScoreboardCallback() { + SWScoreboard.impl.createScoreboard(event.getPlayer(), new ScoreboardCallback() { @Override public String getTitle() { return "§eMissileWars"; @@ -81,7 +81,7 @@ class FightScoreboard implements Listener { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { - SWScoreboard.removeScoreboard(event.getPlayer()); + SWScoreboard.impl.removeScoreboard(event.getPlayer()); } static Scoreboard getScoreboard() { diff --git a/SpigotCore/SpigotCore_21/src/de/steamwar/scoreboard/SWScoreboard21.java b/SpigotCore/SpigotCore_21/src/de/steamwar/scoreboard/SWScoreboard21.java index f3c5f727..019e3898 100644 --- a/SpigotCore/SpigotCore_21/src/de/steamwar/scoreboard/SWScoreboard21.java +++ b/SpigotCore/SpigotCore_21/src/de/steamwar/scoreboard/SWScoreboard21.java @@ -30,7 +30,7 @@ import org.bukkit.scoreboard.Scoreboard; import java.util.HashMap; import java.util.Map; -public class SWScoreboard21 implements SWScoreboard.ISWScoreboard { +public class SWScoreboard21 implements SWScoreboard { private static final HashMap playerBoards = new HashMap<>(); private static final String SIDEBAR = "sw-sidebar"; diff --git a/SpigotCore/SpigotCore_8/src/de/steamwar/scoreboard/SWScoreboard8.java b/SpigotCore/SpigotCore_8/src/de/steamwar/scoreboard/SWScoreboard8.java index 3af0fc93..d65cf989 100644 --- a/SpigotCore/SpigotCore_8/src/de/steamwar/scoreboard/SWScoreboard8.java +++ b/SpigotCore/SpigotCore_8/src/de/steamwar/scoreboard/SWScoreboard8.java @@ -29,7 +29,7 @@ import org.bukkit.entity.Player; import java.util.HashMap; import java.util.Map; -public class SWScoreboard8 implements SWScoreboard.ISWScoreboard { +public class SWScoreboard8 implements SWScoreboard { private static final Reflection.FieldAccessor scoreboardName = Reflection.getField(FlatteningWrapper.scoreboardObjective, String.class, 0); private static final Reflection.FieldAccessor scoreboardAction = Reflection.getField(FlatteningWrapper.scoreboardObjective, int.class, Core.getVersion() > 15 ? 3 : 0); private static final Class scoreboardDisplayEnum = Reflection.getClass("{nms.world.scores.criteria}.IScoreboardCriteria$EnumScoreboardHealthDisplay"); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java index 5fd8808e..1d5c2d79 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java @@ -23,21 +23,9 @@ import de.steamwar.core.Core; import de.steamwar.core.VersionDependent; import org.bukkit.entity.Player; -public class SWScoreboard { - private SWScoreboard() {} +public interface SWScoreboard { + public static final SWScoreboard impl = VersionDependent.getVersionImpl(Core.getInstance()); - private static final ISWScoreboard impl = VersionDependent.getVersionImpl(Core.getInstance()); - - public static boolean createScoreboard(Player player, ScoreboardCallback callback) { - return impl.createScoreboard(player, callback); - } - - public static void removeScoreboard(Player player) { - impl.removeScoreboard(player); - } - - public interface ISWScoreboard { - boolean createScoreboard(Player player, ScoreboardCallback callback); - void removeScoreboard(Player player); - } + boolean createScoreboard(Player player, ScoreboardCallback callback); + void removeScoreboard(Player player); } diff --git a/TNTLeague/src/de/steamwar/tntleague/events/IngameListener.kt b/TNTLeague/src/de/steamwar/tntleague/events/IngameListener.kt index bb9b0415..e688dcbc 100644 --- a/TNTLeague/src/de/steamwar/tntleague/events/IngameListener.kt +++ b/TNTLeague/src/de/steamwar/tntleague/events/IngameListener.kt @@ -43,7 +43,7 @@ object IngameListener: Listener { @EventHandler fun onJoin(e: PlayerJoinEvent) { - SWScoreboard.createScoreboard(e.player, TNTLeagueScoreboard(e.player)) + SWScoreboard.impl.createScoreboard(e.player, TNTLeagueScoreboard(e.player)) } @EventHandler diff --git a/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueGame.kt b/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueGame.kt index 59d4e65c..94191c4e 100644 --- a/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueGame.kt +++ b/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueGame.kt @@ -57,7 +57,7 @@ object TNTLeagueGame { state = GameState.RUNNING - plugin.server.onlinePlayers.forEach { SWScoreboard.createScoreboard(it, TNTLeagueScoreboard(it)) } + plugin.server.onlinePlayers.forEach { SWScoreboard.impl.createScoreboard(it, TNTLeagueScoreboard(it)) } blueTeam.start() redTeam.start() @@ -101,7 +101,7 @@ object TNTLeagueGame { plugin.server.onlinePlayers.forEach { it.gameMode = GameMode.SPECTATOR - SWScoreboard.removeScoreboard(it) + SWScoreboard.impl.removeScoreboard(it) it.playSound(Sound.sound(org.bukkit.Sound.ENTITY_ENDER_DRAGON_DEATH.key, Sound.Source.MASTER, 1f, 1f)) } From c15d523f3b1758196c3a7ae8754ea4365e86b0f2 Mon Sep 17 00:00:00 2001 From: Lixfel Date: Sat, 23 Nov 2024 17:09:58 +0100 Subject: [PATCH 9/9] Hotfix NodeDownload --- CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java b/CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java index 018d95d7..b3792b63 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java +++ b/CommonCore/SQL/src/de/steamwar/sql/NodeDownload.java @@ -40,7 +40,7 @@ public class NodeDownload { private static final Table table = new Table<>(NodeDownload.class); private static final Statement insert = table.insertFields("NodeId", "Link"); - private static final SelectStatement select = table.select("link"); + private static final SelectStatement select = table.selectFields("link"); private static final Statement delete = table.delete(Table.PRIMARY);