542 lines
14 KiB
JavaScript
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.
|
|
*/
|