Skip to content

Commit

Permalink
Use token generated at first launch for authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
DRSchlaubi committed Feb 20, 2025
1 parent 1dac317 commit 1df0678
Show file tree
Hide file tree
Showing 18 changed files with 236 additions and 129 deletions.
4 changes: 2 additions & 2 deletions client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
alias(libs.plugins.compose)
}

version = "1.1.0"
version = "1.2.0"

repositories {
mavenCentral()
Expand All @@ -20,7 +20,7 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.jnativehook)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.websockets)
implementation(libs.ktor.client.resources)
implementation(libs.ktor.client.content.negotiation)
Expand Down
2 changes: 1 addition & 1 deletion client/rules.pro
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ktor
-keepclassmembers class io.ktor.** { volatile <fields>; }
-keep class io.ktor.client.engine.cio.CIOEngineContainer
-keep class io.ktor.client.engine.okhttp.OkHttpEngineContainer
-keep class io.ktor.serialization.kotlinx.json.KotlinxSerializationJsonExtensionProvider

# SLF4j
Expand Down
39 changes: 27 additions & 12 deletions client/src/main/kotlin/core/APIClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import androidx.compose.runtime.setValue
import dev.kord.gateway.retry.LinearRetry
import dev.schlaubi.gtakiller.common.*
import dev.schlaubi.mastermind.core.settings.settings
import dev.schlaubi.mastermind.core.settings.writeSettings
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.resources.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.*
import io.ktor.serialization.kotlinx.*
Expand Down Expand Up @@ -46,6 +48,8 @@ class APIClient(val url: Url) : CoroutineScope {

defaultRequest {
url.takeFrom(this@APIClient.url)
val token = settings.tokens[url.build().hostWithPort]
token?.let { bearerAuth(it) }
}
}

Expand All @@ -56,6 +60,9 @@ class APIClient(val url: Url) : CoroutineScope {

suspend fun connectToWebSocket(isRetry: Boolean = false) {
webSocketSession?.close()
if (url.hostWithPort !in settings.tokens) {
writeSettings(settings.copy(tokens = settings.tokens + (url.hostWithPort to register().token)))
}
val session = try {
client.webSocketSession {
url {
Expand All @@ -69,6 +76,7 @@ class APIClient(val url: Url) : CoroutineScope {
client.href(Route.Events(), this)
}

bearerAuth(settings.tokens[this@APIClient.url.hostWithPort]!!)
headers.append(HttpHeaders.Username, settings.userName)
}
} catch (e: Exception) {
Expand Down Expand Up @@ -99,22 +107,29 @@ class APIClient(val url: Url) : CoroutineScope {

suspend fun getCurrentStatus() = client.get(Route.Status()).body<Status>()

suspend fun sendEvent(event: Event) {
LOG.debug { "Sending event: $event" }
webSocketSession?.sendSerialized(event) ?: error("Not connected")
suspend fun register() = client.post(Route.SignUp()) {
contentType(ContentType.Application.Json)
setBody(UserCreateRequest(settings.userName))
}.body<JWTUser>()

suspend fun updateName(name: String) = client.patch(Route.Me()) {
contentType(ContentType.Application.Json)
setBody(UserUpdateRequest(name))
}.body<JWTUser>().also {
writeSettings(settings.copy(tokens = settings.tokens + (url.hostWithPort to it.token)))
}

fun disconnect() {
client.close()
webSocketSession?.cancel()
private suspend fun handleEvent(event: Event) {
when (event) {
is KillGtaEvent -> dev.schlaubi.mastermind.core.killGta()
else -> {}
}
}
}

suspend fun reportKillCommand() = safeApi.sendEvent(KillGtaEvent)
suspend fun killGta() = client.post(Route.Kill()).body<Unit>()

private suspend fun handleEvent(event: Event) {
when (event) {
is KillGtaEvent -> killGta()
else -> {}
fun disconnect() {
client.close()
webSocketSession?.cancel()
}
}
2 changes: 1 addition & 1 deletion client/src/main/kotlin/core/KeyBoardListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ fun registerKeyBoardListener() = GlobalScreen.addNativeKeyListener(object : Nati
})

suspend fun reportAndKill() {
reportKillCommand()
safeApi.killGta()
killGta()
}
3 changes: 2 additions & 1 deletion client/src/main/kotlin/core/settings/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ data class Settings(
val currentUrl: Url?,
val pastUrls: Set<Url>,
val userName: String,
val hotkey: Int = NativeKeyEvent.VC_F3
val hotkey: Int = NativeKeyEvent.VC_F3,
val tokens: Map<String, String> = emptyMap()
)

fun Settings.addServerOrMoveToTop(serverUrl: Url): Settings {
Expand Down
12 changes: 2 additions & 10 deletions client/src/main/kotlin/ui/components/settings/UserNameInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,8 @@ import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import dev.schlaubi.gtakiller.common.UpdateNameCommand
import dev.schlaubi.gtakiller.common.UpdateNameEvent
import dev.schlaubi.mastermind.core.safeApi
import dev.schlaubi.mastermind.core.settings.settings
Expand All @@ -41,9 +34,8 @@ fun UsernameInput() {
}, "Username", initialValue = settings.userName, enabled = !loading, onSubmit = { newValue ->
loading = true
scope.launch(Dispatchers.IO) {
safeApi.sendEvent(UpdateNameCommand(newValue))
safeApi.updateName(newValue)
writeSettings(settings.copy(userName = newValue))
loading = false
}
})
}
1 change: 1 addition & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dependencies {
api(libs.kotlinx.datetime)
api(libs.ktor.resources)
api(libs.ktor.http)
api(libs.ktor.serialization.kotlinx.json)
}
15 changes: 11 additions & 4 deletions common/src/main/kotlin/Events.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package dev.schlaubi.gtakiller.common

import io.ktor.util.reflect.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

val eventSerializer: (TypeInfo, Any) -> String =
{ info, event -> Json.encodeToString<Event>(event as Event) }
val eventDeserializer: (TypeInfo, String) -> Event =
{ info, event -> Json.decodeFromString<Event>(event) }

@Serializable
sealed interface Event
Expand All @@ -14,10 +21,10 @@ data object KillGtaEvent : Event
@Serializable
data class UpdateKillCounterEvent(val killCount: Int, val kill: Kill) : Event

@SerialName("update_name")
@Serializable
data class UpdateNameCommand(val name: String) : Event

@SerialName("name_updated")
@Serializable
data class UpdateNameEvent(val id: String, val name: String) : Event

@SerialName("ping")
@Serializable
data object PingEvent : Event
9 changes: 9 additions & 0 deletions common/src/main/kotlin/Routes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ class Route {

@Resource("status")
class Status

@Resource("sign-up")
class SignUp

@Resource("kill")
class Kill

@Resource("users/@me")
class Me
}

val HttpHeaders.Username get() = "X-Username"
12 changes: 12 additions & 0 deletions common/src/main/kotlin/UserRequests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.schlaubi.gtakiller.common

import kotlinx.serialization.Serializable

@Serializable
data class UserUpdateRequest(val name: String)

@Serializable
data class UserCreateRequest(val name: String)

@Serializable
data class JWTUser(val id: String, val name: String, val token: String)
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", v
ktor-resources = { group = "io.ktor", name = "ktor-resources", version.ref = "ktor" }
ktor-http = { group = "io.ktor", name = "ktor-http", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-client-resources = { group = "io.ktor", name = "ktor-client-resources", version.ref = "ktor" }
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
ktor-server-auth-jwt = { group = "io.ktor", name = "ktor-server-auth-jwt", version.ref = "ktor" }
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
ktor-server-forwarded-header = { group = "io.ktor", name = "ktor-server-forwarded-header", version.ref = "ktor" }
ktor-server-resources = { group = "io.ktor", name = "ktor-server-resources", version.ref = "ktor" }
Expand All @@ -35,6 +36,8 @@ logback-classic = { group = "ch.qos.logback", name = "logback-classic", version

kord-gateway = { group = "dev.kord", name = "kord-gateway", version = "0.15.0" }

jwt = { group = "com.auth0", name = "java-jwt", version = "4.5.0" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Expand Down
2 changes: 2 additions & 0 deletions server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ dependencies {
implementation(libs.kotlinx.io.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.websockets)
implementation(libs.ktor.server.auth.jwt)
implementation(libs.ktor.server.forwarded.header)
implementation(libs.ktor.server.resources)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.jwt)
implementation(libs.kotlin.logging)
implementation(libs.logback.classic)
}
Expand Down
50 changes: 50 additions & 0 deletions server/src/main/kotlin/Authentication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dev.schlaubi.gtakiller

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import dev.schlaubi.gtakiller.common.JWTUser
import dev.schlaubi.gtakiller.common.Route.SignUp
import dev.schlaubi.gtakiller.common.UserCreateRequest
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.resources.*
import io.ktor.server.response.*
import io.ktor.server.routing.Route
import io.ktor.util.*

private val algorithm = Algorithm.HMAC256("secret")

private val verifier = JWT.require(algorithm).build()

data class JWTUserData(val id: String, val name: String)

fun Application.installAuth() {
install(Authentication) {
jwt {
verifier { verifier }
validate { JWTUserData(it.payload.subject!!, it.getClaim("user", String::class)!!) }
}
}
}

val ApplicationCall.user: JWTUserData
get() {
val token = principal<JWTUserData>()!!

return token
}

fun createToken(name: String, id: String = generateNonce()): JWTUser {
val token = JWT.create().withSubject(id).withClaim("user", name).sign(algorithm)

return JWTUser(id, name, token)
}

fun Route.signUpRoute() {
post<SignUp> {
val (name) = call.receive<UserCreateRequest>()
call.respond(createToken(name))
}
}
45 changes: 45 additions & 0 deletions server/src/main/kotlin/EventServer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dev.schlaubi.gtakiller

import dev.schlaubi.gtakiller.common.Event
import dev.schlaubi.gtakiller.common.Route.Events
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.server.resources.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*

data class AuthenticatedServerWebSocketSession(val id: String, val session: DefaultWebSocketServerSession) :
DefaultWebSocketServerSession by session

private val sessions = mutableListOf<AuthenticatedServerWebSocketSession>()

private val LOG = KotlinLogging.logger { }

fun Route.eventHandler() = resource<Events> {
webSocket {
val (userId) = call.user
val session = AuthenticatedServerWebSocketSession(userId, this)
sessions += session

LOG.info { "User $userId opened WebSocket connection" }

for (frame in incoming) {
LOG.trace { "Received frame: $frame" }
}

LOG.info { "User $userId closed WebSocket connection" }
sessions -= session
}
}

suspend fun broadcastEvent(event: Event, predicate: (AuthenticatedServerWebSocketSession) -> Boolean = { true }) =
sessions.forEach {
if (predicate(it)) try {
LOG.trace { "Sending event $event to $it" }
it.sendSerialized(event)
} catch (e: Exception) {
LOG.warn(e) { "Error while sending event to $it" }
sessions -= it
}
}

suspend fun broadcastEventExceptForUser(event: Event, userId: String) = broadcastEvent(event) { it.id != userId }
35 changes: 35 additions & 0 deletions server/src/main/kotlin/KillGTARoute.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.schlaubi.gtakiller

import dev.schlaubi.gtakiller.common.KillGtaEvent
import dev.schlaubi.gtakiller.common.Route.Kill
import dev.schlaubi.gtakiller.common.UpdateKillCounterEvent
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.Route
import io.ktor.server.resources.post
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Duration.Companion.seconds
import dev.schlaubi.gtakiller.common.Kill as KillObject

private val lastKill = Instant.DISTANT_PAST

fun Route.killGTARoute() {
post<Kill> {
val (userId, name) = call.user

broadcastEventExceptForUser(KillGtaEvent, userId)

val killTime = Clock.System.now()
val timeSinceLastKill = killTime - lastKill
if (timeSinceLastKill > 10.seconds) {
val kill = KillObject(killTime, name, userId)

writeStats(stats.copy(kills = stats.kills + kill))

broadcastEvent(UpdateKillCounterEvent(stats.kills.size, kill))
}

call.respond(HttpStatusCode.Accepted)
}
}
Loading

0 comments on commit 1df0678

Please sign in to comment.