diff --git a/berghain.go b/berghain.go index b032cda..2827191 100644 --- a/berghain.go +++ b/berghain.go @@ -15,8 +15,8 @@ type LevelConfig struct { Duration time.Duration Type ValidationType - // Captcha configuration, required for the turnstile, hcaptcha and - // recaptcha validation types. + // Captcha configuration, required for the turnstile, hcaptcha, + // recaptcha and cap validation types. CaptchaSitekey string CaptchaSecret string // CaptchaVerifyURL overrides the provider siteverify endpoint, diff --git a/cmd/spop/config.go b/cmd/spop/config.go index e2e1c1b..eb7e542 100644 --- a/cmd/spop/config.go +++ b/cmd/spop/config.go @@ -95,12 +95,14 @@ func (c LevelConfig) AsLevelConfig() *berghain.LevelConfig { lc.Type = berghain.ValidationTypeHCaptcha case "recaptcha": lc.Type = berghain.ValidationTypeReCaptcha + case "cap": + lc.Type = berghain.ValidationTypeCap default: Fatal("unknown validation type", "validator", c.Type) } switch lc.Type { - case berghain.ValidationTypeTurnstile, berghain.ValidationTypeHCaptcha, berghain.ValidationTypeReCaptcha: + case berghain.ValidationTypeTurnstile, berghain.ValidationTypeHCaptcha, berghain.ValidationTypeReCaptcha, berghain.ValidationTypeCap: if c.Sitekey == "" || c.Secret == "" { Fatal("captcha types require a sitekey and a secret", "validator", c.Type) } diff --git a/cmd/spop/config.yaml b/cmd/spop/config.yaml index fc39457..04db283 100644 --- a/cmd/spop/config.yaml +++ b/cmd/spop/config.yaml @@ -22,9 +22,16 @@ frontend: - duration: 10s type: pow countdown: 0 - # captcha levels (turnstile, hcaptcha, recaptcha) verify the widget + # captcha levels (turnstile, hcaptcha, recaptcha, cap) verify the widget # token against the provider, so the agent needs outbound HTTPS access - duration: 12h type: turnstile sitekey: 1x00000000000000000000AA # dummy sitekey, always passes secret: 1x0000000000000000000000000000000AA # dummy secret, always passes + - duration: 240s # nb: this must be long enough to cover the time to captcha completion + type: cap + sitekey: b125d3f13a + secret: sk-xxx + verify_url: https://captcha.example.com/b125d3f13a/siteverify # cap uses paths scoped to the sitekey + skip_hostname_check: true # TODO: why? + countdown: 1 diff --git a/validator_captcha.go b/validator_captcha.go index 7e42e3c..206425b 100644 --- a/validator_captcha.go +++ b/validator_captcha.go @@ -80,6 +80,9 @@ func (captchaValidator) isValid(b *Berghain, req *ValidatorRequest, _ *Validator verifyURL = "https://api.hcaptcha.com/siteverify" case ValidationTypeReCaptcha: verifyURL = "https://www.google.com/recaptcha/api/siteverify" + case ValidationTypeCap: + // not very useful to guess a default here + return fmt.Errorf("%w: undefined verify_url", errCaptchaUnavailable) } } diff --git a/validators.go b/validators.go index 918efc6..32815ce 100644 --- a/validators.go +++ b/validators.go @@ -19,6 +19,7 @@ const ( ValidationTypeTurnstile ValidationTypeHCaptcha ValidationTypeReCaptcha + ValidationTypeCap ) type ValidatorResponse struct { @@ -82,7 +83,7 @@ func (v ValidationType) RunValidator(b *Berghain, req *ValidatorRequest, resp *V return validatorNone(b, req, resp) case ValidationTypePOW: return validatorPOW(b, req, resp) - case ValidationTypeTurnstile, ValidationTypeHCaptcha, ValidationTypeReCaptcha: + case ValidationTypeTurnstile, ValidationTypeHCaptcha, ValidationTypeReCaptcha, ValidationTypeCap: return validatorCaptcha(b, req, resp) default: return errors.New("unknown validation type") diff --git a/web/src/challange/challanges.js b/web/src/challange/challanges.js index 53bfef8..4819bfd 100644 --- a/web/src/challange/challanges.js +++ b/web/src/challange/challanges.js @@ -85,6 +85,12 @@ export const captchaProviders = Object.freeze({ // when the script was loaded async. useReady: true, }), + 6: Object.freeze({ + name: "Cap", + script: "https://test.home.lysergic.dev/cap/cap.min.js", // TODO: make this configurable + global: "cap", + useReady: true, + }), }); const captchaOnloadCallback = "__berghainCaptchaLoaded"; @@ -118,6 +124,41 @@ function loadProviderScript(provider, environment){ */ async function loadCaptchaApi(provider, environment){ let api; + + if (provider.name === "Cap"){ + environment.CAP_CUSTOM_WASM_URL = ["/cap/cap_wasm_bg.wasm"]; // TODO: make this configurable + + environment.cap = { + ready: (callback) => { + if (typeof callback === "function"){ + callback(); + } + }, + render: (container, options) => { + const widgetElement = environment.document.createElement("cap-widget"); + container.appendChild(widgetElement); + + const capInstance = new environment.Cap({ + apiEndpoint: `/${options.sitekey}`, + }, widgetElement); + + capInstance.addEventListener("solve", (event) => { + if (options && typeof options.callback === "function"){ + options.callback(event.detail.token); + } + }); + + capInstance.addEventListener("error", () => { + if (options && typeof options["error-callback"] === "function"){ + options["error-callback"](); + } + }); + + return capInstance; + }, + }; + } + try { await loadProviderScript(provider, environment); api = environment[provider.global]; @@ -188,6 +229,8 @@ export function getChallengeSolver(challengeType){ case 4: case 5: return [`Waiting for ${captchaProviders[challengeType].name}...`, challengeCaptcha]; + case 6: + return ["Please solve the challenge...", challengeCaptcha]; default: throw new Error(`Unknown challenge type: ${challengeType}`); } diff --git a/web/test/challanges.test.js b/web/test/challanges.test.js index 3820f46..75f8086 100644 --- a/web/test/challanges.test.js +++ b/web/test/challanges.test.js @@ -18,7 +18,7 @@ function scriptEnvironment(onScript){ } test("describes every captcha challenge type", () => { - assert.deepEqual(Object.keys(captchaProviders), ["3", "4", "5"]); + assert.deepEqual(Object.keys(captchaProviders), ["3", "4", "5", "6"]); for (const provider of Object.values(captchaProviders)){ assert.match(provider.script, /^https:\/\//);