Merge pull request 'Merge' (#1) from public into master
Reviewed-on: #1
Dieser Commit ist enthalten in:
Commit
a275818a31
5
.gitignore
vendored
5
.gitignore
vendored
@ -33,4 +33,7 @@ out/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
.vscode/
|
||||
/config.json
|
||||
/logs/
|
||||
/data/
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 66fc131803d8fad84dbc51e60999301e4a162190
|
||||
Subproject commit 6aefb79e78bc49c2cd83a2ae06e442f52169116d
|
@ -24,18 +24,26 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
implementation("ch.qos.logback:logback-classic:$logback_version")
|
||||
implementation("io.ktor:ktor-server-core-jvm:2.3.0")
|
||||
implementation("io.ktor:ktor-server-content-negotiation-jvm:2.3.0")
|
||||
implementation("io.ktor:ktor-server-cors-jvm:2.3.0")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:2.3.0")
|
||||
implementation("io.ktor:ktor-server-jetty-jvm:2.3.0")
|
||||
implementation("io.ktor:ktor-server-host-common-jvm:2.3.0")
|
||||
implementation("io.ktor:ktor-server-call-logging-jvm:2.3.0")
|
||||
implementation("io.ktor:ktor-server-request-validation:2.3.0")
|
||||
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-cors-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-request-validation:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-auth:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-auth-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-auth-ldap-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-client-core-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-client-java:$ktor_version")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
|
||||
implementation("io.ktor:ktor-client-auth:$ktor_version")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||
implementation("com.mysql:mysql-connector-j:8.0.31")
|
||||
implementation("com.mysql:mysql-connector-j:8.1.0")
|
||||
implementation(project(":CommonCore"))
|
||||
implementation("org.bspfsystems:yamlconfiguration:1.3.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.4.1")
|
||||
testImplementation("io.ktor:ktor-server-tests-jvm:2.3.0")
|
||||
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-rate-limit:$ktor_version")
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
ktor_version=2.2.1
|
||||
ktor_version=2.3.4
|
||||
kotlin_version=1.7.22
|
||||
logback_version=1.2.11
|
||||
kotlin.code.style=official
|
||||
|
0
gradlew
vendored
Ausführbare Datei → Normale Datei
0
gradlew
vendored
Ausführbare Datei → Normale Datei
@ -1,3 +1,3 @@
|
||||
rootProject.name = "sw_event_api"
|
||||
rootProject.name = "WebsiteBackend"
|
||||
|
||||
include("CommonCore")
|
@ -20,19 +20,38 @@
|
||||
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 io.ktor.server.jetty.*
|
||||
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)
|
||||
data class ResponseError(val error: String, val code: String = error)
|
||||
|
||||
@Serializable
|
||||
data class Config(val giteaToken: String)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val config = Json.decodeFromStream<Config>(File("config.json").inputStream())
|
||||
|
||||
fun main() {
|
||||
SchematicType.Normal.name().length
|
||||
embeddedServer(Jetty, port = 1337, host = "127.0.0.1", module = Application::module)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -19,42 +19,68 @@
|
||||
|
||||
package de.steamwar.plugins
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
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.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import java.util.Properties
|
||||
|
||||
val validCodes by lazy {
|
||||
val file = File(System.getProperty("user.home"), "api-codes.txt")
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
|
||||
data class SWAuthPrincipal(val token: Token, val user: SteamwarUser) : Principal
|
||||
|
||||
class SWAuthConfig {
|
||||
var permission: UserPerm? = null
|
||||
var allowedMethods = mutableListOf<HttpMethod>()
|
||||
var userCheck: SWAuthPrincipal.(ApplicationRequest) -> Boolean = { true }
|
||||
var mustAuth: Boolean = false
|
||||
|
||||
fun allowMethod(method: HttpMethod) {
|
||||
allowedMethods.add(method)
|
||||
}
|
||||
|
||||
fun allowMethods(methods: List<HttpMethod>) {
|
||||
allowedMethods.addAll(methods)
|
||||
}
|
||||
|
||||
fun userCheck(check: SWAuthPrincipal.(ApplicationRequest) -> Boolean) {
|
||||
userCheck = check
|
||||
}
|
||||
Properties().apply {
|
||||
load(file.inputStream())
|
||||
}.map { it.key.toString() to it.value.toString() }.toMap()
|
||||
}
|
||||
|
||||
fun isValidCode(code: String): Pair<Boolean, String> {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(code.toByteArray())
|
||||
val hashed = hash.joinToString("") { "%02x".format(it) }
|
||||
return validCodes.contains(hashed) to hashed
|
||||
}
|
||||
val SWPermissionCheck = createRouteScopedPlugin("SWAuth", ::SWAuthConfig) {
|
||||
pluginConfig.apply {
|
||||
on(AuthenticationChecked) { call ->
|
||||
if (call.request.httpMethod in allowedMethods) {
|
||||
if(mustAuth) {
|
||||
val token = call.principal<SWAuthPrincipal>()
|
||||
|
||||
val SWAuth = createApplicationPlugin("SWAuth") {
|
||||
onCall { call ->
|
||||
if (call.request.httpMethod == HttpMethod.Options) {
|
||||
return@onCall
|
||||
}
|
||||
val auth = call.request.headers["X-SW-Auth"] ?: call.request.queryParameters["auth"]
|
||||
if (auth == null) {
|
||||
call.respond(HttpStatusCode.Unauthorized, ResponseError("Missing auth header"))
|
||||
} else if (!isValidCode(auth).first) {
|
||||
call.respond(HttpStatusCode.Unauthorized, ResponseError("Invalid auth code"))
|
||||
if (token == null) {
|
||||
call.respond(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
return@on
|
||||
}
|
||||
|
||||
val token = call.principal<SWAuthPrincipal>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,17 +19,19 @@
|
||||
|
||||
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.callloging.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.plugins.ratelimit.*
|
||||
import io.ktor.server.request.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.slf4j.event.*
|
||||
|
||||
data class Session(val name: String)
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
fun Application.configurePlugins() {
|
||||
install(CORS) {
|
||||
@ -38,25 +40,51 @@ fun Application.configurePlugins() {
|
||||
allowMethod(HttpMethod.Post)
|
||||
allowMethod(HttpMethod.Put)
|
||||
allowMethod(HttpMethod.Delete)
|
||||
allowHeader("x-sw-auth")
|
||||
allowHeader(HttpHeaders.Authorization)
|
||||
allowHeader(HttpHeaders.AccessControlAllowOrigin)
|
||||
allowHeader(HttpHeaders.ContentType)
|
||||
anyHost()
|
||||
allowXHttpMethodOverride()
|
||||
}
|
||||
install(SWAuth)
|
||||
install(RateLimit) {
|
||||
global {
|
||||
rateLimiter(limit = 60, refillPeriod = 60.seconds)
|
||||
requestKey {
|
||||
it.request.headers["X-Forwarded-Proto"] ?: it.request.local.remoteHost
|
||||
}
|
||||
requestWeight { applicationCall, _ ->
|
||||
if(applicationCall.request.headers["X-Forwarded-Proto"] != 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(CallLogging) {
|
||||
level = Level.INFO
|
||||
format {
|
||||
val auth = it.request.headers["X-SW-Auth"]
|
||||
if (auth != null) {
|
||||
val verfied = isValidCode(auth)
|
||||
return@format "Auth: ${verfied.second}, Valid: ${verfied.first}, ${it.request.httpMethod.value} ${it.request.uri}"
|
||||
val verified = it.principal<SWAuthPrincipal>()
|
||||
if (verified != null) {
|
||||
"User (${it.request.local.remoteAddress}): ${verified.token.owner.userName}, Token: ${verified.token.name}, ${it.request.httpMethod.value} ${it.request.uri}, Response: ${it.response.status()?.value}, User-Agent: ${it.request.headers["User-Agent"] ?: "Unknown"}"
|
||||
} else {
|
||||
return@format "No Auth Header found: ${it.request.httpMethod.value} ${it.request.uri}"
|
||||
"Unauthenticated Request (${it.request.local.remoteAddress}): ${it.request.httpMethod.value} ${it.request.uri}, Response: ${it.response.status()?.value}, User-Agent: ${it.request.headers["User-Agent"] ?: "Unknown"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ package de.steamwar.plugins
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import io.ktor.server.request.*
|
||||
|
||||
fun ApplicationRequest.getUser(): SteamwarUser? {
|
||||
SteamwarUser.clear()
|
||||
return SteamwarUser.get(call.parameters["id"]?.toIntOrNull() ?: return null)
|
||||
fun ApplicationRequest.getUser(key: String = "id"): SteamwarUser? {
|
||||
return SteamwarUser.get(call.parameters[key]?.toIntOrNull() ?: return null)
|
||||
}
|
168
src/main/kotlin/de/steamwar/routes/Auth.kt
Normale Datei
168
src/main/kotlin/de/steamwar/routes/Auth.kt
Normale Datei
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.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<SWAuthPrincipal>() != null) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Already logged in", "already_logged_in"))
|
||||
return@post
|
||||
}
|
||||
|
||||
val request = call.receive<AuthLoginRequest>()
|
||||
|
||||
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<SWAuthPrincipal>()
|
||||
|
||||
if(auth == null) {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(Token.listUser(auth.user).map { ResponseToken(it) })
|
||||
}
|
||||
|
||||
post {
|
||||
val auth = call.principal<SWAuthPrincipal>()
|
||||
|
||||
if(auth == null) {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@post
|
||||
}
|
||||
|
||||
val request = call.receive<CreateTokenRequest>()
|
||||
|
||||
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<SWAuthPrincipal>()
|
||||
|
||||
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<SWAuthPrincipal>()
|
||||
|
||||
if(auth == null) {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@post
|
||||
}
|
||||
|
||||
auth.token.delete()
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,27 +21,61 @@ package de.steamwar.routes
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.data.Groups
|
||||
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
|
||||
|
||||
@Serializable
|
||||
data class ResponseSchematicType(val name: String, val db: String)
|
||||
|
||||
@Serializable
|
||||
data class ResponseUser(val id: Int, val name: String, val uuid: String) {
|
||||
constructor(user: SteamwarUser) : this(user.id, user.userName, user.uuid.toString())
|
||||
data class ResponseUser(val id: Int, val name: String, val uuid: String, val prefix: String, val perms: List<String>) {
|
||||
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<Int, ResponseUser>()
|
||||
|
||||
fun get(id: Int): ResponseUser {
|
||||
synchronized(cache) {
|
||||
if(cache.containsKey(id)) {
|
||||
return cache[id]!!
|
||||
}
|
||||
val user = ResponseUser(SteamwarUser.get(id))
|
||||
cache[id] = user
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
synchronized(cache) {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.configureDataRoutes() {
|
||||
fun Route.configureDataRoutes() {
|
||||
route("/data") {
|
||||
get {
|
||||
call.respondText("Hello World!")
|
||||
}
|
||||
get("/schematicTypes") {
|
||||
val types = mutableListOf<SchematicType>()
|
||||
loadSchematicTypes(types, mutableMapOf())
|
||||
@ -49,7 +83,7 @@ fun Routing.configureDataRoutes() {
|
||||
}
|
||||
get("/gamemodes") {
|
||||
call.respond(
|
||||
File("/configs/GameModes/").listFiles()
|
||||
File("/configs/GameModes/").listFiles()!!
|
||||
.filter { it.name.endsWith(".yml") && !it.name.endsWith(".kits.yml") }
|
||||
.map { it.nameWithoutExtension })
|
||||
}
|
||||
@ -69,11 +103,28 @@ fun Routing.configureDataRoutes() {
|
||||
get("/users") {
|
||||
call.respond(SteamwarUser.getAll().map { ResponseUser(it) })
|
||||
}
|
||||
get {
|
||||
call.respondText("Hello World!")
|
||||
get("/team") {
|
||||
call.respond(SteamwarUser.getAll().filter { it.hasPerm(UserPerm.TEAM) }.sortedBy { it.prefix().ordinal }.reversed().map { ResponseUser(it) })
|
||||
}
|
||||
get("/groups") {
|
||||
call.respond(Groups.getAllGroups())
|
||||
}
|
||||
get("/server") {
|
||||
try {
|
||||
val server = fetchData(InetSocketAddress("steamwar.de", 25565), 100)
|
||||
call.respond(server)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
call.respond(HttpStatusCode.InternalServerError, ResponseError(e.message ?: "Unknown error"))
|
||||
return@get
|
||||
}
|
||||
}
|
||||
|
||||
route("/me") {
|
||||
install(SWPermissionCheck)
|
||||
get {
|
||||
call.respond(ResponseUser(call.principal<SWAuthPrincipal>()!!.user))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,9 +21,11 @@ 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.*
|
||||
@ -38,7 +40,7 @@ import java.time.Instant
|
||||
data class ResponseEventFight(val id: Int, val spielmodus: String, val map: String, val blueTeam: ResponseTeam, val redTeam: ResponseTeam, val kampfleiter: ResponseUser, val start: Long, val ergebnis: Int, val group: String?) {
|
||||
constructor(eventFight: EventFight): this(eventFight.fightID, eventFight.spielModus, eventFight.map, ResponseTeam(
|
||||
Team.get(eventFight.teamBlue)), ResponseTeam(Team.get(eventFight.teamRed)),
|
||||
ResponseUser(SteamwarUser.get(eventFight.kampfleiter)), eventFight.startTime.time, eventFight.ergebnis, Groups.getGroup(eventFight.fightID)?.name)
|
||||
ResponseUser.get(eventFight.kampfleiter), eventFight.startTime.time, eventFight.ergebnis, Groups.getGroup(eventFight.fightID)?.name)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@ -52,8 +54,12 @@ data class UpdateEventFight(val blueTeam: Int? = null, val redTeam: 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 kampfleiter: Int? = null, val group: String? = null)
|
||||
|
||||
fun Routing.configureEventFightRoutes() {
|
||||
fun Route.configureEventFightRoutes() {
|
||||
route("/fights") {
|
||||
install(SWPermissionCheck) {
|
||||
allowMethod(HttpMethod.Get)
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
post {
|
||||
val fight = call.receiveNullable<CreateEventFight>()
|
||||
if (fight == null) {
|
||||
|
@ -21,11 +21,8 @@ package de.steamwar.routes
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.data.Groups
|
||||
import de.steamwar.sql.Event
|
||||
import de.steamwar.sql.EventFight
|
||||
import de.steamwar.sql.SchematicType
|
||||
import de.steamwar.sql.Team
|
||||
import de.steamwar.sql.TeamTeilnahme
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
@ -88,8 +85,12 @@ data class UpdateEvent(
|
||||
val spectateSystem: Boolean?
|
||||
)
|
||||
|
||||
fun Routing.configureEventsRoute() {
|
||||
fun Route.configureEventsRoute() {
|
||||
route("/events") {
|
||||
install(SWPermissionCheck) {
|
||||
allowMethod(HttpMethod.Get)
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
get {
|
||||
call.respond(Event.getAllShort().map { ShortEvent(it) })
|
||||
}
|
||||
|
@ -20,7 +20,9 @@
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.Mod
|
||||
import de.steamwar.sql.UserPerm
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
@ -36,8 +38,12 @@ data class ResponseMod(val platform: Int, val modName: String, val modType: Int)
|
||||
@Serializable
|
||||
data class UpdateMod(val modType: String)
|
||||
|
||||
fun Routing.configureModRoutes() {
|
||||
fun Route.configureModRoutes() {
|
||||
route("/mods") {
|
||||
install(SWPermissionCheck) {
|
||||
allowMethod(HttpMethod.Get)
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
get("/unchecked") {
|
||||
call.respond(Mod.getAllUnklassified().map { ResponseMod(it) })
|
||||
}
|
||||
|
263
src/main/kotlin/de/steamwar/routes/Page.kt
Normale Datei
263
src/main/kotlin/de/steamwar/routes/Page.kt
Normale Datei
@ -0,0 +1,263 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.config
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.UserPerm
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.java.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.util.reflect.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import java.util.Base64
|
||||
|
||||
val pathPageIdMap = mutableMapOf<String, Int>()
|
||||
var pageId = 1
|
||||
|
||||
@Serializable
|
||||
data class Identity(val name: String, val email: String)
|
||||
|
||||
@Serializable
|
||||
data class PageResponseList(
|
||||
val path: String,
|
||||
val name: String,
|
||||
val sha: String,
|
||||
val downloadUrl: String,
|
||||
val id: Int
|
||||
) {
|
||||
constructor(res: JsonObject, id: Int) : this(
|
||||
res["path"]?.jsonPrimitive?.content!!,
|
||||
res["name"]?.jsonPrimitive?.content!!,
|
||||
res["sha"]?.jsonPrimitive?.content!!,
|
||||
res["download_url"]?.jsonPrimitive?.content!!,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PageResponse(
|
||||
val path: String,
|
||||
val name: String,
|
||||
val sha: String,
|
||||
val downloadUrl: String,
|
||||
val content: String,
|
||||
val size: Int,
|
||||
val id: Int,
|
||||
) {
|
||||
constructor(res: JsonObject, id: Int) : this(
|
||||
res["path"]?.jsonPrimitive?.content!!,
|
||||
res["name"]?.jsonPrimitive?.content!!,
|
||||
res["sha"]?.jsonPrimitive?.content!!,
|
||||
res["download_url"]?.jsonPrimitive?.content!!,
|
||||
res["content"]?.jsonPrimitive?.content!!,
|
||||
res["size"]?.jsonPrimitive?.int!!,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CreatePageRequest(val path: String)
|
||||
|
||||
@Serializable
|
||||
data class CreateBranchRequest(val branch: String)
|
||||
|
||||
@Serializable
|
||||
data class UpdatePageRequest(val content: String, val sha: String, val message: String)
|
||||
|
||||
@Serializable
|
||||
data class MergeBranchRequest(val branch: String, val message: String)
|
||||
|
||||
@Serializable
|
||||
data class DeletePageRequest(val sha: String, val message: String)
|
||||
|
||||
fun Route.configurePage() {
|
||||
val client = HttpClient(Java) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
defaultRequest {
|
||||
url("https://steamwar.de/devlabs/api/v1/")
|
||||
header("Authorization", "token " + config.giteaToken)
|
||||
}
|
||||
}
|
||||
|
||||
route("page") {
|
||||
install(SWPermissionCheck) {
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
get {
|
||||
val branch = call.request.queryParameters["branch"] ?: "master"
|
||||
val filesToCheck = mutableListOf("src/content")
|
||||
val files = mutableListOf<PageResponseList>()
|
||||
|
||||
while (filesToCheck.isNotEmpty()) {
|
||||
val path = filesToCheck.removeAt(0)
|
||||
val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch")
|
||||
val fileJson = Json.parseToJsonElement(res.bodyAsText())
|
||||
|
||||
if (fileJson is JsonArray) {
|
||||
fileJson.forEach {
|
||||
val obj = it.jsonObject
|
||||
if (obj["type"]?.jsonPrimitive?.content == "dir") {
|
||||
filesToCheck.add(obj["path"]?.jsonPrimitive?.content!!)
|
||||
} else if (obj["type"]?.jsonPrimitive?.content == "file" && (obj["name"]?.jsonPrimitive?.content?.endsWith(".md") == true || 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<CreateBranchRequest>().branch
|
||||
val res = client.post("repos/SteamWar/Website/branches") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CreateGiteaBranchRequest(branch, "master"))
|
||||
}
|
||||
call.respond(res.status)
|
||||
}
|
||||
post("branch/merge") {
|
||||
@Serializable
|
||||
data class CreateGiteaMergeRequest(val base: String, val head: String, val title: String)
|
||||
|
||||
val data = call.receive<MergeBranchRequest>()
|
||||
val createRes = client.post("repos/SteamWar/Website/pulls") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CreateGiteaMergeRequest("master", data.branch, data.message))
|
||||
}
|
||||
|
||||
val id = Json.parseToJsonElement(createRes.bodyAsText()).jsonObject["number"]?.jsonPrimitive?.int!!
|
||||
|
||||
@Serializable
|
||||
data class MergeGiteaMergeRequest(val Do: String)
|
||||
|
||||
val res = client.post("repos/SteamWar/Website/pulls/$id/merge") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(MergeGiteaMergeRequest("merge"))
|
||||
}
|
||||
|
||||
call.respond(res.status)
|
||||
}
|
||||
delete("branch") {
|
||||
val branch = call.receive<CreateBranchRequest>().branch
|
||||
val res = client.delete("repos/SteamWar/Website/branches/$branch")
|
||||
call.respond(res.status)
|
||||
}
|
||||
post {
|
||||
@Serializable
|
||||
data class CreateGiteaPageRequest(val message: String, val content: String, val branch: String, val author: Identity)
|
||||
|
||||
val path = call.receive<CreatePageRequest>().path
|
||||
if(path.startsWith("src/content/")) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Invalid path")
|
||||
return@post
|
||||
}
|
||||
val res = client.post("repos/SteamWar/Website/contents/src/content/$path") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CreateGiteaPageRequest(
|
||||
"Create page $path",
|
||||
Base64.getEncoder().encodeToString("""
|
||||
---
|
||||
title: [Enter Title]
|
||||
description: [Enter Description]
|
||||
slug: [Enter Slug]
|
||||
---
|
||||
|
||||
# $path
|
||||
""".trimIndent().toByteArray()),
|
||||
call.request.queryParameters["branch"] ?: "master",
|
||||
Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de"
|
||||
)))
|
||||
}
|
||||
call.respond(res.status)
|
||||
}
|
||||
get("{id}") {
|
||||
val id = call.parameters["id"]?.toIntOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
|
||||
val path = pathPageIdMap.entries.find { it.value == id }?.key ?: return@get call.respond(HttpStatusCode.NotFound, "Page not found")
|
||||
|
||||
val branch = call.request.queryParameters["branch"] ?: "master"
|
||||
val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch")
|
||||
val fileJson = Json.parseToJsonElement(res.bodyAsText())
|
||||
if (fileJson is JsonArray) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
|
||||
}
|
||||
|
||||
val file = PageResponse(fileJson.jsonObject, id)
|
||||
call.respond(file)
|
||||
}
|
||||
|
||||
delete("{id}") {
|
||||
val data = call.receive<DeletePageRequest>()
|
||||
|
||||
val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@delete call.respond(HttpStatusCode.NotFound, "Page not found")
|
||||
val branch = call.request.queryParameters["branch"] ?: "master"
|
||||
|
||||
@Serializable
|
||||
data class DeleteGiteaPageRequest(val sha: String, val message: String, val branch: String, val author: Identity)
|
||||
|
||||
val res = client.delete("repos/SteamWar/Website/contents/$path") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(DeleteGiteaPageRequest(data.sha, data.message, branch, Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de")))
|
||||
}
|
||||
|
||||
call.respond(res.status)
|
||||
}
|
||||
|
||||
put("{id}") {
|
||||
@Serializable
|
||||
data class UpdateGiteaPageRequest(val content: String, val sha: String, val message: String, val branch: String, val author: Identity)
|
||||
|
||||
val data = call.receive<UpdatePageRequest>()
|
||||
val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@put call.respond(HttpStatusCode.NotFound, "Page not found")
|
||||
|
||||
val res = client.put("repos/SteamWar/Website/contents/$path") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(UpdateGiteaPageRequest(data.content, data.sha, data.message, (call.request.queryParameters["branch"] ?: "master"), Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de")))
|
||||
}
|
||||
|
||||
call.respond(res.status)
|
||||
}
|
||||
}
|
||||
}
|
@ -20,14 +20,21 @@
|
||||
package de.steamwar.routes
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Application.configureRoutes() {
|
||||
routing {
|
||||
configureEventsRoute()
|
||||
configureDataRoutes()
|
||||
configureEventFightRoutes()
|
||||
configureModRoutes()
|
||||
configureUserPerms()
|
||||
authenticate("sw-auth", optional = true) {
|
||||
configureEventsRoute()
|
||||
configureDataRoutes()
|
||||
configureEventFightRoutes()
|
||||
configureModRoutes()
|
||||
configureUserPerms()
|
||||
configureStats()
|
||||
configurePage()
|
||||
configureSchematic()
|
||||
configureAuthRoutes()
|
||||
}
|
||||
}
|
||||
}
|
235
src/main/kotlin/de/steamwar/routes/Schematic.kt
Normale Datei
235
src/main/kotlin/de/steamwar/routes/Schematic.kt
Normale Datei
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.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.time.temporal.TemporalUnit
|
||||
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<ResponseUser>, 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<ResponseBreadcrumb>, val schematics: List<ResponseSchematic>, val players: Map<Int, ResponseUser>) {
|
||||
constructor(schematics: List<ResponseSchematic>, breadcrumbs: List<ResponseBreadcrumb>) : 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() {
|
||||
get("/download/{code}") {
|
||||
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<SWAuthPrincipal>()?.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)
|
||||
}
|
||||
route("/schem") {
|
||||
install(SWPermissionCheck)
|
||||
get {
|
||||
val user = call.principal<SWAuthPrincipal>()!!.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<UploadSchematic>()
|
||||
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<SWAuthPrincipal>()!!.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<SWAuthPrincipal>()!!.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<SWAuthPrincipal>()!!.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) }))
|
||||
}
|
||||
|
||||
get("/download") {
|
||||
val user = call.principal<SWAuthPrincipal>()!!.user
|
||||
val schemId = call.parameters["id"]?.toIntOrNull() ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@get
|
||||
}
|
||||
|
||||
val schem = SchematicNode.getSchematicNode(schemId) ?: run {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
if(schem.owner != user.id) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
return@get
|
||||
}
|
||||
|
||||
val code = generateCode()
|
||||
|
||||
val dl = NodeDownload.addCode(schem, code)
|
||||
val response = SchematicCode(schemId, code, dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).epochSecond);
|
||||
|
||||
call.respond(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
87
src/main/kotlin/de/steamwar/routes/Stats.kt
Normale Datei
87
src/main/kotlin/de/steamwar/routes/Stats.kt
Normale Datei
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.plugins.getUser
|
||||
import de.steamwar.sql.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStats(val eventFightParticipation: Int, val eventParticipation: Int, val acceptedSchematics: Int, 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<SWAuthPrincipal>()
|
||||
|
||||
if (user == null || auth == null) {
|
||||
return@userCheck false
|
||||
}
|
||||
|
||||
return@userCheck user.id == auth.user.id || auth.user.hasPerm(UserPerm.MODERATION)
|
||||
}
|
||||
}
|
||||
get {
|
||||
val user = call.request.getUser()
|
||||
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.NotFound, "User not found")
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(UserStats(user))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ import io.ktor.server.application.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Routing.configureTeamRoutes() {
|
||||
fun Route.configureTeamRoutes() {
|
||||
route("/team") {
|
||||
get {
|
||||
call.respond(Team.getAll().map { ResponseTeam(it) })
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.plugins.getUser
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import de.steamwar.sql.UserPerm
|
||||
@ -38,8 +39,12 @@ data class RespondUserPerms(val prefixes: Map<String, RespondPrefix>, val perms:
|
||||
@Serializable
|
||||
data class RespondUserPermsPrefix(val prefix: RespondPrefix, val perms: List<String>)
|
||||
|
||||
fun Routing.configureUserPerms() {
|
||||
fun Route.configureUserPerms() {
|
||||
route("/perms") {
|
||||
install(SWPermissionCheck) {
|
||||
allowMethod(HttpMethod.Get)
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
get {
|
||||
val perms = mutableListOf<String>()
|
||||
val prefixes = mutableMapOf<String, RespondPrefix>()
|
||||
@ -55,6 +60,11 @@ fun Routing.configureUserPerms() {
|
||||
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) {
|
||||
|
@ -41,7 +41,7 @@ fun loadSchematicTypes(tmpTypes: MutableList<SchematicType>?, tmpFromDB: Mutable
|
||||
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)
|
||||
checktype = SchematicType("C$type", "C$shortcut", SchematicType.Type.CHECK_TYPE, null, material, false)
|
||||
tmpTypes!!.add(checktype)
|
||||
tmpFromDB[checktype.toDB()] = checktype
|
||||
}
|
||||
@ -50,7 +50,8 @@ fun loadSchematicTypes(tmpTypes: MutableList<SchematicType>?, tmpFromDB: Mutable
|
||||
shortcut,
|
||||
if (config.isConfigurationSection("Server")) SchematicType.Type.FIGHT_TYPE else SchematicType.Type.NORMAL,
|
||||
checktype,
|
||||
material
|
||||
material,
|
||||
false
|
||||
)
|
||||
tmpTypes!!.add(current)
|
||||
tmpFromDB[type.lowercase(Locale.getDefault())] = current
|
||||
|
67
src/main/kotlin/de/steamwar/sql/Stats.kt
Normale Datei
67
src/main/kotlin/de/steamwar/sql/Stats.kt
Normale Datei
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.sql
|
||||
|
||||
import de.steamwar.sql.internal.Statement
|
||||
import de.steamwar.sql.internal.Statement.ResultSetUser
|
||||
|
||||
private val getNum: ResultSetUser<Int?> = ResultSetUser { res ->
|
||||
if (res.next()) {
|
||||
res.getInt("num")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val eventFightParticipation = Statement("SELECT FightPlayer.UserID, COUNT(UserID) as num FROM FightPlayer INNER JOIN Fight on FightPlayer.FightID = Fight.FightID WHERE Fight.Server LIKE '%vs%' AND FightPlayer.UserID = ? GROUP BY FightPlayer.UserID")
|
||||
|
||||
fun getEventFightParticipation(user: SteamwarUser): Int? = eventFightParticipation.select(getNum, user.id)
|
||||
|
||||
private val eventParticipation = Statement("SELECT FightPlayer.UserID, COUNT(DISTINCT EventID) as num FROM FightPlayer INNER JOIN core.Fight F on FightPlayer.FightID = F.FightID INNER JOIN core.EventFight EF on F.FightID = EF.Fight WHERE F.FightID = FightPlayer.FightID AND FightPlayer.FightID = EF.Fight AND F.Server LIKE '%vs%' AND FightPlayer.UserID = ? GROUP BY FightPlayer.UserID")
|
||||
|
||||
fun getEventParticipation(user: SteamwarUser): Int? = eventParticipation.select(getNum, user.id)
|
||||
|
||||
private val acceptedSchematics = Statement("SELECT NodeOwner, COUNT(DISTINCT NodeId) AS num FROM SchematicNode WHERE NodeType != 'normal' AND NodeType IS NOT NULL AND NodeType NOT LIKE 'c%' AND NodeOwner = ?")
|
||||
|
||||
fun getAcceptedSchematics(user: SteamwarUser): Int? = acceptedSchematics.select(getNum, user.id)
|
||||
|
||||
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<Pair<String, Int>> = rankedList.select({ res ->
|
||||
val list = mutableListOf<Pair<String, Int>>()
|
||||
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<Triple<String, String, Int>> = fightList.select({ res ->
|
||||
val list = mutableListOf<Triple<String, String, Int>>()
|
||||
while (res.next()) {
|
||||
list.add(Triple(res.getString("Datum"), res.getString("Modus"), res.getInt("Anzahl")))
|
||||
}
|
||||
list
|
||||
})
|
137
src/main/kotlin/de/steamwar/util/ServerPing.kt
Normale Datei
137
src/main/kotlin/de/steamwar/util/ServerPing.kt
Normale Datei
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.util
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import java.io.*
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
|
||||
/**
|
||||
*
|
||||
* @author zh32 <zh32 at zh32.de>
|
||||
</zh32> */
|
||||
private fun readVarInt(`in`: DataInputStream): Int {
|
||||
var i = 0
|
||||
var j = 0
|
||||
while (true) {
|
||||
val k = `in`.readByte().toInt()
|
||||
i = i or (k and 0x7F shl j++ * 7)
|
||||
if (j > 5) throw RuntimeException("VarInt too big")
|
||||
if (k and 0x80 != 128) break
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
private fun writeVarInt(out: DataOutputStream, paramInt: Int) {
|
||||
var 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<Player> = emptyList())
|
||||
|
||||
@Serializable
|
||||
data class Player(val name: String, val id: String)
|
||||
|
||||
@Serializable
|
||||
data class Version(val name: String, val protocol: Int)
|
@ -1,3 +1,22 @@
|
||||
<!--
|
||||
~ This file is a part of the SteamWar software.
|
||||
~
|
||||
~ Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU Affero General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU Affero General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Affero General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
|
5
steamwarci.yml
Normale Datei
5
steamwarci.yml
Normale Datei
@ -0,0 +1,5 @@
|
||||
build:
|
||||
- "./gradlew distZip"
|
||||
|
||||
artifacts:
|
||||
"/binarys/WebsiteBackend.zip": "build/distributions/WebsiteBackend-0.0.1.zip"
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren