diff options
Diffstat (limited to 'toolkit/components/url-classifier/UrlClassifierLib.sys.mjs')
-rw-r--r-- | toolkit/components/url-classifier/UrlClassifierLib.sys.mjs | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/toolkit/components/url-classifier/UrlClassifierLib.sys.mjs b/toolkit/components/url-classifier/UrlClassifierLib.sys.mjs new file mode 100644 index 0000000000..91fe56c245 --- /dev/null +++ b/toolkit/components/url-classifier/UrlClassifierLib.sys.mjs @@ -0,0 +1,227 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// We wastefully reload the same JS files across components. This puts all +// the common JS files used by safebrowsing and url-classifier into a +// single component. + +const PREF_DISABLE_TEST_BACKOFF = + "browser.safebrowsing.provider.test.disableBackoff"; + +/** + * Partially applies a function to a particular "this object" and zero or + * more arguments. The result is a new function with some arguments of the first + * function pre-filled and the value of |this| "pre-specified". + * + * Remaining arguments specified at call-time are appended to the pre- + * specified ones. + * + * Usage: + * var barMethBound = BindToObject(myFunction, myObj, "arg1", "arg2"); + * barMethBound("arg3", "arg4"); + * + * @param fn {string} Reference to the function to be bound + * + * @param self {object} Specifies the object which |this| should point to + * when the function is run. If the value is null or undefined, it will default + * to the global object. + * + * @returns {function} A partially-applied form of the speficied function. + */ +export function BindToObject(fn, self, opt_args) { + var boundargs = fn.boundArgs_ || []; + boundargs = boundargs.concat( + Array.prototype.slice.call(arguments, 2, arguments.length) + ); + + if (fn.boundSelf_) { + self = fn.boundSelf_; + } + if (fn.boundFn_) { + fn = fn.boundFn_; + } + + var newfn = function () { + // Combine the static args and the new args into one big array + var args = boundargs.concat(Array.prototype.slice.call(arguments)); + return fn.apply(self, args); + }; + + newfn.boundArgs_ = boundargs; + newfn.boundSelf_ = self; + newfn.boundFn_ = fn; + + return newfn; +} + +// This implements logic for stopping requests if the server starts to return +// too many errors. If we get MAX_ERRORS errors in ERROR_PERIOD minutes, we +// back off for TIMEOUT_INCREMENT minutes. If we get another error +// immediately after we restart, we double the timeout and add +// TIMEOUT_INCREMENT minutes, etc. +// +// This is similar to the logic used by the search suggestion service. + +// HTTP responses that count as an error. We also include any 5xx response +// as an error. +const HTTP_FOUND = 302; +const HTTP_SEE_OTHER = 303; +const HTTP_TEMPORARY_REDIRECT = 307; + +/** + * @param maxErrors Number of times to request before backing off. + * @param retryIncrement Time (ms) for each retry before backing off. + * @param maxRequests Number the number of requests needed to trigger backoff + * @param requestPeriod Number time (ms) in which maxRequests have to occur to + * trigger the backoff behavior (0 to disable maxRequests) + * @param timeoutIncrement Number time (ms) the starting timeout period + * we double this time for consecutive errors + * @param maxTimeout Number time (ms) maximum timeout period + * @param tolerance Checking next request tolerance. + */ +function RequestBackoff( + maxErrors, + retryIncrement, + maxRequests, + requestPeriod, + timeoutIncrement, + maxTimeout, + tolerance, + provider = null +) { + this.MAX_ERRORS_ = maxErrors; + this.RETRY_INCREMENT_ = retryIncrement; + this.MAX_REQUESTS_ = maxRequests; + this.REQUEST_PERIOD_ = requestPeriod; + this.TIMEOUT_INCREMENT_ = timeoutIncrement; + this.MAX_TIMEOUT_ = maxTimeout; + this.TOLERANCE_ = tolerance; + + // Queue of ints keeping the time of all requests + this.requestTimes_ = []; + + this.numErrors_ = 0; + this.errorTimeout_ = 0; + this.nextRequestTime_ = 0; + + // For test provider, we will disable backoff if preference is set to false. + if (provider === "test") { + this.canMakeRequestDefault = this.canMakeRequest; + this.canMakeRequest = function () { + if (Services.prefs.getBoolPref(PREF_DISABLE_TEST_BACKOFF, true)) { + return true; + } + return this.canMakeRequestDefault(); + }; + } +} + +/** + * Reset the object for reuse. This deliberately doesn't clear requestTimes_. + */ +RequestBackoff.prototype.reset = function () { + this.numErrors_ = 0; + this.errorTimeout_ = 0; + this.nextRequestTime_ = 0; +}; + +/** + * Check to see if we can make a request. + */ +RequestBackoff.prototype.canMakeRequest = function () { + var now = Date.now(); + // Note that nsITimer delay is approximate: the timer can be fired before the + // requested time has elapsed. So, give it a tolerance + if (now + this.TOLERANCE_ < this.nextRequestTime_) { + return false; + } + + return ( + this.requestTimes_.length < this.MAX_REQUESTS_ || + now - this.requestTimes_[0] > this.REQUEST_PERIOD_ + ); +}; + +RequestBackoff.prototype.noteRequest = function () { + var now = Date.now(); + this.requestTimes_.push(now); + + // We only care about keeping track of MAX_REQUESTS + if (this.requestTimes_.length > this.MAX_REQUESTS_) { + this.requestTimes_.shift(); + } +}; + +RequestBackoff.prototype.nextRequestDelay = function () { + return Math.max(0, this.nextRequestTime_ - Date.now()); +}; + +/** + * Notify this object of the last server response. If it's an error, + */ +RequestBackoff.prototype.noteServerResponse = function (status) { + if (this.isErrorStatus(status)) { + this.numErrors_++; + + if (this.numErrors_ < this.MAX_ERRORS_) { + this.errorTimeout_ = this.RETRY_INCREMENT_; + } else if (this.numErrors_ == this.MAX_ERRORS_) { + this.errorTimeout_ = this.TIMEOUT_INCREMENT_; + } else { + this.errorTimeout_ *= 2; + } + + this.errorTimeout_ = Math.min(this.errorTimeout_, this.MAX_TIMEOUT_); + this.nextRequestTime_ = Date.now() + this.errorTimeout_; + } else { + // Reset error timeout, allow requests to go through. + this.reset(); + } +}; + +/** + * We consider 302, 303, 307, 4xx, and 5xx http responses to be errors. + * @param status Number http status + * @return Boolean true if we consider this http status an error + */ +RequestBackoff.prototype.isErrorStatus = function (status) { + return ( + (400 <= status && status <= 599) || + HTTP_FOUND == status || + HTTP_SEE_OTHER == status || + HTTP_TEMPORARY_REDIRECT == status + ); +}; + +// Wrap a general-purpose |RequestBackoff| to a v4-specific one +// since both listmanager and hashcompleter would use it. +// Note that |maxRequests| and |requestPeriod| is still configurable +// to throttle pending requests. +/* exported RequestBackoffV4 */ +function RequestBackoffV4(maxRequests, requestPeriod, provider = null) { + let rand = Math.random(); + let retryInterval = Math.floor(15 * 60 * 1000 * (rand + 1)); // 15 ~ 30 min. + let backoffInterval = Math.floor(30 * 60 * 1000 * (rand + 1)); // 30 ~ 60 min. + + return new RequestBackoff( + 2 /* max errors */, + retryInterval /* retry interval, 15~30 min */, + maxRequests /* num requests */, + requestPeriod /* request time, 60 min */, + backoffInterval /* backoff interval, 60 min */, + 24 * 60 * 60 * 1000 /* max backoff, 24hr */, + 1000 /* tolerance of 1 sec */, + provider /* provider name */ + ); +} + +export function UrlClassifierLib() { + this.wrappedJSObject = { + RequestBackoff, + RequestBackoffV4, + BindToObject, + }; +} + +UrlClassifierLib.prototype.QueryInterface = ChromeUtils.generateQI([]); |