main.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import algorithms from "./algorithms";
  2. // from Xeact
  3. const u = (url: string = "", params: Record<string, any> = {}) => {
  4. let result = new URL(url, window.location.href);
  5. Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v));
  6. return result.toString();
  7. };
  8. const j = (id: string): any | null => {
  9. const elem = document.getElementById(id);
  10. if (elem === null) {
  11. return null;
  12. }
  13. return JSON.parse(elem.textContent);
  14. };
  15. const imageURL = (mood, cacheBuster, basePrefix) =>
  16. u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, {
  17. cacheBuster,
  18. });
  19. // Detect available languages by loading the manifest
  20. const getAvailableLanguages = async () => {
  21. const basePrefix = j("anubis_base_prefix");
  22. if (basePrefix === null) {
  23. return;
  24. }
  25. try {
  26. const response = await fetch(
  27. `${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`,
  28. );
  29. if (response.ok) {
  30. const manifest = await response.json();
  31. return manifest.supportedLanguages || ["en"];
  32. }
  33. } catch (error) {
  34. console.warn(
  35. "Failed to load language manifest, falling back to default languages",
  36. );
  37. }
  38. // Fallback to default languages if manifest loading fails
  39. return ["en"];
  40. };
  41. // Use the browser language from the HTML lang attribute which is set by the server settings or request headers
  42. const getBrowserLanguage = async () => document.documentElement.lang;
  43. // Load translations from JSON files
  44. const loadTranslations = async (lang) => {
  45. const basePrefix = j("anubis_base_prefix");
  46. if (basePrefix === null) {
  47. return;
  48. }
  49. try {
  50. const response = await fetch(
  51. `${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`,
  52. );
  53. return await response.json();
  54. } catch (error) {
  55. console.warn(
  56. `Failed to load translations for ${lang}, falling back to English`,
  57. );
  58. if (lang !== "en") {
  59. return await loadTranslations("en");
  60. }
  61. throw error;
  62. }
  63. };
  64. const getRedirectUrl = () => {
  65. const publicUrl = j("anubis_public_url");
  66. if (publicUrl === null) {
  67. return;
  68. }
  69. if (publicUrl && window.location.href.startsWith(publicUrl)) {
  70. const urlParams = new URLSearchParams(window.location.search);
  71. return urlParams.get("redir");
  72. }
  73. return window.location.href;
  74. };
  75. let translations = {};
  76. let currentLang;
  77. // Initialize translations
  78. const initTranslations = async () => {
  79. currentLang = await getBrowserLanguage();
  80. translations = await loadTranslations(currentLang);
  81. };
  82. const t = (key) => translations[`js_${key}`] || translations[key] || key;
  83. (async () => {
  84. // Initialize translations first
  85. await initTranslations();
  86. const dependencies = [
  87. {
  88. name: "Web Workers",
  89. msg: t("web_workers_error"),
  90. value: window.Worker,
  91. },
  92. {
  93. name: "Cookies",
  94. msg: t("cookies_error"),
  95. value: navigator.cookieEnabled,
  96. },
  97. ];
  98. const status: HTMLParagraphElement = document.getElementById(
  99. "status",
  100. ) as HTMLParagraphElement;
  101. const image: HTMLImageElement = document.getElementById(
  102. "image",
  103. ) as HTMLImageElement;
  104. const title: HTMLHeadingElement = document.getElementById(
  105. "title",
  106. ) as HTMLHeadingElement;
  107. const progress: HTMLDivElement = document.getElementById(
  108. "progress",
  109. ) as HTMLDivElement;
  110. const anubisVersion = j("anubis_version");
  111. const basePrefix = j("anubis_base_prefix");
  112. const details = document.querySelector("details");
  113. let userReadDetails = false;
  114. if (details) {
  115. details.addEventListener("toggle", () => {
  116. if (details.open) {
  117. userReadDetails = true;
  118. }
  119. });
  120. }
  121. const ohNoes = ({ titleMsg, statusMsg, imageSrc }) => {
  122. title.innerHTML = titleMsg;
  123. status.innerHTML = statusMsg;
  124. image.src = imageSrc;
  125. progress.style.display = "none";
  126. };
  127. status.innerHTML = t("calculating");
  128. for (const { value, name, msg } of dependencies) {
  129. if (!value) {
  130. ohNoes({
  131. titleMsg: `${t("missing_feature")} ${name}`,
  132. statusMsg: msg,
  133. imageSrc: imageURL("reject", anubisVersion, basePrefix),
  134. });
  135. return;
  136. }
  137. }
  138. const { challenge, rules } = j("anubis_challenge");
  139. const process = algorithms[rules.algorithm];
  140. if (!process) {
  141. ohNoes({
  142. titleMsg: t("challenge_error"),
  143. statusMsg: t("challenge_error_msg"),
  144. imageSrc: imageURL("reject", anubisVersion, basePrefix),
  145. });
  146. return;
  147. }
  148. status.innerHTML = `${t("calculating_difficulty")} ${rules.difficulty}, `;
  149. progress.style.display = "inline-block";
  150. // the whole text, including "Speed:", as a single node, because some browsers
  151. // (Firefox mobile) present screen readers with each node as a separate piece
  152. // of text.
  153. const rateText = document.createTextNode(`${t("speed")} 0kH/s`);
  154. status.appendChild(rateText);
  155. let lastSpeedUpdate = 0;
  156. let showingApology = false;
  157. const likelihood = Math.pow(16, -rules.difficulty);
  158. try {
  159. const t0 = Date.now();
  160. const { hash, nonce } = await process(
  161. { basePrefix, version: anubisVersion },
  162. challenge.randomData,
  163. rules.difficulty,
  164. null,
  165. (iters) => {
  166. const delta = Date.now() - t0;
  167. // only update the speed every second so it's less visually distracting
  168. if (delta - lastSpeedUpdate > 1000) {
  169. lastSpeedUpdate = delta;
  170. rateText.data = `${t("speed")} ${(iters / delta).toFixed(3)}kH/s`;
  171. }
  172. // the probability of still being on the page is (1 - likelihood) ^ iters.
  173. // by definition, half of the time the progress bar only gets to half, so
  174. // apply a polynomial ease-out function to move faster in the beginning
  175. // and then slow down as things get increasingly unlikely. quadratic felt
  176. // the best in testing, but this may need adjustment in the future.
  177. const probability = Math.pow(1 - likelihood, iters);
  178. const distance = (1 - Math.pow(probability, 2)) * 100;
  179. progress["aria-valuenow"] = distance;
  180. if (progress.firstElementChild !== null) {
  181. (progress.firstElementChild as HTMLElement).style.width =
  182. `${distance}%`;
  183. }
  184. if (probability < 0.1 && !showingApology) {
  185. status.append(
  186. document.createElement("br"),
  187. document.createTextNode(t("verification_longer")),
  188. );
  189. showingApology = true;
  190. }
  191. },
  192. );
  193. const t1 = Date.now();
  194. console.log({ hash, nonce });
  195. if (userReadDetails) {
  196. const container: HTMLDivElement = document.getElementById(
  197. "progress",
  198. ) as HTMLDivElement;
  199. // Style progress bar as a continue button
  200. container.style.display = "flex";
  201. container.style.alignItems = "center";
  202. container.style.justifyContent = "center";
  203. container.style.height = "2rem";
  204. container.style.borderRadius = "1rem";
  205. container.style.cursor = "pointer";
  206. container.style.background = "#b16286";
  207. container.style.color = "white";
  208. container.style.fontWeight = "bold";
  209. container.style.outline = "4px solid #b16286";
  210. container.style.outlineOffset = "2px";
  211. container.style.width = "min(20rem, 90%)";
  212. container.style.margin = "1rem auto 2rem";
  213. container.innerHTML = t("finished_reading");
  214. function onDetailsExpand() {
  215. const redir = getRedirectUrl();
  216. window.location.replace(
  217. u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
  218. id: challenge.id,
  219. response: hash,
  220. nonce,
  221. redir,
  222. elapsedTime: t1 - t0,
  223. }),
  224. );
  225. }
  226. container.onclick = onDetailsExpand;
  227. setTimeout(onDetailsExpand, 30000);
  228. } else {
  229. const redir = getRedirectUrl();
  230. window.location.replace(
  231. u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
  232. id: challenge.id,
  233. response: hash,
  234. nonce,
  235. redir,
  236. elapsedTime: t1 - t0,
  237. }),
  238. );
  239. }
  240. } catch (err) {
  241. ohNoes({
  242. titleMsg: t("calculation_error"),
  243. statusMsg: `${t("calculation_error_msg")} ${err.message}`,
  244. imageSrc: imageURL("reject", anubisVersion, basePrefix),
  245. });
  246. }
  247. })();