mirror of
https://github.com/yairm210/Unciv.git
synced 2025-08-03 04:27:56 -04:00
Goodies before chat retention & UncivServer.jar
Chat Session Management fixes (#13735)
* add `String.isUUID()` and game `gameId` in Chat nullable * refactor `String.isUUID()` * use `java.util.UUID` for better validation * use `UUID` as `ChatStore` key * make `gameId` nullable only for responses * fix reversed logics * introduce `isValidPlayerUuid()` and `isValidGameUuid()` * fix `WebSocketSessionManager` session management * refactor & simplify `UncivServer.jar` * make `gameIdToChat` a `synchronizedMap` * make `checkAndReturnUuiId()` nullable * remove redundant imports * revert `IDChecker` changes
This commit is contained in:
parent
01a4025287
commit
95eb97d517
3
.gitignore
vendored
3
.gitignore
vendored
@ -166,6 +166,9 @@ music/
|
|||||||
SaveFiles/
|
SaveFiles/
|
||||||
scenarios/
|
scenarios/
|
||||||
|
|
||||||
|
# server artifacts
|
||||||
|
server.auth
|
||||||
|
MultiplayerFiles/
|
||||||
|
|
||||||
/.kotlin/errors/*
|
/.kotlin/errors/*
|
||||||
/.kotlin/sessions/*
|
/.kotlin/sessions/*
|
||||||
|
@ -2,11 +2,13 @@ package com.unciv.logic.multiplayer.chat
|
|||||||
|
|
||||||
import com.badlogic.gdx.Gdx
|
import com.badlogic.gdx.Gdx
|
||||||
import com.unciv.ui.screens.worldscreen.chat.ChatPopup
|
import com.unciv.ui.screens.worldscreen.chat.ChatPopup
|
||||||
|
import java.util.Collections.synchronizedMap
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
data class Chat(
|
data class Chat(
|
||||||
val gameId: String,
|
val gameId: UUID,
|
||||||
) {
|
) {
|
||||||
// <civName, message> pairs
|
// <civName, message> pairs
|
||||||
private val messages: MutableList<Pair<String, String>> = mutableListOf(INITIAL_MESSAGE)
|
private val messages: MutableList<Pair<String, String>> = mutableListOf(INITIAL_MESSAGE)
|
||||||
@ -22,7 +24,7 @@ data class Chat(
|
|||||||
*/
|
*/
|
||||||
fun requestMessageSend(civName: String, message: String) {
|
fun requestMessageSend(civName: String, message: String) {
|
||||||
Gdx.app.postRunnable {
|
Gdx.app.postRunnable {
|
||||||
ChatWebSocket.requestMessageSend(Message.Chat(gameId, civName, message))
|
ChatWebSocket.requestMessageSend(Message.Chat(civName, message, gameId.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,16 +52,17 @@ object ChatStore {
|
|||||||
*/
|
*/
|
||||||
var chatPopup: ChatPopup? = null
|
var chatPopup: ChatPopup? = null
|
||||||
|
|
||||||
private var gameIdToChat = mutableMapOf<String, Chat>()
|
private var gameIdToChat: MutableMap<UUID, Chat> = synchronizedMap(mutableMapOf())
|
||||||
|
|
||||||
/** When no [ChatPopup] is open to receive these oddities, we keep them here.
|
/** When no [ChatPopup] is open to receive these oddities, we keep them here.
|
||||||
* Certainly better than not knowing why the socket closed.
|
* Certainly better than not knowing why the socket closed.
|
||||||
*/
|
*/
|
||||||
private var globalMessages: Queue<Pair<String, String>> = LinkedList()
|
private var globalMessages: Queue<Pair<String, String>> = LinkedList()
|
||||||
|
|
||||||
fun getChatByGameId(gameId: String) = gameIdToChat.getOrPut(gameId) { Chat(gameId) }
|
fun getChatByGameId(gameId: UUID): Chat = gameIdToChat.getOrPut(gameId) { Chat(gameId) }
|
||||||
|
fun getChatByGameId(gameId: String): Chat = getChatByGameId(UUID.fromString(gameId))
|
||||||
|
|
||||||
fun getGameIds() = gameIdToChat.keys.toSet()
|
fun getGameIds() = gameIdToChat.keys.map { uuid -> uuid.toString() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears chat by triggering a garbage collection.
|
* Clears chat by triggering a garbage collection.
|
||||||
@ -71,12 +74,21 @@ object ChatStore {
|
|||||||
|
|
||||||
fun relayChatMessage(chat: Response.Chat) {
|
fun relayChatMessage(chat: Response.Chat) {
|
||||||
Gdx.app.postRunnable {
|
Gdx.app.postRunnable {
|
||||||
if (chat.gameId.isNotEmpty()) {
|
if (chat.gameId == null || chat.gameId.isBlank()) {
|
||||||
getChatByGameId(chat.gameId).addMessage(chat.civName, chat.message)
|
relayGlobalMessage(chat.message, chat.civName)
|
||||||
if (chatPopup?.chat?.gameId == chat.gameId) {
|
} else {
|
||||||
|
val gameId = try {
|
||||||
|
UUID.fromString(chat.gameId)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// Discard messages with invalid UUID
|
||||||
|
return@postRunnable
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatByGameId(gameId).addMessage(chat.civName, chat.message)
|
||||||
|
if (chatPopup?.chat?.gameId == gameId) {
|
||||||
chatPopup?.addMessage(chat.civName, chat.message)
|
chatPopup?.addMessage(chat.civName, chat.message)
|
||||||
}
|
}
|
||||||
} else relayGlobalMessage(chat.message, chat.civName)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ sealed class Message {
|
|||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("chat")
|
@SerialName("chat")
|
||||||
data class Chat(
|
data class Chat(
|
||||||
val gameId: String, val civName: String, val message: String
|
val civName: String, val message: String, val gameId: String
|
||||||
) : Message()
|
) : Message()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -57,7 +57,7 @@ sealed class Response {
|
|||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("chat")
|
@SerialName("chat")
|
||||||
data class Chat(
|
data class Chat(
|
||||||
val gameId: String, val civName: String, val message: String
|
val civName: String, val message: String, val gameId: String? = null
|
||||||
) : Response()
|
) : Response()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -22,7 +22,11 @@ import com.unciv.models.metadata.ModCategories
|
|||||||
import com.unciv.models.translations.TranslationFileWriter
|
import com.unciv.models.translations.TranslationFileWriter
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
|
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
|
||||||
import com.unciv.ui.components.extensions.*
|
import com.unciv.ui.components.extensions.addSeparator
|
||||||
|
import com.unciv.ui.components.extensions.disable
|
||||||
|
import com.unciv.ui.components.extensions.setFontColor
|
||||||
|
import com.unciv.ui.components.extensions.toLabel
|
||||||
|
import com.unciv.ui.components.extensions.toTextButton
|
||||||
import com.unciv.ui.components.fonts.FontFamilyData
|
import com.unciv.ui.components.fonts.FontFamilyData
|
||||||
import com.unciv.ui.components.fonts.Fonts
|
import com.unciv.ui.components.fonts.Fonts
|
||||||
import com.unciv.ui.components.input.keyShortcuts
|
import com.unciv.ui.components.input.keyShortcuts
|
||||||
@ -34,12 +38,12 @@ import com.unciv.ui.popups.ConfirmPopup
|
|||||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||||
import com.unciv.utils.Concurrency
|
import com.unciv.utils.Concurrency
|
||||||
import com.unciv.utils.Display
|
import com.unciv.utils.Display
|
||||||
|
import com.unciv.utils.isUUID
|
||||||
import com.unciv.utils.launchOnGLThread
|
import com.unciv.utils.launchOnGLThread
|
||||||
import com.unciv.utils.withoutItem
|
import com.unciv.utils.withoutItem
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
|
||||||
import java.util.zip.Deflater
|
import java.util.zip.Deflater
|
||||||
|
|
||||||
class AdvancedTab(
|
class AdvancedTab(
|
||||||
@ -345,25 +349,23 @@ class AdvancedTab(
|
|||||||
|
|
||||||
private fun addSetUserId() {
|
private fun addSetUserId() {
|
||||||
val idSetLabel = "".toLabel()
|
val idSetLabel = "".toLabel()
|
||||||
val takeUserIdFromClipboardButton = "Take user ID from clipboard".toTextButton()
|
val takeUserIdFromClipboardButton = "Take user ID from clipboard".toTextButton().onClick {
|
||||||
.onClick {
|
val clipboardContents = Gdx.app.clipboard.contents.trim()
|
||||||
try {
|
if (clipboardContents.isUUID()) {
|
||||||
val clipboardContents = Gdx.app.clipboard.contents.trim()
|
ConfirmPopup(
|
||||||
UUID.fromString(clipboardContents)
|
stage,
|
||||||
ConfirmPopup(
|
"Doing this will reset your current user ID to the clipboard contents - are you sure?",
|
||||||
stage,
|
"Take user ID from clipboard"
|
||||||
"Doing this will reset your current user ID to the clipboard contents - are you sure?",
|
) {
|
||||||
"Take user ID from clipboard"
|
settings.multiplayer.setUserId(clipboardContents)
|
||||||
) {
|
idSetLabel.setFontColor(Color.WHITE).setText("ID successfully set!".tr())
|
||||||
settings.multiplayer.setUserId(clipboardContents)
|
}.open(true)
|
||||||
idSetLabel.setFontColor(Color.WHITE).setText("ID successfully set!".tr())
|
idSetLabel.isVisible = true
|
||||||
}.open(true)
|
} else {
|
||||||
idSetLabel.isVisible = true
|
idSetLabel.isVisible = true
|
||||||
} catch (_: Exception) {
|
idSetLabel.setFontColor(Color.RED).setText("Invalid ID!".tr())
|
||||||
idSetLabel.isVisible = true
|
|
||||||
idSetLabel.setFontColor(Color.RED).setText("Invalid ID!".tr())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
add(takeUserIdFromClipboardButton).pad(5f).colspan(2).row()
|
add(takeUserIdFromClipboardButton).pad(5f).colspan(2).row()
|
||||||
add(idSetLabel).colspan(2).row()
|
add(idSetLabel).colspan(2).row()
|
||||||
}
|
}
|
||||||
|
13
core/src/com/unciv/utils/StringExtensions.kt
Normal file
13
core/src/com/unciv/utils/StringExtensions.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.unciv.utils
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a [String] is a valid UUID
|
||||||
|
*/
|
||||||
|
fun String.isUUID(): Boolean = try {
|
||||||
|
UUID.fromString(this)
|
||||||
|
true
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
false
|
||||||
|
}
|
@ -49,7 +49,7 @@ sealed class Message {
|
|||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("chat")
|
@SerialName("chat")
|
||||||
data class Chat(
|
data class Chat(
|
||||||
val gameId: String, val civName: String, val message: String
|
val civName: String, val message: String, val gameId: String
|
||||||
) : Message()
|
) : Message()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -71,7 +71,7 @@ sealed class Response {
|
|||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("chat")
|
@SerialName("chat")
|
||||||
data class Chat(
|
data class Chat(
|
||||||
val gameId: String, val civName: String, val message: String
|
val civName: String, val message: String, val gameId: String? = null
|
||||||
) : Response()
|
) : Response()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -87,37 +87,34 @@ sealed class Response {
|
|||||||
) : Response()
|
) : Response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
private class WebSocketSessionManager {
|
private class WebSocketSessionManager {
|
||||||
private val userId2GameIds = synchronizedMap(mutableMapOf<String, MutableSet<String>>())
|
private val gameId2WSSessions = synchronizedMap(mutableMapOf<Uuid, MutableSet<DefaultWebSocketServerSession>>())
|
||||||
|
private val wsSession2GameIds = synchronizedMap(mutableMapOf<DefaultWebSocketServerSession, MutableSet<Uuid>>())
|
||||||
|
|
||||||
private val gameId2WSSessions =
|
fun isSubscribed(session: DefaultWebSocketServerSession, gameId: Uuid): Boolean =
|
||||||
synchronizedMap(mutableMapOf<String, MutableSet<DefaultWebSocketServerSession>>())
|
gameId2WSSessions.getOrPut(gameId) { synchronizedSet(mutableSetOf()) }.contains(session)
|
||||||
|
|
||||||
fun removeSession(userId: String, session: DefaultWebSocketServerSession) {
|
fun subscribe(session: DefaultWebSocketServerSession, gameIds: List<String>): List<String> {
|
||||||
val gameIds = userId2GameIds.remove(userId)
|
val uuids = gameIds.mapNotNull { it.toUuidOrNull() }
|
||||||
for (gameId in gameIds ?: emptyList()) {
|
|
||||||
gameId2WSSessions[gameId]?.remove(session)
|
wsSession2GameIds.getOrPut(session) { synchronizedSet(mutableSetOf()) }.addAll(uuids)
|
||||||
|
for (uuid in uuids) {
|
||||||
|
gameId2WSSessions.getOrPut(uuid) { synchronizedSet(mutableSetOf()) }.add(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuids.map { it.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unsubscribe(session: DefaultWebSocketServerSession, gameIds: List<String>) {
|
||||||
|
val uuids = gameIds.mapNotNull { it.toUuidOrNull() }
|
||||||
|
wsSession2GameIds[session]?.removeAll(uuids)
|
||||||
|
for (uuid in uuids) {
|
||||||
|
gameId2WSSessions[uuid]?.remove(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe(userId: String, gameIds: List<String>, session: DefaultWebSocketServerSession) {
|
suspend fun publish(gameId: Uuid, message: Response) {
|
||||||
userId2GameIds.getOrPut(userId) { synchronizedSet(mutableSetOf()) }.addAll(gameIds)
|
|
||||||
for (gameId in gameIds) {
|
|
||||||
gameId2WSSessions.getOrPut(gameId) { synchronizedSet(mutableSetOf()) }.add(session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unsubscribe(userId: String, gameIds: List<String>, session: DefaultWebSocketServerSession) {
|
|
||||||
userId2GameIds[userId]?.removeAll(gameIds)
|
|
||||||
for (gameId in gameIds) {
|
|
||||||
gameId2WSSessions[gameId]?.remove(session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasGameId(userId: String, gameId: String) =
|
|
||||||
userId2GameIds.getOrPut(userId) { synchronizedSet(mutableSetOf()) }.contains(gameId)
|
|
||||||
|
|
||||||
suspend fun publish(gameId: String, message: Response) {
|
|
||||||
val sessions = gameId2WSSessions.getOrPut(gameId) { synchronizedSet(mutableSetOf()) }
|
val sessions = gameId2WSSessions.getOrPut(gameId) { synchronizedSet(mutableSetOf()) }
|
||||||
for (session in sessions) {
|
for (session in sessions) {
|
||||||
if (!session.isActive) {
|
if (!session.isActive) {
|
||||||
@ -127,14 +124,34 @@ private class WebSocketSessionManager {
|
|||||||
session.sendSerialized(message)
|
session.sendSerialized(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cleanupSession(session: DefaultWebSocketServerSession) {
|
||||||
|
for (gameId in wsSession2GameIds.remove(session) ?: emptyList()) {
|
||||||
|
val gameIds = gameId2WSSessions[gameId] ?: continue
|
||||||
|
gameIds.remove(session)
|
||||||
|
if (gameIds.isEmpty()) {
|
||||||
|
gameId2WSSessions.remove(gameId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
data class BasicAuthInfo(
|
data class BasicAuthInfo(
|
||||||
val userId: String,
|
val userId: Uuid,
|
||||||
val password: String,
|
val password: String,
|
||||||
val isValidUUID: Boolean = false
|
|
||||||
) : Principal
|
) : Principal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a [String] is a valid UUID
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
private fun String.toUuidOrNull() = try {
|
||||||
|
Uuid.parse(this)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
private class UncivServerRunner : CliktCommand() {
|
private class UncivServerRunner : CliktCommand() {
|
||||||
private val port by option(
|
private val port by option(
|
||||||
"-p", "-port",
|
"-p", "-port",
|
||||||
@ -177,10 +194,12 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// region Auth
|
// region Auth
|
||||||
private val authMap: MutableMap<String, String> = mutableMapOf()
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
private val authMap: MutableMap<Uuid, String> = mutableMapOf()
|
||||||
|
|
||||||
private val wsSessionManager = WebSocketSessionManager()
|
private val wsSessionManager = WebSocketSessionManager()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
private fun loadAuthFile() {
|
private fun loadAuthFile() {
|
||||||
val authFile = File("server.auth")
|
val authFile = File("server.auth")
|
||||||
if (!authFile.exists()) {
|
if (!authFile.exists()) {
|
||||||
@ -188,11 +207,13 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
authFile.createNewFile()
|
authFile.createNewFile()
|
||||||
} else {
|
} else {
|
||||||
authMap.putAll(
|
authMap.putAll(
|
||||||
authFile.readLines().map { it.split(":") }.associate { it[0] to it[1] }
|
authFile.readLines().map { it.split(":") }
|
||||||
|
.associate { Uuid.parse(it[0]) to it[1] }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
private fun saveAuthFile() {
|
private fun saveAuthFile() {
|
||||||
val authFile = File("server.auth")
|
val authFile = File("server.auth")
|
||||||
authFile.writeText(authMap.map { "${it.key}:${it.value}" }.joinToString("\n"))
|
authFile.writeText(authMap.map { "${it.key}:${it.value}" }.joinToString("\n"))
|
||||||
@ -212,10 +233,9 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun validateAuth(authInfo: BasicAuthInfo): Boolean {
|
private fun validateAuth(authInfo: BasicAuthInfo): Boolean {
|
||||||
if (!authV1Enabled)
|
if (!authV1Enabled) return true
|
||||||
return true
|
|
||||||
|
|
||||||
val password = authMap[authInfo.userId]
|
@OptIn(ExperimentalUuidApi::class) val password = authMap[authInfo.userId]
|
||||||
return password == null || password == authInfo.password
|
return password == null || password == authInfo.password
|
||||||
}
|
}
|
||||||
// endregion Auth
|
// endregion Auth
|
||||||
@ -233,15 +253,12 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
basic {
|
basic {
|
||||||
realm = "Optional for /files and /auth, Mandatory for /chat"
|
realm = "Optional for /files and /auth, Mandatory for /chat"
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class) validate {
|
||||||
validate { creds ->
|
return@validate try {
|
||||||
val isValidUUID = try {
|
BasicAuthInfo(userId = Uuid.parse(it.name), password = it.password)
|
||||||
Uuid.parse(creds.name)
|
|
||||||
true
|
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
false
|
null
|
||||||
}
|
}
|
||||||
BasicAuthInfo(creds.name, creds.password, isValidUUID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,21 +282,16 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
call.respond(isAliveInfo)
|
call.respond(isAliveInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate {
|
@OptIn(ExperimentalUuidApi::class) authenticate {
|
||||||
put("/files/{fileName}") {
|
put("/files/{fileName}") {
|
||||||
val fileName = call.parameters["fileName"] ?: return@put call.respond(
|
val fileName = call.parameters["fileName"] ?: return@put call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest, "Missing filename!"
|
||||||
"Missing filename!"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val authInfo = call.principal<BasicAuthInfo>() ?: return@put call.respond(
|
val authInfo = call.principal<BasicAuthInfo>() ?: return@put call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest, "Possibly malformed authentication header!"
|
||||||
"Possibly malformed authentication header!"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!authInfo.isValidUUID)
|
|
||||||
return@put call.respond(HttpStatusCode.BadRequest, "Bad userId")
|
|
||||||
|
|
||||||
// If IdentifyOperators is enabled an Operator IP is displayed
|
// If IdentifyOperators is enabled an Operator IP is displayed
|
||||||
if (identifyOperators) {
|
if (identifyOperators) {
|
||||||
call.application.log.info("Receiving file: $fileName --Operation sourced from ${call.request.local.remoteHost}")
|
call.application.log.info("Receiving file: $fileName --Operation sourced from ${call.request.local.remoteHost}")
|
||||||
@ -288,8 +300,7 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val file = File(fileFolderName, fileName)
|
val file = File(fileFolderName, fileName)
|
||||||
if (!validateGameAccess(file, authInfo))
|
if (!validateGameAccess(file, authInfo)) return@put call.respond(HttpStatusCode.Unauthorized)
|
||||||
return@put call.respond(HttpStatusCode.Unauthorized)
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
file.outputStream().use {
|
file.outputStream().use {
|
||||||
@ -300,8 +311,7 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
}
|
}
|
||||||
get("/files/{fileName}") {
|
get("/files/{fileName}") {
|
||||||
val fileName = call.parameters["fileName"] ?: return@get call.respond(
|
val fileName = call.parameters["fileName"] ?: return@get call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest, "Missing filename!"
|
||||||
"Missing filename!"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// If IdentifyOperators is enabled an Operator IP is displayed
|
// If IdentifyOperators is enabled an Operator IP is displayed
|
||||||
@ -330,14 +340,9 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
get("/auth") {
|
get("/auth") {
|
||||||
call.application.log.info("Received auth request from ${call.request.local.remoteHost}")
|
call.application.log.info("Received auth request from ${call.request.local.remoteHost}")
|
||||||
|
|
||||||
val authInfo =
|
val authInfo = call.principal<BasicAuthInfo>() ?: return@get call.respond(
|
||||||
call.principal<BasicAuthInfo>() ?: return@get call.respond(
|
HttpStatusCode.BadRequest, "Possibly malformed authentication header!"
|
||||||
HttpStatusCode.BadRequest,
|
)
|
||||||
"Possibly malformed authentication header!"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!authInfo.isValidUUID)
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, "Bad userId")
|
|
||||||
|
|
||||||
when (authMap[authInfo.userId]) {
|
when (authMap[authInfo.userId]) {
|
||||||
null -> call.respond(HttpStatusCode.NoContent)
|
null -> call.respond(HttpStatusCode.NoContent)
|
||||||
@ -348,23 +353,16 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
put("/auth") {
|
put("/auth") {
|
||||||
call.application.log.info("Received auth password set from ${call.request.local.remoteHost}")
|
call.application.log.info("Received auth password set from ${call.request.local.remoteHost}")
|
||||||
|
|
||||||
val authInfo =
|
val authInfo = call.principal<BasicAuthInfo>() ?: return@put call.respond(
|
||||||
call.principal<BasicAuthInfo>() ?: return@put call.respond(
|
HttpStatusCode.BadRequest, "Possibly malformed authentication header!"
|
||||||
HttpStatusCode.BadRequest,
|
)
|
||||||
"Possibly malformed authentication header!"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!authInfo.isValidUUID)
|
|
||||||
return@put call.respond(HttpStatusCode.BadRequest, "Bad userId")
|
|
||||||
|
|
||||||
val password = authMap[authInfo.userId]
|
val password = authMap[authInfo.userId]
|
||||||
if (password == null || password == authInfo.password) {
|
if (password == null || password == authInfo.password) {
|
||||||
val newPassword = call.receiveText()
|
val newPassword = call.receiveText()
|
||||||
if (newPassword.length < 6)
|
if (newPassword.length < 6) return@put call.respond(
|
||||||
return@put call.respond(
|
HttpStatusCode.BadRequest, "Password should be at least 6 characters long"
|
||||||
HttpStatusCode.BadRequest,
|
)
|
||||||
"Password should be at least 6 characters long"
|
|
||||||
)
|
|
||||||
authMap[authInfo.userId] = newPassword
|
authMap[authInfo.userId] = newPassword
|
||||||
call.respond(HttpStatusCode.OK)
|
call.respond(HttpStatusCode.OK)
|
||||||
} else {
|
} else {
|
||||||
@ -386,19 +384,28 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
return@webSocket close()
|
return@webSocket close()
|
||||||
}
|
}
|
||||||
|
|
||||||
val userId = authInfo.userId
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (isActive) {
|
||||||
val message = receiveDeserialized<Message>()
|
val message = receiveDeserialized<Message>()
|
||||||
when (message) {
|
when (message) {
|
||||||
is Message.Chat -> {
|
is Message.Chat -> {
|
||||||
if (wsSessionManager.hasGameId(userId, message.gameId)) {
|
val gameId = message.gameId.toUuidOrNull()
|
||||||
wsSessionManager.publish(
|
if (gameId == null) {
|
||||||
message.gameId,
|
sendSerialized(
|
||||||
Response.Chat(
|
Response.Chat(
|
||||||
message.gameId,
|
civName = "Server",
|
||||||
message.civName,
|
message = "Invalid gameId: '${message.gameId}'. Cannot relay the message!",
|
||||||
message.message
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wsSessionManager.isSubscribed(this, gameId)) {
|
||||||
|
wsSessionManager.publish(
|
||||||
|
gameId = gameId, message = Response.Chat(
|
||||||
|
civName = message.civName,
|
||||||
|
message = message.message,
|
||||||
|
gameId = message.gameId,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -407,21 +414,25 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is Message.Join -> {
|
is Message.Join -> {
|
||||||
wsSessionManager.subscribe(userId, message.gameIds, this)
|
sendSerialized(
|
||||||
sendSerialized(Response.JoinSuccess(gameIds = message.gameIds))
|
Response.JoinSuccess(
|
||||||
|
gameIds = wsSessionManager.subscribe(
|
||||||
|
this, message.gameIds
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Message.Leave ->
|
is Message.Leave -> wsSessionManager.unsubscribe(this, message.gameIds)
|
||||||
wsSessionManager.unsubscribe(userId, message.gameIds, this)
|
|
||||||
}
|
}
|
||||||
yield()
|
yield()
|
||||||
}
|
}
|
||||||
} catch (err: Throwable) {
|
} catch (err: Throwable) {
|
||||||
println("An WebSocket session closed due to ${err.message}")
|
println("An WebSocket session closed due to ${err.message}")
|
||||||
wsSessionManager.removeSession(userId, this)
|
wsSessionManager.cleanupSession(this)
|
||||||
} finally {
|
} finally {
|
||||||
println("An WebSocket session closed normally.")
|
println("An WebSocket session closed normally.")
|
||||||
wsSessionManager.removeSession(userId, this)
|
wsSessionManager.cleanupSession(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user