1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
8 ChromeUtils.defineLazyGetter(lazy, "console", () => {
9 return console.createInstance({
10 prefix: "CaptchaDetectionParent",
11 maxLogLevelPref: "captchadetection.loglevel",
15 ChromeUtils.defineESModuleGetters(lazy, {
16 CaptchaDetectionPingUtils:
17 "resource://gre/modules/CaptchaDetectionPingUtils.sys.mjs",
18 CaptchaResponseObserver:
19 "resource://gre/modules/CaptchaResponseObserver.sys.mjs",
23 * Holds the state of each tab.
24 * The state is an object with the following structure:
25 * [key: tabId]: typeof ReturnType<TabState.#defaultValue()>
31 this.#state = new Map();
35 * @param {number} tabId - The tab id.
36 * @returns {Map<any, any>} - The state of the tab.
39 return this.#state.get(tabId);
42 static #defaultValue() {
47 * @param {number} tabId - The tab id.
48 * @param {(state: ReturnType<TabState['get']>) => void} updateFunction - The function to update the state.
50 update(tabId, updateFunction) {
51 if (!this.#state.has(tabId)) {
52 this.#state.set(tabId, TabState.#defaultValue());
54 updateFunction(this.#state.get(tabId));
58 * @param {number} tabId - The tab id.
61 this.#state.delete(tabId);
65 const tabState = new TabState();
68 * This actor parent is responsible for recording the state of captchas
69 * or communicating with parent browsing context.
71 class CaptchaDetectionParent extends JSWindowActorParent {
75 lazy.console.debug("actorCreated");
79 lazy.console.debug("actorDestroy()");
81 if (this.#responseObserver) {
82 this.#responseObserver.unregister();
86 /** @type {CaptchaStateUpdateFunction} */
87 #updateGRecaptchaV2State({ tabId, isPBM, state: { type, changes } }) {
88 lazy.console.debug("updateGRecaptchaV2State", changes);
90 if (changes === "ImagesShown") {
91 tabState.update(tabId, state => {
92 state.set(type + changes, true);
95 // We don't call maybeSubmitPing here because we might end up
96 // submitting the ping without the "GotCheckmark" event.
97 // maybeSubmitPing will be called when "GotCheckmark" event is
98 // received, or when the daily maybeSubmitPing is called.
99 const shownMetric = "googleRecaptchaV2Ps" + (isPBM ? "Pbm" : "");
100 Glean.captchaDetection[shownMetric].add(1);
101 } else if (changes === "GotCheckmark") {
102 const autoCompleted = !tabState.get(tabId)?.has(type + "ImagesShown");
105 (autoCompleted ? " (auto-completed)" : " (manually-completed)")
108 "googleRecaptchaV2" +
109 (autoCompleted ? "Ac" : "Pc") +
110 (isPBM ? "Pbm" : "");
111 Glean.captchaDetection[resultMetric].add(1);
116 /** @type {CaptchaStateUpdateFunction} */
117 #recordCFTurnstileResult({ isPBM, state: { result } }) {
118 lazy.console.debug("recordCFTurnstileResult", result);
120 "cloudflareTurnstile" +
121 (result === "Succeeded" ? "Cc" : "Cf") +
122 (isPBM ? "Pbm" : "");
123 Glean.captchaDetection[resultMetric].add(1);
127 async #datadomeInit() {
128 const parent = this.browsingContext.parentWindowContext;
130 lazy.console.error("Datadome captcha loaded in a top-level window?");
136 actor = parent.getActor("CaptchaDetectionCommunication");
138 lazy.console.error("CaptchaDetection actor not found in parent window");
142 lazy.console.error("Error getting actor", e);
146 await actor.sendQuery("Datadome:AddMessageListener");
149 /** @type {CaptchaStateUpdateFunction} */
150 #recordDatadomeEvent({ isPBM, state: { event, ...payload } }) {
151 lazy.console.debug("recordDatadomeEvent", event, payload);
152 const suffix = isPBM ? "Pbm" : "";
153 if (event === "load") {
154 if (payload.captchaShown) {
155 Glean.captchaDetection["datadomePs" + suffix].add(1);
156 } else if (payload.blocked) {
157 Glean.captchaDetection["datadomeBl" + suffix].add(1);
159 } else if (event === "passed") {
160 Glean.captchaDetection["datadomePc" + suffix].add(1);
163 this.#onMetricSet(0);
166 /** @type {CaptchaStateUpdateFunction} */
167 #recordHCaptchaState({ isPBM, tabId, state: { type, changes } }) {
168 lazy.console.debug("recordHCaptchaEvent", changes);
170 if (changes === "shown") {
171 // I don't think HCaptcha supports auto-completion, but we act
172 // as if it does just in case.
173 tabState.update(tabId, state => {
174 state.set(type + changes, true);
177 // We don't call maybeSubmitPing here because we might end up
178 // submitting the ping without the "passed" event.
179 // maybeSubmitPing will be called when "passed" event is
180 // received, or when the daily maybeSubmitPing is called.
181 const shownMetric = "hcaptchaPs" + (isPBM ? "Pbm" : "");
182 Glean.captchaDetection[shownMetric].add(1);
183 } else if (changes === "passed") {
184 const autoCompleted = !tabState.get(tabId)?.has(type + "shown");
186 "hcaptcha" + (autoCompleted ? "Ac" : "Pc") + (isPBM ? "Pbm" : "");
187 Glean.captchaDetection[resultMetric].add(1);
192 /** @type {CaptchaStateUpdateFunction} */
195 state: { event, success, numSolutionsRequired },
197 if (event === "shown") {
198 // We don't call maybeSubmitPing here because we might end up
199 // submitting the ping without the "completed" event.
200 // maybeSubmitPing will be called when "completed" event is
201 // received, or when the daily maybeSubmitPing is called.
202 const shownMetric = "awswafPs" + (isPBM ? "Pbm" : "");
203 Glean.captchaDetection[shownMetric].add(1);
204 } else if (event === "completed") {
205 const suffix = isPBM ? "Pbm" : "";
206 const resultMetric = "awswaf" + (success ? "Pc" : "Pf") + suffix;
207 Glean.captchaDetection[resultMetric].add(1);
209 const solutionsRequiredMetric =
210 Glean.captchaDetection["awswafSolutionsRequired" + suffix];
211 solutionsRequiredMetric.accumulateSingleSample(numSolutionsRequired);
217 /** @type {CaptchaStateUpdateFunction} */
218 #recordArkoseLabsEvent({
220 state: { event, solved, solutionsSubmitted },
222 if (event === "shown") {
223 // We don't call maybeSubmitPing here because we might end up
224 // submitting the ping without the "completed" event.
225 // maybeSubmitPing will be called when "completed" event is
226 // received, or when the daily maybeSubmitPing is called.
227 const shownMetric = "arkoselabsPs" + (isPBM ? "Pbm" : "");
228 Glean.captchaDetection[shownMetric].add(1);
229 } else if (event === "completed") {
230 const suffix = isPBM ? "Pbm" : "";
231 const resultMetric = "arkoselabs" + (solved ? "Pc" : "Pf") + suffix;
232 Glean.captchaDetection[resultMetric].add(1);
234 const solutionsRequiredMetric =
235 Glean.captchaDetection["arkoselabsSolutionsRequired" + suffix];
236 solutionsRequiredMetric.accumulateSingleSample(solutionsSubmitted);
242 async #awsWafInit() {
243 this.#responseObserver = new lazy.CaptchaResponseObserver(
245 channel.loadInfo?.browsingContextID === this.browsingContext.id &&
247 channel.URI.scheme === "https" &&
248 (Cu.isInAutomation ||
249 channel.URI.host.endsWith(".captcha.awswaf.com")) &&
250 channel.URI.filePath.endsWith(
251 Cu.isInAutomation ? "aws_waf_api.sjs" : "/verify"
253 (_channel, statusCode, responseBody) => {
254 if (statusCode !== Cr.NS_OK) {
260 body = JSON.parse(responseBody);
262 lazy.console.debug("handleResponseBody", "Failed to parse JSON");
267 "handleResponseBody",
268 "Failed to parse JSON",
275 // Check for the presence of the expected keys
277 !["success", "num_solutions_required"].every(key =>
278 body.hasOwnProperty(key)
281 lazy.console.debug("handleResponseBody", "Missing keys", body);
285 this.#recordAWSWafEvent({
286 isPBM: this.browsingContext.usePrivateBrowsing,
289 success: body.success,
290 numSolutionsRequired: body.num_solutions_required,
295 this.#responseObserver.register();
298 async #arkoseLabsInit() {
299 let solutionsSubmitted = 0;
300 this.#responseObserver = new lazy.CaptchaResponseObserver(
302 channel.loadInfo?.browsingContextID === this.browsingContext.id &&
305 ? channel.URI.filePath.endsWith("arkose_labs_api.sjs")
306 : channel.URI.spec === "https://client-api.arkoselabs.com/fc/ca/"),
307 (_channel, statusCode, responseBody) => {
308 if (statusCode !== Cr.NS_OK) {
314 body = JSON.parse(responseBody);
317 "ResponseObserver:ResponseBody",
318 "Failed to parse JSON"
324 "ResponseObserver:ResponseBody",
325 "Failed to parse JSON",
332 // Check for the presence of the expected keys
333 if (["response", "solved"].some(key => !body.hasOwnProperty(key))) {
335 "ResponseObserver:ResponseBody",
342 solutionsSubmitted++;
343 if (body.solved === null) {
347 this.#recordArkoseLabsEvent({
348 isPBM: this.browsingContext.usePrivateBrowsing,
356 solutionsSubmitted = 0;
359 this.#responseObserver.register();
362 #onTabClosed(tabId) {
363 tabState.clear(tabId);
365 if (this.#responseObserver) {
366 this.#responseObserver.unregister();
370 async #onMetricSet(parentDepth = 1) {
371 lazy.CaptchaDetectionPingUtils.maybeSubmitPing();
372 if (Cu.isInAutomation) {
373 await this.#notifyTestMetricIsSet(parentDepth);
378 * Notify the `parentDepth`'nth parent browsing context that the test metric is set.
380 * @param {number} parentDepth - The depth of the parent window context.
381 * The reason we need this param is because Datadome calls this method
382 * not from the captcha iframe, but its parent browsing context. So
383 * it overrides the depth to 0.
385 async #notifyTestMetricIsSet(parentDepth = 1) {
386 if (!Cu.isInAutomation) {
387 throw new Error("This method should only be called in automation");
390 let parent = this.browsingContext.currentWindowContext;
391 for (let i = 0; i < parentDepth; i++) {
392 parent = parent.parentWindowContext;
394 lazy.console.error("No parent window context");
401 actor = parent.getActor("CaptchaDetectionCommunication");
403 lazy.console.error("CaptchaDetection actor not found in parent window");
407 lazy.console.error("Error getting actor", e);
411 await actor.sendQuery("Testing:MetricIsSet");
414 async receiveMessage(message) {
415 lazy.console.debug("receiveMessage", message);
417 switch (message.name) {
418 case "CaptchaState:Update":
419 switch (message.data.state.type) {
420 case "g-recaptcha-v2":
421 this.#updateGRecaptchaV2State(message.data);
424 this.#recordCFTurnstileResult(message.data);
427 this.#recordDatadomeEvent(message.data);
430 this.#recordHCaptchaState(message.data);
433 this.#recordAWSWafEvent(message.data);
437 case "TabState:Closed":
438 // message.name === "TabState:Closed"
439 // => message.data = {
442 this.#onTabClosed(message.data.tabId);
444 case "CaptchaDetection:Init":
445 // message.name === "CaptchaDetection:Init"
446 // => message.data = {
449 switch (message.data.type) {
451 return this.#datadomeInit();
453 return this.#awsWafInit();
455 return this.#arkoseLabsInit();
459 lazy.console.error("Unknown message", message);
466 CaptchaDetectionParent,
467 CaptchaDetectionParent as CaptchaDetectionCommunicationParent,
473 * @property {ConsoleInstance} console - console instance.
474 * @property {typeof import("./CaptchaDetectionPingUtils.sys.mjs").CaptchaDetectionPingUtils} CaptchaDetectionPingUtils - CaptchaDetectionPingUtils module.
475 * @property {typeof import("./CaptchaResponseObserver.sys.mjs").CaptchaResponseObserver} CaptchaResponseObserver - CaptchaResponseObserver module.
479 * @typedef CaptchaStateUpdateMessageData
480 * @property {number} tabId - The tab id.
481 * @property {boolean} isPBM - Whether the tab is in PBM.
482 * @property {object} state - The state of the captcha.
483 * @property {string} state.type - The type of the captcha.
485 * @typedef {(message: CaptchaStateUpdateMessageData) => void} CaptchaStateUpdateFunction