From 9b7821b70444cfaee67f2e4498b89f9f76cddcd3 Mon Sep 17 00:00:00 2001 From: creeper123123321 <7974274+creeper123123321@users.noreply.github.com> Date: Mon, 11 Jul 2022 21:18:35 -0300 Subject: [PATCH] typescript, fix duplicate online mode listening --- .gitignore | 1 + package-lock.json | 180 ++++++ package.json | 17 + src/main/resources/web/index.html | 15 +- src/main/resources/web/js/page.js | 537 ++++++++--------- src/main/resources/web/js/page.ts | 836 +++++++++++++++++++++++++++ src/main/resources/web/js/worker.js | 18 +- src/main/resources/web/tsconfig.json | 19 + 8 files changed, 1303 insertions(+), 320 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/main/resources/web/js/page.ts create mode 100644 src/main/resources/web/tsconfig.json diff --git a/.gitignore b/.gitignore index c8b2f03..29b49ec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /build /config /logs +node_modules/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6259932 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,180 @@ +{ + "name": "viaaas-web", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "viaaas-web", + "devDependencies": { + "@azure/msal-browser": "^2.27.0", + "@types/bootstrap": "^5.1.12", + "@types/jquery": "^3.5.14", + "@types/uuid": "^8.3.4", + "bootstrap": "^5.1.3", + "jquery": "^3.6.0", + "uuid": "^8.3.2" + } + }, + "node_modules/@azure/msal-browser": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.27.0.tgz", + "integrity": "sha512-PyATq2WvK+x32waRqqikym8wvn939iO9UhpFqhLwitNrfLa3PHUgJuuI9oLSQOS3/UzjYb8aqN+XzchU3n/ZuQ==", + "dev": true, + "dependencies": { + "@azure/msal-common": "^7.1.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-7.1.0.tgz", + "integrity": "sha512-WyfqE5mY/rggjqvq0Q5DxLnA33KSb0vfsUjxa95rycFknI03L5GPYI4HTU9D+g0PL5TtsQGnV3xzAGq9BFCVJQ==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", + "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@types/bootstrap": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.12.tgz", + "integrity": "sha512-pSS5BGEgepwzdbsBGswBWFmgrnYpp7c4UfuYe1FJWwkrcjm/JVwfG4gBkOYtd92Otd3RdJK0ByBWMkBROfLEPw==", + "dev": true, + "dependencies": { + "@popperjs/core": "^2.9.2" + } + }, + "node_modules/@types/jquery": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", + "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==", + "dev": true, + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, + "node_modules/bootstrap": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "peerDependencies": { + "@popperjs/core": "^2.10.2" + } + }, + "node_modules/jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + } + }, + "dependencies": { + "@azure/msal-browser": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.27.0.tgz", + "integrity": "sha512-PyATq2WvK+x32waRqqikym8wvn939iO9UhpFqhLwitNrfLa3PHUgJuuI9oLSQOS3/UzjYb8aqN+XzchU3n/ZuQ==", + "dev": true, + "requires": { + "@azure/msal-common": "^7.1.0" + } + }, + "@azure/msal-common": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-7.1.0.tgz", + "integrity": "sha512-WyfqE5mY/rggjqvq0Q5DxLnA33KSb0vfsUjxa95rycFknI03L5GPYI4HTU9D+g0PL5TtsQGnV3xzAGq9BFCVJQ==", + "dev": true + }, + "@popperjs/core": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", + "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==", + "dev": true + }, + "@types/bootstrap": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.12.tgz", + "integrity": "sha512-pSS5BGEgepwzdbsBGswBWFmgrnYpp7c4UfuYe1FJWwkrcjm/JVwfG4gBkOYtd92Otd3RdJK0ByBWMkBROfLEPw==", + "dev": true, + "requires": { + "@popperjs/core": "^2.9.2" + } + }, + "@types/jquery": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", + "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, + "bootstrap": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==", + "dev": true, + "requires": {} + }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..798fbf9 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "description": "Please use Gradle, NPM is mainly used for auto completing on IDE and update checking.", + "name": "viaaas-web", + "private": true, + "devDependencies": { + "@azure/msal-browser": "^2.27.0", + "@types/bootstrap": "^5.1.12", + "@types/jquery": "^3.5.14", + "@types/uuid": "^8.3.4", + "bootstrap": "^5.1.3", + "jquery": "^3.6.0", + "uuid": "^8.3.2" + }, + "scripts": { + "start": "./gradlew run" + } +} diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html index ff9cb77..5486161 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/web/index.html @@ -17,7 +17,7 @@ style-src 'self' https://cdnjs.cloudflare.com/; img-src 'self' data: https://crafthead.net/ https://crafatar.com/; connect-src 'self' http://localhost:*/ https: wss:; -script-src 'self' https://*.cloudflare.com/ https://alcdn.msauth.net/ https://*.cloudflareinsights.com/ 'unsafe-hashes' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='; +script-src 'self' https://*.cloudflare.com/ https://alcdn.msauth.net/ https://*.cloudflareinsights.com/; frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/" http-equiv="Content-Security-Policy"> @@ -26,12 +26,13 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/" - + @@ -41,8 +42,8 @@ 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 e15a3c9..ebefc0f 100644 --- a/src/main/resources/web/js/page.js +++ b/src/main/resources/web/js/page.js @@ -1,12 +1,11 @@ -// Minecraft.id +"use strict"; 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"); - $(() => { if (mcauth_success === "false") { addToast("Couldn't authenticate with Minecraft.ID", urlParams.get("mcauth_msg")); @@ -15,8 +14,6 @@ $(() => { history.replaceState(null, null, "#"); } }); - -// Page let connectionStatus = document.getElementById("connection_status"); let corsStatus = document.getElementById("cors_status"); let listening = document.getElementById("listening"); @@ -24,13 +21,12 @@ let accounts = document.getElementById("accounts-list"); let cors_proxy_txt = document.getElementById("cors-proxy"); let ws_url_txt = document.getElementById("ws-url"); let listenVisible = false; -// + deltaTime means that the clock is ahead let deltaTime = 0; let workers = []; $(() => { workers = new Array(navigator.hardwareConcurrency) .fill(null) - .map(() => new Worker("js/worker.js")) + .map(() => new Worker("js/worker.js")); workers.forEach(it => it.onmessage = onWorkerMsg); }); $(() => { @@ -38,17 +34,13 @@ $(() => { navigator.serviceWorker.register("sw.js") .then(() => setTimeout(() => swRefreshFiles(), 1000)); } -}) - -window.addEventListener('beforeinstallprompt', e => e.preventDefault()); - +}); $(() => { $(".async-css").attr("rel", "stylesheet"); $("form").on("submit", e => e.preventDefault()); - + $("a[href='javascript:']").on("click", e => e.preventDefault()); cors_proxy_txt.value = getCorsProxy(); ws_url_txt.value = getWsUrl(); - $("#form_add_mc").on("submit", () => loginMc($("#mc_email").val(), $("#mc_password").val())); $("#form_add_ms").on("submit", () => loginMs()); $("#form_ws_url").on("submit", () => setWsUrl($("#ws-url").val())); @@ -57,46 +49,40 @@ $(() => { $("#form_send_token").on("submit", () => submittedSendToken()); $("#en_notific").on("click", () => Notification.requestPermission().then(renderActions)); $("#listen_continue").on("click", () => clickedListenContinue()); - + window.addEventListener('beforeinstallprompt', e => e.preventDefault()); ohNo(); - refreshAccountList(); - setInterval(refreshCorsStatus, 10 * 60 * 1000); // Heroku auto sleeps in 30 min + setInterval(refreshCorsStatus, 10 * 60 * 1000); refreshCorsStatus(); resetHtml(); }); - $(() => { connect(); -}) - +}); function swRefreshFiles() { - // https://stackoverflow.com/questions/46830493/is-there-any-way-to-cache-all-files-of-defined-folder-path-in-service-worker navigator.serviceWorker.ready.then(ready => ready.active.postMessage({ action: "cache", urls: performance.getEntriesByType("resource").map(it => it.name) })); } - function setWsStatus(txt) { connectionStatus.innerText = txt; } - function refreshCorsStatus() { corsStatus.innerText = "..."; - icanhazip(true).then(ip => { - return icanhazip(false).then(ip2 => corsStatus.innerText = "OK " + ip + (ip !== ip2 ? " (different IP)" : "")); + getIpAddress(true).then(ip => { + return getIpAddress(false).then(ip2 => corsStatus.innerText = "OK " + ip + (ip !== ip2 ? " (different IP)" : "")); }).catch(e => corsStatus.innerText = "error: " + e); } - function addMcAccountToList(account) { let line = $(`
  • - + ?
  • `); let txt = account.name; - if (account instanceof MicrosoftAccount) txt += " (" + account.msUser + ")"; + if (account instanceof MicrosoftAccount) + txt += " (" + account.msUser + ")"; line.find(".mc-user").text(txt); line.find(".mc-remove").on("click", () => account.logout()); let head = line.find(".mc-head"); @@ -104,14 +90,12 @@ function addMcAccountToList(account) { head.attr("src", "https://crafthead.net/helm/" + account.id); $(accounts).append(line); } - function addUsernameList(username) { let line = $(""); line.text(username); $("#send_token_user").append(line); $("#backend_user_list").append(line.clone()); } - function refreshAccountList() { accounts.innerHTML = ""; $("#send_token_user .mc_username").remove(); @@ -119,41 +103,39 @@ function refreshAccountList() { getActiveAccounts() .sort((a, b) => a.name.localeCompare(b.name)) .forEach(it => { - addMcAccountToList(it) - addUsernameList(it.name) - }); + addMcAccountToList(it); + addUsernameList(it.name); + }); } - $("#mcIdUsername").text(mcIdUsername); - function submittedListen() { let user = $("#listen_username").val(); - if (!user) return; + if (!user) + return; if ($("#listen_online")[0].checked) { - let callbackUrl = new URL(location); + let callbackUrl = new URL(location.href); callbackUrl.search = ""; callbackUrl.hash = "#username=" + encodeURIComponent(user); location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user) + "?callback=" + encodeURIComponent(callbackUrl.toString()); - } else { + } + else { let taskId = Math.random(); - workers.forEach(it => it.postMessage({action: "listen_pow", user: user, id: taskId, deltaTime: deltaTime})); + 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 - })) - }) + let account = findAccountByMcName($("#send_token_user").val()); + account.acquireActiveToken() + .then(() => { + sendSocket(JSON.stringify({ + "action": "save_access_token", + "mc_access_token": account.accessToken + })); + }) .catch(e => addToast("Failed to send access token", e)); } - function clickedListenContinue() { sendSocket(JSON.stringify({ "action": "minecraft_id_login", @@ -163,13 +145,11 @@ function clickedListenContinue() { mcauth_code = null; renderActions(); } - function renderActions() { $("#en_notific").hide(); $("#listen_continue").hide(); $("#listen_open").hide(); $("#send_token_open").hide(); - if (Notification.permission === "default") { $("#en_notific").show(); } @@ -181,31 +161,28 @@ function renderActions() { $("#send_token_open").show(); } } - function onWorkerMsg(e) { - if (e.data.action === "completed_pow") onCompletedPoW(e); + if (e.data.action === "completed_pow") + onCompletedPoW(e); } - function onCompletedPoW(e) { addToast("Offline username", "Completed proof of work"); - workers.forEach(it => it.postMessage({action: "cancel", id: e.data.id})); + workers.forEach(it => it.postMessage({ action: "cancel", id: e.data.id })); sendSocket(e.data.msg); } - -function addListeningList(user, username, token) { - let line = $("

    "); - line.find(".username").text(username || user); +function addListeningList(userId, username, token) { + let line = $("

    ?

    "); + line.find(".username").text(username || userId); line.find(".btn").on("click", () => { removeToken(token); line.remove(); - unlisten(user); + unlisten(userId); }); let head = line.find(".head"); - head.attr("alt", user + "'s head"); - head.attr("src", "https://crafthead.net/helm/" + user); + head.attr("alt", userId + "'s head"); + head.attr("src", "https://crafthead.net/helm/" + userId); $(listening).append(line); } - function addToast(title, msg, yes = null, no = null) { let toast = $(``); toast.find(".toast_title_msg").text(title); - let tBody = toast.find(".toast-body"); tBody.find(".txt").text(msg); - let btns = $(tBody).find(".btns"); let hasButtons = false; if (yes != null) { @@ -239,17 +214,14 @@ function addToast(title, msg, yes = null, no = null) { if (!hasButtons) { btns.addClass("d-none"); } - $("#toasts").prepend(toast); new bootstrap.Toast(toast[0]).show(); } - function resetHtml() { listening.innerHTML = ""; listenVisible = false; renderActions(); } - function ohNo() { try { icanhazepoch().then(sec => { @@ -258,59 +230,56 @@ function ohNo() { addToast("Time isn't synchronized", "Please synchronize your computer time to NTP servers"); deltaTime = calcDelta; console.log("applying delta time " + deltaTime); - } else { + } + else { console.log("time seems synchronized"); } - }) + }); try { new BroadcastChannel("test"); - } catch (e) { + } + catch (e) { addToast("Unsupported browser", "This browser doesn't support required APIs"); } new Date().getDay() === 3 && console.log("it's snapshot day 🐸 my dudes"); new Date().getDate() === 1 && new Date().getMonth() === 3 && addToast("LICENSE EXPIRED", "Your ViaVersion has expired, please renew it at https://viaversion.com/ for only $99"); - } catch (e) { + } + catch (e) { console.log(e); } } - -// Util function checkFetchSuccess(msg) { - return r => { - if (!r.ok) throw r.status + " " + msg; + return (r) => { + if (!r.ok) + throw r.status + " " + msg; return r; }; } - -function icanhazip(cors) { +async function getIpAddress(cors) { return fetch((cors ? getCorsProxy() : "") + "https://ipv4.icanhazip.com") .then(checkFetchSuccess("code")) .then(r => r.text()) .then(it => it.trim()); } - function icanhazepoch() { return fetch("https://icanhazepoch.com") .then(checkFetchSuccess("code")) .then(r => r.text()) - .then(it => parseInt(it.trim())) + .then(it => parseInt(it.trim())); } - -// Notification -let notificationCallbacks = {}; +let notificationCallbacks = new Map(); $(() => { new BroadcastChannel("viaaas-notification").addEventListener("message", handleSWMsg); -}) - +}); function handleSWMsg(event) { console.log("sw msg: " + event); let data = event.data; - let callback = notificationCallbacks[data.tag]; - delete notificationCallbacks[data.tag]; - if (callback == null) return; + let callback = notificationCallbacks.get(data.tag); + notificationCallbacks.delete(data.tag); + if (callback == null) + return; callback(data.action); } - function authNotification(msg, yes, no) { if (!navigator.serviceWorker || Notification.permission !== "granted") { addToast("Allow auth impersonation?", msg, yes, no); @@ -318,66 +287,57 @@ function authNotification(msg, yes, no) { } let tag = uuid.v4(); navigator.serviceWorker.ready.then(r => { - r.showNotification("Click to allow auth impersionation", { + r.showNotification("Click to allow auth impersonation", { body: msg, tag: tag, vibrate: [200, 10, 100, 200, 100, 10, 100, 10, 200], actions: [ - {action: "reject", title: "Reject"}, - {action: "confirm", title: "Confirm"} + { action: "reject", title: "Reject" }, + { action: "confirm", title: "Confirm" } ] }); - notificationCallbacks[tag] = action => { + notificationCallbacks.set(tag, action => { if (action === "reject") { no(); - } else if (!action || action === "confirm") { + } + else if (!action || action === "confirm") { yes(); } - }; + }); setTimeout(() => { - delete notificationCallbacks[tag] + notificationCallbacks.delete(tag); }, 30 * 1000); }); } - -// Cors proxy function defaultCors() { return "https://crp123-cors.herokuapp.com/"; } - function getCorsProxy() { return localStorage.getItem("viaaas_cors_proxy") || defaultCors(); } - function setCorsProxy(url) { localStorage.setItem("viaaas_cors_proxy", url); refreshCorsStatus(); } - -// Account manager let activeAccounts = []; - function loadAccounts() { - (JSON.parse(localStorage.getItem("viaaas_mc_accounts")) || []).forEach(it => { + (JSON.parse(localStorage.getItem("viaaas_mc_accounts")) || []).forEach((it) => { if (it.clientToken) { - addActiveAccount(new MojangAccount(it.id, it.name, it.accessToken, it.clientToken)) - } else if (it.msUser && myMSALObj.getAccountByUsername(it.msUser)) { - addActiveAccount(new MicrosoftAccount(it.id, it.name, it.accessToken, it.msUser)) + addActiveAccount(new MojangAccount(it.id, it.name, it.accessToken, it.clientToken)); } - }) + else if (it.msUser && myMSALObj.getAccountByUsername(it.msUser)) { + addActiveAccount(new MicrosoftAccount(it.id, it.name, it.accessToken, it.msUser)); + } + }); } - $(() => loadAccounts()); - function saveRefreshAccounts() { - localStorage.setItem("viaaas_mc_accounts", JSON.stringify(getActiveAccounts())) - refreshAccountList() + localStorage.setItem("viaaas_mc_accounts", JSON.stringify(getActiveAccounts())); + refreshAccountList(); } - function getActiveAccounts() { return activeAccounts; } - class McAccount { constructor(id, username, accessToken) { this.id = id; @@ -385,232 +345,216 @@ class McAccount { this.accessToken = accessToken; this.loggedOut = false; } - - logout() { + async logout() { activeAccounts = activeAccounts.filter(it => it !== this); saveRefreshAccounts(); this.loggedOut = true; } - - checkActive() { + async checkActive() { return fetch(getCorsProxy() + "https://authserver.mojang.com/validate", { method: "post", body: JSON.stringify({ accessToken: this.accessToken, clientToken: this.clientToken || undefined }), - headers: {"content-type": "application/json"} + headers: { "content-type": "application/json" } }).then(data => data.ok); } - - joinGame(hash) { - return this.acquireActiveToken() + async joinGame(hash) { + await this.acquireActiveToken() .then(() => fetch(getCorsProxy() + "https://sessionserver.mojang.com/session/minecraft/join", { - method: "post", - body: JSON.stringify({ - accessToken: this.accessToken, - selectedProfile: this.id, - serverId: hash - }), - headers: {"content-type": "application/json"} - })).then(checkFetchSuccess("Failed to join session")); + method: "post", + body: JSON.stringify({ + accessToken: this.accessToken, + selectedProfile: this.id, + serverId: hash + }), + headers: { "content-type": "application/json" } + })) + .then(checkFetchSuccess("Failed to join session")); } - - refresh() { + async refresh() { } - - acquireActiveToken() { - return this.checkActive().then(success => { + async acquireActiveToken() { + return this.checkActive() + .then(success => { if (!success) { - return this.refresh().then(() => this); + return this.refresh().then(() => { + }); } - return this; - }).catch(e => addToast("Failed to refresh token!", e)); + return Promise.resolve(); + }) + .catch(e => addToast("Failed to refresh token!", e)); } } - class MojangAccount extends McAccount { constructor(id, username, accessToken, clientToken) { super(id, username, accessToken); this.clientToken = clientToken; } - - logout() { - super.logout(); - fetch(getCorsProxy() + "https://authserver.mojang.com/invalidate", { + async logout() { + await super.logout(); + await fetch(getCorsProxy() + "https://authserver.mojang.com/invalidate", { method: "post", body: JSON.stringify({ accessToken: this.accessToken, clientToken: this.clientToken }), - headers: {"content-type": "application/json"} + headers: { "content-type": "application/json" } }).then(checkFetchSuccess("not success logout")); } - - refresh() { - super.refresh(); - + async refresh() { console.log("refreshing " + this.id); - return fetch(getCorsProxy() + "https://authserver.mojang.com/refresh", { + let jsonResp = await fetch(getCorsProxy() + "https://authserver.mojang.com/refresh", { method: "post", body: JSON.stringify({ accessToken: this.accessToken, clientToken: this.clientToken }), - headers: {"content-type": "application/json"}, + headers: { "content-type": "application/json" }, }) - .then(r => { - if (r.status === 403) { - this.logout(); - throw "403, token expired?"; + .then(async (r) => { + if (r.status === 403) { + try { + await this.logout(); } - return r; - }) + catch (e) { + console.error(e); + } + throw "403, token expired?"; + } + return r; + }) .then(checkFetchSuccess("code")) - .then(r => r.json()) - .then(json => { - console.log("refreshed " + json.selectedProfile.id); - this.accessToken = json.accessToken; - this.clientToken = json.clientToken; - this.name = json.selectedProfile.name; - this.id = json.selectedProfile.id; - saveRefreshAccounts(); - }); + .then(r => r.json()); + console.log("refreshed " + jsonResp.selectedProfile.id); + this.accessToken = jsonResp.accessToken; + this.clientToken = jsonResp.clientToken; + this.name = jsonResp.selectedProfile.name; + this.id = jsonResp.selectedProfile.id; + saveRefreshAccounts(); } } - class MicrosoftAccount extends McAccount { constructor(id, username, accessToken, msUser) { super(id, username, accessToken); this.msUser = msUser; } - - logout() { - super.logout(); - + async logout() { + await super.logout(); let msAccount = myMSALObj.getAccountByUsername(this.msUser); - if (!msAccount) return; - - const logoutRequest = {account: msAccount}; - myMSALObj.logoutPopup(logoutRequest); + if (!msAccount) + return; + const logoutRequest = { account: msAccount }; + await myMSALObj.logoutPopup(logoutRequest); } - - refresh() { - super.refresh(); - return getTokenPopup(this.msUser, getLoginRequest()) - .then(response => fetch("https://user.auth.xboxlive.com/user/authenticate", { - method: "post", - body: JSON.stringify({ - Properties: { - AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", - RpsTicket: "d=" + response.accessToken - }, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT" - }), - headers: {"content-type": "application/json"} - }) - .then(checkFetchSuccess("xbox response not success")) - .then(r => r.json())) - .then(json => fetch("https://xsts.auth.xboxlive.com/xsts/authorize", { - method: "post", - body: JSON.stringify({ - Properties: {SandboxId: "RETAIL", UserTokens: [json.Token]}, - RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT" - }), - headers: {"content-type": "application/json"} - }) - .then(data => { - if (data.status !== 401) return data; - return data.json().then(errorData => { - let error = errorData.XErr; - switch (error) { - case 2148916233: - throw "Xbox account not found"; - case 2148916235: - throw "Xbox Live not available in this country"; - case 2148916238: - throw "Account is underage, add it to a family"; - } - throw "xsts error code " + error; - }); - }) - .then(checkFetchSuccess("xsts response not success")) - .then(r => r.json())) - .then(json => fetch(getCorsProxy() + "https://api.minecraftservices.com/authentication/login_with_xbox", { - method: "post", - body: JSON.stringify({identityToken: "XBL3.0 x=" + json.DisplayClaims.xui[0].uhs + ";" + json.Token}), - headers: {"content-type": "application/json"} - }) - .then(checkFetchSuccess("mc response not success")) - .then(r => r.json())) - .then(json => fetch(getCorsProxy() + "https://api.minecraftservices.com/minecraft/profile", { - method: "get", - headers: {"content-type": "application/json", "authorization": "Bearer " + json.access_token} - }) - .then(profile => { - if (profile.status === 404) throw "Minecraft profile not found"; - if (!profile.ok) throw "profile response not success " + profile.status; - return profile.json(); - }) - .then(jsonProfile => { - this.accessToken = json.access_token; - this.name = jsonProfile.name; - this.id = jsonProfile.id; - saveRefreshAccounts(); - }) - ); + async refresh() { + let msTokenResp = await getTokenPopup(this.msUser, getLoginRequest()); + let xboxJson = await fetch("https://user.auth.xboxlive.com/user/authenticate", { + method: "post", + body: JSON.stringify({ + Properties: { + AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", + RpsTicket: "d=" + msTokenResp.accessToken + }, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT" + }), + headers: { "content-type": "application/json" } + }) + .then(checkFetchSuccess("xbox response not success")) + .then(r => r.json()); + let xstsJson = await fetch("https://xsts.auth.xboxlive.com/xsts/authorize", { + method: "post", + body: JSON.stringify({ + Properties: { SandboxId: "RETAIL", UserTokens: [xboxJson.Token] }, + RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT" + }), + headers: { "content-type": "application/json" } + }) + .then(resp => { + if (resp.status !== 401) + return resp; + return resp.json().then(errorData => { + let error = errorData.XErr; + switch (error) { + case 2148916233: + throw "Xbox account not found"; + case 2148916235: + throw "Xbox Live not available in this country"; + case 2148916238: + throw "Account is underage, add it to a family"; + } + throw "xsts error code " + error; + }); + }) + .then(checkFetchSuccess("xsts response not success")) + .then(r => r.json()); + let mcJson = await fetch(getCorsProxy() + "https://api.minecraftservices.com/authentication/login_with_xbox", { + method: "post", + body: JSON.stringify({ identityToken: "XBL3.0 x=" + xstsJson.DisplayClaims.xui[0].uhs + ";" + xstsJson.Token }), + headers: { "content-type": "application/json" } + }) + .then(checkFetchSuccess("mc response not success")) + .then(r => r.json()); + let jsonProfile = await fetch(getCorsProxy() + "https://api.minecraftservices.com/minecraft/profile", { + method: "get", + headers: { "content-type": "application/json", "authorization": "Bearer " + mcJson.access_token } + }) + .then(profile => { + if (profile.status === 404) + throw "Minecraft profile not found"; + if (!profile.ok) + throw "profile response not success " + profile.status; + return profile.json(); + }); + this.accessToken = mcJson.access_token; + this.name = jsonProfile.name; + this.id = jsonProfile.id; + saveRefreshAccounts(); } - - checkActive() { + async checkActive() { return fetch(getCorsProxy() + "https://api.minecraftservices.com/entitlements/mcstore", { method: "get", - headers: {"authorization": "Bearer " + this.accessToken} + headers: { "authorization": "Bearer " + this.accessToken } }).then(data => data.ok); } } - function findAccountByMcName(name) { return activeAccounts.find(it => it.name.toLowerCase() === name.toLowerCase()); } - function findAccountByMs(username) { return getActiveAccounts().find(it => it.msUser === username); } - function addActiveAccount(acc) { - activeAccounts.push(acc) - saveRefreshAccounts() + activeAccounts.push(acc); + saveRefreshAccounts(); } - function loginMc(user, pass) { const clientToken = uuid.v4(); fetch(getCorsProxy() + "https://authserver.mojang.com/authenticate", { method: "post", body: JSON.stringify({ - agent: {name: "Minecraft", version: 1}, + agent: { name: "Minecraft", version: 1 }, username: user, password: pass, clientToken: clientToken, }), - headers: {"content-type": "application/json"} + headers: { "content-type": "application/json" } }).then(checkFetchSuccess("code")) .then(r => r.json()) .then(data => { - let acc = new MojangAccount(data.selectedProfile.id, data.selectedProfile.name, data.accessToken, data.clientToken); - addActiveAccount(acc); - return acc; - }).catch(e => addToast("Failed to login", e)); + let acc = new MojangAccount(data.selectedProfile.id, data.selectedProfile.name, data.accessToken, data.clientToken); + addActiveAccount(acc); + return acc; + }).catch(e => addToast("Failed to login", e)); $("#form_add_mc input").val(""); } - function getLoginRequest() { - return {scopes: ["XboxLive.signin"]}; + 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; } - const msalConfig = { auth: { clientId: azureClientId, @@ -622,65 +566,56 @@ const msalConfig = { storeAuthStateInCookie: false, } }; - const myMSALObj = new msal.PublicClientApplication(msalConfig); - function loginMs() { let req = getLoginRequest(); - req.prompt = "select_account"; + req["prompt"] = "select_account"; myMSALObj.loginRedirect(req); } - $(() => myMSALObj.handleRedirectPromise().then((resp) => { if (resp) { - let found = findAccountByMs(resp.account.username) + let found = findAccountByMs(resp.account.username); if (!found) { let accNew = new MicrosoftAccount("", "", "", resp.account.username); accNew.refresh() .then(() => addActiveAccount(accNew)) .catch(e => addToast("Failed to get token", e)); - } else { + } + else { found.refresh() .catch(e => addToast("Failed to refresh token", e)); } } })); - function getTokenPopup(username, request) { request.account = myMSALObj.getAccountByUsername(username); request.loginHint = username; - return myMSALObj.acquireTokenSilent(request).catch(error => { + return myMSALObj.acquireTokenSilent(request) + .catch((e) => { console.warn("silent token acquisition fails."); if (error instanceof msal.InteractionRequiredAuthError) { - // fallback to interaction when silent call fails - return myMSALObj.acquireTokenPopup(request).catch(error => console.error(error)); - } else { - console.warn(error); + return myMSALObj.acquireTokenPopup(request).catch((error) => console.error(error)); + } + else { + console.warn(e); } }); } - -// Websocket let wsUrl = getWsUrl(); let socket = null; - function defaultWs() { - let url = new URL("ws", new URL(location)); + let url = new URL("ws", location.href); url.protocol = "wss"; return window.location.host.endsWith("github.io") || !window.location.protocol.startsWith("http") ? "wss://localhost:25543/ws" : url.toString(); } - function getWsUrl() { return localStorage.getItem("viaaas_ws_url") || defaultWs(); } - function setWsUrl(url) { localStorage.setItem("viaaas_ws_url", url); location.reload(); } - -// Tokens function saveToken(token) { let hTokens = JSON.parse(localStorage.getItem("viaaas_tokens")) || {}; let tokens = getTokens(); @@ -688,7 +623,6 @@ function saveToken(token) { hTokens[wsUrl] = tokens; localStorage.setItem("viaaas_tokens", JSON.stringify(hTokens)); } - function removeToken(token) { let hTokens = JSON.parse(localStorage.getItem("viaaas_tokens")) || {}; let tokens = getTokens(); @@ -696,42 +630,35 @@ function removeToken(token) { hTokens[wsUrl] = tokens; localStorage.setItem("viaaas_tokens", JSON.stringify(hTokens)); } - function getTokens() { return (JSON.parse(localStorage.getItem("viaaas_tokens")) || {})[wsUrl] || []; } - -// Websocket function listen(token) { - socket.send(JSON.stringify({"action": "listen_login_requests", "token": token})); + socket.send(JSON.stringify({ "action": "listen_login_requests", "token": token })); } - function unlisten(id) { - socket.send(JSON.stringify({"action": "unlisten_login_requests", "uuid": id})); + socket.send(JSON.stringify({ "action": "unlisten_login_requests", "uuid": id })); } - function confirmJoin(hash) { - socket.send(JSON.stringify({action: "session_hash_response", session_hash: hash})); + socket.send(JSON.stringify({ action: "session_hash_response", session_hash: hash })); } - function handleJoinRequest(parsed) { authNotification("Allow auth impersonation from VIAaaS instance?\nAccount: " + parsed.user + "\nServer Message: \n" - + parsed.message.split(/[\r\n]+/).map(it => "> " + it).join('\n'), () => { + + parsed.message.split(/[\r\n]+/).map((it) => "> " + it).join('\n'), () => { let account = findAccountByMcName(parsed.user); if (account) { account.joinGame(parsed.session_hash) - .then(checkFetchSuccess("code")) .finally(() => confirmJoin(parsed.session_hash)) .catch((e) => addToast("Couldn't contact session server", "Error: " + e)); - } else { + } + else { confirmJoin(parsed.session_hash); addToast("Couldn't find account", "Couldn't find " + parsed.user + ", check Accounts tab"); } }, () => confirmJoin(parsed.session_hash)); } - -function onSocketMsg(event) { +function onWsMsg(event) { let parsed = JSON.parse(event.data); switch (parsed.action) { case "ad_login_methods": @@ -741,7 +668,8 @@ function onSocketMsg(event) { case "login_result": if (!parsed.success) { addToast("Couldn't verify Minecraft account", "VIAaaS returned failed response"); - } else { + } + else { listen(parsed.token); saveToken(parsed.token); } @@ -749,7 +677,8 @@ function onSocketMsg(event) { case "listen_login_requests_result": if (parsed.success) { addListeningList(parsed.user, parsed.username, parsed.token); - } else { + } + else { removeToken(parsed.token); } break; @@ -761,7 +690,6 @@ function onSocketMsg(event) { break; } } - function handleParametersRequest(parsed) { let url = new URL("https://" + $("#connect_address").val()); socket.send(JSON.stringify({ @@ -769,48 +697,41 @@ function handleParametersRequest(parsed) { callback: parsed["callback"], version: $("#connect_version").val(), host: url.hostname, - port: parseInt(url.port || 25565), + port: parseInt(url.port) || 25565, frontOnline: $("#connect_online").val(), backName: $("#connect_user").val() || undefined })); } - function listenStoredTokens() { getTokens().forEach(listen); } - -function onConnect() { +function onWsConnect() { setWsStatus("connected"); resetHtml(); listenStoredTokens(); } - function onWsError(e) { console.log(e); setWsStatus("socket error"); resetHtml(); } - -function onDisconnect(evt) { +function onWsClose(evt) { setWsStatus("disconnected with close code " + evt.code + " and reason: " + evt.reason); resetHtml(); setTimeout(connect, 5000); } - function connect() { setWsStatus("connecting..."); socket = new WebSocket(wsUrl); - socket.onerror = onWsError; - socket.onopen = onConnect; - socket.onclose = onDisconnect - socket.onmessage = onSocketMsg; + socket.onopen = onWsConnect; + socket.onclose = onWsClose; + socket.onmessage = onWsMsg; } - function sendSocket(msg) { if (!socket) { console.error("couldn't send msg, socket isn't set"); - return + return; } socket.send(msg); } diff --git a/src/main/resources/web/js/page.ts b/src/main/resources/web/js/page.ts new file mode 100644 index 0000000..8ae34e1 --- /dev/null +++ b/src/main/resources/web/js/page.ts @@ -0,0 +1,836 @@ +/// +"use strict" + +// Minecraft.id +let urlParams = new URLSearchParams(); +window.location.hash.substring(1).split("?") + .map(it => new URLSearchParams(it) + .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"); + +$(() => { + if (mcauth_success === "false") { + addToast("Couldn't authenticate with Minecraft.ID", urlParams.get("mcauth_msg")); + } + if (mcauth_code != null) { + history.replaceState(null, null, "#"); + } +}); + +// Page +let connectionStatus = document.getElementById("connection_status"); +let corsStatus = document.getElementById("cors_status"); +let listening = document.getElementById("listening"); +let accounts = document.getElementById("accounts-list"); +let cors_proxy_txt = document.getElementById("cors-proxy") as HTMLInputElement; +let ws_url_txt = document.getElementById("ws-url") as HTMLInputElement; +let listenVisible = false; +// + deltaTime means that the clock is ahead +let deltaTime = 0; +let workers: Array = []; +$(() => { + workers = new Array(navigator.hardwareConcurrency) + .fill(null) + .map(() => new Worker("js/worker.js")) + workers.forEach(it => it.onmessage = onWorkerMsg); +}); +$(() => { + if (navigator.serviceWorker) { + navigator.serviceWorker.register("sw.js") + .then(() => setTimeout(() => swRefreshFiles(), 1000)); + } +}) + +$(() => { + $(".async-css").attr("rel", "stylesheet"); + $("form").on("submit", e => e.preventDefault()); + $("a[href='javascript:']").on("click", e => e.preventDefault()); + + cors_proxy_txt.value = getCorsProxy(); + ws_url_txt.value = getWsUrl(); + + $("#form_add_mc").on("submit", () => loginMc($("#mc_email").val() as string, $("#mc_password").val() as string)); + $("#form_add_ms").on("submit", () => loginMs()); + $("#form_ws_url").on("submit", () => setWsUrl($("#ws-url").val() as string)); + $("#form_cors_proxy").on("submit", () => setCorsProxy($("#cors-proxy").val() as string)); + $("#form_listen").on("submit", () => submittedListen()); + $("#form_send_token").on("submit", () => submittedSendToken()); + $("#en_notific").on("click", () => Notification.requestPermission().then(renderActions)); + $("#listen_continue").on("click", () => clickedListenContinue()); + window.addEventListener('beforeinstallprompt', e => e.preventDefault()); + + ohNo(); + + refreshAccountList(); + setInterval(refreshCorsStatus, 10 * 60 * 1000); // Heroku auto sleeps in 30 min + refreshCorsStatus(); + resetHtml(); +}); + +$(() => { + connect(); +}) + +function swRefreshFiles() { + // https://stackoverflow.com/questions/46830493/is-there-any-way-to-cache-all-files-of-defined-folder-path-in-service-worker + navigator.serviceWorker.ready.then(ready => ready.active.postMessage({ + action: "cache", + urls: performance.getEntriesByType("resource").map(it => it.name) + })); +} + +function setWsStatus(txt: string) { + connectionStatus.innerText = txt; +} + +function refreshCorsStatus() { + corsStatus.innerText = "..."; + getIpAddress(true).then(ip => { + return getIpAddress(false).then(ip2 => corsStatus.innerText = "OK " + ip + (ip !== ip2 ? " (different IP)" : "")); + }).catch(e => corsStatus.innerText = "error: " + e); +} + +function addMcAccountToList(account: McAccount) { + let line = $(`
  • + ? + + +
  • `); + let txt = account.name; + if (account instanceof MicrosoftAccount) txt += " (" + account.msUser + ")"; + line.find(".mc-user").text(txt); + line.find(".mc-remove").on("click", () => account.logout()); + let head = line.find(".mc-head"); + head.attr("alt", account.name + "'s head"); + head.attr("src", "https://crafthead.net/helm/" + account.id); + $(accounts).append(line); +} + +function addUsernameList(username: string) { + let line = $(""); + line.text(username); + $("#send_token_user").append(line); + $("#backend_user_list").append(line.clone()); +} + +function refreshAccountList() { + accounts.innerHTML = ""; + $("#send_token_user .mc_username").remove(); + $("#backend_user_list .mc_username").remove(); + getActiveAccounts() + .sort((a, b) => a.name.localeCompare(b.name)) + .forEach(it => { + addMcAccountToList(it) + addUsernameList(it.name) + }); +} + +$("#mcIdUsername").text(mcIdUsername); + +function submittedListen() { + let user = $("#listen_username").val() as string; + if (!user) return; + if (($("#listen_online")[0] as HTMLInputElement).checked) { + let callbackUrl = new URL(location.href); + 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() { + let account = findAccountByMcName($("#send_token_user").val() as string) + account.acquireActiveToken() + .then(() => { + sendSocket(JSON.stringify({ + "action": "save_access_token", + "mc_access_token": account.accessToken + })) + }) + .catch(e => addToast("Failed to send access token", e)); +} + +function clickedListenContinue() { + sendSocket(JSON.stringify({ + "action": "minecraft_id_login", + "username": mcIdUsername, + "code": mcauth_code + })); + mcauth_code = null; + renderActions(); +} + +function renderActions() { + $("#en_notific").hide(); + $("#listen_continue").hide(); + $("#listen_open").hide(); + $("#send_token_open").hide(); + + if (Notification.permission === "default") { + $("#en_notific").show(); + } + if (listenVisible) { + if (mcIdUsername != null && mcauth_code != null) { + $("#listen_continue").show(); + } + $("#listen_open").show(); + $("#send_token_open").show(); + } +} + +function onWorkerMsg(e: MessageEvent) { + if (e.data.action === "completed_pow") onCompletedPoW(e); +} + +function onCompletedPoW(e: MessageEvent) { + addToast("Offline username", "Completed proof of work"); + workers.forEach(it => it.postMessage({action: "cancel", id: e.data.id})); + sendSocket(e.data.msg); +} + +function addListeningList(userId: string, username: string, token: string) { + let line = $("

    ?

    "); + line.find(".username").text(username || userId); + line.find(".btn").on("click", () => { + removeToken(token); + line.remove(); + unlisten(userId); + }); + let head = line.find(".head"); + head.attr("alt", userId + "'s head"); + head.attr("src", "https://crafthead.net/helm/" + userId); + $(listening).append(line); +} + +function addToast(title: string, msg: string, yes: () => void = null, no: () => void = null) { + let toast = $(``); + toast.find(".toast_title_msg").text(title); + + let tBody = toast.find(".toast-body"); + tBody.find(".txt").text(msg); + + let btns = $(tBody).find(".btns"); + let hasButtons = false; + if (yes != null) { + hasButtons = true; + let btn = $(""); + btn.on("click", yes); + btns.append(btn); + } + if (no != null) { + hasButtons = true; + let btn = $(""); + btn.on("click", no); + btns.append(btn); + } + if (!hasButtons) { + btns.addClass("d-none"); + } + + $("#toasts").prepend(toast); + // @ts-ignore + new bootstrap.Toast(toast[0]).show(); +} + +function resetHtml() { + listening.innerHTML = ""; + listenVisible = false; + renderActions(); +} + +function ohNo() { + try { + icanhazepoch().then(sec => { + const calcDelta = Date.now() - sec * 1000; + if (Math.abs(calcDelta) > 10000) { + addToast("Time isn't synchronized", "Please synchronize your computer time to NTP servers"); + deltaTime = calcDelta; + console.log("applying delta time " + deltaTime); + } else { + console.log("time seems synchronized"); + } + }) + try { + new BroadcastChannel("test"); + } catch (e) { + addToast("Unsupported browser", "This browser doesn't support required APIs"); + } + new Date().getDay() === 3 && console.log("it's snapshot day 🐸 my dudes"); + new Date().getDate() === 1 && new Date().getMonth() === 3 && addToast("LICENSE EXPIRED", "Your ViaVersion has expired, please renew it at https://viaversion.com/ for only $99"); + } catch (e) { + console.log(e); + } +} + +// Util +function checkFetchSuccess(msg: String): (a: Response) => Response { + return (r: Response): Response => { + if (!r.ok) throw r.status + " " + msg; + return r; + }; +} + +async function getIpAddress(cors: boolean): Promise { + return fetch((cors ? getCorsProxy() : "") + "https://ipv4.icanhazip.com") + .then(checkFetchSuccess("code")) + .then(r => r.text()) + .then(it => it.trim()); +} + +function icanhazepoch() { + return fetch("https://icanhazepoch.com") + .then(checkFetchSuccess("code")) + .then(r => r.text()) + .then(it => parseInt(it.trim())) +} + +// Notification +let notificationCallbacks: Map void> = new Map(); +$(() => { + new BroadcastChannel("viaaas-notification").addEventListener("message", handleSWMsg); +}) + +function handleSWMsg(event: MessageEvent) { + console.log("sw msg: " + event); + let data = event.data; + let callback = notificationCallbacks.get(data.tag as string); + notificationCallbacks.delete(data.tag as string); + if (callback == null) return; + callback(data.action); +} + +function authNotification(msg: string, yes: () => void, no: () => void) { + if (!navigator.serviceWorker || Notification.permission !== "granted") { + addToast("Allow auth impersonation?", msg, yes, no); + return; + } + // @ts-ignore + let tag = uuid.v4(); + navigator.serviceWorker.ready.then(r => { + r.showNotification("Click to allow auth impersonation", { + body: msg, + tag: tag, + vibrate: [200, 10, 100, 200, 100, 10, 100, 10, 200], + actions: [ + {action: "reject", title: "Reject"}, + {action: "confirm", title: "Confirm"} + ] + }); + notificationCallbacks.set(tag, action => { + if (action === "reject") { + no(); + } else if (!action || action === "confirm") { + yes(); + } + }); + setTimeout(() => { + notificationCallbacks.delete(tag); + }, 30 * 1000); + }); +} + +// Cors proxy +function defaultCors() { + return "https://crp123-cors.herokuapp.com/"; +} + +function getCorsProxy() { + return localStorage.getItem("viaaas_cors_proxy") || defaultCors(); +} + +function setCorsProxy(url: string) { + localStorage.setItem("viaaas_cors_proxy", url); + refreshCorsStatus(); +} + +// Account manager +let activeAccounts: Array = []; + +function loadAccounts() { + (JSON.parse(localStorage.getItem("viaaas_mc_accounts")) || []).forEach((it: any) => { + if (it.clientToken) { + addActiveAccount(new MojangAccount(it.id, it.name, it.accessToken, it.clientToken)) + } else if (it.msUser && myMSALObj.getAccountByUsername(it.msUser)) { + addActiveAccount(new MicrosoftAccount(it.id, it.name, it.accessToken, it.msUser)) + } + }) +} + +$(() => loadAccounts()); + +function saveRefreshAccounts() { + localStorage.setItem("viaaas_mc_accounts", JSON.stringify(getActiveAccounts())) + refreshAccountList() +} + +function getActiveAccounts() { + return activeAccounts; +} + +class McAccount { + public id: string; + public name: string; + public accessToken: string; + public loggedOut: boolean; + + constructor(id: string, username: string, accessToken: string) { + this.id = id; + this.name = username; + this.accessToken = accessToken; + this.loggedOut = false; + } + + async logout(): Promise { + activeAccounts = activeAccounts.filter(it => it !== this); + saveRefreshAccounts(); + this.loggedOut = true; + } + + async checkActive(): Promise { + return fetch(getCorsProxy() + "https://authserver.mojang.com/validate", { + method: "post", + body: JSON.stringify({ + accessToken: this.accessToken, + clientToken: (this as any).clientToken || undefined + }), + headers: {"content-type": "application/json"} + }).then(data => data.ok); + } + + async joinGame(hash: string): Promise { + await this.acquireActiveToken() + .then(() => fetch(getCorsProxy() + "https://sessionserver.mojang.com/session/minecraft/join", { + method: "post", + body: JSON.stringify({ + accessToken: this.accessToken, + selectedProfile: this.id, + serverId: hash + }), + headers: {"content-type": "application/json"} + })) + .then(checkFetchSuccess("Failed to join session")); + } + + async refresh(): Promise { + } + + async acquireActiveToken(): Promise { + return this.checkActive() + .then(success => { + if (!success) { + return this.refresh().then(() => { + }); + } + return Promise.resolve(); + }) + .catch(e => addToast("Failed to refresh token!", e)); + } +} + +class MojangAccount extends McAccount { + public clientToken: string; + + constructor(id: string, username: string, accessToken: string, clientToken: string) { + super(id, username, accessToken); + this.clientToken = clientToken; + } + + override async logout() { + await super.logout(); + await fetch(getCorsProxy() + "https://authserver.mojang.com/invalidate", { + method: "post", + body: JSON.stringify({ + accessToken: this.accessToken, + clientToken: this.clientToken + }), + headers: {"content-type": "application/json"} + }).then(checkFetchSuccess("not success logout")); + } + + override async refresh() { + console.log("refreshing " + this.id); + let jsonResp = await fetch(getCorsProxy() + "https://authserver.mojang.com/refresh", { + method: "post", + body: JSON.stringify({ + accessToken: this.accessToken, + clientToken: this.clientToken + }), + headers: {"content-type": "application/json"}, + }) + .then(async r => { + if (r.status === 403) { + try { + await this.logout(); + } catch (e) { + console.error(e); + } + throw "403, token expired?"; + } + return r; + }) + .then(checkFetchSuccess("code")) + .then(r => r.json()); + + console.log("refreshed " + jsonResp.selectedProfile.id); + this.accessToken = jsonResp.accessToken; + this.clientToken = jsonResp.clientToken; + this.name = jsonResp.selectedProfile.name; + this.id = jsonResp.selectedProfile.id; + saveRefreshAccounts(); + } +} + +class MicrosoftAccount extends McAccount { + public msUser: string; + + constructor(id: string, username: string, accessToken: string, msUser: string) { + super(id, username, accessToken); + this.msUser = msUser; + } + + override async logout() { + await super.logout(); + + let msAccount = myMSALObj.getAccountByUsername(this.msUser); + if (!msAccount) return; + + const logoutRequest = {account: msAccount}; + await myMSALObj.logoutPopup(logoutRequest); + } + + override async refresh(): Promise { + let msTokenResp = await getTokenPopup(this.msUser, getLoginRequest()); + // noinspection HttpUrlsUsage + let xboxJson = await fetch("https://user.auth.xboxlive.com/user/authenticate", { + method: "post", + body: JSON.stringify({ + Properties: { + AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", + RpsTicket: "d=" + msTokenResp.accessToken + }, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT" + }), + headers: {"content-type": "application/json"} + }) + .then(checkFetchSuccess("xbox response not success")) + .then(r => r.json()); + let xstsJson = await fetch("https://xsts.auth.xboxlive.com/xsts/authorize", { + method: "post", + body: JSON.stringify({ + Properties: {SandboxId: "RETAIL", UserTokens: [xboxJson.Token]}, + RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT" + }), + headers: {"content-type": "application/json"} + }) + .then(resp => { + if (resp.status !== 401) return resp; + return resp.json().then(errorData => { + let error = errorData.XErr; + switch (error) { + case 2148916233: + throw "Xbox account not found"; + case 2148916235: + throw "Xbox Live not available in this country"; + case 2148916238: + throw "Account is underage, add it to a family"; + } + throw "xsts error code " + error; + }); + }) + .then(checkFetchSuccess("xsts response not success")) + .then(r => r.json()); + let mcJson = await fetch(getCorsProxy() + "https://api.minecraftservices.com/authentication/login_with_xbox", { + method: "post", + body: JSON.stringify({identityToken: "XBL3.0 x=" + xstsJson.DisplayClaims.xui[0].uhs + ";" + xstsJson.Token}), + headers: {"content-type": "application/json"} + }) + .then(checkFetchSuccess("mc response not success")) + .then(r => r.json()); + let jsonProfile = await fetch(getCorsProxy() + "https://api.minecraftservices.com/minecraft/profile", { + method: "get", + headers: {"content-type": "application/json", "authorization": "Bearer " + mcJson.access_token} + }) + .then(profile => { + if (profile.status === 404) throw "Minecraft profile not found"; + if (!profile.ok) throw "profile response not success " + profile.status; + return profile.json(); + }); + + this.accessToken = mcJson.access_token; + this.name = jsonProfile.name; + this.id = jsonProfile.id; + saveRefreshAccounts(); + } + + override async checkActive() { + return fetch(getCorsProxy() + "https://api.minecraftservices.com/entitlements/mcstore", { + method: "get", + headers: {"authorization": "Bearer " + this.accessToken} + }).then(data => data.ok); + } +} + +function findAccountByMcName(name: string) { + return activeAccounts.find(it => it.name.toLowerCase() === name.toLowerCase()); +} + +function findAccountByMs(username: string) { + return getActiveAccounts().find(it => (it as any).msUser === username); +} + +function addActiveAccount(acc: McAccount) { + activeAccounts.push(acc) + saveRefreshAccounts() +} + +function loginMc(user: string, pass: string) { + // @ts-ignore + const 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 => { + let acc = new MojangAccount(data.selectedProfile.id, data.selectedProfile.name, data.accessToken, data.clientToken); + addActiveAccount(acc); + return acc; + }).catch(e => addToast("Failed to login", e)); + $("#form_add_mc input").val(""); +} + +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; +} + +const msalConfig = { + auth: { + clientId: azureClientId, + authority: "https://login.microsoftonline.com/consumers/", + redirectUri: redirectUrl, + }, + cache: { + cacheLocation: "localStorage", + storeAuthStateInCookie: false, + } +}; + +// @ts-ignore +const myMSALObj = new msal.PublicClientApplication(msalConfig); + +function loginMs() { + let req = getLoginRequest(); + (req as any)["prompt"] = "select_account"; + myMSALObj.loginRedirect(req); +} + +$(() => myMSALObj.handleRedirectPromise().then((resp: any) => { + if (resp) { + let found = findAccountByMs(resp.account.username) + if (!found) { + let accNew = new MicrosoftAccount("", "", "", resp.account.username); + accNew.refresh() + .then(() => addActiveAccount(accNew)) + .catch(e => addToast("Failed to get token", e)); + } else { + found.refresh() + .catch(e => addToast("Failed to refresh token", e)); + } + } +})); + +function getTokenPopup(username: string, request: any) { + request.account = myMSALObj.getAccountByUsername(username); + request.loginHint = username; + return myMSALObj.acquireTokenSilent(request) + .catch((e: any) => { + console.warn("silent token acquisition fails."); + // @ts-ignore + if (error instanceof msal.InteractionRequiredAuthError) { + // fallback to interaction when silent call fails + return myMSALObj.acquireTokenPopup(request).catch((error: any) => console.error(error)); + } else { + console.warn(e); + } + }); +} + +// Websocket +let wsUrl = getWsUrl(); +let socket: WebSocket | null = null; + +function defaultWs() { + let url = new URL("ws", location.href); + url.protocol = "wss"; + return window.location.host.endsWith("github.io") || !window.location.protocol.startsWith("http") + ? "wss://localhost:25543/ws" : url.toString(); +} + +function getWsUrl() { + return localStorage.getItem("viaaas_ws_url") || defaultWs(); +} + +function setWsUrl(url: string) { + localStorage.setItem("viaaas_ws_url", url); + location.reload(); +} + +// Tokens +function saveToken(token: string) { + let hTokens = JSON.parse(localStorage.getItem("viaaas_tokens")) || {}; + let tokens = getTokens(); + tokens.push(token); + hTokens[wsUrl] = tokens; + localStorage.setItem("viaaas_tokens", JSON.stringify(hTokens)); +} + +function removeToken(token: string) { + let hTokens = JSON.parse(localStorage.getItem("viaaas_tokens")) || {}; + let tokens = getTokens(); + tokens = tokens.filter(it => it !== token); + hTokens[wsUrl] = tokens; + localStorage.setItem("viaaas_tokens", JSON.stringify(hTokens)); +} + +function getTokens(): Array { + return (JSON.parse(localStorage.getItem("viaaas_tokens")) || {})[wsUrl] || []; +} + +// Websocket +function listen(token: String) { + socket.send(JSON.stringify({"action": "listen_login_requests", "token": token})); +} + +function unlisten(id: String) { + socket.send(JSON.stringify({"action": "unlisten_login_requests", "uuid": id})); +} + +function confirmJoin(hash: String) { + socket.send(JSON.stringify({action: "session_hash_response", session_hash: hash})); +} + +function handleJoinRequest(parsed: any) { + authNotification("Allow auth impersonation from VIAaaS instance?\nAccount: " + + parsed.user + "\nServer Message: \n" + + parsed.message.split(/[\r\n]+/).map((it: string) => "> " + it).join('\n'), () => { + let account = findAccountByMcName(parsed.user); + if (account) { + account.joinGame(parsed.session_hash) + .finally(() => confirmJoin(parsed.session_hash)) + .catch((e) => addToast("Couldn't contact session server", "Error: " + e)); + } else { + confirmJoin(parsed.session_hash); + addToast("Couldn't find account", "Couldn't find " + parsed.user + ", check Accounts tab"); + } + }, () => confirmJoin(parsed.session_hash)); +} + +function onWsMsg(event: MessageEvent) { + let parsed = JSON.parse(event.data); + switch (parsed.action) { + case "ad_login_methods": + listenVisible = true; + renderActions(); + break; + case "login_result": + if (!parsed.success) { + addToast("Couldn't verify Minecraft account", "VIAaaS returned failed response"); + } else { + listen(parsed.token); + saveToken(parsed.token); + } + break; + case "listen_login_requests_result": + if (parsed.success) { + addListeningList(parsed.user, parsed.username, parsed.token); + } else { + removeToken(parsed.token); + } + break; + case "session_hash_request": + handleJoinRequest(parsed); + break; + case "parameters_request": + handleParametersRequest(parsed); + break; + } +} + +function handleParametersRequest(parsed: any) { + let url = new URL("https://" + $("#connect_address").val()); + socket.send(JSON.stringify({ + action: "parameters_response", + callback: parsed["callback"], + version: $("#connect_version").val(), + host: url.hostname, + port: parseInt(url.port) || 25565, + frontOnline: $("#connect_online").val(), + backName: $("#connect_user").val() || undefined + })); +} + +function listenStoredTokens() { + getTokens().forEach(listen); +} + +function onWsConnect() { + setWsStatus("connected"); + resetHtml(); + listenStoredTokens(); +} + +function onWsError(e: any) { + console.log(e); + setWsStatus("socket error"); + resetHtml(); +} + +function onWsClose(evt: CloseEvent) { + setWsStatus("disconnected with close code " + evt.code + " and reason: " + evt.reason); + resetHtml(); + setTimeout(connect, 5000); +} + +function connect() { + setWsStatus("connecting..."); + socket = new WebSocket(wsUrl); + + socket.onerror = onWsError; + socket.onopen = onWsConnect; + socket.onclose = onWsClose; + socket.onmessage = onWsMsg; +} + +function sendSocket(msg: string) { + if (!socket) { + console.error("couldn't send msg, socket isn't set"); + return + } + socket.send(msg); +} diff --git a/src/main/resources/web/js/worker.js b/src/main/resources/web/js/worker.js index 0f34163..e8642c4 100644 --- a/src/main/resources/web/js/worker.js +++ b/src/main/resources/web/js/worker.js @@ -1,11 +1,12 @@ +"use strict"; importScripts("https://cdnjs.cloudflare.com/ajax/libs/js-sha512/0.8.0/sha512.min.js"); let pending = []; -onmessage = function (e) { +self.addEventListener("message", e => { if (e.data.action === "listen_pow") startPoW(e); if (e.data.action === "cancel") removePending(e.data.id); -} +}); function removePending(id) { pending = pending.filter(it => it !== id); @@ -16,12 +17,16 @@ function startPoW(e) { listenPoW(e); } +function isPending(id) { + return pending.includes(id); +} + function listenPoW(e) { let user = e.data.user; let msg = null; let endTime = Date.now() + 1000; do { - if (!pending.includes(e.data.id)) return; // cancelled + if (!isPending(e.data.id)) return; // cancelled msg = JSON.stringify({ action: "offline_login", @@ -31,10 +36,13 @@ function listenPoW(e) { }); if (Date.now() >= endTime) { - setTimeout(() => listenPoW(e), 0); + setTimeout(() => listenPoW(e)); return; } } while (!sha512(msg).startsWith("00000")); - postMessage({id: e.data.id, action: "completed_pow", msg: msg}); + setTimeout(() => { + if (!isPending(e.data.id)) return; + postMessage({id: e.data.id, action: "completed_pow", msg: msg}); + }) } \ No newline at end of file diff --git a/src/main/resources/web/tsconfig.json b/src/main/resources/web/tsconfig.json new file mode 100644 index 0000000..d866106 --- /dev/null +++ b/src/main/resources/web/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "ES2015", + "lib": [ + "ES2018", + "DOM" + ], + "moduleResolution": "Node", + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "removeComments": true + }, + "include": [ + "js/**.ts" + ] +} \ No newline at end of file