Dieser Commit ist enthalten in:
Chaoscaot 2023-09-06 23:07:32 +02:00
Ursprung eaff459715
Commit a81a7e758f
Signiert von: Chaoscaot
GPG-Schlüssel-ID: BDF8FADD7D5EDB7A
12 geänderte Dateien mit 145 neuen und 64 gelöschten Zeilen

Datei anzeigen

@ -24,18 +24,22 @@ 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-jetty-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")
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")
}

Datei anzeigen

@ -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

Datei anzeigen

@ -19,42 +19,66 @@
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 {
public var permission: UserPerm = UserPerm.MODERATION
public var allowedMethods = mutableListOf<HttpMethod>()
public var userCheck: SWAuthPrincipal.(ApplicationRequest) -> Boolean = { true }
public 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, "Invalid token")
}
}
return@on
}
val token = call.principal<SWAuthPrincipal>()
if (token == null) {
call.respond(HttpStatusCode.Unauthorized, "Invalid token")
return@on
}
if (!token.user.hasPerm(permission)) {
call.respond(HttpStatusCode.Forbidden, "Insufficient permissions")
}
if (!token.userCheck(call.request)) {
call.respond(HttpStatusCode.Forbidden, "Insufficient permissions")
}
}
}
}

Datei anzeigen

@ -19,17 +19,28 @@
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.application.*
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.*
import java.security.MessageDigest
import java.util.Base64
import kotlin.time.Duration.Companion.seconds
data class Session(val name: String)
fun hashToken(token: String): String {
val md = MessageDigest.getInstance("SHA-512")
return Base64.getEncoder().encodeToString(md.digest(token.toByteArray()))
}
fun Application.configurePlugins() {
install(CORS) {
@ -44,19 +55,35 @@ fun Application.configurePlugins() {
anyHost()
allowXHttpMethodOverride()
}
install(SWAuth)
install(RateLimit) {
global {
rateLimiter(limit = 60, refillPeriod = 60.seconds)
}
}
authentication {
bearer("sw-auth") {
realm = "SteamWar API"
authenticate { call ->
val token = Token.get(hashToken(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: ${verified.token.owner.userName}, Token: ${verified.token.name}, ${it.request.httpMethod.value} ${it.request.uri}"
} else {
return@format "No Auth Header found: ${it.request.httpMethod.value} ${it.request.uri}"
"Unauthenticated Request: ${it.request.httpMethod.value} ${it.request.uri}"
}
}
}

Datei anzeigen

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

Datei anzeigen

@ -40,7 +40,7 @@ data class ResponseUser(val id: Int, val name: String, val uuid: String) {
constructor(user: SteamwarUser) : this(user.id, user.userName, user.uuid.toString())
}
fun Routing.configureDataRoutes() {
fun Route.configureDataRoutes() {
route("/data") {
get("/schematicTypes") {
val types = mutableListOf<SchematicType>()
@ -49,7 +49,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 })
}

Datei anzeigen

@ -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.*
@ -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) {

Datei anzeigen

@ -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) })
}

Datei anzeigen

@ -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) })
}

Datei anzeigen

@ -20,14 +20,17 @@
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()
}
}
}

Datei anzeigen

@ -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) })

Datei anzeigen

@ -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) {