mirror of
https://github.com/hykilpikonna/AquaDX.git
synced 2026-05-14 00:00:03 -05:00
feat: implement title server encryption
This commit is contained in:
parent
2e21926189
commit
15f37953d0
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -84,3 +84,6 @@ src/main/resources/meta/*/*.json
|
|||
test-diff
|
||||
htmlReport
|
||||
docs/logs
|
||||
|
||||
# Sensitive keys
|
||||
game_encryption.json
|
||||
|
|
@ -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
|
||||
|
||||
################################
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
services:
|
||||
app:
|
||||
build: .
|
||||
image: hykilpikonna/aquadx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"}""" }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"}""" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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"}""" }
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user