1
0
Fork 0
firefox/toolkit/components/captchadetection/CaptchaDetectionChild.sys.mjs
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

542 lines
14 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
/** @type {lazy} */
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "console", () => {
return console.createInstance({
prefix: "CaptchaDetectionChild",
maxLogLevelPref: "captchadetection.loglevel",
});
});
/**
* Abstract class for handling captchas.
*/
class CaptchaHandler {
static type = "abstract";
/**
* @param {CaptchaDetectionChild} actor - The window actor.
* @param {Event} _event - The initial event that created the actor.
* @param {boolean} skipConstructedNotif - Whether to skip the constructed notification. Used for captchas with multiple frames.
*/
constructor(actor, _event, skipConstructedNotif = false) {
/** @type {CaptchaDetectionChild} */
this.actor = actor;
if (!skipConstructedNotif) {
this.notifyConstructed();
}
}
notifyConstructed() {
lazy.console.debug(`CaptchaHandler constructed: ${this.constructor.type}`);
this.actor.sendAsyncMessage("CaptchaHandler:Constructed", {
type: this.constructor.type,
});
}
static matches(_document) {
throw new Error("abstract method");
}
updateState(state) {
this.actor.sendAsyncMessage("CaptchaState:Update", state);
}
onActorDestroy() {
lazy.console.debug("CaptchaHandler destroyed");
}
/**
* @param {Event} event - The event to handle.
*/
handleEvent(event) {
lazy.console.debug("CaptchaHandler got event:", event);
}
/**
* @param {ReceiveMessageArgument} message - The message to handle.
*/
receiveMessage(message) {
lazy.console.debug("CaptchaHandler got message:", message);
}
}
/**
* Handles Google Recaptcha v2 captchas.
*
* ReCaptcha v2 places two iframes in the page. One for the
* challenge and one for the checkmark. This handler listens
* for the checkmark being clicked or the challenge being
* shown. When either of these events happen, the handler
* sends a message to the parent actor. The handler then
* disconnects the mutation observer to avoid further
* processing.
*/
class GoogleRecaptchaV2Handler extends CaptchaHandler {
#enabled;
#mutationObserver;
static type = "g-recaptcha-v2";
constructor(actor, event) {
super(
actor,
event,
actor.document.location.pathname.endsWith("/bframe") ||
(Cu.isInAutomation &&
actor.document.location.pathname.endsWith(
"g_recaptcha_v2_checkbox.html"
))
);
this.#enabled = true;
this.#mutationObserver = new this.actor.contentWindow.MutationObserver(
this.#mutationHandler.bind(this)
);
this.#mutationObserver.observe(this.actor.document, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style"],
});
}
static matches(document) {
if (Cu.isInAutomation) {
return (
document
.getElementById("captchaType")
?.getAttribute("data-captcha-type") === GoogleRecaptchaV2Handler.type
);
}
return (
[
"https://www.google.com/recaptcha/api2/",
"https://www.google.com/recaptcha/enterprise/",
].some(match => document.location.href.startsWith(match)) &&
!document.location.search.includes("size=invisible")
);
}
#mutationHandler(_mutations, observer) {
if (!this.#enabled) {
return;
}
const token = this.actor.document.getElementById("recaptcha-token");
const initialized = token && token.value !== "";
if (!initialized) {
return;
}
const checkmark = this.actor.document.getElementById("recaptcha-anchor");
if (checkmark && checkmark.ariaChecked === "true") {
this.updateState({
type: GoogleRecaptchaV2Handler.type,
changes: "GotCheckmark",
});
this.#enabled = false;
observer.disconnect();
return;
}
const images = this.actor.document.getElementById("rc-imageselect");
if (images) {
this.updateState({
type: GoogleRecaptchaV2Handler.type,
changes: "ImagesShown",
});
this.#enabled = false;
observer.disconnect();
}
}
onActorDestroy() {
super.onActorDestroy();
this.#mutationObserver.disconnect();
}
}
/**
* Handles Cloudflare Turnstile captchas.
*
* Cloudflare Turnstile captchas have a success and fail div
* that are displayed when the captcha is completed. This
* handler listens for the success or fail div to be displayed
* and sends a message to the parent actor when either of
* these events happen. The handler then disconnects the
* mutation observer to avoid further processing.
* We use two mutation observers to detect shadowroot
* creation and then observe the shadowroot for the success
* or fail div.
*/
class CFTurnstileHandler extends CaptchaHandler {
#observingShadowRoot;
#mutationObserver;
static type = "cf-turnstile";
constructor(actor, event) {
super(actor, event);
this.#observingShadowRoot = false;
if (this.actor.document.body?.openOrClosedShadowRoot) {
this.#observeShadowRoot(this.actor.document.body.openOrClosedShadowRoot);
return;
}
this.#mutationObserver = new this.actor.contentWindow.MutationObserver(
this.#mutationHandler.bind(this)
);
this.#mutationObserver.observe(this.actor.document.documentElement, {
attributes: true,
});
}
static matchesRegex = new RegExp(
"https://challenges.cloudflare.com/cdn-cgi/challenge-platform/.+?/turnstile/if/ov2/av0/rcv/"
);
static matches(document) {
if (Cu.isInAutomation) {
return (
document
.getElementById("captchaType")
?.getAttribute("data-captcha-type") === CFTurnstileHandler.type
);
}
return CFTurnstileHandler.matchesRegex.test(document.location.href);
}
#mutationHandler(_mutations, observer) {
lazy.console.debug(_mutations);
if (this.#observingShadowRoot) {
return;
}
const shadowRoot = this.actor.document.body?.openOrClosedShadowRoot;
if (!shadowRoot) {
return;
}
observer.disconnect();
lazy.console.debug("Found shadowRoot", shadowRoot);
this.#observeShadowRoot(shadowRoot);
}
#observeShadowRoot(shadowRoot) {
if (this.#observingShadowRoot) {
return;
}
this.#observingShadowRoot = true;
this.#mutationObserver = new this.actor.contentWindow.MutationObserver(
(_mutations, observer) => {
const fail = shadowRoot.getElementById("fail");
const success = shadowRoot.getElementById("success");
if (!fail || !success) {
return;
}
if (fail.style.display !== "none") {
lazy.console.debug("Captcha failed");
this.updateState({
type: CFTurnstileHandler.type,
result: "Failed",
});
observer.disconnect();
return;
}
if (success.style.display !== "none") {
lazy.console.debug("Captcha succeeded");
this.updateState({
type: CFTurnstileHandler.type,
result: "Succeeded",
});
observer.disconnect();
}
}
).observe(shadowRoot, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style"],
});
}
onActorDestroy() {
super.onActorDestroy();
this.#mutationObserver.disconnect();
}
}
/**
* Handles Datadome captchas.
*
* Datadome works by placing an iframe that postMessages to
* the parent window. This actor attaches to the iframe
* that posts messages to the parent window. Therefore, we
* ask the parent actor to initialize a new actor in the
* parent window. That actor will then listen for messages
* from the iframe and send a message to the parent actor
* when the captcha is completed.
*/
class DatadomeHandler extends CaptchaHandler {
static type = "datadome";
constructor(actor, event) {
super(actor, event);
event.stopImmediatePropagation();
this.actor
.sendQuery("CaptchaDetection:Init", { type: DatadomeHandler.type })
.then(() => {
// re-dispatch the event
event.target.dispatchEvent(event);
});
}
static matches(document) {
if (Cu.isInAutomation) {
return (
document
.getElementById("captchaType")
?.getAttribute("data-captcha-type") === DatadomeHandler.type
);
}
return document.location.href.startsWith(
"https://geo.captcha-delivery.com/captcha/"
);
}
}
/**
* Handles hCaptcha captchas.
*
* hCaptcha works by placing two iframes in the page. One for
* the challenge and one for the checkbox. This handler listens
* for the challenge being shown or the checkbox being checked.
* When either of these events happen, the handler sends a
* message to the parent actor. The handler then disconnects
* the mutation observer to avoid further processing.
*/
class HCaptchaHandler extends CaptchaHandler {
#shown;
#checked;
#mutationObserver;
static type = "hCaptcha";
constructor(actor, event) {
let params = null;
try {
params = new URLSearchParams(actor.document.location.hash.slice(1));
} catch {
// invalid URL
super(actor, event, true);
return;
}
const frameType = params.get("frame");
super(actor, event, frameType === "challenge");
if (frameType === "challenge") {
this.#initChallengeHandler();
} else if (frameType === "checkbox") {
this.#initCheckboxHandler();
}
}
static matches(document) {
if (Cu.isInAutomation) {
return (
document
.getElementById("captchaType")
?.getAttribute("data-captcha-type") === HCaptchaHandler.type
);
}
return (
document.location.href.startsWith(
"https://newassets.hcaptcha.com/captcha/v1/"
) &&
document.location.pathname.endsWith("/static/hcaptcha.html") &&
!document.location.hash.includes("size=invisible") &&
!document.location.hash.includes("frame=checkbox-invisible")
);
}
#initChallengeHandler() {
this.#shown = false;
this.#mutationObserver = new this.actor.contentWindow.MutationObserver(
this.#challengeMutationHandler.bind(this)
);
this.#mutationObserver.observe(this.actor.document.body, {
attributes: true,
attributeFilter: ["aria-hidden"],
});
}
#challengeMutationHandler(_mutations, observer) {
if (this.#shown) {
return;
}
this.#shown = this.actor.document.body.ariaHidden !== "true";
if (!this.#shown) {
return;
}
this.updateState({
type: HCaptchaHandler.type,
changes: "shown",
});
observer.disconnect();
}
#initCheckboxHandler() {
this.#checked = false;
this.#mutationObserver = new this.actor.contentWindow.MutationObserver(
this.#checkboxMutationHandler.bind(this)
);
this.#mutationObserver.observe(this.actor.document, {
subtree: true,
attributes: true,
attributeFilter: ["aria-checked"],
});
}
#checkboxMutationHandler(_mutations, observer) {
if (this.#checked) {
return;
}
const checkbox = this.actor.document.getElementById("checkbox");
if (checkbox?.ariaChecked === "true") {
this.#checked = true;
this.updateState({
type: HCaptchaHandler.type,
changes: "passed",
});
observer.disconnect();
}
}
onActorDestroy() {
super.onActorDestroy();
this.#mutationObserver.disconnect();
}
}
/**
* Handles Arkose Labs captchas.
*
* We listen for network requests to the
* captcha API. When the response is received, we check for
* the presence of the expected keys and send a message to
* the parent actor with the state of the captcha.
*/
class ArkoseLabsHandler extends CaptchaHandler {
static type = "arkoseLabs";
constructor(actor) {
super(actor);
this.actor.sendQuery("CaptchaDetection:Init", {
type: ArkoseLabsHandler.type,
});
}
static matches(document) {
if (Cu.isInAutomation) {
return (
document
.getElementById("captchaType")
?.getAttribute("data-captcha-type") === ArkoseLabsHandler.type
);
}
return document.location.href.startsWith(
"https://client-api.arkoselabs.com/fc/assets/ec-game-core/game-core/"
);
}
}
/**
* This actor runs in the captcha's frame. It provides information
* about the captcha's state to the parent actor.
*/
export class CaptchaDetectionChild extends JSWindowActorChild {
actorCreated() {
lazy.console.debug("actorCreated");
}
/**
* @constant
* @type {Array<CaptchaHandler>}
*/
static #handlers = [
GoogleRecaptchaV2Handler,
CFTurnstileHandler,
DatadomeHandler,
HCaptchaHandler,
ArkoseLabsHandler,
];
/**
* @param {Event} event - The event that created the actor.
*/
#initCaptchaHandler(event) {
for (const handler of CaptchaDetectionChild.#handlers) {
if (handler.matches(this.document)) {
this.handler = new handler(this, event);
return;
}
}
}
actorDestroy() {
lazy.console.debug("actorDestroy()");
this.handler?.onActorDestroy();
}
/**
* @param {Event} event - The event to handle.
*/
handleEvent(event) {
if (
!this.handler &&
(event.type === "DOMContentLoaded" || event.type === "pageshow")
) {
this.#initCaptchaHandler(event);
return;
}
if (event.type === "pagehide") {
this.sendAsyncMessage("Page:Hide");
}
this.handler?.handleEvent(event);
}
/**
* @param {ReceiveMessageArgument} message - The message to handle.
*/
receiveMessage(message) {
if (this.handler) {
this.handler.receiveMessage(message);
}
}
}
/**
* @typedef lazy
* @type {object}
* @property {ConsoleInstance} console - console instance.
*/