diff --git a/src/main/kotlin/com/viaversion/aas/Util.kt b/src/main/kotlin/com/viaversion/aas/Util.kt index 7a56bd1..9aa4aac 100644 --- a/src/main/kotlin/com/viaversion/aas/Util.kt +++ b/src/main/kotlin/com/viaversion/aas/Util.kt @@ -173,3 +173,9 @@ fun generateServerId() = ByteArray(13).let { } fun Int.parseProtocol() = ProtocolVersion.getProtocol(this) + +fun sha512Hex(data: ByteArray): String { + return MessageDigest.getInstance("SHA-512").digest(data) + .asUByteArray() + .joinToString("") { it.toString(16).padStart(2, '0') } +} \ No newline at end of file diff --git a/src/main/kotlin/com/viaversion/aas/web/ViaWebApp.kt b/src/main/kotlin/com/viaversion/aas/web/ViaWebApp.kt index f48cb2e..21a77bb 100644 --- a/src/main/kotlin/com/viaversion/aas/web/ViaWebApp.kt +++ b/src/main/kotlin/com/viaversion/aas/web/ViaWebApp.kt @@ -1,9 +1,9 @@ package com.viaversion.aas.web import com.viaversion.aas.viaWebServer -import com.viaversion.aas.webLogger import io.ktor.application.* import io.ktor.features.* +import io.ktor.http.* import io.ktor.http.cio.websocket.* import io.ktor.http.content.* import io.ktor.routing.* @@ -17,6 +17,11 @@ class ViaWebApp { fun Application.main() { install(DefaultHeaders) install(ConditionalHeaders) + install(CachingHeaders) { + options { + CachingOptions(CacheControl.MaxAge(600, visibility = CacheControl.Visibility.Public)) + } + } install(CallLogging) { level = Level.INFO this.format { diff --git a/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt b/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt index a625244..fb59010 100644 --- a/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt +++ b/src/main/kotlin/com/viaversion/aas/web/WebLogin.kt @@ -2,17 +2,16 @@ package com.viaversion.aas.web import com.google.gson.Gson import com.google.gson.JsonObject -import com.viaversion.aas.generateOfflinePlayerUuid -import com.viaversion.aas.httpClient -import com.viaversion.aas.parseUndashedId +import com.viaversion.aas.* import com.viaversion.aas.util.StacklessException -import com.viaversion.aas.webLogger import io.ktor.client.request.forms.* import io.ktor.http.* import io.ktor.http.cio.websocket.* import kotlinx.coroutines.future.await import java.net.URLEncoder +import java.time.Duration import java.util.* +import kotlin.math.absoluteValue class WebLogin : WebState { override suspend fun start(webClient: WebClient) { @@ -25,6 +24,11 @@ class WebLogin : WebState { 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.ofMinutes(2).toMillis()) { + throw StacklessException("Invalid PoW date") + } val username = obj.get("username").asString.trim() val uuid = generateOfflinePlayerUuid(username) @@ -85,7 +89,7 @@ class WebLogin : WebState { webLogger.info("Listen: ${webClient.id}: $user") } else { response.addProperty("success", false) - webLogger.info("Token fail: ${webClient.id}") + webLogger.info("Listen fail: ${webClient.id}") } webClient.ws.send(response.toString()) } diff --git a/src/main/resources/web/auth.js b/src/main/resources/web/auth.js deleted file mode 100644 index c0ee354..0000000 --- a/src/main/resources/web/auth.js +++ /dev/null @@ -1,470 +0,0 @@ -// SW -if (navigator.serviceWorker) { - navigator.serviceWorker.register("sw.js"); -} - -// Minecraft.id -let urlParams = new URLSearchParams(); -window.location.hash.substr(1).split("?").map(it => new URLSearchParams(it).forEach((a, b) => urlParams.append(b, a))); -var mcIdUsername = urlParams.get("username"); -var mcauth_code = urlParams.get("mcauth_code"); -var mcauth_success = urlParams.get("mcauth_success"); -if (mcauth_success == "false") { - addToast("Couldn't authenticate with Minecraft.ID", urlParams.get("mcauth_msg")); -} -if (mcauth_code != null) { - history.replaceState(null, null, "#"); -} - -// WS url -function defaultWs() { - let url = new URL("ws", new URL(location)); - url.protocol = "wss"; - return window.location.host == "viaversion.github.io" || !window.location.host ? "wss://localhost:25543/ws" : url.toString(); -} -function getWsUrl() { - let url = localStorage.getItem("ws-url") || defaultWs(); - localStorage.setItem("ws-url", url); - return url; -} - -var wsUrl = getWsUrl(); -var socket = null; -var connectionStatus = document.getElementById("connection_status"); -var corsStatus = document.getElementById("cors_status"); -var listening = document.getElementById("listening"); -var actions = document.getElementById("actions"); -var accounts = document.getElementById("accounts-list"); -var listenVisible = false; - -// Util -isMojang = it => !!it.clientToken; -isNotMojang = it => !it.clientToken; -isSuccess = status => status >= 200 && status < 300; -checkFetchSuccess = msg => r => { - if (!isSuccess(r.status)) throw r.status + " " + msg; - return r; -}; -function icanhazip(cors) { - return fetch((cors ? getCorsProxy() : "") + "https://ipv4.icanhazip.com").then(checkFetchSuccess("code")) - .then(r => r.text()).then(it => it.trim()); -} - -// Proxy -function defaultCors() { - return "https://crp123-cors.herokuapp.com/"; -} -function getCorsProxy() { - return localStorage.getItem("cors-proxy") || defaultCors(); -} -function setCorsProxy(url) { - localStorage.setItem("cors-proxy", url); - refreshCorsStatus(); -} - -// Tokens -function saveToken(token) { - let hTokens = JSON.parse(localStorage.getItem("tokens")) || {}; - let tokens = hTokens[wsUrl] || []; - tokens.push(token); - hTokens[wsUrl] = tokens; - localStorage.setItem("tokens", JSON.stringify(hTokens)); -} -function removeToken(token) { - let hTokens = JSON.parse(localStorage.getItem("tokens")) || {}; - let tokens = hTokens[wsUrl] || []; - tokens = tokens.filter(it => it != token); - hTokens[wsUrl] = tokens; - localStorage.setItem("tokens", JSON.stringify(hTokens)); -} -function getTokens() { - return (JSON.parse(localStorage.getItem("tokens")) || {})[wsUrl] || []; -} - -// Accounts -function storeMcAccount(accessToken, clientToken, name, id, msUser = null) { - let accounts = JSON.parse(localStorage.getItem("mc_accounts")) || []; - let account = {accessToken: accessToken, clientToken: clientToken, name: name, id: id, msUser: msUser}; - accounts.push(account); - localStorage.setItem("mc_accounts", JSON.stringify(accounts)); - refreshAccountList(); - return account; -} -function removeMcAccount(id) { - let accounts = JSON.parse(localStorage.getItem("mc_accounts")) || []; - accounts = accounts.filter(it => it.id != id); - localStorage.setItem("mc_accounts", JSON.stringify(accounts)); - refreshAccountList(); -} -function getMcAccounts() { - return JSON.parse(localStorage.getItem("mc_accounts")) || []; -} -function findAccountByMcName(name) { - return getMcAccounts().reverse().find(it => it.name.toLowerCase() == name.toLowerCase()); -} -function findAccountByMs(username) { - return getMcAccounts().filter(isNotMojang).find(it => it.msUser == username); -} - -// Mojang account -function loginMc(user, pass) { - var clientToken = uuid.v4(); - fetch(getCorsProxy() + "https://authserver.mojang.com/authenticate", { - method: "post", - body: JSON.stringify({ - agent: {name: "Minecraft", version: 1}, - username: user, - password: pass, - clientToken: clientToken, - }), - headers: {"content-type": "application/json"} - }).then(checkFetchSuccess("code")) - .then(r => r.json()) - .then(data => { - storeMcAccount(data.accessToken, data.clientToken, data.selectedProfile.name, data.selectedProfile.id); - }).catch(e => addToast("Failed to login", e)); - $("#form_add_mc input").val(""); -} -function logoutMojang(id) { - getMcAccounts().filter(isMojang).filter(it => it.id == id).forEach(it => { - fetch(getCorsProxy() + "https://authserver.mojang.com/invalidate", {method: "post", - body: JSON.stringify({ - accessToken: it.accessToken, - clientToken: it.clientToken - }), - headers: {"content-type": "application/json"} - }) - .then(checkFetchSuccess("not success logout")) - .then(data => removeMcAccount(id)) - .catch(e => { - if (confirm("failed to invalidate token! error: " + e + " remove account?")) { - removeMcAccount(id); - } - }); - }); -} -function refreshMojangAccount(it) { - console.log("refreshing " + it.id); - return fetch(getCorsProxy() + "https://authserver.mojang.com/refresh", { - method: "post", - body: JSON.stringify({ - accessToken: it.accessToken, - clientToken: it.clientToken - }), - headers: {"content-type": "application/json"}, - }).then(checkFetchSuccess("code")) - .then(r => r.json()) - .then(json => { - console.log("refreshed " + json.selectedProfile.id); - removeMcAccount(json.selectedProfile.id); - return storeMcAccount(json.accessToken, json.clientToken, json.selectedProfile.name, json.selectedProfile.id); - }); -} - -// Minecraft api -function getMcUserToken(account) { - return validateToken(account).then(data => { - if (!isSuccess(data.status)) { - if (isMojang(account)) { - return refreshMojangAccount(account); - } else { - return refreshTokenMs(account.msUser); - } - } - return account; - }).catch(e => addToast("Failed to refresh token!", e)); -} -function validateToken(account) { - return fetch(getCorsProxy() + "https://authserver.mojang.com/validate", {method: "post", - body: JSON.stringify({ - accessToken: account.accessToken, - clientToken: account.clientToken || undefined - }), - headers: {"content-type": "application/json"} - }); -} -function joinGame(token, id, hash) { - return fetch(getCorsProxy() + "https://sessionserver.mojang.com/session/minecraft/join", { - method: "post", - body: JSON.stringify({ - accessToken: token, - selectedProfile: id, - serverId: hash - }), - headers: {"content-type": "application/json"} - }); -} - -// html -function refreshCorsStatus() { - corsStatus.innerText = "..."; - icanhazip(true).then(ip => { - return icanhazip(false).then(ip2 => corsStatus.innerText = "OK " + ip + (ip != ip2 ? " (different IP)" : "")); - }).catch(e => corsStatus.innerText = "error: " + e); -} -function addMcAccountToList(id, name, msUser = null) { - let p = document.createElement("p"); - let head = document.createElement("img"); - let n = document.createElement("span"); - let remove = document.createElement("a"); - n.innerText = " " + name + " " + (msUser == null ? "" : "(" + msUser + ") "); - remove.innerText = "Logout"; - remove.href = "javascript:"; - remove.onclick = () => { - if (msUser == null) { - logoutMojang(id); - } else { - logoutMs(msUser); - } - }; - head.className = "account_head"; - head.alt = name + "'s head"; - head.src = (id.length == 36 || id.length == 32) ? "https://crafatar.com/avatars/" + id + "?overlay" : "https://crafthead.net/helm/" + id; - p.append(head); - p.append(n); - p.append(remove); - accounts.appendChild(p); -} -function refreshAccountList() { - accounts.innerHTML = ""; - getMcAccounts().filter(isMojang).sort((a, b) => a.name.localeCompare(b.name)).forEach(it => addMcAccountToList(it.id, it.name)); - (myMSALObj.getAllAccounts() || []).sort((a, b) => a.username.localeCompare(b.username)).forEach(msAccount => { - let mcAcc = findAccountByMs(msAccount.username) || {id: "MHF_Question", name: "..."}; - addMcAccountToList(mcAcc.id, mcAcc.name, msAccount.username); - }); -} -function renderActions() { - actions.innerHTML = ""; - if (Notification.permission == "default") { - actions.innerHTML += '
'; - $("#notificate").on("click", e => Notification.requestPermission().then(renderActions)); // i'm lazy - } - if (listenVisible) { - if (mcIdUsername != null && mcauth_code != null) { - addAction("Listen to " + mcIdUsername, () => { - socket.send(JSON.stringify({ - "action": "minecraft_id_login", - "username": mcIdUsername, - "code": mcauth_code})); - mcauth_code = null; - }); - } - addAction("Listen to premium login in VIAaaS instance", () => { - let user = prompt("Premium username (case-sensitive): ", ""); - if (!user) return; - let callbackUrl = new URL(location); - callbackUrl.search = ""; - callbackUrl.hash = "#username=" + encodeURIComponent(user); - location = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user) - + "?callback=" + encodeURIComponent(callbackUrl); - }); - addAction("Listen to offline login in VIAaaS instance", () => { - let user = prompt("Offline username (case-sensitive):", ""); - if (!user) return; - // todo: do { a = JSON.stringify({username: user, date: Date.now(), rand: Math.random()}) } while (!sha512(a).startsWith("00000")); console.log(a); - socket.send(JSON.stringify({action: "offline_login", username: user})); - }); - } -} -function addAction(text, onClick) { - let p = document.createElement("p"); - let link = document.createElement("a"); - p.appendChild(link); - link.innerText = text; - link.href = "javascript:"; - link.onclick = onClick; - actions.appendChild(p); -} -function addListeningList(user) { - let p = document.createElement("p"); - let head = document.createElement("img"); - let n = document.createElement("span"); - let remove = document.createElement("a"); - n.innerText = " " + user + " "; - remove.innerText = "Unlisten"; - remove.href = "javascript:"; - remove.onclick = () => { - // todo remove the token - listening.removeChild(p); - unlisten(user); - }; - head.className = "account_head"; - head.alt = user + "'s head"; - head.src = "https://crafatar.com/avatars/" + user + "?overlay"; - p.append(head); - p.append(n); - p.append(remove); - listening.appendChild(p); -} -function addToast(title, msg) { - let toast = document.createElement("div"); - document.getElementById("toasts").prepend(toast); - $(toast) - .attr("class", "toast") - .attr("role", "alert") - .attr("aria-live", "assertive") - .attr("aria-atomic", "true") // todo sanitize date \/ - .html(` -