diff --git a/html/index.html b/html/index.html
index 03e95de..206d143 100644
--- a/html/index.html
+++ b/html/index.html
@@ -68,6 +68,7 @@
const internal_url = "https://bolt-internal";
var platform = null;
var credentials = [];
+ var credentialsAreDirty = false;
var pendingOauth = null;
var pendingGameAuth = [];
var rs3LinuxInstalledHash = null;
@@ -82,9 +83,10 @@
var launchGameButtons = document.createElement("div");
var footer = document.createElement("div");
- // checks if `credentials` are about to expire or have already expired,
- // and renews them using the oauth endpoint if so
- // returns null on success or an http status code on failure
+ // Checks if `credentials` are about to expire or have already expired,
+ // and renews them using the oauth endpoint if so.
+ // Does not save credentials but sets credentialsAreDirty as appropriate.
+ // Returns null on success or an http status code on failure
function checkRenewCreds(creds, url, client_id) {
return new Promise((resolve, reject) => {
// only renew if less than 30 seconds left
@@ -101,6 +103,7 @@
const c = parseCredentials(xml.response);
if (c) {
Object.assign(creds, c);
+ credentialsAreDirty = true;
resolve(null);
} else {
resolve(0);
@@ -215,6 +218,7 @@
rs3LinuxInstalledHash = query.get("rs3_linux_installed_hash");
const c = query.get("credentials");
if (c) {
+ // no need to set credentialsAreDirty here because the contents came directly from the file
credentials = JSON.parse(c);
}
@@ -241,9 +245,13 @@
if (xml.status == 200) {
const creds = parseCredentials(xml.response);
if (creds) {
- credentials.push(creds);
- saveAllCreds();
- handleLogin(s, pending.win, creds, exchange_url, client_id);
+ handleLogin(s, pending.win, creds, exchange_url, client_id).then((x) => {
+ if (x) {
+ credentials.push(creds);
+ credentialsAreDirty = true;
+ saveAllCreds();
+ }
+ });
} else {
err(`Error: invalid credentials received`, false);
pending.win.close();
@@ -292,45 +300,14 @@
if (xml.status == 200) {
const accounts_url = atob(s.auth_api).concat("/accounts");
const session_id = JSON.parse(xml.response).sessionId;
- var xml2 = new XMLHttpRequest();
- xml2.onreadystatechange = () => {
- if (xml2.readyState == 4) {
- if (xml2.status == 200) {
- pending.account_info_promise.then((account_info) => {
- if (typeof account_info !== "number") {
- const name_text = `${account_info.displayName}#${account_info.suffix}`;
- msg(`Successfully added login for ${name_text}`);
- var select = document.createElement("select");
- JSON.parse(xml2.response).forEach((acc) => {
- var opt = document.createElement("option");
- opt.value = acc.accountId;
- if (acc.displayName) {
- opt.name = acc.displayName;
- opt.innerText = acc.displayName;
- } else {
- opt.innerText = `#${acc.accountId}`;
- }
- select.appendChild(opt);
- });
- const gen_login_vars = (f, element, opt) => {
- const acc_id = opt.value;
- const acc_name = opt.name;
- f(s, element, null, null, session_id, acc_id, acc_name);
- };
- addNewAccount(name_text, gen_login_vars, select);
- } else {
- err(`Error getting account info: ${account_info}`, false);
- }
- });
- } else {
- err(`Error: from ${accounts_url}: ${xml2.status}: ${xml2.response}`, false);
- }
+ handleNewSessionId(s, session_id, accounts_url, pending.account_info_promise).then((x) => {
+ if (x) {
+ pending.creds.session_id = session_id;
+ credentials.push(pending.creds);
+ credentialsAreDirty = true;
+ saveAllCreds();
}
- };
- xml2.open('GET', accounts_url, true);
- xml2.setRequestHeader("Accept", "application/json");
- xml2.setRequestHeader("Authorization", "Bearer ".concat(session_id));
- xml2.send();
+ });
} else {
err(`Error: from ${sessions_url}: ${xml.status}: ${xml.response}`, false);
}
@@ -435,35 +412,95 @@
(async () => {
if (credentials.length > 0) {
- const promises = credentials.map(async (x) => { return { creds: x, result: await checkRenewCreds(x, exchange_url, client_id) }; });
+ const old_credentials_size = credentials.length;
+ const promises = credentials.map(async (x) => {
+ const result = await checkRenewCreds(x, exchange_url, client_id);
+ if (result !== null && result !== 0) {
+ err(`Discarding expired login for #${x.sub}`, false);
+ }
+ if (result === null && await handleLogin(s, null, x, exchange_url, client_id)) {
+ return { creds: x, valid: true };
+ } else {
+ return { creds: x, valid: result === 0 };
+ }
+ });
const responses = await Promise.all(promises);
- credentials = responses.filter((x) => {
- return x.result === null || x.result === 0;
- }).map((x) => x.creds);
- credentials.forEach((x) => { handleLogin(s, null, x, exchange_url, client_id); });
+ credentials = responses.filter((x) => x.valid).map((x) => x.creds);
+ credentialsAreDirty |= credentials.length != old_credentials_size;
saveAllCreds();
}
loading.remove();
})();
}
- // called on new successful login with credentials, delegates to a specific handler based on login_provider value
- function handleLogin(s, win, creds, exchange_url, client_id) {
+ // Handles a new session id as part of the login flow. Can also be called on startup with a
+ // persisted session id. Adds page elements and returns true on success, or returns false
+ // on failure.
+ function handleNewSessionId(s, session_id, accounts_url, account_info_promise) {
+ return new Promise((resolve, reject) => {
+ var xml = new XMLHttpRequest();
+ xml.onreadystatechange = () => {
+ if (xml.readyState == 4) {
+ if (xml.status == 200) {
+ account_info_promise.then((account_info) => {
+ if (typeof account_info !== "number") {
+ const name_text = `${account_info.displayName}#${account_info.suffix}`;
+ msg(`Successfully added login for ${name_text}`);
+ var select = document.createElement("select");
+ JSON.parse(xml.response).forEach((acc) => {
+ var opt = document.createElement("option");
+ opt.value = acc.accountId;
+ if (acc.displayName) {
+ opt.name = acc.displayName;
+ opt.innerText = acc.displayName;
+ } else {
+ opt.innerText = `#${acc.accountId}`;
+ }
+ select.appendChild(opt);
+ });
+ const gen_login_vars = (f, element, opt) => {
+ const acc_id = opt.value;
+ const acc_name = opt.name;
+ f(s, element, null, null, session_id, acc_id, acc_name);
+ };
+ addNewAccount(name_text, gen_login_vars, select);
+ resolve(true);
+ } else {
+ err(`Error getting account info: ${account_info}`, false);
+ resolve(false);
+ }
+ });
+ } else {
+ err(`Error: from ${accounts_url}: ${xml.status}: ${xml.response}`, false);
+ resolve(false);
+ }
+ }
+ };
+ xml.open('GET', accounts_url, true);
+ xml.setRequestHeader("Accept", "application/json");
+ xml.setRequestHeader("Authorization", "Bearer ".concat(session_id));
+ xml.send();
+ });
+ }
+
+ // Called on new successful login with credentials. Delegates to a specific handler based on login_provider value.
+ // This function is responsible for adding elements to the page (either directly or indirectly via a GameAuth message),
+ // however it does not save credentials. You should call saveAllCreds after calling this function any number of times.
+ // Returns true if the credentials should be treated as valid by the caller immediately after return, or false if not.
+ async function handleLogin(s, win, creds, exchange_url, client_id) {
switch (creds.login_provider) {
case atob(s.provider):
if (win) {
win.close();
}
- handleGameLogin(s, creds, exchange_url, client_id);
- break;
+ return await handleGameLogin(s, creds, exchange_url, client_id);
default:
- handleStandardLogin(s, win, creds, exchange_url, client_id);
- break;
+ return await handleStandardLogin(s, win, creds, exchange_url, client_id);
}
}
// called when login was successful, but with absent or unrecognised login_provider
- function handleStandardLogin(s, w, creds, refresh_url, client_id) {
+ async function handleStandardLogin(s, w, creds, refresh_url, client_id) {
const state = makeRandomState();
const nonce = crypto.randomUUID();
const location = atob(s.origin).concat("/oauth2/auth?").concat(new URLSearchParams({
@@ -476,38 +513,31 @@
client_id: "1fddee4e-b100-4f4e-b2b0-097f9088f9d2",
scope: "openid offline"
}));
+ var account_info_promise = getStandardAccountInfo(s, creds);
var win = w;
if (win) {
win.location.href = location;
+ pendingGameAuth.push({
+ state: state,
+ nonce: nonce,
+ creds: creds,
+ win: win,
+ account_info_promise: account_info_promise
+ });
+ return false;
} else {
- win = window.open(location, "", "width=480,height=720");
+ if (!creds.session_id) {
+ err("Rejecting stored credentials with missing session_id", false);
+ return false;
+ }
+
+ return await handleNewSessionId(s, creds.session_id, atob(s.auth_api).concat("/accounts"), account_info_promise);
}
- pendingGameAuth.push({
- state: state,
- nonce: nonce,
- win: win,
- account_info_promise: new Promise((resolve, reject) => {
- const url = atob(s.api).concat("/users/").concat(creds.sub).concat("/displayName");
- var xml = new XMLHttpRequest();
- xml.onreadystatechange = () => {
- if (xml.readyState == 4) {
- if (xml.status == 200) {
- resolve(JSON.parse(xml.response));
- } else {
- resolve(xml.status);
- }
- }
- };
- xml.open('GET', url, true);
- xml.setRequestHeader("Authorization", "Bearer ".concat(creds.access_token));
- xml.send();
- })
- });
}
// called after successful login using a game account as the login_provider
- function handleGameLogin(s, creds, refresh_url, client_id) {
+ async function handleGameLogin(s, creds, refresh_url, client_id) {
const auth_url = atob(s.profile_api).concat("/profile");
var xml = new XMLHttpRequest();
xml.onreadystatechange = () => {
@@ -543,6 +573,28 @@
xml.open('GET', auth_url, true);
xml.setRequestHeader("Authorization", "Bearer ".concat(creds.id_token));
xml.send();
+ return true;
+ }
+
+ // makes a request to the account_info endpoint and returns the promise
+ // the promise will return either a JSON object on success or a status code on failure
+ function getStandardAccountInfo(s, creds) {
+ return new Promise((resolve, reject) => {
+ const url = `${atob(s.api)}/users/${creds.sub}/displayName`;
+ var xml = new XMLHttpRequest();
+ xml.onreadystatechange = () => {
+ if (xml.readyState == 4) {
+ if (xml.status == 200) {
+ resolve(JSON.parse(xml.response));
+ } else {
+ resolve(xml.status);
+ }
+ }
+ }
+ xml.open('GET', url, true);
+ xml.setRequestHeader("Authorization", "Bearer ".concat(creds.access_token));
+ xml.send();
+ });
}
// use oauth creds to get a response from the "shield" endpoint
@@ -550,8 +602,8 @@
function getShieldTokens(creds, url, display_name, refresh_url, client_id) {
return new Promise((resolve, reject) => {
checkRenewCreds(creds, refresh_url, client_id).then((status) => {
+ saveAllCreds();
if (status === null) {
- saveAllCreds();
var xml = new XMLHttpRequest();
xml.open('POST', url, true);
@@ -582,15 +634,18 @@
// sends a request to save all credentials to their config file,
// overwriting the previous file, if any
async function saveAllCreds() {
- var xml = new XMLHttpRequest();
- xml.open('POST', "/save-credentials", true);
- xml.setRequestHeader("Content-Type", "application/json");
- xml.onreadystatechange = () => {
- if (xml.readyState == 4) {
- msg(`Save-credentials status: ${xml.responseText.trim()}`);
- }
- };
- xml.send(JSON.stringify(credentials));
+ if (credentialsAreDirty) {
+ var xml = new XMLHttpRequest();
+ xml.open('POST', "/save-credentials", true);
+ xml.setRequestHeader("Content-Type", "application/json");
+ xml.onreadystatechange = () => {
+ if (xml.readyState == 4) {
+ msg(`Save-credentials status: ${xml.responseText.trim()}`);
+ }
+ };
+ xml.send(JSON.stringify(credentials));
+ credentialsAreDirty = false;
+ }
}
// adds an option to the accounts drop-down menu at the top of the page