Skip to content

Commit 0bab484

Browse files
committed
feat(web): explain missing challenge capabilities
1 parent 2f7a3de commit 0bab484

7 files changed

Lines changed: 150 additions & 3 deletions

File tree

web/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ <h3 class="heading">Request on Hold</h3>
2222
</p>
2323
</noscript>
2424
<p id="cookie-warning" style="display: none">You need to enable Cookies to verify your Browser.</p>
25+
<div id="capability-errors" class="capability-errors" style="display: none"></div>
2526
<p>If you suspect that you are being blocked by mistake, make sure that you have the
2627
latest browser version installed and all active browser plugins are disabled.</p>
2728
<p>If you see this and the verification repeats, please try a different browser.</p>

web/src/challange/capabilities.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const advice = {
2+
textEncoder: {
3+
name: "Text encoding",
4+
message: "This challenge needs the browser TextEncoder API.",
5+
fix: "Use an up-to-date browser with JavaScript enabled.",
6+
},
7+
webCrypto: {
8+
name: "Web Crypto",
9+
message: "This challenge needs the Web Crypto API.",
10+
fix: "Use an up-to-date browser over HTTPS and disable extensions that block crypto.subtle.",
11+
},
12+
worker: {
13+
name: "Web Workers",
14+
message: "This challenge must run in a Web Worker.",
15+
fix: "Disable extensions that block Web Workers or try a standard browser configuration.",
16+
},
17+
};
18+
19+
/**
20+
* Return advice only for missing capabilities required by this challenge.
21+
*
22+
* @param {number} challengeType
23+
* @param {{environment?: object, nativeCrypto?: boolean}} [options]
24+
* @return {{name: string, message: string, fix: string}[]}
25+
*/
26+
export function detectMissingCapabilities(challengeType, {
27+
environment = globalThis,
28+
nativeCrypto = import.meta.env.VITE_NATIVE_CRYPTO === "true",
29+
} = {}){
30+
if (challengeType !== 1 && challengeType !== 2){
31+
return [];
32+
}
33+
34+
const missing = [];
35+
if (typeof environment.TextEncoder !== "function"){
36+
missing.push(advice.textEncoder);
37+
}
38+
if (nativeCrypto && !environment.crypto?.subtle){
39+
missing.push(advice.webCrypto);
40+
}
41+
if (challengeType === 2 && typeof environment.Worker !== "function"){
42+
missing.push(advice.worker);
43+
}
44+
return missing;
45+
}

web/src/challange/challanger.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {detectMissingCapabilities} from "./capabilities";
12
import {getChallengeSolver} from "./challanges";
23
import * as loader from "./loader.js";
34

@@ -33,6 +34,12 @@ export async function doChallenge(){
3334
const {t} = challenge;
3435
countdown = challenge.c;
3536

37+
const missing = detectMissingCapabilities(t);
38+
if (missing.length){
39+
loader.showCapabilities(missing);
40+
throw new Error(`Required browser feature unavailable: ${missing.map(({name}) => name).join(", ")}`);
41+
}
42+
3643
/* @berghain:inline challenge-start */
3744

3845
const [name, solver] = getChallengeSolver(t);

web/src/challange/loader.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,39 @@ export function start(){
1414

1515
export function showError(error){
1616
const errors = /** @type {HTMLDivElement} */ (document.querySelector(".error-container"));
17-
errors.insertAdjacentHTML("beforeend", `<code>${error}</code>`);
17+
if (!errors){
18+
return;
19+
}
20+
const message = document.createElement("code");
21+
message.textContent = error;
22+
errors.append(message);
23+
}
24+
25+
/**
26+
* Show actionable advice for capabilities required by the issued challenge.
27+
*
28+
* @param {{name: string, message: string, fix: string}[]} missing
29+
*/
30+
export function showCapabilities(missing){
31+
const container = document.getElementById("capability-errors");
32+
if (!container){
33+
return;
34+
}
35+
36+
const summary = document.createElement("p");
37+
summary.textContent = "This browser is missing features required by the current challenge:";
38+
39+
const list = document.createElement("ul");
40+
for (const capability of missing){
41+
const item = document.createElement("li");
42+
const name = document.createElement("strong");
43+
name.textContent = capability.name;
44+
item.append(name, `: ${capability.message} ${capability.fix}`);
45+
list.append(item);
46+
}
47+
48+
container.replaceChildren(summary, list);
49+
container.style.display = "block";
1850
}
1951

2052
export function setChallengeInfo(text){

web/src/main.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {doChallenge} from "./challange/challanger";
77
/* @berghain:inline init */
88

99
if (!navigator.cookieEnabled){
10-
const noCookie = document.getElementById("no-cookie");
11-
noCookie.style.display = "block";
10+
const cookieWarning = document.getElementById("cookie-warning");
11+
if (cookieWarning){
12+
cookieWarning.style.display = "block";
13+
}
1214
return;
1315
}
1416

web/src/style.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@
3434
position: absolute;
3535
}
3636

37+
.capability-errors {
38+
margin: .5em 0;
39+
padding: .5em 1em;
40+
border-left: 3px solid #b65f0a;
41+
background-color: #fdf6ec;
42+
text-align: left;
43+
44+
ul {
45+
margin: .25em 0 0;
46+
padding-left: 1.2em;
47+
}
48+
}
49+
3750
code {
3851
padding: 16px;
3952
overflow: auto;

web/test/capabilities.test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import {detectMissingCapabilities} from "../src/challange/capabilities.js";
5+
6+
const completeEnvironment = {
7+
TextEncoder: class {},
8+
Worker: class {},
9+
crypto: {subtle: {}},
10+
};
11+
12+
test("does not probe capabilities unused by a no-op challenge", () => {
13+
assert.deepEqual(detectMissingCapabilities(0, {
14+
environment: {},
15+
nativeCrypto: true,
16+
}), []);
17+
});
18+
19+
test("requires only text encoding for bundled-crypto POW", () => {
20+
assert.deepEqual(detectMissingCapabilities(1, {
21+
environment: completeEnvironment,
22+
nativeCrypto: false,
23+
}), []);
24+
25+
const missing = detectMissingCapabilities(1, {
26+
environment: {},
27+
nativeCrypto: false,
28+
});
29+
assert.deepEqual(missing.map(({name}) => name), ["Text encoding"]);
30+
});
31+
32+
test("requires Web Crypto only in the native build", () => {
33+
const missing = detectMissingCapabilities(1, {
34+
environment: {TextEncoder: class {}},
35+
nativeCrypto: true,
36+
});
37+
assert.deepEqual(missing.map(({name}) => name), ["Web Crypto"]);
38+
});
39+
40+
test("requires a Worker only for worker POW", () => {
41+
const environment = {...completeEnvironment, Worker: undefined};
42+
const missing = detectMissingCapabilities(2, {
43+
environment,
44+
nativeCrypto: false,
45+
});
46+
assert.deepEqual(missing.map(({name}) => name), ["Web Workers"]);
47+
});

0 commit comments

Comments
 (0)