diff --git a/build.gradle.kts b/build.gradle.kts index 8f9fe1c..a5a48ef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -57,8 +57,8 @@ dependencies { implementation(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) - val vvVer = "4.3.0-22w19a-SNAPSHOT" - val vbVer = "4.3.0-22w19a-SNAPSHOT" + val vvVer = "4.3.0-1.19-pre1-SNAPSHOT" + val vbVer = "4.3.0-1.19-pre1-SNAPSHOT" val vrVer = "d189537" implementation("com.viaversion:viaversion:$vvVer") { isTransitive = false } implementation("com.viaversion:viabackwards:$vbVer") { isTransitive = false } diff --git a/src/main/java/com/viaversion/aas/codec/MinecraftCodec.java b/src/main/java/com/viaversion/aas/codec/MinecraftCodec.java index 889cc49..17c914d 100644 --- a/src/main/java/com/viaversion/aas/codec/MinecraftCodec.java +++ b/src/main/java/com/viaversion/aas/codec/MinecraftCodec.java @@ -1,5 +1,6 @@ package com.viaversion.aas.codec; +import com.viaversion.aas.UtilKt; import com.viaversion.aas.codec.packet.Packet; import com.viaversion.aas.codec.packet.PacketRegistry; import com.viaversion.aas.handler.MinecraftHandler; @@ -51,6 +52,7 @@ public class MinecraftCodec extends MessageToMessageCodec { handler.getFrontEnd() ? Direction.SERVERBOUND : Direction.CLIENTBOUND )); if (msg.isReadable()) { + UtilKt.getMcLogger().debug("Remaining bytes in packet {}", out); throw new StacklessException("Remaining bytes!!!"); } } diff --git a/src/main/java/com/viaversion/aas/codec/packet/login/LoginStart.java b/src/main/java/com/viaversion/aas/codec/packet/login/LoginStart.java index cf201a1..8dc7a98 100644 --- a/src/main/java/com/viaversion/aas/codec/packet/login/LoginStart.java +++ b/src/main/java/com/viaversion/aas/codec/packet/login/LoginStart.java @@ -4,13 +4,14 @@ import com.viaversion.aas.codec.packet.Packet; import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; import com.viaversion.viaversion.api.type.Type; import com.viaversion.viaversion.api.type.types.StringType; -import com.viaversion.viaversion.libs.opennbt.tag.builtin.CompoundTag; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.NotNull; public class LoginStart implements Packet { private String username; - private CompoundTag publicKey; + private long timestamp; + private byte[] key; + private byte[] signature; public String getUsername() { return username; @@ -25,7 +26,9 @@ public class LoginStart implements Packet { username = new StringType(16).read(byteBuf); if (protocolVersion >= ProtocolVersion.v1_19.getVersion()) { if (byteBuf.readBoolean()) { - publicKey = Type.NBT.read(byteBuf); + timestamp = byteBuf.readLong(); + key = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf); + signature = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf); } } } @@ -34,11 +37,13 @@ public class LoginStart implements Packet { public void encode(@NotNull ByteBuf byteBuf, int protocolVersion) throws Exception { Type.STRING.write(byteBuf, username); if (protocolVersion >= ProtocolVersion.v1_19.getVersion()) { - if (publicKey == null) { + if (key == null) { byteBuf.writeBoolean(false); } else { byteBuf.writeBoolean(true); - Type.NBT.write(byteBuf, publicKey); + byteBuf.writeLong(timestamp); + Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, key); + Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, signature); } } } diff --git a/src/main/kotlin/com/viaversion/aas/VIAaaS.kt b/src/main/kotlin/com/viaversion/aas/VIAaaS.kt index a7104c2..5914455 100644 --- a/src/main/kotlin/com/viaversion/aas/VIAaaS.kt +++ b/src/main/kotlin/com/viaversion/aas/VIAaaS.kt @@ -10,7 +10,7 @@ import com.viaversion.viaversion.api.protocol.version.ProtocolVersion import de.gerrygames.viarewind.api.ViaRewindConfigImpl import io.ktor.server.application.* import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.apache.logging.log4j.Level import org.apache.logging.log4j.io.IoBuilder @@ -21,7 +21,7 @@ fun main(args: Array) { try { setupSystem() printSplash() - CoroutineScope(Dispatchers.IO).launch { viaaasLogger.info(AspirinServer.updaterCheckMessage()) } + CoroutineScope(Job()).launch { viaaasLogger.info(AspirinServer.updaterCheckMessage()) } AspirinServer.generateCert() initVia() AspirinServer.listenPorts(args) diff --git a/src/main/kotlin/com/viaversion/aas/command/sub/VIAaaSSubCommand.kt b/src/main/kotlin/com/viaversion/aas/command/sub/VIAaaSSubCommand.kt index 67cf6b4..b8ce77c 100644 --- a/src/main/kotlin/com/viaversion/aas/command/sub/VIAaaSSubCommand.kt +++ b/src/main/kotlin/com/viaversion/aas/command/sub/VIAaaSSubCommand.kt @@ -4,7 +4,7 @@ import com.viaversion.aas.AspirinServer import com.viaversion.viaversion.api.command.ViaCommandSender import com.viaversion.viaversion.api.command.ViaSubCommand import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch object VIAaaSSubCommand : ViaSubCommand() { @@ -12,7 +12,7 @@ object VIAaaSSubCommand : ViaSubCommand() { override fun description(): String = "Info about VIAaaS" override fun execute(p0: ViaCommandSender, p1: Array): Boolean { p0.sendMessage("VIAaaS version ${AspirinServer.version}") - CoroutineScope(Dispatchers.IO).launch { p0.sendMessage(AspirinServer.updaterCheckMessage()) } + CoroutineScope(Job()).launch { p0.sendMessage(AspirinServer.updaterCheckMessage()) } return true } } \ No newline at end of file diff --git a/src/main/kotlin/com/viaversion/aas/handler/MinecraftHandler.kt b/src/main/kotlin/com/viaversion/aas/handler/MinecraftHandler.kt index 0abcc2f..62755b0 100644 --- a/src/main/kotlin/com/viaversion/aas/handler/MinecraftHandler.kt +++ b/src/main/kotlin/com/viaversion/aas/handler/MinecraftHandler.kt @@ -10,7 +10,7 @@ import io.netty.channel.SimpleChannelInboundHandler import io.netty.handler.proxy.ProxyConnectException import io.netty.handler.proxy.ProxyHandler import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import java.net.SocketAddress import java.nio.channels.ClosedChannelException @@ -22,7 +22,7 @@ class MinecraftHandler( lateinit var endRemoteAddress: SocketAddress val other: Channel? get() = if (frontEnd) data.backChannel else data.frontChannel var loggedDc = false - val coroutineScope = CoroutineScope(Dispatchers.Default) + val coroutineScope = CoroutineScope(Job()) override fun channelRead0(ctx: ChannelHandlerContext, packet: Packet) { if (!ctx.channel().isActive) return diff --git a/src/main/kotlin/com/viaversion/aas/handler/autoprotocol/ProtocolDetector.kt b/src/main/kotlin/com/viaversion/aas/handler/autoprotocol/ProtocolDetector.kt index 79dec5d..3f3d262 100644 --- a/src/main/kotlin/com/viaversion/aas/handler/autoprotocol/ProtocolDetector.kt +++ b/src/main/kotlin/com/viaversion/aas/handler/autoprotocol/ProtocolDetector.kt @@ -24,7 +24,7 @@ import io.netty.channel.ChannelOption import io.netty.handler.timeout.ReadTimeoutHandler import io.netty.resolver.NoopAddressResolverGroup import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.net.InetSocketAddress import java.util.concurrent.CompletableFuture @@ -33,7 +33,7 @@ import java.util.concurrent.TimeUnit object ProtocolDetector { private val loader = CacheLoader.from> { address -> val future = CompletableFuture() - CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Job()).launch { try { val proxyUri = VIAaaSConfig.backendProxy val proxySocket = if (proxyUri == null) null else { diff --git a/src/main/kotlin/com/viaversion/aas/web/AddressInfo.kt b/src/main/kotlin/com/viaversion/aas/web/AddressInfo.kt new file mode 100644 index 0000000..73c0d26 --- /dev/null +++ b/src/main/kotlin/com/viaversion/aas/web/AddressInfo.kt @@ -0,0 +1,10 @@ +package com.viaversion.aas.web + +import com.google.common.net.HostAndPort + +data class AddressInfo( + val backVersion: Int, + val backHostAndPort: HostAndPort, + var frontOnline: Boolean? = null, + var backName: String? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/viaversion/aas/web/UserInfo.kt b/src/main/kotlin/com/viaversion/aas/web/UserInfo.kt new file mode 100644 index 0000000..2e049a3 --- /dev/null +++ b/src/main/kotlin/com/viaversion/aas/web/UserInfo.kt @@ -0,0 +1,5 @@ +package com.viaversion.aas.web + +import java.util.* + +data class UserInfo(val id: UUID, val name: String?, val expiration: Date) \ No newline at end of file diff --git a/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt b/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt index 8ea8760..9f6aa06 100644 --- a/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt +++ b/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt @@ -8,12 +8,14 @@ import com.viaversion.aas.* import com.viaversion.aas.util.StacklessException import com.viaversion.viaversion.api.protocol.version.ProtocolVersion import io.ktor.client.call.* +import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.http.* import io.ktor.server.websocket.* import kotlinx.coroutines.future.await import java.net.URLEncoder import java.time.Duration +import java.time.Instant import java.util.* import kotlin.math.absoluteValue @@ -35,6 +37,7 @@ class WebLogin : WebState { "unlisten_login_requests" -> handleUnlisten(webClient, obj) "session_hash_response" -> handleSessionResponse(webClient, obj) "parameters_response" -> handleParametersResponse(webClient, obj) + "save_access_token" -> handleSaveAccessToken(webClient, obj) else -> throw StacklessException("invalid action!") } @@ -144,7 +147,7 @@ class WebLogin : WebState { private fun handleParametersResponse(webClient: WebClient, obj: JsonObject) { val callback = UUID.fromString(obj["callback"].asString) webClient.server.addressCallbacks[callback].complete( - WebServer.AddressInfo( + AddressInfo( backVersion = obj["version"].asString.let { var protocol = Ints.tryParse(it) if (protocol == null) { @@ -159,4 +162,13 @@ class WebLogin : WebState { ) ) } + + private suspend fun handleSaveAccessToken(webClient: WebClient, obj: JsonObject) { + val accessToken = obj["mc_access_token"].asString + val profile = AspirinServer.httpClient.get("https://api.minecraftservices.com/minecraft/profile") { + header("Authorization", "Bearer $accessToken") + }.body() + val uuid = parseUndashedId(profile["id"].asString) + webClient.server.minecraftAccessTokens.put(uuid, accessToken) + } } diff --git a/src/main/kotlin/com/viaversion/aas/web/WebServer.kt b/src/main/kotlin/com/viaversion/aas/web/WebServer.kt index 69722a5..2d87771 100644 --- a/src/main/kotlin/com/viaversion/aas/web/WebServer.kt +++ b/src/main/kotlin/com/viaversion/aas/web/WebServer.kt @@ -7,13 +7,14 @@ import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import com.google.common.collect.MultimapBuilder import com.google.common.collect.Multimaps -import com.google.common.net.HostAndPort import com.google.gson.JsonObject import com.viaversion.aas.* import com.viaversion.aas.config.VIAaaSConfig import com.viaversion.aas.util.StacklessException import io.ktor.client.call.body import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* import io.ktor.server.netty.* import io.ktor.server.websocket.* import io.ktor.websocket.* @@ -32,12 +33,48 @@ import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit -import kotlin.coroutines.coroutineContext class WebServer { - // I don't think i'll need more than 1k/day val clients = ConcurrentHashMap() val jwtAlgorithm = Algorithm.HMAC256(VIAaaSConfig.jwtSecret) + val coroutineScope = CoroutineScope(Job()) + + // Minecraft account -> WebClient + val listeners = Multimaps.synchronizedSetMultimap( + MultimapBuilder.SetMultimapBuilder + .hashKeys() + .hashSetValues() + .build() + ) + val usernameToIdCache = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build>(CacheLoader.from { name -> + coroutineScope.async { + AspirinServer.httpClient + .get("https://api.mojang.com/users/profiles/minecraft/$name") + .body()?.get("id")?.asString?.let { parseUndashedId(it) } + }.asCompletableFuture() + }) + + val idToProfileCache = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build>(CacheLoader.from { id -> + coroutineScope.async { + AspirinServer.httpClient + .get("https://sessionserver.mojang.com/session/minecraft/profile/$id") + .body() + }.asCompletableFuture() + }) + + val sessionHashCallbacks = CacheBuilder.newBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .build>(CacheLoader.from { _ -> CompletableFuture() }) + val addressCallbacks = CacheBuilder.newBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .build>(CacheLoader.from { _ -> CompletableFuture() }) + val minecraftAccessTokens = CacheBuilder.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) + .build() fun generateToken(account: UUID, username: String): String { return JWT.create() @@ -61,8 +98,6 @@ class WebServer { return generateToken(info.id, name) } - data class UserInfo(val id: UUID, val name: String?, val expiration: Date) - fun parseToken(token: String): UserInfo? { return try { val verified = JWT.require(jwtAlgorithm) @@ -79,47 +114,6 @@ class WebServer { } } - // Minecraft account -> WebClient - val listeners = Multimaps.synchronizedSetMultimap( - MultimapBuilder.SetMultimapBuilder - .hashKeys() - .hashSetValues() - .build() - ) - val usernameToIdCache = CacheBuilder.newBuilder() - .expireAfterWrite(1, TimeUnit.HOURS) - .build>(CacheLoader.from { name -> - CoroutineScope(Dispatchers.IO).async { - AspirinServer.httpClient - .get("https://api.mojang.com/users/profiles/minecraft/$name") - .body()?.get("id")?.asString?.let { parseUndashedId(it) } - }.asCompletableFuture() - }) - - val idToProfileCache = CacheBuilder.newBuilder() - .expireAfterWrite(1, TimeUnit.HOURS) - .build>(CacheLoader.from { id -> - CoroutineScope(Dispatchers.IO).async { - AspirinServer.httpClient - .get("https://sessionserver.mojang.com/session/minecraft/profile/$id") - .body() - }.asCompletableFuture() - }) - - val sessionHashCallbacks = CacheBuilder.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .build>(CacheLoader.from { _ -> CompletableFuture() }) - val addressCallbacks = CacheBuilder.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .build>(CacheLoader.from { _ -> CompletableFuture() }) - - data class AddressInfo( - val backVersion: Int, - val backHostAndPort: HostAndPort, - var frontOnline: Boolean? = null, - var backName: String? = null - ) - suspend fun requestAddressInfo(frontName: String): CompletableFuture { var onlineId: UUID? = null try { @@ -131,7 +125,7 @@ class WebServer { val callbackId = UUID.randomUUID() val future = addressCallbacks.get(callbackId) - CoroutineScope(coroutineContext).apply { + coroutineScope.apply { launch(Dispatchers.IO) { run sending@{ onlineId?.let { @@ -164,16 +158,31 @@ class WebServer { return sent } + suspend fun joinWithCachedToken(playerId: UUID, hash: String): Boolean { + val accessToken = minecraftAccessTokens.getIfPresent(playerId) ?: return false + return AspirinServer.httpClient.post("https://sessionserver.mojang.com/session/minecraft/join") { + setBody(JsonObject().also { + it.addProperty("accessToken", accessToken) + it.addProperty("selectedProfile", playerId.toString().replace("-", "")) + it.addProperty("serverId", hash) + }) + contentType(ContentType.Application.Json) + }.bodyAsText().isEmpty() + } + suspend fun requestSessionJoin( frontName: String, id: UUID, name: String, hash: String, address: SocketAddress, backAddress: SocketAddress ): CompletableFuture { + if (frontName.equals(name, ignoreCase = true) && joinWithCachedToken(id, hash)) { + return CompletableFuture.completedFuture(Unit) + } val future = sessionHashCallbacks[hash] if (!listeners.containsKey(id)) { future.completeExceptionally(StacklessException("UUID $id ($frontName) isn't listened. Go to web auth.")) } else { - CoroutineScope(coroutineContext).apply { + coroutineScope.apply { launch(Dispatchers.IO) { var info: JsonObject? = null var ptr: String? = null diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html index 8ac329b..552dd3e 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/web/index.html @@ -92,19 +92,24 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"

WebSocket connection status: ?

CORS Proxy status: ?


-

Listening to frontend logins from:

+

Listening to front-end logins from:

- - + +

Connecting to backend server:

-
- + + - +
+

You can also use the address + generator to specify the server

-

You can also use address - generator

@@ -256,6 +262,67 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
+ + + +
diff --git a/src/main/resources/web/js/page.js b/src/main/resources/web/js/page.js index cd0ae2f..2e9ee4e 100644 --- a/src/main/resources/web/js/page.js +++ b/src/main/resources/web/js/page.js @@ -2,7 +2,7 @@ let urlParams = new URLSearchParams(); window.location.hash.substring(1).split("?") .map(it => new URLSearchParams(it) - .forEach((a, b) => urlParams.append(b, a))); + .forEach((a, b) => urlParams.append(b, a))); let mcIdUsername = urlParams.get("username"); let mcauth_code = urlParams.get("mcauth_code"); let mcauth_success = urlParams.get("mcauth_success"); @@ -53,6 +53,10 @@ $(() => { $("#form_add_ms").on("submit", () => loginMs()); $("#form_ws_url").on("submit", () => setWsUrl($("#ws-url").val())); $("#form_cors_proxy").on("submit", () => setCorsProxy($("#cors-proxy").val())); + $("#form_listen").on("submit", () => submittedListen()); + $("#form_send_token").on("submit", () => submittedSendToken()); + $("#en_notific").on("click", () => Notification.requestPermission().then(renderActions)); + $("#listen_continue").on("click", () => clickedListenContinue()); ohNo(); @@ -101,40 +105,54 @@ function addMcAccountToList(account) { $(accounts).append(line); } +function addListSendToken(username) { + let line = $(""); + line.text(username); + $("#send_token_user").append(line); +} + function refreshAccountList() { accounts.innerHTML = ""; + $("#send_token_user .mc_username").remove(); getActiveAccounts() - .filter(it => it instanceof MojangAccount) .sort((a, b) => a.name.localeCompare(b.name)) - .forEach(it => addMcAccountToList(it)); - getMicrosoftUsers() - .sort((a, b) => a.localeCompare(b)) - .forEach(username => { - let mcAcc = findAccountByMs(username); - if (!mcAcc) return; - addMcAccountToList(mcAcc); + .forEach(it => { + addMcAccountToList(it) + addListSendToken(it.name) }); } -$("#en_notific").on("click", () => Notification.requestPermission().then(renderActions)); -$("#listen_premium").on("click", () => { - let user = prompt("Premium username (case-sensitive): ", ""); - if (!user) return; - let callbackUrl = new URL(location); - callbackUrl.search = ""; - callbackUrl.hash = "#username=" + encodeURIComponent(user); - location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user) - + "?callback=" + encodeURIComponent(callbackUrl.toString()); -}); -$("#listen_offline").on("click", () => { - let user = prompt("Offline username (case-sensitive): ", ""); - if (!user) return; - let taskId = Math.random(); - workers.forEach(it => it.postMessage({action: "listen_pow", user: user, id: taskId, deltaTime: deltaTime})); - addToast("Offline username", "Please wait a minute..."); -}); $("#mcIdUsername").text(mcIdUsername); -$("#listen_continue").on("click", () => { + +function submittedListen() { + let user = $("#listen_username").val(); + if (!user) return; + if ($("#listen_online")[0].checked) { + let callbackUrl = new URL(location); + callbackUrl.search = ""; + callbackUrl.hash = "#username=" + encodeURIComponent(user); + location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user) + + "?callback=" + encodeURIComponent(callbackUrl.toString()); + } else { + let taskId = Math.random(); + workers.forEach(it => it.postMessage({action: "listen_pow", user: user, id: taskId, deltaTime: deltaTime})); + addToast("Offline username", "Please wait a minute..."); + } +} + +function submittedSendToken() { + findAccountByMcName($("#send_token_user").val()) + .acquireActiveToken() + .then(acc => { + sendSocket(JSON.stringify({ + "action": "save_access_token", + "mc_access_token": acc.accessToken + })) + }) + .catch(e => addToast("Failed to send access token", e)); +} + +function clickedListenContinue() { sendSocket(JSON.stringify({ "action": "minecraft_id_login", "username": mcIdUsername, @@ -142,13 +160,13 @@ $("#listen_continue").on("click", () => { })); mcauth_code = null; renderActions(); -}); +} function renderActions() { $("#en_notific").hide(); $("#listen_continue").hide(); - $("#listen_premium").hide(); - $("#listen_offline").hide(); + $("#listen_open").hide(); + $("#send_token_open").hide(); if (Notification.permission === "default") { $("#en_notific").show(); @@ -157,8 +175,8 @@ function renderActions() { if (mcIdUsername != null && mcauth_code != null) { $("#listen_continue").show(); } - $("#listen_premium").show(); - $("#listen_offline").show(); + $("#listen_open").show(); + $("#send_token_open").show(); } } @@ -358,10 +376,6 @@ function getActiveAccounts() { return activeAccounts; } -function getMicrosoftUsers() { - return (myMSALObj.getAllAccounts() || []).map(it => it.username); -} - class McAccount { constructor(id, username, accessToken) { this.id = id; @@ -589,6 +603,7 @@ function loginMc(user, pass) { function getLoginRequest() { return {scopes: ["XboxLive.signin"]}; } + let redirectUrl = "https://viaversion.github.io/VIAaaS/src/main/resources/web/"; if (location.hostname === "localhost" || whitelistedOrigin.includes(location.origin)) { redirectUrl = location.origin + location.pathname;