various improvements and fixes to PoW challenge UI and token refresh
This commit is contained in:
@@ -30,7 +30,6 @@ self.onmessage = async (event) => {
|
|||||||
nonce = data.nonce;
|
nonce = data.nonce;
|
||||||
|
|
||||||
const c = data.challenge;
|
const c = data.challenge;
|
||||||
// decode salt to Uint8Array
|
|
||||||
const salt = new Uint8Array(c.s.length / 2);
|
const salt = new Uint8Array(c.s.length / 2);
|
||||||
for (let i = 0; i < c.s.length; i += 2) {
|
for (let i = 0; i < c.s.length; i += 2) {
|
||||||
salt[i / 2] = parseInt(c.s.slice(i, i + 2), 16);
|
salt[i / 2] = parseInt(c.s.slice(i, i + 2), 16);
|
||||||
@@ -99,7 +98,7 @@ const solve = async () => {
|
|||||||
self.postMessage({ type: "solved", nonce: solution.nonce });
|
self.postMessage({ type: "solved", nonce: solution.nonce });
|
||||||
active = false;
|
active = false;
|
||||||
} else {
|
} else {
|
||||||
if (Date.now() - lastNotify > 1000) {
|
if (Date.now() - lastNotify >= 500) {
|
||||||
console.log("Last nonce", nonce, "Hashes", hashesSinceLastNotify);
|
console.log("Last nonce", nonce, "Hashes", hashesSinceLastNotify);
|
||||||
self.postMessage({ type: "progress", hashes: hashesSinceLastNotify });
|
self.postMessage({ type: "progress", hashes: hashesSinceLastNotify });
|
||||||
lastNotify = Date.now();
|
lastNotify = Date.now();
|
||||||
|
|||||||
@@ -344,10 +344,11 @@ router.post("/maintenance", (req, res) => {
|
|||||||
case "setDifficulty": {
|
case "setDifficulty": {
|
||||||
const selected = req.body["pow-difficulty"];
|
const selected = req.body["pow-difficulty"];
|
||||||
const valid = ["low", "medium", "high", "extreme"];
|
const valid = ["low", "medium", "high", "extreme"];
|
||||||
if (!selected || !valid.includes(selected)) {
|
const isNumber = Number.isInteger(Number(selected));
|
||||||
throw new HttpError(400, "Invalid difficulty" + selected);
|
if (!selected || !valid.includes(selected) && !isNumber) {
|
||||||
|
throw new HttpError(400, "Invalid difficulty " + selected);
|
||||||
}
|
}
|
||||||
config.powDifficultyLevel = selected;
|
config.powDifficultyLevel = isNumber ? Number(selected) : selected;
|
||||||
invalidatePowChallenges();
|
invalidatePowChallenges();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,15 +38,20 @@
|
|||||||
<h3>Difficulty Level</h3>
|
<h3>Difficulty Level</h3>
|
||||||
<div>
|
<div>
|
||||||
<label for="difficulty">Difficulty Level:</label>
|
<label for="difficulty">Difficulty Level:</label>
|
||||||
<span id="currentDifficulty">Current: <%= difficulty %></span>
|
<select name="difficulty" id="difficulty" onchange="difficultyChanged(event)">
|
||||||
<select name="difficulty" id="difficulty">
|
|
||||||
<option value="low">Low</option>
|
<option value="low">Low</option>
|
||||||
<option value="medium">Medium</option>
|
<option value="medium">Medium</option>
|
||||||
<option value="high">High</option>
|
<option value="high">High</option>
|
||||||
<option value="extreme">Extreme</option>
|
<option value="extreme">Extreme</option>
|
||||||
|
<option value="custom">Custom</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div id="custom-difficulty-container" style="display: none">
|
||||||
|
<label for="customDifficulty">Hashes required (average):</label>
|
||||||
|
<input type="number" id="customDifficulty" value="0" min="1" max="1000000000" />
|
||||||
|
</div>
|
||||||
<button onclick='doAction("setDifficulty")'>Update Difficulty</button>
|
<button onclick='doAction("setDifficulty")'>Update Difficulty</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div><span id="currentDifficulty">Current Difficulty: <%= difficulty %></span></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<form id="maintenanceForm" action="/admin/manage/maintenance" method="post">
|
<form id="maintenanceForm" action="/admin/manage/maintenance" method="post">
|
||||||
<input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
<input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
||||||
@@ -63,15 +68,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2>IP Whitelists and Blacklists</h2>
|
<h2>IP Whitelists and Blacklists</h2>
|
||||||
<p>
|
<p>
|
||||||
You can specify IP ranges to whitelist or blacklist from accessing the proxy. Note that changes here are not
|
You can specify IP ranges to whitelist or blacklist from accessing the proxy. Entries can be specified as single
|
||||||
persisted across server restarts. If you want to make changes permanent, you can copy the values to your deployment
|
addresses or
|
||||||
configuration.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Entries can be specified as single addresses or
|
|
||||||
<a href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation">CIDR notation</a>. IPv6 is
|
<a href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation">CIDR notation</a>. IPv6 is
|
||||||
supported but not recommended for use with the current version of the proxy.
|
supported but not recommended for use with the current version of the proxy.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> Changes here are not persisted across server restarts. If you want to make changes permanent,
|
||||||
|
you can copy the values to your deployment configuration.
|
||||||
|
</p>
|
||||||
<% for (let i = 0; i < whitelists.length; i++) { %>
|
<% for (let i = 0; i < whitelists.length; i++) { %>
|
||||||
<%- include("partials/admin-cidr-widget", { list: whitelists[i] }) %>
|
<%- include("partials/admin-cidr-widget", { list: whitelists[i] }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -99,10 +104,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function difficultyChanged(event) {
|
||||||
|
const value = event.target.value;
|
||||||
|
if (value === "custom") {
|
||||||
|
document.getElementById("custom-difficulty-container").style.display = "block";
|
||||||
|
} else {
|
||||||
|
document.getElementById("custom-difficulty-container").style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function doAction(action) {
|
function doAction(action) {
|
||||||
document.getElementById("hiddenAction").value = action;
|
document.getElementById("hiddenAction").value = action;
|
||||||
if (action === "setDifficulty") {
|
if (action === "setDifficulty") {
|
||||||
document.getElementById("hiddenDifficulty").value = document.getElementById("difficulty").value;
|
const selected = document.getElementById("difficulty").value;
|
||||||
|
const hiddenDifficulty = document.getElementById("hiddenDifficulty");
|
||||||
|
if (selected === "custom") {
|
||||||
|
hiddenDifficulty.value = document.getElementById("customDifficulty").value;
|
||||||
|
} else {
|
||||||
|
hiddenDifficulty.value = selected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
document.getElementById("maintenanceForm").submit();
|
document.getElementById("maintenanceForm").submit();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -171,7 +171,7 @@ function getSelfServiceLinks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `<div class="self-service-links">${links
|
return `<div class="self-service-links">${links
|
||||||
.map(([text, link]) => `<a target="_blank" href="${link}">${text}</a>`)
|
.map(([text, link]) => `<a href="${link}">${text}</a>`)
|
||||||
.join(" | ")}</div>`;
|
.join(" | ")}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
<p>
|
<p>
|
||||||
Next refresh: <time><%- nextQuotaRefresh %></time>
|
Next refresh: <time><%- nextQuotaRefresh %></time>
|
||||||
</p>
|
</p>
|
||||||
<table class="striped">
|
<%
|
||||||
|
const quotaTableId = Math.random().toString(36).slice(2);
|
||||||
|
%>
|
||||||
|
<div>
|
||||||
|
<label for="quota-family-filter-<%= quotaTableId %>">Filter:</label>
|
||||||
|
<input type="text" id="quota-family-filter-<%= quotaTableId %>" oninput="filterQuotaTable(this, '<%= quotaTableId %>')" />
|
||||||
|
</div>
|
||||||
|
<table class="striped" id="quota-table-<%= quotaTableId %>">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Model Family</th>
|
<th scope="col">Model Family</th>
|
||||||
@@ -50,3 +57,18 @@
|
|||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<script>
|
||||||
|
function filterQuotaTable(input, tableId) {
|
||||||
|
const filter = input.value.toLowerCase();
|
||||||
|
const table = document.getElementById("quota-table-" + tableId);
|
||||||
|
const rows = table.querySelectorAll("tbody tr");
|
||||||
|
for (const row of rows) {
|
||||||
|
const modelFamily = row.querySelector("th").textContent;
|
||||||
|
if (modelFamily.toLowerCase().includes(filter)) {
|
||||||
|
row.style.display = "";
|
||||||
|
} else {
|
||||||
|
row.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
+33
-25
@@ -187,7 +187,7 @@ function verifyTokenRefreshable(token: string, req: express.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.log.info({ token }, "Allowing token refresh");
|
req.log.info({ token: `...${token.slice(-5)}` }, "Allowing token refresh");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,51 +227,57 @@ router.post("/verify", async (req, res) => {
|
|||||||
const ip = req.ip;
|
const ip = req.ip;
|
||||||
req.log.info("Got verification request");
|
req.log.info("Got verification request");
|
||||||
if (recentAttempts.has(ip)) {
|
if (recentAttempts.has(ip)) {
|
||||||
res
|
const error = "Rate limited; wait a minute before trying again";
|
||||||
.status(429)
|
req.log.info({ error }, "Verification rejected");
|
||||||
.json({ error: "Rate limited; wait a minute before trying again" });
|
res.status(429).json({ error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = verifySchema.safeParse(req.body);
|
const result = verifySchema.safeParse(req.body);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
res
|
const error = "Invalid verify request";
|
||||||
.status(400)
|
req.log.info({ error, result }, "Verification rejected");
|
||||||
.json({ error: "Invalid verify request", details: result.error });
|
res.status(400).json({ error, details: result.error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { challenge, signature, solution } = result.data;
|
const { challenge, signature, solution } = result.data;
|
||||||
if (signMessage(challenge, powKeySalt) !== signature) {
|
if (signMessage(challenge, powKeySalt) !== signature) {
|
||||||
res.status(400).json({
|
const error =
|
||||||
error:
|
"Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.";
|
||||||
"Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.",
|
req.log.info({ error }, "Verification rejected");
|
||||||
});
|
res.status(400).json({ error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.proxyKey && result.data.proxyKey !== config.proxyKey) {
|
if (config.proxyKey && result.data.proxyKey !== config.proxyKey) {
|
||||||
res.status(401).json({ error: "Invalid proxy password" });
|
const error = "Invalid proxy password";
|
||||||
|
req.log.info({ error }, "Verification rejected");
|
||||||
|
res.status(401).json({ error, password: result.data.proxyKey });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (challenge.ip && challenge.ip !== ip) {
|
if (challenge.ip && challenge.ip !== ip) {
|
||||||
req.log.warn("Attempt to verify from different IP address");
|
const error = "Solution must be verified from original IP address";
|
||||||
res.status(400).json({
|
req.log.info(
|
||||||
error: "Solution must be verified from original IP address",
|
{ error, challengeIp: challenge.ip, clientIp: ip },
|
||||||
});
|
"Verification rejected"
|
||||||
|
);
|
||||||
|
res.status(400).json({ error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (solves.has(signature)) {
|
if (solves.has(signature)) {
|
||||||
req.log.warn("Attempt to reuse signature");
|
const error = "Reused signature";
|
||||||
res.status(400).json({ error: "Reused signature" });
|
req.log.info({ error }, "Verification rejected");
|
||||||
|
res.status(400).json({ error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() > challenge.e) {
|
if (Date.now() > challenge.e) {
|
||||||
req.log.warn("Verification took too long");
|
const error = "Verification took too long";
|
||||||
res.status(400).json({ error: "Verification took too long" });
|
req.log.info({ error }, "Verification rejected");
|
||||||
|
res.status(400).json({ error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +291,7 @@ router.post("/verify", async (req, res) => {
|
|||||||
const success = await verifySolution(challenge, solution, req.log);
|
const success = await verifySolution(challenge, solution, req.log);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
recentAttempts.set(ip, Date.now() + 1000 * 60 * 60 * 6);
|
recentAttempts.set(ip, Date.now() + 1000 * 60 * 60 * 6);
|
||||||
req.log.warn("Solution failed verification");
|
req.log.warn("Bogus solution, client blocked");
|
||||||
res.status(400).json({ error: "Solution failed verification" });
|
res.status(400).json({ error: "Solution failed verification" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -299,10 +305,12 @@ router.post("/verify", async (req, res) => {
|
|||||||
if (challenge.token) {
|
if (challenge.token) {
|
||||||
const user = getUser(challenge.token);
|
const user = getUser(challenge.token);
|
||||||
if (user) {
|
if (user) {
|
||||||
user.expiresAt = Date.now() + config.powTokenHours * 60 * 60 * 1000;
|
upsertUser({
|
||||||
user.disabledAt = undefined;
|
token: challenge.token,
|
||||||
user.disabledReason = undefined;
|
expiresAt: Date.now() + config.powTokenHours * 60 * 60 * 1000,
|
||||||
upsertUser(user);
|
disabledAt: null,
|
||||||
|
disabledReason: null,
|
||||||
|
});
|
||||||
req.log.info(
|
req.log.info(
|
||||||
{ token: `...${challenge.token.slice(-5)}` },
|
{ token: `...${challenge.token.slice(-5)}` },
|
||||||
"Token refreshed"
|
"Token refreshed"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
</noscript>
|
</noscript>
|
||||||
<style>
|
<style>
|
||||||
#captcha-container {
|
#captcha-container {
|
||||||
max-width: 500px;
|
max-width: 550px;
|
||||||
margin: 50px auto;
|
margin: 20px auto;
|
||||||
}
|
}
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
#captcha-container {
|
#captcha-container {
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
#captcha-progress-text {
|
#captcha-progress-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 18rem;
|
height: 20rem;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
@@ -70,13 +70,22 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #76c7c0;
|
background-color: #76c7c0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#copy-token {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
filter: saturate(0);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#copy-token:hover {
|
||||||
|
filter: saturate(1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="display: none" id="captcha-container">
|
<div style="display: none" id="captcha-container">
|
||||||
<p>
|
<p>
|
||||||
Your device needs to perform a verification task before you can receive a token. This might take anywhere from a few
|
Your device needs to be verified before you can receive a token. This might take anywhere from a few seconds to a
|
||||||
seconds to a few minutes, depending on your device and the proxy's security settings.
|
few minutes, depending on your device and the proxy's security settings.
|
||||||
</p>
|
</p>
|
||||||
<p>Click the button below to start.</p>
|
|
||||||
<details>
|
<details>
|
||||||
<summary>What is this?</summary>
|
<summary>What is this?</summary>
|
||||||
<p>
|
<p>
|
||||||
@@ -107,18 +116,6 @@
|
|||||||
faster than the first one.
|
faster than the first one.
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
<details>
|
|
||||||
<summary>What is the "Workers" setting?</summary>
|
|
||||||
<p>
|
|
||||||
This controls how many CPU cores will be used to solve the verification task. If your device gets too hot or slows
|
|
||||||
down too much during verification, reduce the number of workers.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
For fastest verification, set this to the number of physical CPU cores in your device. Setting more workers than
|
|
||||||
you have actual cores will generally only slow down verification.
|
|
||||||
</p>
|
|
||||||
<p>If you don't understand what this means, leave it at the default setting.</p>
|
|
||||||
</details>
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Other important information</summary>
|
<summary>Other important information</summary>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -134,15 +131,27 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Settings</summary>
|
||||||
|
<div>
|
||||||
|
<label for="workers">Workers:</label>
|
||||||
|
<input type="number" id="workers" value="1" min="1" max="32" onchange="spawnWorkers()" />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
This controls how many CPU cores will be used to solve the verification task. If your device gets too hot or slows
|
||||||
|
down too much during verification, reduce the number of workers.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For fastest verification, set this to the number of physical CPU cores in your device. Setting more workers than
|
||||||
|
you have actual cores will generally only slow down verification.
|
||||||
|
</p>
|
||||||
|
<p>If you don't understand what this means, leave it at the default setting.</p>
|
||||||
|
</details>
|
||||||
<form id="captcha-form" style="display: none">
|
<form id="captcha-form" style="display: none">
|
||||||
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
||||||
<input type="hidden" name="tokenLifetime" value="<%= tokenLifetime %>" />
|
<input type="hidden" name="tokenLifetime" value="<%= tokenLifetime %>" />
|
||||||
</form>
|
</form>
|
||||||
<div id="captcha-control">
|
<div id="captcha-control">
|
||||||
<div>
|
|
||||||
<label for="workers">Workers:</label>
|
|
||||||
<input type="number" id="workers" value="1" min="1" max="32" onchange="spawnWorkers()" />
|
|
||||||
</div>
|
|
||||||
<button id="worker-control" onclick="toggleWorker()">Start verification</button>
|
<button id="worker-control" onclick="toggleWorker()">Start verification</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="captcha-progress-container" style="display: none">
|
<div id="captcha-progress-container" style="display: none">
|
||||||
@@ -185,6 +194,9 @@
|
|||||||
function handleWorkerMessage(e) {
|
function handleWorkerMessage(e) {
|
||||||
switch (e.data.type) {
|
switch (e.data.type) {
|
||||||
case "progress":
|
case "progress":
|
||||||
|
if (solution) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
totalHashes += e.data.hashes;
|
totalHashes += e.data.hashes;
|
||||||
reports++;
|
reports++;
|
||||||
break;
|
break;
|
||||||
@@ -206,13 +218,13 @@
|
|||||||
}
|
}
|
||||||
workers.forEach((w, i) => {
|
workers.forEach((w, i) => {
|
||||||
w.postMessage({ type: "stop" });
|
w.postMessage({ type: "stop" });
|
||||||
setTimeout(() => w.terminate(), 1000 + i * 100)
|
setTimeout(() => w.terminate(), 1000 + i * 100);
|
||||||
});
|
});
|
||||||
workers = [];
|
workers = [];
|
||||||
active = false;
|
active = false;
|
||||||
solution = e.data.nonce;
|
solution = e.data.nonce;
|
||||||
document.getElementById("captcha-result").textContent =
|
document.getElementById("captcha-result").textContent =
|
||||||
"Verification completed. Submitting solution for verification...";
|
"Solution found. Verifying with server...";
|
||||||
document.getElementById("captcha-control").style.display = "none";
|
document.getElementById("captcha-control").style.display = "none";
|
||||||
submitVerification();
|
submitVerification();
|
||||||
break;
|
break;
|
||||||
@@ -233,6 +245,21 @@
|
|||||||
estimateProgress();
|
estimateProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
textArea.remove();
|
||||||
|
} else {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
alert("Copied to clipboard.");
|
||||||
|
}
|
||||||
|
|
||||||
function loadNewChallenge(c, s) {
|
function loadNewChallenge(c, s) {
|
||||||
const btn = document.getElementById("worker-control");
|
const btn = document.getElementById("worker-control");
|
||||||
btn.textContent = "Start verification";
|
btn.textContent = "Start verification";
|
||||||
@@ -248,6 +275,7 @@
|
|||||||
startTime = 0;
|
startTime = 0;
|
||||||
lastUpdateTime = 0;
|
lastUpdateTime = 0;
|
||||||
elapsedTime = 0;
|
elapsedTime = 0;
|
||||||
|
totalHashes = 0;
|
||||||
const targetValue = challenge.d.slice(0, -1);
|
const targetValue = challenge.d.slice(0, -1);
|
||||||
const hashLength = challenge.hl;
|
const hashLength = challenge.hl;
|
||||||
workFactor = Number(BigInt(2) ** BigInt(8 * hashLength) / BigInt(targetValue));
|
workFactor = Number(BigInt(2) ** BigInt(8 * hashLength) / BigInt(targetValue));
|
||||||
@@ -329,52 +357,53 @@
|
|||||||
document.getElementById("captcha-progress").style.display = "none";
|
document.getElementById("captcha-progress").style.display = "none";
|
||||||
document.getElementById("captcha-result").innerHTML = `
|
document.getElementById("captcha-result").innerHTML = `
|
||||||
<p style="color: green">Verification complete</p>
|
<p style="color: green">Verification complete</p>
|
||||||
<p>Your user token is: <code>${data.token}</code></p>
|
<p>Your user token is: <code>${data.token}</code> <button id="copy-token" onclick="copyToClipboard('${data.token}')">📋</button></p>
|
||||||
<p>Valid until: ${new Date(Date.now() + lifetime * 3600 * 1000).toLocaleString()}</p>
|
<p>Valid until: ${new Date(Date.now() + lifetime * 3600 * 1000).toLocaleString()}</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTime(time) {
|
||||||
|
if (time < 60) {
|
||||||
|
return time.toFixed(1) + "s";
|
||||||
|
} else if (time < 3600) {
|
||||||
|
const minutes = Math.floor(time / 60);
|
||||||
|
const seconds = Math.floor(time % 60);
|
||||||
|
return minutes + "m " + seconds + "s";
|
||||||
|
} else {
|
||||||
|
const hours = Math.floor(time / 3600);
|
||||||
|
const minutes = Math.floor((time % 3600) / 60);
|
||||||
|
return hours + "h " + minutes + "m";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function estimateProgress() {
|
function estimateProgress() {
|
||||||
if (reports % workers.length !== 0) {
|
// if (reports % workers.length !== 0) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
if (Date.now() - lastUpdateTime < 500) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
elapsedTime += (Date.now() - lastUpdateTime) / 1000;
|
elapsedTime += (Date.now() - lastUpdateTime) / 1000;
|
||||||
lastUpdateTime = Date.now();
|
lastUpdateTime = Date.now();
|
||||||
const hashRate = totalHashes / elapsedTime;
|
const hashRate = totalHashes / elapsedTime;
|
||||||
const timeRemaining = (workFactor - totalHashes) / hashRate;
|
const timeRemaining = (workFactor - totalHashes) / hashRate;
|
||||||
const progress = 100 * (1 - Math.exp(-totalHashes / workFactor));
|
// const progress = 100 * (1 - Math.exp(-totalHashes / workFactor));
|
||||||
|
|
||||||
const formatTime = (time) => {
|
|
||||||
if (time < 60) {
|
|
||||||
return time.toFixed(1) + "s";
|
|
||||||
} else if (time < 3600) {
|
|
||||||
const minutes = Math.floor(time / 60);
|
|
||||||
const seconds = Math.floor(time % 60);
|
|
||||||
return minutes + "m " + seconds + "s";
|
|
||||||
} else {
|
|
||||||
const hours = Math.floor(time / 3600);
|
|
||||||
const minutes = Math.floor((time % 3600) / 60);
|
|
||||||
return hours + "h " + minutes + "m";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const p = 1 / workFactor;
|
const p = 1 / workFactor;
|
||||||
const odds = ((1 - p) ** totalHashes * 100).toFixed(2);
|
const odds = ((1 - p) ** totalHashes * 100).toFixed(3);
|
||||||
|
const progress = 100 - odds;
|
||||||
|
|
||||||
let note = "";
|
// let note = " (" + odds + "% odds of no solution yet)";
|
||||||
if (odds < 33) {
|
|
||||||
note = " (" + odds + "% odds of no solution yet)";
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("captcha-progress").style.width = Math.min(progress, 100) + "%";
|
document.querySelector("#captcha-progress>.progress").style.width = Math.min(progress, 100) + "%";
|
||||||
document.getElementById("captcha-progress-text").value = `
|
document.getElementById("captcha-progress-text").value = `
|
||||||
Solution probability: 1 in ${workFactor.toLocaleString()} hashes
|
Solution probability: 1 in ${workFactor.toLocaleString()} hashes
|
||||||
Hashes computed: ${totalHashes.toLocaleString()}${note}
|
Hashes computed: ${totalHashes.toLocaleString()}
|
||||||
|
Luckiness: ${odds}%
|
||||||
Elapsed time: ${formatTime(elapsedTime)}
|
Elapsed time: ${formatTime(elapsedTime)}
|
||||||
Hash rate: ${hashRate.toFixed(2)} H/s
|
Hash rate: ${hashRate.toFixed(2)} H/s
|
||||||
Workers: ${workers.length}${isMobileWebkit ? " (iOS/iPadOS detected)" : ""}
|
Workers: ${workers.length}${isMobileWebkit ? " (iOS/iPadOS detected)" : ""}
|
||||||
${active ? `Average time remaining: ${formatTime(timeRemaining)}` : "Verification task stopped"}`.trim();
|
${active ? `Average time remaining: ${formatTime(timeRemaining)}` : "Verification stopped"}`.trim();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
<%- include("partials/shared_header", { title: "Request User Token" }) %>
|
<%- include("partials/shared_header", { title: "Request User Token" }) %>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#request-buttons {
|
#request-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
margin-top: 20px;
|
align-items: center;
|
||||||
width: 400px;
|
margin: 20px 0;
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#request-buttons button {
|
#request-container button {
|
||||||
margin: 0 10px;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#refresh-token-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<h1>Request User Token</h1>
|
<h1>Request User Token</h1>
|
||||||
@@ -28,37 +35,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<div id="existing-token" style="display: none">
|
<div id="request-container">
|
||||||
<p>It looks like you might have an older temporary user token. If it has expired, you can try to refresh it.</p>
|
<button id="request-token" onclick="requestChallenge('new')">Get a new token</button>
|
||||||
<strong id="existing-token-value">Existing token:</strong>
|
<button id="refresh-token-toggle" onclick="switchSection('refresh')">Refresh an old token</button>
|
||||||
|
<h6 id="existing-token-value" style="display: none">Existing token:</h6>
|
||||||
</div>
|
</div>
|
||||||
<div id="request-buttons">
|
<div id="back-to-menu" style="display: none">
|
||||||
<button disabled id="refresh-token" onclick="requestChallenge('refresh')">Refresh old token</button>
|
<a href="#" onclick="switchSection('root')">« Back</a>
|
||||||
<button id="request_token" onclick="requestChallenge('new')">Request new token</button>
|
</div>
|
||||||
|
<div id="refresh-container" style="display: none">
|
||||||
|
<div id="existing-token">
|
||||||
|
<p>
|
||||||
|
If you have an existing or expired token, enter it here to try to refresh it by completing a shorter verification.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label for="refresh-token-input">Existing token:</label>
|
||||||
|
<input type="text" id="refresh-token-input" />
|
||||||
|
<button id="refresh-token" onclick="requestChallenge('refresh')">Refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include("partials/user_challenge_widget") %>
|
<%- include("partials/user_challenge_widget") %>
|
||||||
<script>
|
<script>
|
||||||
|
function switchSection(sectionId) {
|
||||||
|
const backToMenu = document.getElementById("back-to-menu");
|
||||||
|
const captchaSection = document.getElementById("captcha-container");
|
||||||
|
const requestSection = document.getElementById("request-container");
|
||||||
|
const refreshSection = document.getElementById("refresh-container");
|
||||||
|
[backToMenu, captchaSection, requestSection, refreshSection].forEach((element) => (element.style.display = "none"));
|
||||||
|
switch (sectionId) {
|
||||||
|
case "root":
|
||||||
|
requestSection.style.display = "flex";
|
||||||
|
maybeLoadExistingToken();
|
||||||
|
break;
|
||||||
|
case "captcha":
|
||||||
|
captchaSection.style.display = "block";
|
||||||
|
backToMenu.style.display = "block";
|
||||||
|
break;
|
||||||
|
case "refresh":
|
||||||
|
refreshSection.style.display = "block";
|
||||||
|
backToMenu.style.display = "block";
|
||||||
|
document.getElementById("refresh-token-input").focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function requestChallenge(action) {
|
function requestChallenge(action) {
|
||||||
const token = localStorage.getItem("captcha-temp-token");
|
const savedToken = localStorage.getItem("captcha-temp-token");
|
||||||
if (token && action === "new") {
|
const refreshInput = document.getElementById("refresh-token-input").value;
|
||||||
const data = JSON.parse(token);
|
if (savedToken && action === "new") {
|
||||||
const { expires } = data;
|
const confirmation = confirm(
|
||||||
const expiresDate = new Date(expires);
|
"It looks like you might already have an existing token. Are you sure you want to request a new one?"
|
||||||
const now = new Date();
|
);
|
||||||
if (expiresDate > now) {
|
if (!confirmation) {
|
||||||
if (!confirm("You already have an existing token. Are you sure you want to request a new one?")) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
localStorage.removeItem("captcha-temp-token");
|
|
||||||
document.getElementById("existing-token").style.display = "none";
|
|
||||||
document.getElementById("refresh-token").disabled = true;
|
|
||||||
}
|
}
|
||||||
} else if (!token && action === "refresh") {
|
localStorage.removeItem("captcha-temp-token");
|
||||||
alert("You don't have an existing token to refresh");
|
document.getElementById("existing-token").style.display = "none";
|
||||||
|
document.getElementById("refresh-token").disabled = true;
|
||||||
|
} else if (!refreshInput?.length && action === "refresh") {
|
||||||
|
alert("You need to provide a token to refresh.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshToken = token && action === "refresh" ? JSON.parse(token).token : undefined;
|
const refreshToken = action === "refresh" ? refreshInput : undefined;
|
||||||
const keyInput = document.getElementById("proxy-key");
|
const keyInput = document.getElementById("proxy-key");
|
||||||
const proxyKey = (keyInput && keyInput.value) || undefined;
|
const proxyKey = (keyInput && keyInput.value) || undefined;
|
||||||
if (!proxyKey?.length) {
|
if (!proxyKey?.length) {
|
||||||
@@ -79,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
const { challenge, signature } = data;
|
const { challenge, signature } = data;
|
||||||
loadNewChallenge(challenge, signature);
|
loadNewChallenge(challenge, signature);
|
||||||
document.getElementById("request-buttons").style.display = "none";
|
switchSection("captcha");
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -87,22 +127,26 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingToken = localStorage.getItem("captcha-temp-token");
|
function maybeLoadExistingToken() {
|
||||||
if (existingToken) {
|
const existingToken = localStorage.getItem("captcha-temp-token");
|
||||||
const data = JSON.parse(existingToken);
|
if (existingToken) {
|
||||||
const { token, expires } = data;
|
const data = JSON.parse(existingToken);
|
||||||
const expiresDate = new Date(expires);
|
const { token, expires } = data;
|
||||||
document.getElementById(
|
const expiresDate = new Date(expires);
|
||||||
"existing-token-value"
|
document.getElementById(
|
||||||
).textContent = `Your token: ${token} (valid until ${expiresDate.toLocaleString()})`;
|
"existing-token-value"
|
||||||
document.getElementById("existing-token").style.display = "block";
|
).textContent = `User token: ${token} (valid until ${expiresDate.toLocaleString()})`;
|
||||||
document.getElementById("refresh-token").disabled = false;
|
document.getElementById("existing-token-value").style.display = "block";
|
||||||
|
document.getElementById("refresh-token-input").value = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyKey = localStorage.getItem("captcha-proxy-key");
|
||||||
|
if (proxyKey && document.getElementById("proxy-key")) {
|
||||||
|
document.getElementById("proxy-key").value = proxyKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyKey = localStorage.getItem("captcha-proxy-key");
|
switchSection("root");
|
||||||
if (proxyKey && document.getElementById("proxy-key")) {
|
|
||||||
document.getElementById("proxy-key").value = proxyKey;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<%- include("partials/user_footer") %>
|
<%- include("partials/user_footer") %>
|
||||||
|
|||||||
Reference in New Issue
Block a user