mirror of
https://github.com/yairm210/Unciv.git
synced 2025-08-03 20:48:49 -04:00
more detailed password authentication status (#13198)
* more detailed password authentication status * fix compile errors * resole some concerns * remove stray `Concurrency.run { }` * fix no space at end error * modify current behavior of UncivServer.jar * refactor code * accept idea suggestions * rename `authString` -> `authHeader` for clarity * send `Bad Request` instead of `Unauthorized` if `Authorization` header is not present * run `checkAuthStatus()` on a separate thread * fix no space at end test failure
This commit is contained in:
parent
a5a148cc51
commit
1bced0df7b
@ -727,7 +727,11 @@ Password must be at least 6 characters long =
|
|||||||
Failed to set password! =
|
Failed to set password! =
|
||||||
Password set successfully for server [serverURL] =
|
Password set successfully for server [serverURL] =
|
||||||
Password =
|
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 =
|
Set a password to secure your userId =
|
||||||
Authenticate =
|
Authenticate =
|
||||||
This server does not support authentication =
|
This server does not support authentication =
|
||||||
|
@ -85,6 +85,10 @@ class ApiV2FileStorageEmulator(private val api: ApiV2) : FileStorage {
|
|||||||
return runBlocking { api.auth.loginOnly(userId, password) }
|
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 {
|
override fun setPassword(newPassword: String): Boolean {
|
||||||
return runBlocking { api.account.setPassword(newPassword, suppress = true) }
|
return runBlocking { api.account.setPassword(newPassword, suppress = true) }
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,10 @@ object DropBox: FileStorage {
|
|||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun checkAuthStatus(userId: String, password: String): AuthStatus {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
override fun setPassword(newPassword: String): Boolean {
|
override fun setPassword(newPassword: String): Boolean {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,13 @@ interface FileMetaData {
|
|||||||
fun getLastModified(): Date?
|
fun getLastModified(): Date?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class AuthStatus {
|
||||||
|
UNAUTHORIZED,
|
||||||
|
UNREGISTERED,
|
||||||
|
VERIFIED,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
interface FileStorage {
|
interface FileStorage {
|
||||||
/**
|
/**
|
||||||
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
|
* @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
|
* @throws MultiplayerAuthException if the authentication failed
|
||||||
*/
|
*/
|
||||||
fun setPassword(newPassword: String): Boolean
|
fun setPassword(newPassword: String): Boolean
|
||||||
|
|
||||||
|
fun checkAuthStatus(userId: String, password: String): AuthStatus
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,21 @@ object UncivServerFileStorage : FileStorage {
|
|||||||
return authenticated
|
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 {
|
override fun setPassword(newPassword: String): Boolean {
|
||||||
if (authHeader == null)
|
if (authHeader == null)
|
||||||
return false
|
return false
|
||||||
|
@ -4,23 +4,23 @@ import com.badlogic.gdx.Application
|
|||||||
import com.badlogic.gdx.Gdx
|
import com.badlogic.gdx.Gdx
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||||
import com.unciv.UncivGame
|
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
|
import com.unciv.UncivGame
|
||||||
import com.unciv.logic.files.IMediaFinder
|
import com.unciv.logic.files.IMediaFinder
|
||||||
import com.unciv.logic.multiplayer.Multiplayer
|
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.FileStorageRateLimitReached
|
||||||
import com.unciv.logic.multiplayer.storage.MultiplayerAuthException
|
import com.unciv.logic.multiplayer.storage.MultiplayerAuthException
|
||||||
import com.unciv.models.metadata.GameSettings
|
import com.unciv.models.metadata.GameSettings
|
||||||
import com.unciv.models.metadata.GameSettings.GameSetting
|
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.addSeparator
|
||||||
import com.unciv.ui.components.extensions.brighten
|
|
||||||
import com.unciv.ui.components.extensions.format
|
import com.unciv.ui.components.extensions.format
|
||||||
import com.unciv.ui.components.extensions.isEnabled
|
import com.unciv.ui.components.extensions.isEnabled
|
||||||
import com.unciv.ui.components.extensions.toLabel
|
import com.unciv.ui.components.extensions.toLabel
|
||||||
import com.unciv.ui.components.extensions.toTextButton
|
import com.unciv.ui.components.extensions.toTextButton
|
||||||
import com.unciv.ui.components.input.onChange
|
import com.unciv.ui.components.input.onChange
|
||||||
import com.unciv.ui.components.input.onClick
|
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.AuthPopup
|
||||||
import com.unciv.ui.popups.Popup
|
import com.unciv.ui.popups.Popup
|
||||||
import com.unciv.ui.popups.options.SettingsSelect.SelectItem
|
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("Set password".toLabel()).padTop(16f).colspan(2).row()
|
||||||
serverIpTable.add(passwordTextField).colspan(2).growX().padBottom(8f).row()
|
serverIpTable.add(passwordTextField).colspan(2).growX().padBottom(8f).row()
|
||||||
|
|
||||||
val passwordStatusTable = Table().apply {
|
// initially assume no password
|
||||||
add(
|
val authStatusLabel = "Set a password to secure your userId".toLabel()
|
||||||
|
|
||||||
if (settings.multiplayer.passwords.containsKey(settings.multiplayer.server)) {
|
if (settings.multiplayer.passwords.containsKey(settings.multiplayer.server)) {
|
||||||
"Your userId is password secured"
|
val userId = settings.multiplayer.userId
|
||||||
} else {
|
val password = settings.multiplayer.passwords[settings.multiplayer.server] ?: ""
|
||||||
"Set a password to secure your userId"
|
|
||||||
}.toLabel()
|
|
||||||
)
|
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(authStatusLabel)
|
||||||
add(setPasswordButton.onClick {
|
add(setPasswordButton.onClick {
|
||||||
setPassword(passwordTextField.text, optionsPopup)
|
setPassword(passwordTextField.text, optionsPopup)
|
||||||
}).padLeft(16f)
|
}).padLeft(16f)
|
||||||
|
@ -50,7 +50,7 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
help = "Enable Authentication"
|
help = "Enable Authentication"
|
||||||
).flag("-no-auth", default = false)
|
).flag("-no-auth", default = false)
|
||||||
|
|
||||||
private val IdentifyOperators by option(
|
private val identifyOperators by option(
|
||||||
"-i", "-Identify",
|
"-i", "-Identify",
|
||||||
envvar = "UncivServerIdentify",
|
envvar = "UncivServerIdentify",
|
||||||
help = "Display each operation archive request IP to assist management personnel"
|
help = "Display each operation archive request IP to assist management personnel"
|
||||||
@ -99,20 +99,18 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
val (userId, password) = extractAuth(authString) ?: return false
|
val (userId, password) = extractAuth(authString) ?: return false
|
||||||
if (authMap[userId] == null || authMap[userId] == password)
|
return authMap[userId] == null || authMap[userId] == password
|
||||||
return true
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractAuth(authString: String?): Pair<String, String>? {
|
private fun extractAuth(authHeader: String?): Pair<String, String>? {
|
||||||
if (!authV1Enabled)
|
if (!authV1Enabled)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
// If auth is enabled a auth string is required
|
// If auth is enabled an authorization header is required
|
||||||
if (authString == null || !authString.startsWith("Basic "))
|
if (authHeader == null || !authHeader.startsWith("Basic "))
|
||||||
return null
|
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)
|
val splitAuthString = decodedString.split(":", limit=2)
|
||||||
if (splitAuthString.size != 2)
|
if (splitAuthString.size != 2)
|
||||||
return null
|
return null
|
||||||
@ -136,11 +134,11 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
put("/files/{fileName}") {
|
put("/files/{fileName}") {
|
||||||
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
|
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
|
||||||
|
|
||||||
// If IdentifyOperators is enabled a Operator IP is displayed
|
// If IdentifyOperators is enabled an Operator IP is displayed
|
||||||
if (IdentifyOperators) {
|
if (identifyOperators) {
|
||||||
log.info("Receiving file: ${fileName} --Operation sourced from ${call.request.local.remoteHost}")
|
log.info("Receiving file: $fileName --Operation sourced from ${call.request.local.remoteHost}")
|
||||||
}else{
|
}else{
|
||||||
log.info("Receiving file: ${fileName}")
|
log.info("Receiving file: $fileName")
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = File(fileFolderName, fileName)
|
val file = File(fileFolderName, fileName)
|
||||||
@ -160,9 +158,9 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
get("/files/{fileName}") {
|
get("/files/{fileName}") {
|
||||||
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
|
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
|
||||||
|
|
||||||
// If IdentifyOperators is enabled a Operator IP is displayed
|
// If IdentifyOperators is enabled an Operator IP is displayed
|
||||||
if (IdentifyOperators) {
|
if (identifyOperators) {
|
||||||
log.info("File requested: ${fileName} --Operation sourced from ${call.request.local.remoteHost}")
|
log.info("File requested: $fileName --Operation sourced from ${call.request.local.remoteHost}")
|
||||||
}else{
|
}else{
|
||||||
log.info("File requested: $fileName")
|
log.info("File requested: $fileName")
|
||||||
}
|
}
|
||||||
@ -170,9 +168,9 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
val file = File(fileFolderName, fileName)
|
val file = File(fileFolderName, fileName)
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
|
|
||||||
// If IdentifyOperators is enabled a Operator IP is displayed
|
// If IdentifyOperators is enabled an Operator IP is displayed
|
||||||
if (IdentifyOperators) {
|
if (identifyOperators) {
|
||||||
log.info("File ${fileName} not found --Operation sourced from ${call.request.local.remoteHost}")
|
log.info("File $fileName not found --Operation sourced from ${call.request.local.remoteHost}")
|
||||||
} else {
|
} else {
|
||||||
log.info("File $fileName not found")
|
log.info("File $fileName not found")
|
||||||
}
|
}
|
||||||
@ -186,11 +184,21 @@ private class UncivServerRunner : CliktCommand() {
|
|||||||
if (authV1Enabled) {
|
if (authV1Enabled) {
|
||||||
get("/auth") {
|
get("/auth") {
|
||||||
log.info("Received auth request from ${call.request.local.remoteHost}")
|
log.info("Received auth request from ${call.request.local.remoteHost}")
|
||||||
val authHeader = call.request.headers["Authorization"]
|
|
||||||
if (validateAuth(authHeader)) {
|
val authHeader = call.request.headers["Authorization"] ?: run {
|
||||||
call.respond(HttpStatusCode.OK)
|
call.respond(HttpStatusCode.BadRequest, "Missing authorization header!")
|
||||||
} else {
|
return@get
|
||||||
call.respond(HttpStatusCode.Unauthorized)
|
}
|
||||||
|
|
||||||
|
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") {
|
put("/auth") {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user