[+] Counter measure

pull/29/head
Azalea 2024-04-03 08:08:02 -04:00
parent 501bf06ada
commit 355c9e2a3d
7 changed files with 25 additions and 11 deletions

View File

@ -184,7 +184,7 @@ class UserRegistrar(
@API("/keychip")
@Doc("Get a Keychip ID so that the user can connect to the server.", "Success message")
suspend fun setupConnection(@RP token: Str) = jwt.auth(token) { u ->
if (u.keychip != null) return mapOf("keychip" to u.keychip)
u.keychip?.let { return mapOf("keychip" to it) }
log.info("Net: /user/keychip setup: ${u.auId} for ${u.username}")
// Generate a keychip id with 10 digits (e.g. A1234567890)

View File

@ -73,5 +73,5 @@ class JWT(
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
final inline fun auth(token: Str, block: (AquaNetUser) -> Any) = block(auth(token))
final inline fun <T> auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token))
}

View File

@ -76,7 +76,7 @@ class AquaNetUser(
@OneToOne(cascade = [CascadeType.ALL])
@JoinColumn(name = "gameOptions", unique = true, nullable = true)
var gameOptions: AquaGameOptions? = null
var gameOptions: AquaGameOptions? = null,
) : Serializable {
val computedName get() = displayName.ifEmpty { username }

View File

@ -31,16 +31,26 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
playlogRepo.findByUserCardExtId(card.extId)
}
private var rankingCache: Pair<Long, List<GenericRankingPlayer>>? = null
private var rankingCache: MutableMap<Long, Pair<Long, List<GenericRankingPlayer>>> = mutableMapOf()
private val rankingCacheDuration = 240_000
@API("ranking")
fun ranking(): List<GenericRankingPlayer> {
// Read from cache if we just computed it less than 2 minutes ago
rankingCache?.let { (t, r) ->
if (millis() - t < 120_000) return r
fun ranking(@RP token: String?): List<GenericRankingPlayer> {
val reqUser = token?.let { us.jwt.auth(it) { u ->
// Optimization: If the user is not banned, we don't need to process user information
if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned }) null
else u
} }
val cacheKey = reqUser?.auId ?: -1
// Read from cache if we just computed it less than duration ago
rankingCache[cacheKey]?.let { (t, r) ->
if (millis() - t < rankingCacheDuration) return r
}
// TODO: pagination
// Shadow-ban: Do not show banned cards in the ranking except for the user who owns the card
val players = userDataRepo.findAll().sortedByDescending { it.playerRating }
.filter { it.card?.rankingBanned != true || it.card?.aquaUser?.let { it == reqUser } ?: false }
return players.filter { it.card != null }.mapIndexed { i, user ->
val plays = playlogRepo.findByUserCardExtId(user.card!!.extId)
@ -54,7 +64,7 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
lastSeen = user.lastPlayDate.toString(),
username = user.card!!.aquaUser?.username ?: "user${user.card!!.id}"
)
}.also { rankingCache = millis() to it } // Update the cache
}.also { rankingCache[cacheKey] = millis() to it } // Update cache
}
@API("playlog")

View File

@ -125,7 +125,7 @@ open class UserDataEntity : BaseEntity() {
interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> {
fun findByCard(card: Card): T?
fun findByCard_ExtId(extId: Long): Optional<T>
@Query("select count(*) from #{#entityName} where playerRating > :rating")
@Query("select count(*) from #{#entityName} e where e.playerRating > :rating and e.card.rankingBanned = false")
fun getRanking(rating: Int): Long
}

View File

@ -39,8 +39,11 @@ class Card(
var aquaUser: AquaNetUser? = null,
// Whether the card is a ghost card
@Column(name = "is_ghost")
var isGhost: Boolean = false,
// Unfortunately some people decide to cheat and upload all perfect scores :(
// This will not affect gameplay behavior, but will hide the user from ranking
var rankingBanned: Boolean = false,
) {
@Suppress("unused") // Used by serialization
val isLinked get() = aquaUser != null

View File

@ -0,0 +1 @@
ALTER TABLE sega_card ADD COLUMN `ranking_banned` BIT(1) NOT NULL DEFAULT 0;