Fix race conditions
All checks were successful
Android Build / publish (push) Successful in 52s
Linux Build / publish (push) Successful in 49s

This commit is contained in:
olcxja 2026-06-02 11:58:18 +02:00
commit b24b36adec
7 changed files with 219 additions and 130 deletions

View file

@ -86,7 +86,6 @@
async function start() { async function start() {
updateLoadingStatus("loading.loading"); updateLoadingStatus("loading.loading");
try { try {
gotoHome();
if (host != null) if (host != null)
{ {
await updateProtocolAndUrl(host); await updateProtocolAndUrl(host);
@ -109,6 +108,7 @@
//if fails continue loading encryption may be broken //if fails continue loading encryption may be broken
console.error(e); console.error(e);
} }
gotoHome();
} else { } else {
showBlahNotification("error:auth.failed.redirect.to.login"); showBlahNotification("error:auth.failed.redirect.to.login");
await delay(2000); await delay(2000);

View file

@ -468,29 +468,49 @@ async function decryptString(base64Text, passphrase) {
} }
let requestMutex = Promise.resolve();
async function fetchPost(url, value) { async function fetchPost(url, value) {
let response = await fetch(url, { let release;
method: "POST", const lock = new Promise(resolve => release = resolve);
body: value, const prevMutex = requestMutex;
headers: { requestMutex = prevMutex.then(() => lock);
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash)) await prevMutex;
} try {
}); let response = await fetch(url, {
let data = await response.text(); method: "POST",
return data; body: value,
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text();
return data;
} finally {
release();
}
} }
async function fetchPostEnc(url, value) { async function fetchPostEnc(url, value) {
let nonce = await getNonce(id, passwordHash); let release;
let response = await fetch(url, { const lock = new Promise(resolve => release = resolve);
method: "POST", const prevMutex = requestMutex;
body: await encryptWithNonce(value, passwordHash, nonce), requestMutex = prevMutex.then(() => lock);
headers: { await prevMutex;
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce) try {
} let nonce = await getNonce(id, passwordHash);
}); let response = await fetch(url, {
let data = await response.text(); method: "POST",
return data; body: await encryptWithNonce(value, passwordHash, nonce),
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
}
});
let data = await response.text();
return data;
} finally {
release();
}
} }
async function fetchAsync(url) { async function fetchAsync(url) {
@ -503,15 +523,24 @@ async function fetchAsync(url) {
} }
async function fetchAsyncWAuth(url) { async function fetchAsyncWAuth(url) {
let response = await fetch(url, { let release;
method: "GET", const lock = new Promise(resolve => release = resolve);
headers: { const prevMutex = requestMutex;
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash)) requestMutex = prevMutex.then(() => lock);
} await prevMutex;
}); try {
let response = await fetch(url, {
method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text(); let data = await response.text();
return data; return data;
} finally {
release();
}
} }
async function getServerInfo(host) { async function getServerInfo(host) {
@ -520,48 +549,64 @@ async function getServerInfo(host) {
} }
async function Auth(loginUsername, loginPassword) { async function Auth(loginUsername, loginPassword) {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${loginUsername}`); let release;
if (!resolvedId || resolvedId.trim() === "" || resolvedId.startsWith("error:")) { const lock = new Promise(resolve => release = resolve);
return "error:invalid.username.or.password"; const prevMutex = requestMutex;
} requestMutex = prevMutex.then(() => lock);
let actualId = resolvedId.split(":")[0]; await prevMutex;
try {
let passwordHash = await hashSHA3_512(loginPassword); let resolvedId = await fetchAsync(`${url}/nametoid?u=${loginUsername}`);
let response = await fetch(`${url}/auth?id=${actualId}`, { if (!resolvedId || resolvedId.trim() === "" || resolvedId.startsWith("error:")) {
method: "GET", return "error:invalid.username.or.password";
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
} }
}); let actualId = resolvedId.split(":")[0];
let data = await response.text(); let passwordHash = await hashSHA3_512(loginPassword);
if (data.startsWith("success:")) { let response = await fetch(`${url}/auth?id=${actualId}`, {
data += "|" + actualId; method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
}
});
let data = await response.text();
if (data.startsWith("success:")) {
data += "|" + actualId;
}
return data;
} finally {
release();
} }
return data;
} }
async function fetchEncrypted(request, body = "") { async function fetchEncrypted(request, body = "") {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try {
let nonce = await getNonce(id, passwordHash);
let nonce = await getNonce(id, passwordHash); let response = await fetch(`${url}/encryptedrequest?id=${id}`, {
method: "POST",
body: await encryptWithNonce(
JSON.stringify({
string1: request,
string2: body
})
let response = await fetch(`${url}/encryptedrequest?id=${id}`, { , passwordHash, nonce),
method: "POST", headers: {
body: await encryptWithNonce( "secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
JSON.stringify({ }
string1: request, });
string2: body let data = await response.text();
}) return await decryptString(data, passwordHash);
} finally {
, passwordHash, nonce), release();
headers: { }
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
}
});
let data = await response.text();
return decryptString(data, passwordHash);
} }
function power(base, exponent, mod) { function power(base, exponent, mod) {

View file

@ -121,7 +121,7 @@
"placeholder.username": "cat name", "placeholder.username": "cat name",
"placeholder.captcha.code": "captcha code", "placeholder.captcha.code": "captcha code",
"placeholder.message.input": "meow...", "placeholder.message.input": "meow...",
"desc.messages.encryption.active": "meows are end-to-end encrypted :3", "desc.messages.loading": "loading meows...",
"desc.no.dms": "no direct meowchats :c", "desc.no.dms": "no direct meowchats :c",
"action.dm.opening": "opening meowchat...", "action.dm.opening": "opening meowchat...",
"dm.open.failed": "failed to open meowchat :c", "dm.open.failed": "failed to open meowchat :c",

View file

@ -120,7 +120,7 @@
"placeholder.username": "username", "placeholder.username": "username",
"placeholder.captcha.code": "captcha code", "placeholder.captcha.code": "captcha code",
"placeholder.message.input": "Message...", "placeholder.message.input": "Message...",
"desc.messages.encryption.active": "Messages are end-to-end encrypted", "desc.messages.loading": "Fetching messages...",
"desc.no.dms": "No direct messages", "desc.no.dms": "No direct messages",
"action.dm.opening": "Opening DM...", "action.dm.opening": "Opening DM...",
"dm.open.failed": "Failed to open DM", "dm.open.failed": "Failed to open DM",

View file

@ -86,7 +86,6 @@
async function start() { async function start() {
updateLoadingStatus("loading.loading"); updateLoadingStatus("loading.loading");
try { try {
gotoHome();
if (host != null) if (host != null)
{ {
await updateProtocolAndUrl(host); await updateProtocolAndUrl(host);
@ -109,6 +108,7 @@
//if fails continue loading encryption may be broken //if fails continue loading encryption may be broken
console.error(e); console.error(e);
} }
gotoHome();
} else { } else {
showBlahNotification("error:auth.failed.redirect.to.login"); showBlahNotification("error:auth.failed.redirect.to.login");
await delay(2000); await delay(2000);

View file

@ -468,29 +468,49 @@ async function decryptString(base64Text, passphrase) {
} }
let requestMutex = Promise.resolve();
async function fetchPost(url, value) { async function fetchPost(url, value) {
let response = await fetch(url, { let release;
method: "POST", const lock = new Promise(resolve => release = resolve);
body: value, const prevMutex = requestMutex;
headers: { requestMutex = prevMutex.then(() => lock);
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash)) await prevMutex;
} try {
}); let response = await fetch(url, {
let data = await response.text(); method: "POST",
return data; body: value,
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text();
return data;
} finally {
release();
}
} }
async function fetchPostEnc(url, value) { async function fetchPostEnc(url, value) {
let nonce = await getNonce(id, passwordHash); let release;
let response = await fetch(url, { const lock = new Promise(resolve => release = resolve);
method: "POST", const prevMutex = requestMutex;
body: await encryptWithNonce(value, passwordHash, nonce), requestMutex = prevMutex.then(() => lock);
headers: { await prevMutex;
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce) try {
} let nonce = await getNonce(id, passwordHash);
}); let response = await fetch(url, {
let data = await response.text(); method: "POST",
return data; body: await encryptWithNonce(value, passwordHash, nonce),
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
}
});
let data = await response.text();
return data;
} finally {
release();
}
} }
async function fetchAsync(url) { async function fetchAsync(url) {
@ -503,15 +523,24 @@ async function fetchAsync(url) {
} }
async function fetchAsyncWAuth(url) { async function fetchAsyncWAuth(url) {
let response = await fetch(url, { let release;
method: "GET", const lock = new Promise(resolve => release = resolve);
headers: { const prevMutex = requestMutex;
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash)) requestMutex = prevMutex.then(() => lock);
} await prevMutex;
}); try {
let response = await fetch(url, {
method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text(); let data = await response.text();
return data; return data;
} finally {
release();
}
} }
async function getServerInfo(host) { async function getServerInfo(host) {
@ -520,48 +549,64 @@ async function getServerInfo(host) {
} }
async function Auth(loginUsername, loginPassword) { async function Auth(loginUsername, loginPassword) {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${loginUsername}`); let release;
if (!resolvedId || resolvedId.trim() === "" || resolvedId.startsWith("error:")) { const lock = new Promise(resolve => release = resolve);
return "error:invalid.username.or.password"; const prevMutex = requestMutex;
} requestMutex = prevMutex.then(() => lock);
let actualId = resolvedId.split(":")[0]; await prevMutex;
try {
let passwordHash = await hashSHA3_512(loginPassword); let resolvedId = await fetchAsync(`${url}/nametoid?u=${loginUsername}`);
let response = await fetch(`${url}/auth?id=${actualId}`, { if (!resolvedId || resolvedId.trim() === "" || resolvedId.startsWith("error:")) {
method: "GET", return "error:invalid.username.or.password";
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
} }
}); let actualId = resolvedId.split(":")[0];
let data = await response.text(); let passwordHash = await hashSHA3_512(loginPassword);
if (data.startsWith("success:")) { let response = await fetch(`${url}/auth?id=${actualId}`, {
data += "|" + actualId; method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
}
});
let data = await response.text();
if (data.startsWith("success:")) {
data += "|" + actualId;
}
return data;
} finally {
release();
} }
return data;
} }
async function fetchEncrypted(request, body = "") { async function fetchEncrypted(request, body = "") {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try {
let nonce = await getNonce(id, passwordHash);
let nonce = await getNonce(id, passwordHash); let response = await fetch(`${url}/encryptedrequest?id=${id}`, {
method: "POST",
body: await encryptWithNonce(
JSON.stringify({
string1: request,
string2: body
})
let response = await fetch(`${url}/encryptedrequest?id=${id}`, { , passwordHash, nonce),
method: "POST", headers: {
body: await encryptWithNonce( "secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
JSON.stringify({ }
string1: request, });
string2: body let data = await response.text();
}) return await decryptString(data, passwordHash);
} finally {
, passwordHash, nonce), release();
headers: { }
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
}
});
let data = await response.text();
return decryptString(data, passwordHash);
} }
function power(base, exponent, mod) { function power(base, exponent, mod) {
@ -1807,7 +1852,7 @@ async function openDm(dmId, username, targetId) {
let msgContainer = document.getElementById("chat-messages"); let msgContainer = document.getElementById("chat-messages");
if (msgContainer) { if (msgContainer) {
msgContainer.innerHTML = `<div style="text-align: center; opacity: 0.5; padding: 1rem;"><blah>desc.messages.encryption.active</blah></div>`; msgContainer.innerHTML = `<div style="text-align: center; opacity: 0.5; padding: 1rem;"><blah>desc.messages.loading</blah></div>`;
let blahTags = msgContainer.getElementsByTagName("blah"); let blahTags = msgContainer.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) { for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML); blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);

View file

@ -132,7 +132,6 @@ var invitesEntry = `
var chatScreen = ` var chatScreen = `
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;"> <div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;"> <div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
<!-- messages go here -->
</div> </div>
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;"> <div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
<input type="text" id="chat-input" class="forminput" style="flex-grow: 1; margin: 0;" placeholder="{blah(placeholder.message.input)}"> <input type="text" id="chat-input" class="forminput" style="flex-grow: 1; margin: 0;" placeholder="{blah(placeholder.message.input)}">