typescript, fix duplicate online mode listening

This commit is contained in:
creeper123123321 2022-07-11 21:18:35 -03:00
parent adfa957e66
commit 9b7821b704
8 changed files with 1303 additions and 320 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/build /build
/config /config
/logs /logs
node_modules/

180
package-lock.json generated Normal file
View File

@ -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
}
}
}

17
package.json Normal file
View File

@ -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"
}
}

View File

@ -17,7 +17,7 @@
style-src 'self' https://cdnjs.cloudflare.com/; style-src 'self' https://cdnjs.cloudflare.com/;
img-src 'self' data: https://crafthead.net/ https://crafatar.com/; img-src 'self' data: https://crafthead.net/ https://crafatar.com/;
connect-src 'self' http://localhost:*/ https: wss:; 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/" frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
http-equiv="Content-Security-Policy"> http-equiv="Content-Security-Policy">
<meta content="no-referrer" name="referrer"> <meta content="no-referrer" name="referrer">
@ -26,12 +26,13 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
<!-- https://www.srihash.org/ --> <!-- https://www.srihash.org/ -->
<link class="async-css" rel="preload" as="style" <link class="async-css" rel="preload" as="style"
href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.1/css/bootstrap.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css"
integrity="sha512-6KY5s6UI5J7SVYuZB4S/CZMyPylqyyNZco376NM2Z8Sb8OxEdp02e1jkKk/wZxIEmjQ6DRCEBhni+gpr9c4tvA==" integrity="sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w=="
crossorigin="anonymous" referrerpolicy="no-referrer"/> crossorigin="anonymous" referrerpolicy="no-referrer"/>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.1/js/bootstrap.min.js" <script defer src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.3/js/bootstrap.min.js"
integrity="sha512-ewfXo9Gq53e1q1+WDTjaHAGZ8UvCWq0eXONhwDuIoaH8xz2r96uoAYaQCm1oQhnBfRXrvJztNXFsTloJfgbL5Q==" integrity="sha512-OvBgP9A2JBgiRad/mM36mkzXSXaJE9BEIENnVEmeZdITvwT09xnxLtT4twkCa8m/loMbPHsvPl0T8lRGVBwjlQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Safari workaround -->
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/HTML5Notification/3.0.0/Notification.min.js" <script defer src="https://cdnjs.cloudflare.com/ajax/libs/HTML5Notification/3.0.0/Notification.min.js"
integrity="sha512-gx0m7Qoum1Bb0KrP6AEZSt0e+o2xMEyStAz2TNGXGqR4HSVgPveWFQdtE06FRvJmmp8HdkJklOLYiV3aZN6tQg==" integrity="sha512-gx0m7Qoum1Bb0KrP6AEZSt0e+o2xMEyStAz2TNGXGqR4HSVgPveWFQdtE06FRvJmmp8HdkJklOLYiV3aZN6tQg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
@ -41,8 +42,8 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
<script defer crossorigin="anonymous" <script defer crossorigin="anonymous"
integrity="sha512-UNM1njAgOFUa74Z0bADwAq8gbTcqZC8Ej4xPSzpnh0l6KMevwvkBvbldF9uR++qKeJ+MOZHRjV1HZjoRvjDfNQ==" integrity="sha512-UNM1njAgOFUa74Z0bADwAq8gbTcqZC8Ej4xPSzpnh0l6KMevwvkBvbldF9uR++qKeJ+MOZHRjV1HZjoRvjDfNQ=="
src="https://cdnjs.cloudflare.com/ajax/libs/uuid/8.3.2/uuid.min.js"></script> src="https://cdnjs.cloudflare.com/ajax/libs/uuid/8.3.2/uuid.min.js"></script>
<script defer src="https://alcdn.msauth.net/browser/2.19.0/js/msal-browser.min.js" <script defer src="https://alcdn.msauth.net/browser/2.27.0/js/msal-browser.min.js"
integrity="sha384-VK+6hHt27itNFksZdEeXofJXdAhmlizHbC/a1TUUJm/Yq6gOuAjXKkiiCaGbFsnd" integrity="sha384-IlUQkOwOI6mWk8GNIWu8hpPE1sasxSg3gGjZo0dncq6IhHsTlH51mp5mhFYS5po1"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script defer src="js/config.js"></script> <script defer src="js/config.js"></script>
<script defer src="js/page.js"></script> <script defer src="js/page.js"></script>

View File

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

View File

@ -0,0 +1,836 @@
/// <reference path='config.js' />
"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<Worker> = [];
$(() => {
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 = $(`<li class='input-group d-flex'>
<span class='input-group-text'><img alt="?" src="?" loading="lazy" width=24 class='mc-head'/></span>
<span class='form-control mc-user'></span>
<button type="button" class='btn btn-danger mc-remove'>Logout</button>
</li>`);
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 = $("<option class='mc_username'></option>");
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 = $("<p><img alt='?' src='?' loading='lazy' width=24 class='head'/> <span class='username'></span> <button class='btn btn-danger' type='button'>Unlisten</button></p>");
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 = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto toast_title_msg"></strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
<pre class="txt"></pre>
<div class="btns mt-2 pt-2 border-top"></div>
</div>
</div>`);
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 = $("<button type='button' data-bs-dismiss='toast' class='btn btn-primary btn-sm'>Yes</button>");
btn.on("click", yes);
btns.append(btn);
}
if (no != null) {
hasButtons = true;
let btn = $("<button type='button' data-bs-dismiss='toast' class='btn btn-secondary btn-sm'>No</button>");
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<String> {
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<string, (action: string) => 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<McAccount> = [];
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<void> {
activeAccounts = activeAccounts.filter(it => it !== this);
saveRefreshAccounts();
this.loggedOut = true;
}
async checkActive(): Promise<boolean> {
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<void> {
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<void> {
}
async acquireActiveToken(): Promise<void> {
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<void> {
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<String> {
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);
}

View File

@ -1,11 +1,12 @@
"use strict";
importScripts("https://cdnjs.cloudflare.com/ajax/libs/js-sha512/0.8.0/sha512.min.js"); importScripts("https://cdnjs.cloudflare.com/ajax/libs/js-sha512/0.8.0/sha512.min.js");
let pending = []; let pending = [];
onmessage = function (e) { self.addEventListener("message", e => {
if (e.data.action === "listen_pow") startPoW(e); if (e.data.action === "listen_pow") startPoW(e);
if (e.data.action === "cancel") removePending(e.data.id); if (e.data.action === "cancel") removePending(e.data.id);
} });
function removePending(id) { function removePending(id) {
pending = pending.filter(it => it !== id); pending = pending.filter(it => it !== id);
@ -16,12 +17,16 @@ function startPoW(e) {
listenPoW(e); listenPoW(e);
} }
function isPending(id) {
return pending.includes(id);
}
function listenPoW(e) { function listenPoW(e) {
let user = e.data.user; let user = e.data.user;
let msg = null; let msg = null;
let endTime = Date.now() + 1000; let endTime = Date.now() + 1000;
do { do {
if (!pending.includes(e.data.id)) return; // cancelled if (!isPending(e.data.id)) return; // cancelled
msg = JSON.stringify({ msg = JSON.stringify({
action: "offline_login", action: "offline_login",
@ -31,10 +36,13 @@ function listenPoW(e) {
}); });
if (Date.now() >= endTime) { if (Date.now() >= endTime) {
setTimeout(() => listenPoW(e), 0); setTimeout(() => listenPoW(e));
return; return;
} }
} while (!sha512(msg).startsWith("00000")); } while (!sha512(msg).startsWith("00000"));
setTimeout(() => {
if (!isPending(e.data.id)) return;
postMessage({id: e.data.id, action: "completed_pow", msg: msg}); postMessage({id: e.data.id, action: "completed_pow", msg: msg});
})
} }

View File

@ -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"
]
}