diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 73606ea..9cd1560 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -214,6 +214,7 @@ nicksnyder nobots NONINFRINGEMENT nosleep +nullglob OCOB ogtag oklch @@ -278,6 +279,7 @@ Seo setsebool shellcheck shirou +shopt Sidetrade simprint sitemap diff --git a/lib/challenge/metarefresh/metarefresh_templ.go b/lib/challenge/metarefresh/metarefresh_templ.go index 0c7a837..048260b 100644 --- a/lib/challenge/metarefresh/metarefresh_templ.go +++ b/lib/challenge/metarefresh/metarefresh_templ.go @@ -93,9 +93,9 @@ func page(redir string, difficulty int, loc *localization.SimpleLocalizer) templ return templ_7745c5c3_Err } var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir)) + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty+1, redir)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 85} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { diff --git a/lib/challenge/preact/build.sh b/lib/challenge/preact/build.sh index c7f2e3f..ebd6d59 100755 --- a/lib/challenge/preact/build.sh +++ b/lib/challenge/preact/build.sh @@ -40,9 +40,9 @@ for the JavaScript code in this page. mkdir -p static/js -for file in js/*.jsx; do +for file in js/*.tsx; do filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx" - output="${filename%.jsx}.js" # Changes "app.jsx" to "app.js" + output="${filename%.tsx}.js" # Changes "app.jsx" to "app.js" echo $output esbuild "${file}" --minify --bundle --outfile=static/"${output}" --banner:js="${LICENSE}" diff --git a/lib/challenge/preact/js/app.jsx b/lib/challenge/preact/js/app.jsx deleted file mode 100644 index 4aafae9..0000000 --- a/lib/challenge/preact/js/app.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import { render, h, Fragment } from 'preact'; -import { useState, useEffect } from 'preact/hooks'; -import { g, j, u, x } from "./xeact.js"; -import { Sha256 } from '@aws-crypto/sha256-js'; - -/** @jsx h */ -/** @jsxFrag Fragment */ - -function toHexString(arr) { - return Array.from(arr) - .map((c) => c.toString(16).padStart(2, "0")) - .join(""); -} - -const App = () => { - const [state, setState] = useState(null); - const [imageURL, setImageURL] = useState(null); - const [passed, setPassed] = useState(false); - const [challenge, setChallenge] = useState(null); - - useEffect(() => { - setState(j("preact_info")); - }); - - useEffect(() => { - setImageURL(state.pensive_url); - const hash = new Sha256(''); - hash.update(state.challenge); - setChallenge(toHexString(hash.digestSync())); - }, [state]); - - useEffect(() => { - const timer = setTimeout(() => { - setPassed(true); - }, state.difficulty * 125); - - return () => clearTimeout(timer); - }, [challenge]); - - useEffect(() => { - window.location.href = u(state.redir, { - result: challenge, - }); - }, [passed]); - - return ( - <> - {imageURL !== null && ( - - )} - {state !== null && ( - <> -

{state.loading_message}

-

{state.connection_security_message}

- - )} - - ); -}; - -x(g("app")); -render(, g("app")); \ No newline at end of file diff --git a/lib/challenge/preact/js/app.tsx b/lib/challenge/preact/js/app.tsx new file mode 100644 index 0000000..efa90bd --- /dev/null +++ b/lib/challenge/preact/js/app.tsx @@ -0,0 +1,87 @@ +import { render, h, Fragment } from "preact"; +import { useState, useEffect } from "preact/hooks"; +import { g, j, r, u, x } from "./xeact.js"; +import { Sha256 } from "@aws-crypto/sha256-js"; + +/** @jsx h */ +/** @jsxFrag Fragment */ + +function toHexString(arr: Uint8Array) { + return Array.from(arr) + .map((c) => c.toString(16).padStart(2, "0")) + .join(""); +} + +interface PreactInfo { + redir: string; + challenge: string; + difficulty: number; + connection_security_message: string; + loading_message: string; + pensive_url: string; +} + +const App = () => { + const [state, setState] = useState(); + const [imageURL, setImageURL] = useState(null); + const [passed, setPassed] = useState(false); + const [challenge, setChallenge] = useState(null); + + useEffect(() => { + setState(j("preact_info")); + }); + + useEffect(() => { + if (state === undefined) { + return; + } + + setImageURL(state?.pensive_url); + const hash = new Sha256(""); + hash.update(state.challenge); + setChallenge(toHexString(hash.digestSync())); + }, [state]); + + useEffect(() => { + if (state === undefined) { + return; + } + + const timer = setTimeout(() => { + setPassed(true); + }, state?.difficulty * 125); + + return () => clearTimeout(timer); + }, [challenge]); + + useEffect(() => { + if (state === undefined) { + return; + } + + if (challenge === null) { + return; + } + + window.location.href = u(state.redir, { + result: challenge, + }); + }, [passed]); + + return ( + <> + {imageURL !== null && ( + + )} + {state !== undefined && ( + <> +

{state.loading_message}

+

{state.connection_security_message}

+ + )} + + ); +}; + +x(g("app")); +render(, g("app")); diff --git a/web/build.sh b/web/build.sh index 84aa654..d0818e0 100755 --- a/web/build.sh +++ b/web/build.sh @@ -39,9 +39,18 @@ for the JavaScript code in this page. mkdir -p static/locales cp ../lib/localization/locales/*.json static/locales/ -for file in js/*.mjs js/worker/*.mjs; do - esbuild "${file}" --sourcemap --bundle --minify --outfile=static/"${file}" --banner:js="${LICENSE}" - gzip -f -k -n static/${file} - zstd -f -k --ultra -22 static/${file} - brotli -fZk static/${file} +shopt -s nullglob globstar + +for file in js/**/*.ts js/**/*.mjs; do + out="static/${file}" + if [[ "$file" == *.ts ]]; then + out="static/${file%.ts}.mjs" + fi + + mkdir -p "$(dirname "$out")" + + esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE" + gzip -f -k -n "$out" + zstd -f -k --ultra -22 "$out" + brotli -fZk "$out" done diff --git a/web/js/algorithms/fast.mjs b/web/js/algorithms/fast.ts similarity index 68% rename from web/js/algorithms/fast.mjs rename to web/js/algorithms/fast.ts index ee08a19..178cef8 100644 --- a/web/js/algorithms/fast.mjs +++ b/web/js/algorithms/fast.ts @@ -1,11 +1,21 @@ +type ProgressCallback = (nonce: number) => void; + +interface ProcessOptions { + basePrefix: string; + version: string; +} + +const getHardwareConcurrency = () => + navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1; + export default function process( - { basePrefix, version }, - data, - difficulty = 5, - signal = null, - progressCallback = null, - threads = Math.trunc(Math.max(navigator.hardwareConcurrency / 2, 1)), -) { + options: ProcessOptions, + data: string, + difficulty: number = 5, + signal: AbortSignal | null = null, + progressCallback?: ProgressCallback, + threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)), +): Promise { console.debug("fast algo"); let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs"; @@ -16,13 +26,17 @@ export default function process( } return new Promise((resolve, reject) => { - let webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${version}`; + let webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${options.version}`; - console.log(webWorkerURL); - - const workers = []; + const workers: Worker[] = []; let settled = false; + const onAbort = () => { + console.log("PoW aborted"); + cleanup(); + reject(new DOMException("Aborted", "AbortError")); + }; + const cleanup = () => { if (settled) { return; @@ -34,12 +48,6 @@ export default function process( } }; - const onAbort = () => { - console.log("PoW aborted"); - cleanup(); - reject(new DOMException("Aborted", "AbortError")); - }; - if (signal != null) { if (signal.aborted) { return onAbort(); diff --git a/web/js/algorithms/index.mjs b/web/js/algorithms/index.ts similarity index 80% rename from web/js/algorithms/index.mjs rename to web/js/algorithms/index.ts index cc1ae5d..5b57183 100644 --- a/web/js/algorithms/index.mjs +++ b/web/js/algorithms/index.ts @@ -1,4 +1,4 @@ -import fast from "./fast.mjs"; +import fast from "./fast"; export default { fast: fast, diff --git a/web/js/bench.mjs b/web/js/bench.ts similarity index 78% rename from web/js/bench.mjs rename to web/js/bench.ts index a3c4f09..719b167 100644 --- a/web/js/bench.mjs +++ b/web/js/bench.ts @@ -1,20 +1,24 @@ -import algorithms from "./algorithms/index.mjs"; +import algorithms from "./algorithms"; const defaultDifficulty = 4; -const status = document.getElementById("status"); -const difficultyInput = document.getElementById("difficulty-input"); -const algorithmSelect = document.getElementById("algorithm-select"); -const compareSelect = document.getElementById("compare-select"); -const header = document.getElementById("table-header"); -const headerCompare = document.getElementById("table-header-compare"); -const results = document.getElementById("results"); +const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement; +const difficultyInput: HTMLInputElement = document.getElementById("difficulty-input") as HTMLInputElement; +const algorithmSelect: HTMLSelectElement = document.getElementById("algorithm-select") as HTMLSelectElement; +const compareSelect: HTMLSelectElement = document.getElementById("compare-select") as HTMLSelectElement; +const header: HTMLTableRowElement = document.getElementById("table-header") as HTMLTableRowElement; +const headerCompare: HTMLTableSectionElement = document.getElementById("table-header-compare") as HTMLTableSectionElement; +const results: HTMLTableRowElement = document.getElementById("results") as HTMLTableRowElement; const setupControls = () => { - difficultyInput.value = defaultDifficulty; + if (defaultDifficulty == null) { + return; + } + + difficultyInput.value = defaultDifficulty.toString(); for (const alg of Object.keys(algorithms)) { const option1 = document.createElement("option"); - algorithmSelect.append(option1); + algorithmSelect?.append(option1); const option2 = document.createElement("option"); compareSelect.append(option2); option1.value = option1.innerText = option2.value = option2.innerText = alg; @@ -116,13 +120,13 @@ const benchmarkLoop = async (controller) => { await benchmarkLoop(controller); }; -let controller = null; +let controller: AbortController | null = null; const reset = () => { stats.time = stats.iters = 0; comparison.time = comparison.iters = 0; results.innerHTML = status.innerText = ""; - const table = results.parentElement; + const table = results.parentElement as HTMLElement; if (compareSelect.value !== "NONE") { table.style.gridTemplateColumns = "repeat(4,auto)"; header.style.display = "none"; diff --git a/web/js/main.mjs b/web/js/main.ts similarity index 84% rename from web/js/main.mjs rename to web/js/main.ts index 1d68627..e37c536 100644 --- a/web/js/main.mjs +++ b/web/js/main.ts @@ -1,12 +1,21 @@ -import algorithms from "./algorithms/index.mjs"; +import algorithms from "./algorithms"; // from Xeact -const u = (url = "", params = {}) => { +const u = (url: string = "", params: Record = {}) => { let result = new URL(url, window.location.href); Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v)); return result.toString(); }; +const j = (id: string): any | null => { + const elem = document.getElementById(id); + if (elem === null) { + return null; + } + + return JSON.parse(elem.textContent); +}; + const imageURL = (mood, cacheBuster, basePrefix) => u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster, @@ -14,9 +23,10 @@ const imageURL = (mood, cacheBuster, basePrefix) => // Detect available languages by loading the manifest const getAvailableLanguages = async () => { - const basePrefix = JSON.parse( - document.getElementById("anubis_base_prefix").textContent, - ); + const basePrefix = j("anubis_base_prefix"); + if (basePrefix === null) { + return; + } try { const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`); @@ -38,9 +48,11 @@ const getBrowserLanguage = async () => // Load translations from JSON files const loadTranslations = async (lang) => { - const basePrefix = JSON.parse( - document.getElementById("anubis_base_prefix").textContent, - ); + const basePrefix = j("anubis_base_prefix"); + if (basePrefix === null) { + return; + } + try { const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`); return await response.json(); @@ -54,9 +66,10 @@ const loadTranslations = async (lang) => { }; const getRedirectUrl = () => { - const publicUrl = JSON.parse( - document.getElementById("anubis_public_url").textContent, - ); + const publicUrl = j("anubis_public_url"); + if (publicUrl === null) { + return; + } if (publicUrl && window.location.href.startsWith(publicUrl)) { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('redir'); @@ -91,16 +104,14 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key; value: navigator.cookieEnabled, }, ]; - const status = document.getElementById("status"); - const image = document.getElementById("image"); - const title = document.getElementById("title"); - const progress = document.getElementById("progress"); - const anubisVersion = JSON.parse( - document.getElementById("anubis_version").textContent, - ); - const basePrefix = JSON.parse( - document.getElementById("anubis_base_prefix").textContent, - ); + + const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement; + const image: HTMLImageElement = document.getElementById("image") as HTMLImageElement; + const title: HTMLHeadingElement = document.getElementById("title") as HTMLHeadingElement; + const progress: HTMLDivElement = document.getElementById("progress") as HTMLDivElement; + + const anubisVersion = j("anubis_version"); + const basePrefix = j("anubis_base_prefix"); const details = document.querySelector("details"); let userReadDetails = false; @@ -132,9 +143,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key; } } - const { challenge, rules } = JSON.parse( - document.getElementById("anubis_challenge").textContent, - ); + const { challenge, rules } = j("anubis_challenge"); const process = algorithms[rules.algorithm]; if (!process) { @@ -182,7 +191,9 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key; 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 (progress.firstElementChild !== null) { + (progress.firstElementChild as HTMLElement).style.width = `${distance}%`; + } if (probability < 0.1 && !showingApology) { status.append( @@ -197,7 +208,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key; console.log({ hash, nonce }); if (userReadDetails) { - const container = document.getElementById("progress"); + const container: HTMLDivElement = document.getElementById("progress") as HTMLDivElement; // Style progress bar as a continue button container.style.display = "flex"; diff --git a/web/js/worker/sha256-purejs.mjs b/web/js/worker/sha256-purejs.ts similarity index 97% rename from web/js/worker/sha256-purejs.mjs rename to web/js/worker/sha256-purejs.ts index 3211b44..6906061 100644 --- a/web/js/worker/sha256-purejs.mjs +++ b/web/js/worker/sha256-purejs.ts @@ -6,7 +6,7 @@ const calculateSHA256 = (text) => { return hash.digest(); }; -function toHexString(arr) { +function toHexString(arr: Uint8Array): string { return Array.from(arr) .map((c) => c.toString(16).padStart(2, "0")) .join(""); diff --git a/web/js/worker/sha256-webcrypto.mjs b/web/js/worker/sha256-webcrypto.ts similarity index 94% rename from web/js/worker/sha256-webcrypto.mjs rename to web/js/worker/sha256-webcrypto.ts index c2b071a..c83f466 100644 --- a/web/js/worker/sha256-webcrypto.mjs +++ b/web/js/worker/sha256-webcrypto.ts @@ -1,10 +1,11 @@ const encoder = new TextEncoder(); -const calculateSHA256 = async (input) => { + +const calculateSHA256 = async (input: string) => { const data = encoder.encode(input); return await crypto.subtle.digest("SHA-256", data); }; -const toHexString = (byteArray) => { +const toHexString = (byteArray: Uint8Array) => { return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); };