Files
OAI-Proxy/src/user/web/views/partials/user_challenge_widget.ejs
T

373 lines
12 KiB
Plaintext

<noscript>
<p style="color: darkorange; background-color: #ffeecc; padding: 1em">
JavaScript needs to be enabled to complete verification.
</p>
</noscript>
<style>
#captcha-container {
max-width: 500px;
margin: 50px auto;
}
@media (max-width: 1000px) {
#captcha-container {
max-width: unset;
margin: 30px;
}
}
#captcha-container details {
margin: 10px 0;
}
#captcha-container details p {
margin-left: 20px;
}
#captcha-container details li {
margin: 2px 0 0 20px;
line-height: 1.5;
}
#captcha-control {
display: flex;
flex-direction: column;
justify-content: space-between;
}
#captcha-control button {
flex-grow: 1;
margin: 10px;
padding: 10px 20px;
}
#captcha-progress-text {
width: 100%;
height: 18rem;
resize: vertical;
font-family: monospace;
}
#captcha-progress-container {
margin: 20px 0;
}
#captcha-progress-container textarea {
margin-top: 5px;
background-color: transparent;
color: inherit;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #e0e6f6;
border-radius: 5px;
overflow: hidden;
}
.progress {
width: 0;
height: 100%;
background-color: #76c7c0;
}
</style>
<div style="display: none" id="captcha-container">
<p>
Your device needs to perform a verification task before you can receive a token. This might take anywhere from a few
seconds to a few minutes, depending on your device and the proxy's security settings.
</p>
<p>Click the button below to start.</p>
<details>
<summary>What is this?</summary>
<p>
This is a <a href="https://en.wikipedia.org/wiki/Proof_of_work" target="_blank">proof-of-work</a> verification
task designed to slow down automated abuse. It requires your device's CPU to find a solution to a cryptographic
puzzle, after which a user token will be issued.
</p>
</details>
<details>
<summary>How long does verification take?</summary>
<p>
It depends on the device you're using and the current difficulty level (<code><%= difficultyLevel %></code>). The
faster your device, the quicker it will solve the task.
</p>
<p>
An estimate will be displayed once verification starts. Because the task is probabilistic, your device could solve
it more quickly or take longer than the estimate.
</p>
</details>
<details>
<summary>How often do I need to do this?</summary>
<p>
Once you've earned a user token, you can use it for <strong><%= `${tokenLifetime} hours` %></strong> before it
expires.
</p>
<p>
You can refresh an expired token by returning to this page and verifying again. Subsequent verifications will go
faster than the first one.
</p>
</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>
<summary>Other important information</summary>
<ul>
<li>Don't change your IP address during verification.</li>
<li>Don't close this tab until verification is complete.</li>
<li>
Verification must be finished within <strong><%= `${challengeTimeout} minutes` %></strong>.
</li>
<li>Your user token will be registered to your current IP address.</li>
<li>
Up to <strong><%= tokenMaxIps || "unlimited" %></strong> IP addresses total can be registered to your user
token.
</li>
</ul>
</details>
<form id="captcha-form" style="display: none">
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
<input type="hidden" name="tokenLifetime" value="<%= tokenLifetime %>" />
</form>
<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>
</div>
<div id="captcha-progress-container" style="display: none">
<label for="captcha-progress-text">Status:</label>
<div id="captcha-progress" class="progress-bar">
<div class="progress"></div>
</div>
<textarea disabled id="captcha-progress-text"></textarea>
</div>
<div id="captcha-result"></div>
</div>
<script>
let workers = [];
let challenge = null;
let signature = null;
let solution = null;
let totalHashes = 0;
let startTime = 0;
let lastUpdateTime = 0;
let reports = 0;
let elapsedTime = 0;
let workFactor = 0;
let active = false;
// Safari is all kinds of fucked and throws WASM Memory errors when memory
// pressure is high. Batch size and worker count need to be reduced to prevent
// this.
function isIOSiPadOSWebKit() {
const userAgent = navigator.userAgent;
const isWebKit = userAgent.includes("Safari") && !userAgent.includes("Chrome") && !userAgent.includes("Android");
const isIOS =
/iPad|iPhone|iPod/.test(navigator.platform) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
return isWebKit && isIOS;
}
let isMobileWebkit = isIOSiPadOSWebKit();
function handleWorkerMessage(e) {
switch (e.data.type) {
case "progress":
totalHashes += e.data.hashes;
reports++;
break;
case "started":
active = true;
document.getElementById("worker-control").textContent = "Pause verification";
startTime = Date.now();
lastUpdateTime = startTime;
document.getElementById("captcha-progress-container").style.display = "block";
break;
case "paused":
active = false;
document.getElementById("worker-control").textContent = "Start verification";
document.getElementById("workers").disabled = false;
break;
case "solved":
if (solution) {
return;
}
workers.forEach((w) => w.postMessage({ type: "stop" }));
active = false;
solution = e.data.nonce;
document.getElementById("captcha-result").textContent =
"Verification completed. Submitting solution for verification...";
document.getElementById("captcha-control").style.display = "none";
submitVerification();
break;
case "error":
workers.forEach((w) => w.postMessage({ type: "stop" }));
active = false;
const msg = e.data.error || "An unknown error occurred.";
const debug = e.data.debug || "";
document.getElementById("captcha-result").innerHTML = `
<p style="color:red">Error: ${msg}</p>
<pre style="color: red">${debug.stack}</pre>
<pre style="color: red">${debug.lastNonce}, ${String(debug.targetValue)}</pre>
<p>Refresh the page and try again. Use another device or browser if the problem persists, or lower the number of workers.</p>
`;
break;
}
estimateProgress();
}
function loadNewChallenge(c, s) {
const btn = document.getElementById("worker-control");
btn.textContent = "Start verification";
document.getElementById("captcha-container").style.display = "block";
document.getElementById("workers").disabled = false;
const maxWorkers = isMobileWebkit ? 6 : 16;
document.getElementById("workers").value = Math.min(maxWorkers, navigator.hardwareConcurrency || 4).toString();
challenge = c;
signature = s;
solution = null;
nonce = 0;
startTime = 0;
lastUpdateTime = 0;
elapsedTime = 0;
const targetValue = challenge.d.slice(0, -1);
const hashLength = challenge.hl;
workFactor = Number(BigInt(2) ** BigInt(8 * hashLength) / BigInt(targetValue));
spawnWorkers();
}
function spawnWorkers() {
for (const worker of workers) {
worker.terminate();
}
workers = [];
const selectedWorkers = document.getElementById("workers").value;
const workerCount = Math.min(32, Math.max(1, parseInt(selectedWorkers)));
for (let i = 0; i < workerCount; i++) {
const worker = new Worker("/res/js/hash-worker.js");
worker.onmessage = handleWorkerMessage;
workers.push(worker);
}
}
function toggleWorker() {
if (active) {
workers.forEach((w) => w.postMessage({ type: "stop" }));
} else {
const workerCount = workers.length;
const hashSpace = BigInt(challenge.hl * 8) ** BigInt(2);
const workerSpace = hashSpace / BigInt(workerCount);
const alreadyHashed = Math.floor(totalHashes / workerCount);
document.getElementById("workers").disabled = true;
for (let i = 0; i < workerCount; i++) {
const startNonce = workerSpace * BigInt(i) + BigInt(alreadyHashed);
workers[i].postMessage({
type: "start",
challenge: challenge,
signature: signature,
nonce: startNonce,
isMobileWebkit,
});
}
}
}
function submitVerification() {
if (!solution) {
return;
}
const body = {
challenge: challenge,
signature: signature,
solution: String(solution),
_csrf: document.querySelector("meta[name=csrf-token]").getAttribute("content"),
};
fetch("/user/captcha/verify", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
.then((res) => res.json())
.then((data) => {
if (data.error) {
document.getElementById("captcha-result").textContent = "Error: " + data.error;
} else {
const lifetime = document.getElementById("captcha-form").querySelector('input[name="tokenLifetime"]').value;
window.localStorage.setItem(
"captcha-temp-token",
JSON.stringify({
token: data.token,
expires: Date.now() + lifetime * 3600 * 1000,
})
);
document.getElementById("captcha-progress").style.display = "none";
document.getElementById("captcha-result").innerHTML = `
<p style="color: green">Verification complete</p>
<p>Your user token is: <code>${data.token}</code></p>
<p>Valid until: ${new Date(Date.now() + lifetime * 3600 * 1000).toLocaleString()}</p>
`;
}
});
}
function estimateProgress() {
if (reports % workers.length !== 0) {
return;
}
elapsedTime += (Date.now() - lastUpdateTime) / 1000;
lastUpdateTime = Date.now();
const hashRate = totalHashes / elapsedTime;
const expectedTimeRemaining = (workFactor - totalHashes) / hashRate;
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 odds = ((1 - p) ** totalHashes * 100).toFixed(2);
let note = "";
if (odds < 33) {
note = " (" + odds + "% odds of no solution yet)";
}
document.getElementById("captcha-progress").style.width = Math.min(progress, 100) + "%";
document.getElementById("captcha-progress-text").value = `
Average hashes needed: ${workFactor.toLocaleString()}
Hashes computed: ${totalHashes.toLocaleString()}${note}
Elapsed time: ${formatTime(elapsedTime)}
Hash rate: ${hashRate.toFixed(2)} H/s
Workers: ${workers.length}${isMobileWebkit ? " (iOS/iPadOS detected)" : ""}
${active ? `Approx. time remaining: ${formatTime(expectedTimeRemaining)}` : "Verification task stopped"}`.trim();
}
</script>