diff --git a/build.gradle.kts b/build.gradle.kts index 1852626..2158011 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -71,7 +71,7 @@ dependencies { implementation("io.netty:netty-tcnative-boringssl-static:2.0.43.Final") implementation("io.netty.incubator:netty-incubator-transport-native-io_uring:0.0.8.Final:linux-x86_64") - implementation("com.google.guava:guava:30.1.1-jre") + implementation("com.google.guava:guava:31.0-jre") implementation("com.velocitypowered:velocity-native:3.0.1") implementation("net.coobird:thumbnailator:0.4.14") implementation("org.powernukkit.fastutil:fastutil-lite:8.1.1") diff --git a/src/main/kotlin/com/viaversion/aas/web/WebDashboardServer.kt b/src/main/kotlin/com/viaversion/aas/web/WebDashboardServer.kt index 0f14355..b9f8573 100644 --- a/src/main/kotlin/com/viaversion/aas/web/WebDashboardServer.kt +++ b/src/main/kotlin/com/viaversion/aas/web/WebDashboardServer.kt @@ -39,22 +39,26 @@ class WebDashboardServer { val clients = ConcurrentHashMap() val jwtAlgorithm = Algorithm.HMAC256(VIAaaSConfig.jwtSecret) - fun generateToken(account: UUID): String { + fun generateToken(account: UUID, username: String): String { return JWT.create() + .withIssuedAt(Date()) .withExpiresAt(Date.from(Instant.now().plus(Duration.ofDays(30)))) .withSubject(account.toString()) + .withClaim("name", username) .withAudience("viaaas_listen") .withIssuer("viaaas") .sign(jwtAlgorithm) } - fun checkToken(token: String): UUID? { + data class UserInfo(val id: UUID, val name: String?) + + fun parseToken(token: String): UserInfo? { return try { val verified = JWT.require(jwtAlgorithm) .withAnyOfAudience("viaaas_listen") .build() .verify(token) - UUID.fromString(verified.subject) + UserInfo(UUID.fromString(verified.subject), verified.getClaim("name").asString()) } catch (e: JWTVerificationException) { null } diff --git a/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt b/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt index 666f970..14bac48 100644 --- a/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt +++ b/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt @@ -19,95 +19,108 @@ class WebLogin : WebState { webClient.ws.flush() } + private fun loginSuccessJson(username: String, uuid: UUID, token: String): JsonObject { + return JsonObject().also { + it.addProperty("action", "login_result") + it.addProperty("success", true) + it.addProperty("username", username) + it.addProperty("uuid", uuid.toString()) + it.addProperty("token", token) + } + } + + private fun loginNotSuccess(): JsonObject { + return JsonObject().also { + it.addProperty("action", "login_result") + it.addProperty("success", false) + } + } + + private suspend fun handleOfflineLogin(webClient: WebClient, msg: String, obj: JsonObject) { + if (!sha512Hex(msg.toByteArray(Charsets.UTF_8)).startsWith("00000")) throw StacklessException("PoW failed") + if ((obj.getAsJsonPrimitive("date").asLong - System.currentTimeMillis()) + .absoluteValue > Duration.ofSeconds(20).toMillis() + ) { + throw StacklessException("Invalid PoW date") + } + val username = obj["username"].asString.trim() + val uuid = generateOfflinePlayerUuid(username) + + val token = webClient.server.generateToken(uuid, username) + webClient.ws.send(loginSuccessJson(username, uuid, token).toString()) + + webLogger.info("Token gen: ${webClient.id}: offline $username $uuid") + } + + private suspend fun handleMcIdLogin(webClient: WebClient, msg: String, obj: JsonObject) { + val username = obj["username"].asString + val code = obj["code"].asString + + val check = AspirinServer.httpClient.submitForm( + "https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}", + formParameters = parametersOf("code", code), + ) + + if (check.getAsJsonPrimitive("valid").asBoolean) { + val mcIdUser = check["username"].asString + val uuid = check["uuid"]?.asString?.let { parseUndashedId(it.replace("-", "")) } + ?: webClient.server.usernameIdCache[mcIdUser].await() + ?: throw StacklessException("Failed to get UUID from minecraft.id") + + val token = webClient.server.generateToken(uuid, mcIdUser) + webClient.ws.send(loginSuccessJson(mcIdUser, uuid, token).toString()) + + webLogger.info("Token gen: ${webClient.id}: $mcIdUser $uuid") + } else { + webClient.ws.send(loginNotSuccess().toString()) + webLogger.info("Token gen fail: ${webClient.id}: $username") + } + } + + private suspend fun handleListenLogins(webClient: WebClient, msg: String, obj: JsonObject) { + val token = obj.getAsJsonPrimitive("token").asString + val user = webClient.server.parseToken(token) + val response = JsonObject().also { + it.addProperty("action", "listen_login_requests_result") + it.addProperty("token", token) + } + if (user != null && webClient.listenId(user.id)) { + response.addProperty("success", true) + response.addProperty("user", user.id.toString()) + response.addProperty("username", user.name) + webLogger.info("Listen: ${webClient.id}: $user") + } else { + response.addProperty("success", false) + webLogger.info("Listen fail: ${webClient.id}") + } + webClient.ws.send(response.toString()) + } + + private suspend fun handleUnlisten(webClient: WebClient, msg: String, obj: JsonObject) { + val uuid = UUID.fromString(obj.getAsJsonPrimitive("uuid").asString) + webLogger.info("Unlisten: ${webClient.id}: $uuid") + val response = JsonObject().also { + it.addProperty("action", "unlisten_login_requests_result") + it.addProperty("uuid", uuid.toString()) + it.addProperty("success", webClient.unlistenId(uuid)) + } + webClient.ws.send(response.toString()) + } + + private suspend fun handleSessionResponse(webClient: WebClient, msg: String, obj: JsonObject) { + val hash = obj["session_hash"].asString + webClient.server.sessionHashCallbacks.getIfPresent(hash)?.complete(Unit) + } + override suspend fun onMessage(webClient: WebClient, msg: String) { val obj = JsonParser.parseString(msg) as JsonObject when (obj.getAsJsonPrimitive("action").asString) { - "offline_login" -> { - if (!sha512Hex(msg.toByteArray(Charsets.UTF_8)).startsWith("00000")) throw StacklessException("PoW failed") - if ((obj.getAsJsonPrimitive("date").asLong - System.currentTimeMillis()) - .absoluteValue > Duration.ofSeconds(20).toMillis() - ) { - throw StacklessException("Invalid PoW date") - } - val username = obj["username"].asString.trim() - val uuid = generateOfflinePlayerUuid(username) - - val token = webClient.server.generateToken(uuid) - webClient.ws.send(JsonObject().also { - it.addProperty("action", "login_result") - it.addProperty("success", true) - it.addProperty("username", username) - it.addProperty("uuid", uuid.toString()) - it.addProperty("token", token) - }.toString()) - - webLogger.info("Token gen: ${webClient.id}: offline $username $uuid") - } - "minecraft_id_login" -> { - val username = obj["username"].asString - val code = obj["code"].asString - - val check = AspirinServer.httpClient.submitForm( - "https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}", - formParameters = parametersOf("code", code), - ) - - if (check.getAsJsonPrimitive("valid").asBoolean) { - val mcIdUser = check["username"].asString - val uuid = check["uuid"]?.asString?.let { parseUndashedId(it.replace("-", "")) } - ?: webClient.server.usernameIdCache[mcIdUser].await() - ?: throw StacklessException("Failed to get UUID from minecraft.id") - - val token = webClient.server.generateToken(uuid) - webClient.ws.send(JsonObject().also { - it.addProperty("action", "login_result") - it.addProperty("success", true) - it.addProperty("username", mcIdUser) - it.addProperty("uuid", uuid.toString()) - it.addProperty("token", token) - }.toString()) - - webLogger.info("Token gen: ${webClient.id}: $mcIdUser $uuid") - } else { - webClient.ws.send(JsonObject().also { - it.addProperty("action", "login_result") - it.addProperty("success", false) - }.toString()) - webLogger.info("Token gen fail: ${webClient.id}: $username") - } - } - "listen_login_requests" -> { - val token = obj.getAsJsonPrimitive("token").asString - val user = webClient.server.checkToken(token) - val response = JsonObject().also { - it.addProperty("action", "listen_login_requests_result") - it.addProperty("token", token) - } - if (user != null && webClient.listenId(user)) { - response.addProperty("success", true) - response.addProperty("user", user.toString()) - webLogger.info("Listen: ${webClient.id}: $user") - } else { - response.addProperty("success", false) - webLogger.info("Listen fail: ${webClient.id}") - } - webClient.ws.send(response.toString()) - } - "unlisten_login_requests" -> { - val uuid = UUID.fromString(obj.getAsJsonPrimitive("uuid").asString) - webLogger.info("Unlisten: ${webClient.id}: $uuid") - val response = JsonObject().also { - it.addProperty("action", "unlisten_login_requests_result") - it.addProperty("uuid", uuid.toString()) - it.addProperty("success", webClient.unlistenId(uuid)) - } - webClient.ws.send(response.toString()) - } - "session_hash_response" -> { - val hash = obj["session_hash"].asString - webClient.server.sessionHashCallbacks.getIfPresent(hash)?.complete(Unit) - } + "offline_login" -> handleOfflineLogin(webClient, msg, obj) + "minecraft_id_login" -> handleMcIdLogin(webClient, msg, obj) + "listen_login_requests" -> handleListenLogins(webClient, msg, obj) + "unlisten_login_requests" -> handleUnlisten(webClient, msg, obj) + "session_hash_response" -> handleSessionResponse(webClient, msg, obj) else -> throw StacklessException("invalid action!") } diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html index 36b2628..941360a 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/web/index.html @@ -24,7 +24,7 @@ script-src 'self' https://*.cloudflare.com/ https://alcdn.msauth.net/ https://*. - @@ -48,8 +48,6 @@ script-src 'self' https://*.cloudflare.com/ https://alcdn.msauth.net/ https://*. crossorigin="anonymous"> - -