| <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; |
| |
| |
| |
| |
| 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> |
|
|