344 / src /user /web /views /partials /user_challenge_widget.ejs
aukaru's picture
Upload 236 files
5c5b371 verified
<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: 550px;
margin: 20px 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: 20rem;
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;
}
#copy-token {
border: none;
background: none;
filter: saturate(0);
padding: 0;
}
#copy-token:hover {
filter: saturate(1);
}
</style>
<div style="display: none" id="captcha-container">
<p>
Your device needs to be verified 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>
<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>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>
<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">
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
<input type="hidden" name="tokenLifetime" value="<%= tokenLifetime %>" />
</form>
<div id="captcha-control">
<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) {
if (solution) {
return;
}
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":
workers.forEach((w, i) => {
w.postMessage({ type: "stop" });
setTimeout(() => w.terminate(), 1000 + i * 100);
});
workers = [];
active = false;
solution = e.data.nonce;
document.getElementById("captcha-result").textContent =
"Solution found. Verifying with server...";
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 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) {
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;
totalHashes = 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"),
};
if (localStorage.getItem("captcha-proxy-key")) {
body.proxyKey = localStorage.getItem("captcha-proxy-key");
}
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> <button id="copy-token" onclick="copyToClipboard('${data.token}')">📋</button></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() {
if (Date.now() - lastUpdateTime < 500) {
return;
}
elapsedTime += (Date.now() - lastUpdateTime) / 1000;
lastUpdateTime = Date.now();
const hashRate = totalHashes / elapsedTime;
const timeRemaining = (workFactor - totalHashes) / hashRate;
const p = 1 / workFactor;
const odds = ((1 - p) ** totalHashes * 100).toFixed(3);
const progress = 100 - odds;
document.querySelector("#captcha-progress>.progress").style.width = Math.min(progress, 100) + "%";
document.getElementById("captcha-progress-text").value = `
Solution probability: 1 in ${workFactor.toLocaleString()} hashes
Hashes computed: ${totalHashes.toLocaleString()}
Luckiness: ${odds}%
Elapsed time: ${formatTime(elapsedTime)}
Hash rate: ${hashRate.toFixed(2)} H/s
Workers: ${workers.length}${isMobileWebkit ? " (iOS/iPadOS detected)" : ""}
${active ? `Average time remaining: ${formatTime(timeRemaining)}` : "Verification stopped"}`.trim();
}
</script>