264 lines
6.8 KiB
JavaScript
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);
|
|
}
|
|
}
|