feat: implement title server encryption

This commit is contained in:
Raymond 2026-04-08 02:24:01 -04:00
parent 2e21926189
commit 15f37953d0
18 changed files with 214 additions and 74 deletions

3
.gitignore vendored
View File

@ -84,3 +84,6 @@ src/main/resources/meta/*/*.json
test-diff
htmlReport
docs/logs
# Sensitive keys
game_encryption.json

View File

@ -19,7 +19,7 @@ allnet.server.hide-port=true
allnet.server.place-name=AquaDX
## This enables client serial validation during power on request using keychip table in database.
## Only enable this if you know what you are doing.
allnet.server.check-keychip=true
allnet.server.check-keychip=false
## Interval between keychip session clean up checks in ms. Default is 1 day.
allnet.server.keychip-ses-clean-interval=86400000
## Token that haven't been used for this amount of time will be removed from the database. Default is 2 days.
@ -86,9 +86,9 @@ spring.servlet.multipart.max-request-size=20MB
## Database Setting
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.username=aqua
spring.datasource.password=aqua
spring.datasource.url=jdbc:mariadb://localhost:3306/aqua?allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.username=cat
spring.datasource.password=meow
spring.datasource.url=jdbc:mariadb://localhost:3369/main?allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.hikari.maximum-pool-size=10
################################

View File

@ -1,5 +1,6 @@
services:
app:
build: .
image: hykilpikonna/aquadx:latest
ports:
- "80:80"

View File

@ -181,20 +181,7 @@ class AllNet(
// If keychip authentication is enabled, the game URLs will be set to /gs/{token}/{game}/...
val base = if (session != null) "gs/$session" else "g"
return "http://$addr/$base/" + when (gameId) {
"SDBT" -> "chu2/$ver/$session/"
"SDHD" -> "chu3/$ver/"
"SDGS" -> "chu3/$ver/" // International (c3exp)
"SBZV" -> "diva/"
"SDDT" -> "ongeki/$ver/"
"SDEY" -> "mai/"
"SDGA" -> "mai2/" // International (Exp)
"SDGB" -> "mai2/" // International (China) - TODO: Test it
"SDEZ" -> "mai2/"
"SDFE" -> "wacca" // Note: Wacca must not end with a trailing slash
"SDED" -> "card/"
else -> ""
}
return "http://$addr/$base/$gameId/$ver/"
}
companion object {

View File

@ -19,7 +19,7 @@ import java.time.format.DateTimeFormatter
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
@RequestMapping("/g/card")
@RequestMapping("/g/SDED/{version}")
class CardMakerController(
val mapper: BasicMapper,
@param:Value("\${allnet.server.host:}") val ALLNET_HOST: String,

View File

@ -17,13 +17,18 @@ import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.RestController
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Suppress("unused")
@RestController
@API(value = ["/g/chu3/{version}/ChuniServlet", "/g/chu3/{version}"])
@API(value = [
"/g/SDHD/{version}/ChuniServlet", "/g/SDHD/{version}",
"/g/SDGS/{version}/ChuniServlet", "/g/SDGS/{version}",
])
class ChusanController(
val mapper: StringMapper,
val cmMapper: BasicMapper,
@ -37,14 +42,19 @@ class ChusanController(
): MeowApi({ api, resp ->
if (resp is String) resp
else (if ("CM" in api || api == "GetUserItemApi" ) cmMapper else mapper).write(resp)
},
@kotlin.ExperimentalStdlibApi
{ endpoint: String ->
db.gameData.chu3GameEncryption.map{
val suffix = when (it.code) { "SDGS" -> "C3Exp" else -> "" }
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
.generateSecret(
PBEKeySpec((endpoint + suffix).toCharArray(), it.salt!!.hexToByteArray(), it.iterations, 128)
).encoded.toHexString().substring(0, 32)
}
}) {
val log = LoggerFactory.getLogger(ChusanController::class.java)
val noopEndpoint = setOf("UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi",
"UpsertClientSettingApi", "UpsertClientTestmodeApi", "CreateTokenApi", "RemoveTokenApi", "UpsertClientUploadApi",
"PrinterLoginApi", "PrinterLogoutApi", "Ping", "GameLogoutApi", "RemoveMatchingMemberApi",
"UpsertClientPlayTimeApi", "UpsertClientGameStartApi", "UpsertClientGameEndApi")
init { chusanInit() }
val handlers = initH
@ -60,28 +70,27 @@ class ChusanController(
data["c3exp"] = true
}
val apiFriendlyName = if (endpoint.lowercase() == endpoint && endpoint.length == 32)
hashes.filter { it.value.contains(api) }.keys.first().str + " (encrypt)" else api
if (api.startsWith("CM") && api !in handlers) api = api.removePrefix("CM")
val token = TokenChecker.tokenShort()
log.info("$token : $api < ${data.toJson()}")
log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":"1","apiName":"$api"}"""
if (api !in noopEndpoint && !handlers.containsKey(api)) {
log.warn("$token : $api > not found")
if (!handlers.containsKey(api)) {
log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_chusan_api_call", "api" to api).increment()
if (api in noopEndpoint) {
log.info("$token : $api > no-op")
return noop
}
return try {
Metrics.timer("aquadx_chusan_api_latency", "api" to api).recordCallable {
serialize(api, handlers[api]!!(ctx) ?: noop).also {
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
log.info("$token : $api > ${it.truncate(500)}")
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {

View File

@ -431,5 +431,23 @@ fun ChusanController.chusanInit() {
// process()
"""{"returnCode":"1"}"""
// NOTE: no-op APIs moved to respect encryption
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" }
"UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" }
"UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" }
"UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" }
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
"CreateToken" static { """{returnCode":1, "apiName":"CreateTokenApi"}""" }
"RemoveToken" static { """{returnCode":1, "apiName":"RemoveTokenApi"}""" }
"UpsertClientUpload" static { """{returnCode":1, "apiName":"UpsertClientUploadApi"}""" }
"PrinterLogin" static { """{returnCode":1, "apiName":"PrinterLoginApi"}""" }
"PrinterLogout" static { """{returnCode":1, "apiName":"PrinterLogoutApi"}""" }
"Ping" static { """{returnCode":1, "apiName":"Ping"}""" }
"GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" }
"RemoveMatchingMember" static { """{returnCode":1, "apiName":"RemoveMatchingMemberApi"}""" }
"UpsertClientPlayTime" static { """{returnCode":1, "apiName":"UpsertClientPlayTimeApi"}""" }
"UpsertClientGameStart" static { """{returnCode":1, "apiName":"UpsertClientGameStartApi"}""" }
"UpsertClientGameEnd" static { """{returnCode":1, "apiName":"UpsertClientGameEndApi"}""" }
}
}

View File

@ -35,7 +35,7 @@ val DIVA_INIT = mapOf("db_close" to "0,0", "retry_time" to "FFFF")
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
@RequestMapping("/g/diva")
@RequestMapping("/g/SBZV/{version}")
class DivaController(
val attendHandler: AttendHandler,
val cardProcedureHandler: CardProcedureHandler,

View File

@ -31,11 +31,23 @@ typealias PagePost = (MutJDict) -> Unit
data class PagedProcessor(val add: JDict?, val fn: PagedHandler, var post: PagePost? = null)
// A very :3 way of declaring APIs
abstract class MeowApi(val serialize: (String, Any) -> String) {
abstract class MeowApi(
val serialize: (String, Any) -> String,
val endpointHash: ((String) -> List<String>)? = null
) {
val initH = mutableMapOf<String, SpecialHandler>()
val hashes = mutableMapOf<String, MutableList<String>>()
infix operator fun String.invoke(fn: SpecialHandler) {
if (initH.containsKey("${this}Api")) error("Duplicate API $this found! Someone is not smart 👀")
initH["${this}Api"] = fn
if (endpointHash != null) {
// Keep track of hashes for logging; they're not necessary for anything else
hashes["${this}Api"] = mutableListOf()
for (hashedEndpoint in endpointHash("${this}Api")) {
initH[hashedEndpoint] = fn
hashes["${this}Api"]?.add(hashedEndpoint)
}
}
}
infix fun String.static(fn: () -> Any) = serialize(this, fn()).let { resp -> this { resp } }

View File

@ -5,6 +5,8 @@ import ext.logger
import ext.toJson
import icu.samnyan.aqua.net.components.GeoIP
import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
import icu.samnyan.aqua.sega.util.GameDataService
import icu.samnyan.aqua.sega.util.ZLib
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
@ -14,6 +16,9 @@ import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingResponseWrapper
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
@ -21,21 +26,46 @@ import java.util.*
*/
@Component
class CompressionFilter(
val geoip: GeoIP
val geoip: GeoIP,
val gameData: GameDataService
) : OncePerRequestFilter() {
companion object {
val log = logger()
val b64d = Base64.getMimeDecoder()
val b64e = Base64.getMimeEncoder()
}
var keys = gameData.chu3GameEncryption + gameData.mai2GameEncryption + gameData.ogkGameEncryption
fun getEncryptionKeys(path: String): GameEncryptionKey? {
val endpoint = path.split("/").last()
if (endpoint.lowercase() != endpoint || endpoint.length != 32) return null
val game = path.split("/")[2]
val version = path.split("/")[3].filterNot { it == '.' }.toInt()
return keys.find { it.version == version && it.code == game }
}
@OptIn(ExperimentalStdlibApi::class)
override fun doFilterInternal(req: HttpServletRequest, resp: HttpServletResponse, chain: FilterChain) {
val isDeflate = req.getHeader("content-encoding") == "deflate"
val isDfi = req.getHeader("pragma") == "DFI"
val keys = getEncryptionKeys(req.servletPath)
// Decode input
val reqSrc = try {
req.inputStream.readAllBytes().let {
if (keys != null) {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(keys.key!!.hexToByteArray(), "AES"),
IvParameterSpec(keys.iv!!.hexToByteArray())
)
cipher.doFinal(it)
} else it
}.let {
if (isDeflate) ZLib.decompress(it)
else if (isDfi) ZLib.decompress(b64d.decode(it))
else it
@ -50,7 +80,19 @@ class CompressionFilter(
val respW = ContentCachingResponseWrapper(resp)
val result = try {
chain.doFilter(CompressRequestWrapper(req, reqSrc), respW)
ZLib.compress(respW.contentAsByteArray).let { if (isDfi) b64e.encode(it) else it }
ZLib.compress(respW.contentAsByteArray)
.let { if (isDfi) b64e.encode(it) else it }
.let {
if (keys != null) {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(keys.key!!.hexToByteArray(), "AES"),
IvParameterSpec(keys.iv!!.hexToByteArray())
)
cipher.doFinal(it)
} else it
}
} finally {
if (respW.status != 200) {
val details = mapOf(

View File

@ -0,0 +1,14 @@
package icu.samnyan.aqua.sega.general.model
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.LocalDateTime
class GameEncryptionKey {
var code: String? = null
var version = 0
var key: String? = null
var salt: String? = null
var iv: String? = null
var iterations = 0
}

View File

@ -424,8 +424,20 @@ fun Maimai2ServletController.initApis() {
)
) }
"UpsertUserPlaceCircleRegist" static { mapOf(
"returnCode" to 0,
"apiName" to "UpsertUserPlaceCircleRegistApi"
) }
// NOTE: no-op APIs moved to respect encryption
"UpsertUserPlaceCircleRegist" static { mapOf( "returnCode" to 0, "apiName" to "com.sega.maimai2servlet.api.UpsertUserPlaceCircleRegistApi") }
"GetUserScoreRanking" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetUserScoreRankingApi"}""" }
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientBookkeepingApi"}""" }
"UpsertClientSetting" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientSettingApi"}""" }
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientTestmodeApi"}""" }
"UpsertClientUpload" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientUploadApi"}""" }
"Ping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.Ping"}""" }
"RemoveToken" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.RemoveTokenApi"}""" }
"CMLogin" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLoginApi"}""" }
"CMLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLogoutApi"}""" }
"CMUpsertBuyCard" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMUpsertBuyCardApi"}""" }
"UserLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UserLogoutApi"}""" }
"GetGameMapAreaCondition" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetGameMapAreaConditionApi"}""" }
"UpsertUserChargelog" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertUserChargelogApi"}""" }
"UpsertClientPlayTime" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientPlayTimeApi"}""" }
}

View File

@ -11,12 +11,14 @@ import icu.samnyan.aqua.sega.maimai2.handler.*
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
import kotlinx.io.bytestring.hexToByteString
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.security.MessageDigest
import java.time.format.DateTimeFormatter
import kotlin.reflect.full.declaredMemberProperties
@ -25,7 +27,11 @@ import kotlin.reflect.full.declaredMemberProperties
*/
@Suppress("unused")
@RestController
@RequestMapping(path = ["/g/mai2/Maimai2Servlet/", "/g/mai2/"])
@RequestMapping(path = [
"/g/SDGA/{version}/Maimai2Servlet/", "/g/SDGA/{version}",
"/g/SDEZ/{version}/Maimai2Servlet/", "/g/SDEZ/{version}",
"/g/SDGB/{version}/Maimai2Servlet/", "/g/SDGB/{version}",
])
class Maimai2ServletController(
val upsertUserAll: UpsertUserAllHandler,
val getUserItem: GetUserItemHandler,
@ -40,7 +46,18 @@ class Maimai2ServletController(
val getGameRanking: GetGameRankingHandler,
val db: Mai2Repos,
val net: Maimai2,
): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) {
): MeowApi(
serialize = { _, resp -> if (resp is String) resp else resp.toJson() },
@kotlin.ExperimentalStdlibApi
{ endpoint: String ->
db.gameData.mai2GameEncryption.map{
val suffix = when (it.code) { "SDGA" -> "MaimaiExp" "SDGB" -> "MaimaiChn" else -> "" }
MessageDigest.getInstance("MD5")
.digest((endpoint + suffix).toByteArray() + it.salt!!.hexToByteArray())
.toHexString()
}
}
) {
companion object {
private val log = logger()
private val empty = listOf<Any>()
@ -55,11 +72,6 @@ class Maimai2ServletController(
"CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi","CMGetUserItemApi","CMUpsertUserPrintApi",
"GetUserFavoriteItemApi")
val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi",
"UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi",
"CMLoginApi", "CMLogoutApi", "CMUpsertBuyCardApi", "UserLogoutApi", "GetGameMapAreaConditionApi",
"UpsertUserChargelogApi","UpsertClientPlayTimeApi")
val members = this::class.declaredMemberProperties
val handlers: Map<String, SpecialHandler> = initH + endpointList.associateWith { api ->
val name = api.replace("Api", "").lowercase()
@ -72,27 +84,25 @@ class Maimai2ServletController(
@API("/{api}")
fun handle(@PathVariable api: String, @RequestBody data: Map<String, Any>, req: HttpServletRequest): Any {
val token = TokenChecker.tokenShort()
log.info("$token : $api < ${data.toJson()}")
val apiFriendlyName = if (api.lowercase() == api && api.length == 32)
((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)"
log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}"""
if (api !in noopEndpoint && !handlers.containsKey(api)) {
log.warn("$token : $api > not found")
if (!handlers.containsKey(api)) {
log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_maimai2_api_call", "api" to api).increment()
if (api in noopEndpoint) {
log.info("$token : $api > no-op")
return noop
}
return try {
Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable {
val ctx = RequestContext(req, data.mut)
serialize(api, handlers[api]!!(ctx) ?: noop).also {
log.info("$token : $api > ${it.truncate(500)}")
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {

View File

@ -147,7 +147,7 @@ class Mai2Repos(
val userKaleidx: MAi2UserKaleidxRepo,
val userIntimate: MAi2UserIntimateRepo,
val userRegions: Mai2UserRegionsRepo,
gameData: GameDataService
val gameData: GameDataService
) {
val gameCharge = StaticRepo(gameData.mai2Charges) { it.orderId }
val gameEvent = StaticRepo(gameData.mai2Events) { it.id }

View File

@ -70,4 +70,16 @@ fun OngekiController.ongekiInit() {
"GetClientTestmode" {
empty.staticLst("clientTestmodeList") + mapOf("placeId" to data["placeId"])
}
// NOTE: no-op APIs moved to respect encryption
"ExtendLockTime" static { """{returnCode":1, "apiName":"ExtendLockTimeApi"}""" }
"GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" }
"RegisterPromotionCard" static { """{returnCode":1, "apiName":"RegisterPromotionCardApi"}""" }
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" }
"UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" }
"UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" }
"UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" }
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
"UpsertUserGplog" static { """{returnCode":1, "apiName":"UpsertUserGplogApi"}""" }
"Ping" static { """{returnCode":1, "apiName":"Ping"}""" }
}

View File

@ -8,14 +8,18 @@ import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.MeowApi
import icu.samnyan.aqua.sega.general.RequestContext
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.Maimai2ServletController
import icu.samnyan.aqua.sega.util.BasicMapper
import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.RestController
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
@Suppress("unused")
@RestController
@API("/g/ongeki/{version}", "/g/ongeki")
@API("/g/SDDT/{version}", "/g/SDDT")
class OngekiController(
val mapper: BasicMapper,
val db: OngekiUserRepos,
@ -23,14 +27,23 @@ class OngekiController(
val us: AquaUserServices,
val pop: GameMusicPopularity,
val cardService: CardService,
): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) {
): MeowApi(
{ _, resp -> if (resp is String) resp else mapper.write(resp) },
@OptIn(ExperimentalStdlibApi::class)
{ endpoint: String ->
gdb.gameData.ogkGameEncryption.map{
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
.generateSecret(PBEKeySpec(
endpoint.toCharArray(),
it.salt!!.hexToByteArray(),
it.iterations, 128
)).encoded.toHexString().substring(0, 32)
}
}
) {
val log = logger()
val noopEndpoint = setOf("ExtendLockTimeApi", "GameLogoutApi", "RegisterPromotionCardApi",
"UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi", "UpsertClientSettingApi",
"UpsertClientTestmodeApi", "UpsertUserGplogApi", "Ping")
init { ongekiInit() }
val handlers = initH
@ -40,26 +53,25 @@ class OngekiController(
version?.let { data["version"] = it }
val token = TokenChecker.tokenShort()
log.info("$token : $api < ${data.toJson()}")
val apiFriendlyName = if (api.lowercase() == api && api.length == 32)
((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)"
log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":1,"apiName":"${api.substringBefore("Api").firstCharLower()}"}"""
if (api !in noopEndpoint && !handlers.containsKey(api)) {
log.warn("$token : $api > not found")
if (!handlers.containsKey(api)) {
log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_ongeki_api_call", "api" to api).increment()
if (api in noopEndpoint) {
log.info("$token : $api > no-op")
return noop
}
return try {
Metrics.timer("aquadx_ongeki_api_latency", "api" to api).recordCallable {
serialize(api, handlers[api]!!(ctx) ?: noop).also {
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
log.info("$token : $api > ${it.truncate(500)}")
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {

View File

@ -3,6 +3,7 @@ package icu.samnyan.aqua.sega.util
import com.fasterxml.jackson.core.type.TypeReference
import ext.logger
import ext.toJson
import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
import icu.samnyan.aqua.sega.chusan.model.GameCharge as Chu3GameCharge
import icu.samnyan.aqua.sega.chusan.model.GameEvent as Chu3GameEvent
import icu.samnyan.aqua.sega.chusan.model.GameGacha as Chu3GameGacha
@ -45,7 +46,8 @@ class GameDataService() {
return mapper.read(resStream.bufferedReader().use { it.readText() }, object : TypeReference<List<T>>() {})
}
log.warn("Game data file $f or resource $resPath not found, using empty list")
if (file != "game_encryption.json")
log.warn("Game data file $f or resource $resPath not found, using empty list")
return emptyList()
}
}
@ -54,6 +56,7 @@ class GameDataService() {
lateinit var mai2Events: List<Mai2GameEvent>
lateinit var mai2Charges: List<Mai2GameCharge>
lateinit var mai2SellingCards: List<Mai2GameSellingCard>
lateinit var mai2GameEncryption: List<GameEncryptionKey>
// chusan
lateinit var chu3GameLinkedVerses: List<Chu3GameLinkedVerse>
@ -63,6 +66,7 @@ class GameDataService() {
lateinit var chu3GameGachas: List<Chu3GameGacha>
lateinit var chu3GameLoginBonusPresets: List<Chu3GameLoginBonusPreset>
lateinit var chu3GameLoginBonuses: List<Chu3GameLoginBonus>
lateinit var chu3GameEncryption: List<GameEncryptionKey>
// ongeki
lateinit var ogkGameCards: List<OgkGameCard>
@ -75,6 +79,7 @@ class GameDataService() {
lateinit var ogkGameSkills: List<OgkGameSkill>
lateinit var ogkGameGachaCards: List<OgkGameGachaCard>
lateinit var ogkGameGachas: List<OgkGameGacha>
lateinit var ogkGameEncryption: List<GameEncryptionKey>
init {
load()
@ -84,6 +89,7 @@ class GameDataService() {
mai2Events = load("maimai2", "game_event.json")
mai2Charges = load("maimai2", "game_charge.json")
mai2SellingCards = load("maimai2", "game_selling_card.json")
mai2GameEncryption = load(game = "maimai2", file = "game_encryption.json")
chu3GameLinkedVerses = load("chusan", "game_linked_verse.json")
chu3GameCharges = load("chusan", "game_charge.json")
@ -92,6 +98,7 @@ class GameDataService() {
chu3GameGachas = load("chusan", "game_gacha.json")
chu3GameLoginBonusPresets = load("chusan", "game_login_bonus_preset.json")
chu3GameLoginBonuses = load("chusan", "game_login_bonus.json")
chu3GameEncryption = load(game = "chusan", file = "game_encryption.json")
ogkGameCards = load("ongeki", "game_card.json")
ogkGameCharas = load("ongeki", "game_chara.json")
@ -103,6 +110,7 @@ class GameDataService() {
ogkGameSkills = load("ongeki", "game_skill.json")
ogkGameGachaCards = load("ongeki", "game_gacha_card.json")
ogkGameGachas = load("ongeki", "game_gacha.json")
ogkGameEncryption = load(game = "ongeki", file = "game_encryption.json")
}
}

View File

@ -25,7 +25,7 @@ import kotlin.math.max
import kotlin.math.min
@RestController
@API("/g/wacca/")
@API("/g/SDFE/{version}")
class WaccaServer {
// These are lateinit autowired instead of constructor injection because the tests depend on creating
// an instance of this class with no arguments.