diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index f2473b586d..3c020a068e 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -727,7 +727,11 @@ Password must be at least 6 characters long = Failed to set password! = Password set successfully for server [serverURL] = Password = -Your userId is password secured = +Validating your authentication status... = +Your current password was rejected from the server = +You userId is unregistered! Set password to secure your userId = +Your current password has been succesfully verified = +Your authentication status could not be determined = Set a password to secure your userId = Authenticate = This server does not support authentication = diff --git a/core/src/com/unciv/logic/multiplayer/storage/ApiV2FileStorageEmulator.kt b/core/src/com/unciv/logic/multiplayer/storage/ApiV2FileStorageEmulator.kt index 958ae8e5c3..a8b25737dd 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/ApiV2FileStorageEmulator.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/ApiV2FileStorageEmulator.kt @@ -85,6 +85,10 @@ class ApiV2FileStorageEmulator(private val api: ApiV2) : FileStorage { return runBlocking { api.auth.loginOnly(userId, password) } } + override fun checkAuthStatus(userId: String, password: String): AuthStatus { + TODO("Not yet implemented") + } + override fun setPassword(newPassword: String): Boolean { return runBlocking { api.account.setPassword(newPassword, suppress = true) } } diff --git a/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt b/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt index 117311b82c..4c9a7f6e33 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt @@ -107,6 +107,10 @@ object DropBox: FileStorage { throw NotImplementedError() } + override fun checkAuthStatus(userId: String, password: String): AuthStatus { + throw NotImplementedError() + } + override fun setPassword(newPassword: String): Boolean { throw NotImplementedError() } diff --git a/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt b/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt index 4ee74f0586..ac33347e46 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt @@ -13,6 +13,13 @@ interface FileMetaData { fun getLastModified(): Date? } +enum class AuthStatus { + UNAUTHORIZED, + UNREGISTERED, + VERIFIED, + UNKNOWN +} + interface FileStorage { /** * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time @@ -45,4 +52,6 @@ interface FileStorage { * @throws MultiplayerAuthException if the authentication failed */ fun setPassword(newPassword: String): Boolean + + fun checkAuthStatus(userId: String, password: String): AuthStatus } diff --git a/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt b/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt index f823fd0d6d..d84fbdb0d2 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt @@ -76,6 +76,21 @@ object UncivServerFileStorage : FileStorage { return authenticated } + override fun checkAuthStatus(userId: String, password: String): AuthStatus { + var authStatus = AuthStatus.UNKNOWN + val preEncodedAuthValue = "$userId:$password" + authHeader = mapOf("Authorization" to "Basic ${Base64Coder.encodeString(preEncodedAuthValue)}") + SimpleHttp.sendGetRequest("$serverUrl/auth", timeout = timeout, header = authHeader) { _, _, code -> + authStatus = when (code) { + 200 -> AuthStatus.VERIFIED + 204 -> AuthStatus.UNREGISTERED + 401 -> AuthStatus.UNAUTHORIZED + else -> AuthStatus.UNKNOWN + } + } + return authStatus + } + override fun setPassword(newPassword: String): Boolean { if (authHeader == null) return false diff --git a/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt b/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt index a76c766300..24b7b5cfef 100644 --- a/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt +++ b/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt @@ -4,23 +4,23 @@ import com.badlogic.gdx.Application import com.badlogic.gdx.Gdx import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.TextField -import com.unciv.UncivGame import com.unciv.Constants +import com.unciv.UncivGame import com.unciv.logic.files.IMediaFinder import com.unciv.logic.multiplayer.Multiplayer +import com.unciv.logic.multiplayer.storage.AuthStatus import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.MultiplayerAuthException import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings.GameSetting -import com.unciv.ui.components.widgets.UncivTextField import com.unciv.ui.components.extensions.addSeparator -import com.unciv.ui.components.extensions.brighten import com.unciv.ui.components.extensions.format import com.unciv.ui.components.extensions.isEnabled import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.components.input.onChange import com.unciv.ui.components.input.onClick +import com.unciv.ui.components.widgets.UncivTextField import com.unciv.ui.popups.AuthPopup import com.unciv.ui.popups.Popup import com.unciv.ui.popups.options.SettingsSelect.SelectItem @@ -163,14 +163,34 @@ private fun addMultiplayerServerOptions( serverIpTable.add("Set password".toLabel()).padTop(16f).colspan(2).row() serverIpTable.add(passwordTextField).colspan(2).growX().padBottom(8f).row() + // initially assume no password + val authStatusLabel = "Set a password to secure your userId".toLabel() + + if (settings.multiplayer.passwords.containsKey(settings.multiplayer.server)) { + val userId = settings.multiplayer.userId + val password = settings.multiplayer.passwords[settings.multiplayer.server] ?: "" + + + authStatusLabel.setText("Validating your authentication status...") + Concurrency.run { + val authStatus = UncivGame.Current.onlineMultiplayer.multiplayerServer.fileStorage() + .checkAuthStatus(userId, password) + + val newAuthStatusText = when (authStatus) { + AuthStatus.UNAUTHORIZED -> "Your current password was rejected from the server" + AuthStatus.UNREGISTERED -> "You userId is unregistered! Set password to secure your userId" + AuthStatus.VERIFIED -> "Your current password has been succesfully verified" + AuthStatus.UNKNOWN -> "Your authentication status could not be determined" + } + + Concurrency.runOnGLThread { + authStatusLabel.setText(newAuthStatusText) + } + } + } + val passwordStatusTable = Table().apply { - add( - if (settings.multiplayer.passwords.containsKey(settings.multiplayer.server)) { - "Your userId is password secured" - } else { - "Set a password to secure your userId" - }.toLabel() - ) + add(authStatusLabel) add(setPasswordButton.onClick { setPassword(passwordTextField.text, optionsPopup) }).padLeft(16f) diff --git a/server/src/com/unciv/app/server/UncivServer.kt b/server/src/com/unciv/app/server/UncivServer.kt index ae15891513..8f9fd4f509 100644 --- a/server/src/com/unciv/app/server/UncivServer.kt +++ b/server/src/com/unciv/app/server/UncivServer.kt @@ -50,7 +50,7 @@ private class UncivServerRunner : CliktCommand() { help = "Enable Authentication" ).flag("-no-auth", default = false) - private val IdentifyOperators by option( + private val identifyOperators by option( "-i", "-Identify", envvar = "UncivServerIdentify", help = "Display each operation archive request IP to assist management personnel" @@ -99,20 +99,18 @@ private class UncivServerRunner : CliktCommand() { return true val (userId, password) = extractAuth(authString) ?: return false - if (authMap[userId] == null || authMap[userId] == password) - return true - return false + return authMap[userId] == null || authMap[userId] == password } - private fun extractAuth(authString: String?): Pair? { + private fun extractAuth(authHeader: String?): Pair? { if (!authV1Enabled) return null - // If auth is enabled a auth string is required - if (authString == null || !authString.startsWith("Basic ")) + // If auth is enabled an authorization header is required + if (authHeader == null || !authHeader.startsWith("Basic ")) return null - val decodedString = String(Base64.getDecoder().decode(authString.drop(6))) + val decodedString = String(Base64.getDecoder().decode(authHeader.drop(6))) val splitAuthString = decodedString.split(":", limit=2) if (splitAuthString.size != 2) return null @@ -136,11 +134,11 @@ private class UncivServerRunner : CliktCommand() { put("/files/{fileName}") { val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!") - // If IdentifyOperators is enabled a Operator IP is displayed - if (IdentifyOperators) { - log.info("Receiving file: ${fileName} --Operation sourced from ${call.request.local.remoteHost}") + // If IdentifyOperators is enabled an Operator IP is displayed + if (identifyOperators) { + log.info("Receiving file: $fileName --Operation sourced from ${call.request.local.remoteHost}") }else{ - log.info("Receiving file: ${fileName}") + log.info("Receiving file: $fileName") } val file = File(fileFolderName, fileName) @@ -160,20 +158,20 @@ private class UncivServerRunner : CliktCommand() { get("/files/{fileName}") { val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!") - // If IdentifyOperators is enabled a Operator IP is displayed - if (IdentifyOperators) { - log.info("File requested: ${fileName} --Operation sourced from ${call.request.local.remoteHost}") + // If IdentifyOperators is enabled an Operator IP is displayed + if (identifyOperators) { + log.info("File requested: $fileName --Operation sourced from ${call.request.local.remoteHost}") }else{ log.info("File requested: $fileName") } val file = File(fileFolderName, fileName) if (!file.exists()) { - - // If IdentifyOperators is enabled a Operator IP is displayed - if (IdentifyOperators) { - log.info("File ${fileName} not found --Operation sourced from ${call.request.local.remoteHost}") - }else{ + + // If IdentifyOperators is enabled an Operator IP is displayed + if (identifyOperators) { + log.info("File $fileName not found --Operation sourced from ${call.request.local.remoteHost}") + } else { log.info("File $fileName not found") } call.respond(HttpStatusCode.NotFound, "File does not exist") @@ -186,11 +184,21 @@ private class UncivServerRunner : CliktCommand() { if (authV1Enabled) { get("/auth") { log.info("Received auth request from ${call.request.local.remoteHost}") - val authHeader = call.request.headers["Authorization"] - if (validateAuth(authHeader)) { - call.respond(HttpStatusCode.OK) - } else { - call.respond(HttpStatusCode.Unauthorized) + + val authHeader = call.request.headers["Authorization"] ?: run { + call.respond(HttpStatusCode.BadRequest, "Missing authorization header!") + return@get + } + + val (userId, password) = extractAuth(authHeader) ?: run { + call.respond(HttpStatusCode.BadRequest, "Malformed authorization header!") + return@get + } + + when (authMap[userId]) { + null -> call.respond(HttpStatusCode.NoContent) + password -> call.respond(HttpStatusCode.OK) + else -> call.respond(HttpStatusCode.Unauthorized) } } put("/auth") {