AquaDX/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt

203 lines
7.2 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package icu.samnyan.aqua.sega.allnet
import ext.*
import icu.samnyan.aqua.net.db.AquaNetUserRepo
import icu.samnyan.aqua.sega.util.AllNetBillingDecoder.decodeAllNet
import icu.samnyan.aqua.sega.util.AquaConst
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
import java.io.InputStream
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.time.LocalDateTime
import java.util.*
@Configuration
@ConfigurationProperties(prefix = "allnet.server")
class AllNetProps {
var host: String = ""
var port: Int? = null
val keychipSesExpire: Long = 172800000 // milliseconds
var checkKeychip: Boolean = false
var keychipPermissiveForTesting: Boolean = false
var redirect: String = "web"
var placeName: String = ""
var placeId: String = "123"
var region0: String = "1"
var regionName0: String = "W"
var regionName1: String = "X"
var regionName2: String = "Y"
var regionName3: String = "Z"
var regionCountry: String = "JPN"
// Java assumes every application.properties as ISO-8859-1 (wtf), so we need to "correctly" convert it to UTF-8
// More better way to this is to use XML or yaml format as these treated as UTF-8
// but I rather use hack than breaking backward compatibility.. for now
// TODO: Fix this
val normalizedPlaceName: String by lazy {
String(placeName.toByteArray(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)
}
val map: Map<String, String> by lazy { mapOf(
"stat" to "1",
"name" to "",
"place_id" to placeId,
"region0" to region0,
"region_name0" to regionName0,
"region_name1" to regionName1,
"region_name2" to regionName2,
"region_name3" to regionName3,
"country" to regionCountry,
"nickname" to normalizedPlaceName,
) }
}
@Suppress("HttpUrlsUsage")
@RestController
class AllNet(
val userRepo: AquaNetUserRepo,
val keychipSessionService: KeychipSessionService,
val keychipRepo: KeyChipRepo,
val props: AllNetProps
) {
@API("/")
fun root(response: HttpServletResponse) {
response.sendRedirect(props.redirect)
}
@API("/sys/test")
fun selfTest() = "Server running"
@API("/naomitest.html")
fun naomiTest() = "naomi ok"
@PostMapping("/sys/servlet/DownloadOrder", produces = ["text/plain"])
fun downloadOrder(dataStream: InputStream, req: HttpServletRequest?): String {
val bytes = dataStream.readAllBytes()
val reqMap = decodeAllNet(bytes)
logger.info("AllNet /DownloadOrder : $reqMap")
val serial = reqMap["serial"] ?: AquaConst.DEFAULT_KEYCHIP_ID
val resp = mapOf(
"stat" to "1",
"serial" to serial
)
logger.info("> Response: $resp")
return resp.toUrl() + "\n"
}
@PostMapping("/sys/servlet/PowerOn", produces = ["text/plain"])
fun powerOn(dataStream: InputStream, req: HttpServletRequest): String {
val localAddr = req.localAddr
val localPort = req.localPort.toString()
// game_id SDEZ, ver 1.35, serial A0000001234, ip, firm_ver 50000, boot_ver 0000,
// encode UTF-8, format_ver 3, hops 1 token 2010451813
val reqMap = decodeAllNet(dataStream.readAllBytes())
val serial = reqMap["serial"] ?: ""
logger.info("AllNet /PowerOn : $reqMap")
var session: String? = null
// Proper keychip authentication
if (props.checkKeychip) {
// If it's a user keychip, it should be in user database
val u = userRepo.findByKeychip(serial)
if (u != null) {
// Create a new session for the user
logger.info("> Keychip authenticated: ${u.auId} ${u.computedName}")
session = keychipSessionService.new(u, reqMap["game_id"] ?: "").token
}
// Check if it's a whitelisted keychip
else if (!serial.isEmpty() && keychipRepo.existsByKeychipId(serial)) {
session = keychipSessionService.new(null, reqMap["game_id"] ?: "").token
}
else if (props.keychipPermissiveForTesting) {
logger.warn("> Accepted invalid keychip $serial in permissive mode")
session = keychipSessionService.new(null, reqMap["game_id"] ?: "").token
}
else {
// This will cause an allnet auth bad on client side
return "".also { logger.warn("> Rejected: Keychip not found") }
}
}
val gameId = reqMap["game_id"] ?: return "".also { logger.warn("> Rejected: No game_id provided") }
val ver = reqMap["ver"] ?: "1.0"
val formatVer = reqMap["format_ver"] ?: ""
val resp = props.map.toMutableMap() + mapOf(
"uri" to switchUri(localAddr, localPort, gameId, ver, session),
"host" to props.host.ifBlank { localAddr },
)
// Different responses for different versions
if (formatVer.startsWith("2")) {
val now = LocalDateTime.now()
resp += mapOf(
"year" to now.year,
"month" to now.month.value,
"day" to now.dayOfMonth,
"hour" to now.hour,
"minute" to now.minute,
"second" to now.second,
"setting" to "1",
"timezone" to "+09:00",
"res_class" to "PowerOnResponseV2"
).mapValues { it.value.toString() }
} else {
resp += mapOf(
"allnet_id" to "456",
"client_timezone" to "+0900",
"utc_time" to Instant.now().toString().substring(0, 19) + "Z",
"setting" to "",
"res_ver" to "3",
"token" to reqMap["token"]
).mapValues { it.value.toString() }
}
logger.info("> Response: $resp")
return resp.toUrl() + "\n"
}
private fun switchUri(localAddr: Str, localPort: Str, gameId: Str, ver: Str, session: Str?): Str {
val addr = props.host.ifBlank { localAddr }
val port = props.port?.toString() ?: localPort
// 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:$port/$base/" + when (gameId) {
"SDBT" -> "chu2/$ver/$session/"
"SDHD" -> "chu3/$ver/"
"SDGS" -> "chu3/$ver/" // International (c3exp)
"SBZV" -> "diva/"
"SDDT" -> "ongeki/"
"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 -> ""
}
}
companion object {
val logger: Logger = LoggerFactory.getLogger(AllNet::class.java)
}
}