Files
tubestation/toolkit/components/captchadetection/CaptchaDetectionChild.sys.mjs

264 lines
6.8 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/. */
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "console", () => {
return console.createInstance({
prefix: "CaptchaDetectionChild",
maxLogLevelPref: "captchadetection.loglevel",
});
});
/**
* Abstract class for handling captchas.
*/
class CaptchaHandler {
constructor(actor) {
this.actor = actor;
this.tabId = this.actor.docShell.browserChild.tabId;
this.isPBM = this.actor.browsingContext.usePrivateBrowsing;
}
static matches(_document) {
throw new Error("abstract method");
}
updateState(state) {
this.actor.sendAsyncMessage("CaptchaState:Update", {
tabId: this.tabId,
isPBM: this.isPBM,
state,
});
}
onActorDestroy() {
lazy.console.debug("CaptchaHandler destroyed");
}
handleEvent(event) {
lazy.console.debug("CaptchaHandler got event:", event);
}
}
/**
* 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;
constructor(actor) {
super(actor);
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) {
return [
"https://www.google.com/recaptcha/api2/",
"https://www.google.com/recaptcha/enterprise/",
].some(match => document.location.href.startsWith(match));
}
#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: "g-recaptcha-v2",
changes: "GotCheckmark",
});
this.#enabled = false;
observer.disconnect();
return;
}
const images = this.actor.document.getElementById("rc-imageselect");
if (images) {
this.updateState({
type: "g-recaptcha-v2",
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;
constructor(actor) {
super(actor);
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 matches(document) {
return document.location.href.startsWith(
"https://challenges.cloudflare.com/cdn-cgi/challenge-platform/h/b/turnstile/if/ov2/av0/rcv/"
);
}
#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: "cf-turnstile",
result: "Failed",
});
observer.disconnect();
return;
}
if (success.style.display !== "none") {
lazy.console.debug("Captcha succeeded");
this.updateState({
type: "cf-turnstile",
result: "Succeeded",
});
observer.disconnect();
}
}
).observe(shadowRoot, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style"],
});
}
onActorDestroy() {
super.onActorDestroy();
this.#mutationObserver.disconnect();
}
}
/**
* 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");
}
static #handlers = [GoogleRecaptchaV2Handler, CFTurnstileHandler];
#initCaptchaHandler() {
for (const handler of CaptchaDetectionChild.#handlers) {
if (handler.matches(this.document)) {
this.handler = new handler(this);
return;
}
}
}
actorDestroy() {
lazy.console.debug("actorDestroy()");
this.handler?.onActorDestroy();
}
handleEvent(event) {
if (
!this.handler &&
(event.type === "DOMContentLoaded" || event.type === "pageshow")
) {
this.#initCaptchaHandler();
return;
}
if (event.type === "pagehide") {
this.sendAsyncMessage("TabState:Closed", {
tabId: this.docShell.browserChild.tabId,
});
}
this.handler?.handleEvent(event);
}
}