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

115 lines
4.3 KiB
Kotlin

package icu.samnyan.aqua.sega.allnet
import ext.Str
import icu.samnyan.aqua.net.FrontierProps
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletRequestWrapper
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Configuration
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
/**
* Handle token check for secure requests.
*
* When checkKeychip is enabled, the game URLs will be set to /gs/SS{token}/{game}/...
* This interceptor will check if the token exists in the database.
*/
@Component
@ConditionalOnBean(AllNetSecureInit::class)
class TokenChecker(
val keyChipRepo: KeyChipRepo,
val keychipSessionService: KeychipSessionService,
val frontierProps: FrontierProps,
) : HandlerInterceptor {
val log = LoggerFactory.getLogger(TokenChecker::class.java)
companion object {
private val log = LoggerFactory.getLogger(TokenChecker::class.java)
private val currentSession = ThreadLocal<KeychipSession?>()
fun getCurrentSession() = currentSession.get()
}
/**
* Handle request before it's processed.
*/
override fun preHandle(req: HttpServletRequest, resp: HttpServletResponse, handler: Any): Boolean {
// Skip the interceptor if the request is already forwarded
if (req.getAttribute("token") != null) return true
// Parse the token from the request path
val token = extractTokenFromPath(req.requestURI)
log.debug("PreHandle: ${req.requestURI} from ip ${req.remoteAddr}, token: $token")
// Check whether the token exists in the database
// The token can either be a keychip id (old method) or a session id (new method)
// Or the frontier token
val session = keychipSessionService.find(token)
if (token.isNotBlank() && (keyChipRepo.existsByKeychipId(token) || session != null
|| (frontierProps.enabled && frontierProps.ftk == token)))
{
currentSession.set(session)
// Forward the request
val w = RewriteWrapper(req, token).apply { setAttribute("token", token) }
req.getRequestDispatcher(w.requestURI).forward(w, resp)
// Prevent the request from being processed twice
return false
}
// Token doesn't exist, reject the request
resp.status = HttpServletResponse.SC_FORBIDDEN
log.warn("Request rejected: ${req.requestURI} from ip ${req.remoteAddr}")
return false
}
/**
* Extract the token from the request path.
* Example: "/gs/12033897/mai2/SomeEndpoint" -> "12033897"
*/
fun extractTokenFromPath(path: String) = path.substringAfter("/gs/", "").substringBefore("/", "")
}
/**
* Request wrapper for rewriting the URI after token check.
*/
class RewriteWrapper(req: HttpServletRequest, token: Str) : HttpServletRequestWrapper(req) {
val replace = "/gs/$token/"
val newUri = req.requestURI.replace(replace, "/g/")
val newUrl = req.requestURL.toString().replace(replace, "/g/")
val newSp = req.servletPath.replace(replace, "/g/")
override fun getRequestURI() = newUri
override fun getRequestURL() = StringBuffer(newUrl)
override fun getServletPath() = newSp
}
/**
* Register the token checker interceptor
*/
@Configuration
@ConditionalOnProperty(
value = ["allnet.server.check-keychip"],
havingValue = "true",
matchIfMissing = false
)
class AllNetSecureInit(
val tokenChecker: TokenChecker,
) : WebMvcConfigurer {
val log = LoggerFactory.getLogger(AllNetSecureInit::class.java)
override fun addInterceptors(reg: InterceptorRegistry) {
log.info("AllNet: Added token interceptor to secure requests.")
reg.addInterceptor(tokenChecker).addPathPatterns("/gs/**", "/g/**")
}
}