/* 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: "CaptchaDetectionResponseObserver", maxLogLevelPref: "captchadetection.loglevel", }); }); ChromeUtils.defineESModuleGetters(lazy, { CommonUtils: "resource://services-common/utils.sys.mjs", }); /** * This class is responsible for observing HTTP responses * * Do note that it won't work for cached responses. * We don't need to handle cached responses because * we are only interested in API responses from * captcha services. */ export class CaptchaResponseObserver { /** * @param {function(nsIHttpChannel): boolean} shouldIntercept - A function that returns true if the response should be intercepted. * @param {function(nsIHttpChannel, nsresult, string): void} onResponseBody - A function that is called when the response body is available. */ constructor(shouldIntercept, onResponseBody) { /** @type {Map} */ this.requestToTeePipe = new WeakMap(); this.shouldIntercept = shouldIntercept; this.onResponseBody = onResponseBody; } register() { Services.obs.addObserver(this, "http-on-examine-response"); } unregister() { Services.obs.removeObserver(this, "http-on-examine-response"); } observe(channel) { if (!(channel instanceof Ci.nsIHttpChannel)) { return; } channel.QueryInterface(Ci.nsITraceableChannel); if (!this.shouldIntercept(channel)) { return; } const pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); pipe.init(false, false, 0, 0xffffffff, null); const tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance( Ci.nsIStreamListenerTee ); const originalListener = channel.setNewListener(tee); tee.init(originalListener, pipe.outputStream, this); this.requestToTeePipe.set(channel, pipe); } onStartRequest() {} /** * @param {nsIHttpChannel} channel - The channel. * @param {nsIXPCComponents_Values} statusCode - The status code, not to be confused with the HTTP status code. */ async onStopRequest(channel, statusCode) { const pipe = this.requestToTeePipe.get(channel); pipe.outputStream.close(); this.requestToTeePipe.delete(channel); let length = 0; try { length = pipe.inputStream.available(); } catch (e) { lazy.console.error("Error reading response body", e); return; } let responseBody = ""; if (length) { const sin = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( Ci.nsIScriptableInputStream ); sin.init(pipe.inputStream); responseBody = sin.readBytes(length); sin.close(); } if ( channel instanceof Ci.nsIEncodedChannel && channel.contentEncodings && !channel.applyConversion && !channel.hasContentDecompressed ) { const encodingHeader = channel.getResponseHeader("Content-Encoding"); const encodings = encodingHeader.split(/\s*\t*,\s*\t*/); for (const encoding of encodings) { responseBody = lazy.CommonUtils.convertString( responseBody, encoding, "uncompressed" ); } } this.onResponseBody(channel, statusCode, responseBody); } QueryInterface = ChromeUtils.generateQI([ Ci.nsIObserver, Ci.nsIRequestObserver, ]); } /** * @typedef lazy * @type {object} * @property {ConsoleInstance} console - console instance. * @property {typeof import("services/common/utils.sys.mjs").CommonUtils} CommonUtils - CommonUtils instance. */