131 lines
3.7 KiB
JavaScript
131 lines
3.7 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: "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<nsIChannel, nsIPipe>} */
|
|
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.
|
|
*/
|