diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index db6999a..e95e0c8 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The Dockerfile has been removed as it is no longer in use - Developer documentation has been added to the docs site - Show more errors when some predictable challenge page errors happen ([#150](https://github.com/TecharoHQ/anubis/issues/150)) +- Verification page now shows hash rate and a progress bar for completion probability. ## v1.15.0 diff --git a/web/index.templ b/web/index.templ index 8aa626c..1899ae3 100644 --- a/web/index.templ +++ b/web/index.templ @@ -27,120 +27,28 @@ templ base(title string, body templ.Component) { text-align: center; } - .lds-roller, - .lds-roller div, - .lds-roller div:after { - box-sizing: border-box; + #status { + font-variant-numeric: tabular-nums; } - .lds-roller { - display: inline-block; - position: relative; - width: 80px; - height: 80px; + #progress { + display: none; + width: min(20rem, 90%); + height: 2rem; + border-radius: 1rem; + overflow: hidden; + margin: 1rem 0 2rem; + outline-color: #b16286; + outline-offset: 2px; + outline-style: solid; + outline-width: 4px; } - .lds-roller div { - animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; - transform-origin: 40px 40px; - } - - .lds-roller div:after { - content: " "; - display: block; - position: absolute; - width: 7.2px; - height: 7.2px; - border-radius: 50%; - background: currentColor; - margin: -3.6px 0 0 -3.6px; - } - - .lds-roller div:nth-child(1) { - animation-delay: -0.036s; - } - - .lds-roller div:nth-child(1):after { - top: 62.62742px; - left: 62.62742px; - } - - .lds-roller div:nth-child(2) { - animation-delay: -0.072s; - } - - .lds-roller div:nth-child(2):after { - top: 67.71281px; - left: 56px; - } - - .lds-roller div:nth-child(3) { - animation-delay: -0.108s; - } - - .lds-roller div:nth-child(3):after { - top: 70.90963px; - left: 48.28221px; - } - - .lds-roller div:nth-child(4) { - animation-delay: -0.144s; - } - - .lds-roller div:nth-child(4):after { - top: 72px; - left: 40px; - } - - .lds-roller div:nth-child(5) { - animation-delay: -0.18s; - } - - .lds-roller div:nth-child(5):after { - top: 70.90963px; - left: 31.71779px; - } - - .lds-roller div:nth-child(6) { - animation-delay: -0.216s; - } - - .lds-roller div:nth-child(6):after { - top: 67.71281px; - left: 24px; - } - - .lds-roller div:nth-child(7) { - animation-delay: -0.252s; - } - - .lds-roller div:nth-child(7):after { - top: 62.62742px; - left: 17.37258px; - } - - .lds-roller div:nth-child(8) { - animation-delay: -0.288s; - } - - .lds-roller div:nth-child(8):after { - top: 56px; - left: 12.28719px; - } - - .mx-auto { - margin-left: auto; - margin-right: auto; - } - - @keyframes lds-roller { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } + .bar-inner { + background-color: #b16286; + height: 100%; + width: 0; + transition: width 0.25s ease-in; } @templ.JSONScript("anubis_version", anubis.Version) @@ -181,15 +89,8 @@ templ index() { />

Loading...

Why am I seeing this?

You are seeing this because the administrator of this website has set up Anubis to protect the server against the scourge of AI companies aggressively scraping websites. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.

Anubis is a compromise. Anubis uses a Proof-of-Work scheme in the vein of Hashcash, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.

Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.

Please note that Anubis requires the use of modern JavaScript features that plugins like JShelter will disable. Please disable JShelter or other such plugins for this domain.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">
Why am I seeing this?

You are seeing this because the administrator of this website has set up Anubis to protect the server against the scourge of AI companies aggressively scraping websites. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.

Anubis is a compromise. Anubis uses a Proof-of-Work scheme in the vein of Hashcash, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.

Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.

Please note that Anubis requires the use of modern JavaScript features that plugins like JShelter will disable. Please disable JShelter or other such plugins for this domain.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -195,7 +195,7 @@ func errorPage(message string) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/sad.webp?cacheBuster=" + anubis.Version) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 216, Col: 90} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 117, Col: 90} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -208,7 +208,7 @@ func errorPage(message string) templ.Component { var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 218, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 119, Col: 14} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { diff --git a/web/js/main.mjs b/web/js/main.mjs index daaafcf..01f21f0 100644 --- a/web/js/main.mjs +++ b/web/js/main.mjs @@ -37,7 +37,7 @@ const dependencies = [ const status = document.getElementById('status'); const image = document.getElementById('image'); const title = document.getElementById('title'); - const spinner = document.getElementById('spinner'); + const progress = document.getElementById('progress'); const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent); const ohNoes = ({ @@ -46,8 +46,7 @@ const dependencies = [ title.innerHTML = titleMsg; status.innerHTML = statusMsg; image.src = imageSrc; - spinner.innerHTML = ""; - spinner.style.display = "none"; + progress.style.display = "none"; }; if (!window.isSecureContext) { @@ -68,8 +67,7 @@ const dependencies = [ // title.innerHTML = "Oh no!"; // status.innerHTML = "Checks failed. Please check your browser's settings and try again."; // image.src = imageURL("sad"); - // spinner.innerHTML = ""; - // spinner.style.display = "none"; + // progress.style.display = "none"; // return; // } @@ -112,20 +110,59 @@ const dependencies = [ return; } - status.innerHTML = `Calculating...
Difficulty: ${rules.report_as}`; - spinner.style.display = "block"; + status.innerHTML = `Calculating...
Difficulty: ${rules.report_as}, `; + progress.style.display = "inline-block"; + // the whole text, including "Speed:", as a single node, because some browsers + // (Firefox mobile) present screen readers with each node as a separate piece + // of text. + const rateText = document.createTextNode("Speed: 0kH/s"); + status.appendChild(rateText); + + let lastSpeedUpdate = 0; + let showingApology = false; + const likelihood = Math.pow(16, -rules.report_as); try { const t0 = Date.now(); - const { hash, nonce } = await process(challenge, rules.difficulty); + const { hash, nonce } = await process( + challenge, + rules.difficulty, + (iters) => { + const delta = Date.now() - t0; + // only update the speed every second so it's less visually distracting + if (delta - lastSpeedUpdate > 1000) { + lastSpeedUpdate = delta; + rateText.data = `Speed: ${(iters / delta).toFixed(3)}kH/s`; + } + + // the probability of still being on the page is (1 - likelihood) ^ iters. + // by definition, half of the time the progress bar only gets to half, so + // apply a polynomial ease-out function to move faster in the beginning + // and then slow down as things get increasingly unlikely. quadratic felt + // the best in testing, but this may need adjustment in the future. + const probability = Math.pow(1 - likelihood, iters); + const distance = (1 - Math.pow(probability, 2)) * 100; + progress["aria-valuenow"] = distance; + progress.firstElementChild.style.width = `${distance}%`; + + if (probability < 0.1 && !showingApology) { + status.append( + document.createElement("br"), + document.createTextNode( + "Verification is taking longer than expected. Please do not refresh the page.", + ), + ); + showingApology = true; + } + }, + ); const t1 = Date.now(); console.log({ hash, nonce }); title.innerHTML = "Success!"; status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`; image.src = imageURL("happy", anubisVersion); - spinner.innerHTML = ""; - spinner.style.display = "none"; + progress.style.display = "none"; setTimeout(() => { const redir = window.location.href; diff --git a/web/js/proof-of-work-slow.mjs b/web/js/proof-of-work-slow.mjs index e30dc21..6522c0b 100644 --- a/web/js/proof-of-work-slow.mjs +++ b/web/js/proof-of-work-slow.mjs @@ -1,6 +1,11 @@ // https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm -export default function process(data, difficulty = 5, _threads = 1) { +export default function process( + data, + difficulty = 5, + progressCallback = null, + _threads = 1, +) { console.debug("slow algo"); return new Promise((resolve, reject) => { let webWorkerURL = URL.createObjectURL(new Blob([ @@ -10,8 +15,12 @@ export default function process(data, difficulty = 5, _threads = 1) { let worker = new Worker(webWorkerURL); worker.onmessage = (event) => { - worker.terminate(); - resolve(event.data); + if (typeof event.data === "number") { + progressCallback?.(event.data); + } else { + worker.terminate(); + resolve(event.data); + } }; worker.onerror = (event) => { @@ -47,6 +56,9 @@ function processTask() { let hash; let nonce = 0; do { + if (nonce & 1023 === 0) { + postMessage(nonce); + } hash = await sha256(data + nonce++); } while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')); diff --git a/web/js/proof-of-work.mjs b/web/js/proof-of-work.mjs index b4f9c53..60d8d61 100644 --- a/web/js/proof-of-work.mjs +++ b/web/js/proof-of-work.mjs @@ -1,4 +1,9 @@ -export default function process(data, difficulty = 5, threads = (navigator.hardwareConcurrency || 1)) { +export default function process( + data, + difficulty = 5, + progressCallback = null, + threads = (navigator.hardwareConcurrency || 1), +) { console.debug("fast algo"); return new Promise((resolve, reject) => { let webWorkerURL = URL.createObjectURL(new Blob([ @@ -11,9 +16,12 @@ export default function process(data, difficulty = 5, threads = (navigator.hardw let worker = new Worker(webWorkerURL); worker.onmessage = (event) => { - workers.forEach(worker => worker.terminate()); - worker.terminate(); - resolve(event.data); + if (typeof event.data === "number") { + progressCallback?.(event.data); + } else { + workers.forEach(worker => worker.terminate()); + resolve(event.data); + } }; worker.onerror = (event) => { @@ -55,6 +63,8 @@ function processTask() { let nonce = event.data.nonce; let threads = event.data.threads; + const threadId = nonce; + while (true) { const currentHash = await sha256(data + nonce); const thisHash = new Uint8Array(currentHash); @@ -78,7 +88,21 @@ function processTask() { break; } + const oldNonce = nonce; nonce += threads; + + // send a progess update every 1024 iterations. since each thread checks + // separate values, one simple way to do this is by bit masking the + // nonce for multiples of 1024. unfortunately, if the number of threads + // is not prime, only some of the threads will be sending the status + // update and they will get behind the others. this is slightly more + // complicated but ensures an even distribution between threads. + if ( + nonce > oldNonce | 1023 && // we've wrapped past 1024 + (nonce >> 10) % threads === threadId // and it's our turn + ) { + postMessage(nonce); + } } postMessage({