From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../UrlClassifierHashCompleter.sys.mjs | 965 +++++++++++++++++++++ 1 file changed, 965 insertions(+) create mode 100644 toolkit/components/url-classifier/UrlClassifierHashCompleter.sys.mjs (limited to 'toolkit/components/url-classifier/UrlClassifierHashCompleter.sys.mjs') diff --git a/toolkit/components/url-classifier/UrlClassifierHashCompleter.sys.mjs b/toolkit/components/url-classifier/UrlClassifierHashCompleter.sys.mjs new file mode 100644 index 0000000000..521b33dca3 --- /dev/null +++ b/toolkit/components/url-classifier/UrlClassifierHashCompleter.sys.mjs @@ -0,0 +1,965 @@ +/* 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/. */ + +// COMPLETE_LENGTH and PARTIAL_LENGTH copied from nsUrlClassifierDBService.h, +// they correspond to the length, in bytes, of a hash prefix and the total +// hash. +const COMPLETE_LENGTH = 32; +const PARTIAL_LENGTH = 4; + +// Upper limit on the server response minimumWaitDuration +const MIN_WAIT_DURATION_MAX_VALUE = 24 * 60 * 60 * 1000; +const PREF_DEBUG_ENABLED = "browser.safebrowsing.debug"; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gDbService", + "@mozilla.org/url-classifier/dbservice;1", + "nsIUrlClassifierDBService" +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gUrlUtil", + "@mozilla.org/url-classifier/utils;1", + "nsIUrlClassifierUtils" +); + +let loggingEnabled = false; + +// Log only if browser.safebrowsing.debug is true +function log(...stuff) { + if (!loggingEnabled) { + return; + } + + var d = new Date(); + let msg = "hashcompleter: " + d.toTimeString() + ": " + stuff.join(" "); + dump(Services.urlFormatter.trimSensitiveURLs(msg) + "\n"); +} + +// Map the HTTP response code to a Telemetry bucket +// https://developers.google.com/safe-browsing/developers_guide_v2?hl=en +// eslint-disable-next-line complexity +function httpStatusToBucket(httpStatus) { + var statusBucket; + switch (httpStatus) { + case 100: + case 101: + // Unexpected 1xx return code + statusBucket = 0; + break; + case 200: + // OK - Data is available in the HTTP response body. + statusBucket = 1; + break; + case 201: + case 202: + case 203: + case 205: + case 206: + // Unexpected 2xx return code + statusBucket = 2; + break; + case 204: + // No Content - There are no full-length hashes with the requested prefix. + statusBucket = 3; + break; + case 300: + case 301: + case 302: + case 303: + case 304: + case 305: + case 307: + case 308: + // Unexpected 3xx return code + statusBucket = 4; + break; + case 400: + // Bad Request - The HTTP request was not correctly formed. + // The client did not provide all required CGI parameters. + statusBucket = 5; + break; + case 401: + case 402: + case 405: + case 406: + case 407: + case 409: + case 410: + case 411: + case 412: + case 414: + case 415: + case 416: + case 417: + case 421: + case 426: + case 428: + case 429: + case 431: + case 451: + // Unexpected 4xx return code + statusBucket = 6; + break; + case 403: + // Forbidden - The client id is invalid. + statusBucket = 7; + break; + case 404: + // Not Found + statusBucket = 8; + break; + case 408: + // Request Timeout + statusBucket = 9; + break; + case 413: + // Request Entity Too Large - Bug 1150334 + statusBucket = 10; + break; + case 500: + case 501: + case 510: + // Unexpected 5xx return code + statusBucket = 11; + break; + case 502: + case 504: + case 511: + // Local network errors, we'll ignore these. + statusBucket = 12; + break; + case 503: + // Service Unavailable - The server cannot handle the request. + // Clients MUST follow the backoff behavior specified in the + // Request Frequency section. + statusBucket = 13; + break; + case 505: + // HTTP Version Not Supported - The server CANNOT handle the requested + // protocol major version. + statusBucket = 14; + break; + default: + statusBucket = 15; + } + return statusBucket; +} + +function FullHashMatch(table, hash, duration) { + this.tableName = table; + this.fullHash = hash; + this.cacheDuration = duration; +} + +FullHashMatch.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIFullHashMatch"]), + + tableName: null, + fullHash: null, + cacheDuration: null, +}; + +export function HashCompleter() { + // The current HashCompleterRequest in flight. Once it is started, it is set + // to null. It may be used by multiple calls to |complete| in succession to + // avoid creating multiple requests to the same gethash URL. + this._currentRequest = null; + // An Array of ongoing gethash requests which is used to find requests for + // the same hash prefix. + this._ongoingRequests = []; + // A map of gethashUrls to HashCompleterRequests that haven't yet begun. + this._pendingRequests = {}; + + // A map of gethash URLs to RequestBackoff objects. + this._backoffs = {}; + + // Whether we have been informed of a shutdown by the shutdown event. + this._shuttingDown = false; + + // A map of gethash URLs to next gethash time in miliseconds + this._nextGethashTimeMs = {}; + + Services.obs.addObserver(this, "quit-application"); + Services.prefs.addObserver(PREF_DEBUG_ENABLED, this); + + loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED); +} + +HashCompleter.prototype = { + classID: Components.ID("{9111de73-9322-4bfc-8b65-2b727f3e6ec8}"), + QueryInterface: ChromeUtils.generateQI([ + "nsIUrlClassifierHashCompleter", + "nsIRunnable", + "nsIObserver", + "nsISupportsWeakReference", + "nsITimerCallback", + ]), + + // This is mainly how the HashCompleter interacts with other components. + // Even though it only takes one partial hash and callback, subsequent + // calls are made into the same HTTP request by using a thread dispatch. + complete: function HC_complete( + aPartialHash, + aGethashUrl, + aTableName, + aCallback + ) { + if (!aGethashUrl) { + throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); + } + + // Check ongoing requests before creating a new HashCompleteRequest + for (let r of this._ongoingRequests) { + if (r.find(aPartialHash, aGethashUrl, aTableName)) { + log( + "Merge gethash request in " + + aTableName + + " for prefix : " + + btoa(aPartialHash) + ); + r.add(aPartialHash, aCallback, aTableName); + return; + } + } + + if (!this._currentRequest) { + this._currentRequest = new HashCompleterRequest(this, aGethashUrl); + } + if (this._currentRequest.gethashUrl == aGethashUrl) { + this._currentRequest.add(aPartialHash, aCallback, aTableName); + } else { + if (!this._pendingRequests[aGethashUrl]) { + this._pendingRequests[aGethashUrl] = new HashCompleterRequest( + this, + aGethashUrl + ); + } + this._pendingRequests[aGethashUrl].add( + aPartialHash, + aCallback, + aTableName + ); + } + + if (!this._backoffs[aGethashUrl]) { + // Initialize request backoffs separately, since requests are deleted + // after they are dispatched. + var jslib = + Cc["@mozilla.org/url-classifier/jslib;1"].getService().wrappedJSObject; + + // Using the V4 backoff algorithm for both V2 and V4. See bug 1273398. + this._backoffs[aGethashUrl] = new jslib.RequestBackoffV4( + 10 /* keep track of max requests */, + 0 /* don't throttle on successful requests per time period */, + lazy.gUrlUtil.getProvider(aTableName) /* used by testcase */ + ); + } + + if (!this._nextGethashTimeMs[aGethashUrl]) { + this._nextGethashTimeMs[aGethashUrl] = 0; + } + + // Start off this request. Without dispatching to a thread, every call to + // complete makes an individual HTTP request. + Services.tm.dispatchToMainThread(this); + }, + + // This is called after several calls to |complete|, or after the + // currentRequest has finished. It starts off the HTTP request by making a + // |begin| call to the HashCompleterRequest. + run() { + // Clear everything on shutdown + if (this._shuttingDown) { + this._currentRequest = null; + this._pendingRequests = null; + this._nextGethashTimeMs = null; + + for (var url in this._backoffs) { + this._backoffs[url] = null; + } + throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); + } + + // If we don't have an in-flight request, make one + let pendingUrls = Object.keys(this._pendingRequests); + if (!this._currentRequest && pendingUrls.length) { + let nextUrl = pendingUrls[0]; + this._currentRequest = this._pendingRequests[nextUrl]; + delete this._pendingRequests[nextUrl]; + } + + if (this._currentRequest) { + try { + if (this._currentRequest.begin()) { + this._ongoingRequests.push(this._currentRequest); + } + } finally { + // If |begin| fails, we should get rid of our request. + this._currentRequest = null; + } + } + }, + + // Pass the server response status to the RequestBackoff for the given + // gethashUrl and fetch the next pending request, if there is one. + finishRequest(aRequest, aStatus) { + this._ongoingRequests = this._ongoingRequests.filter(v => v != aRequest); + + this._backoffs[aRequest.gethashUrl].noteServerResponse(aStatus); + Services.tm.dispatchToMainThread(this); + }, + + // Returns true if we can make a request from the given url, false otherwise. + canMakeRequest(aGethashUrl) { + return ( + this._backoffs[aGethashUrl].canMakeRequest() && + Date.now() >= this._nextGethashTimeMs[aGethashUrl] + ); + }, + + // Notifies the RequestBackoff of a new request so we can throttle based on + // max requests/time period. This must be called before a channel is opened, + // and finishRequest must be called once the response is received. + noteRequest(aGethashUrl) { + return this._backoffs[aGethashUrl].noteRequest(); + }, + + observe: function HC_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "quit-application": + this._shuttingDown = true; + Services.obs.removeObserver(this, "quit-application"); + break; + case "nsPref:changed": + if (aData == PREF_DEBUG_ENABLED) { + loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED); + } + break; + } + }, +}; + +function HashCompleterRequest(aCompleter, aGethashUrl) { + // HashCompleter object that created this HashCompleterRequest. + this._completer = aCompleter; + // The internal set of hashes and callbacks that this request corresponds to. + this._requests = []; + // nsIChannel that the hash completion query is transmitted over. + this._channel = null; + // Response body of hash completion. Created in onDataAvailable. + this._response = ""; + // Whether we have been informed of a shutdown by the quit-application event. + this._shuttingDown = false; + this.gethashUrl = aGethashUrl; + + this.provider = ""; + // Multiple partial hashes can be associated with the same tables + // so we use a map here. + this.tableNames = new Map(); + + this.telemetryProvider = ""; + this.telemetryClockStart = 0; +} +HashCompleterRequest.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsIStreamListener", + "nsIObserver", + ]), + + // This is called by the HashCompleter to add a hash and callback to the + // HashCompleterRequest. It must be called before calling |begin|. + add: function HCR_add(aPartialHash, aCallback, aTableName) { + this._requests.push({ + partialHash: aPartialHash, + callback: aCallback, + tableName: aTableName, + response: { matches: [] }, + }); + + if (aTableName) { + let isTableNameV4 = aTableName.endsWith("-proto"); + if (0 === this.tableNames.size) { + // Decide if this request is v4 by the first added partial hash. + this.isV4 = isTableNameV4; + } else if (this.isV4 !== isTableNameV4) { + log( + 'ERROR: Cannot mix "proto" tables with other types within ' + + "the same gethash URL." + ); + } + if (!this.tableNames.has(aTableName)) { + this.tableNames.set(aTableName); + } + + // Assuming all tables with the same gethash URL have the same provider + if (this.provider == "") { + this.provider = lazy.gUrlUtil.getProvider(aTableName); + } + + if (this.telemetryProvider == "") { + this.telemetryProvider = lazy.gUrlUtil.getTelemetryProvider(aTableName); + } + } + }, + + find: function HCR_find(aPartialHash, aGetHashUrl, aTableName) { + if (this.gethashUrl != aGetHashUrl || !this.tableNames.has(aTableName)) { + return false; + } + + return this._requests.find(function (r) { + return r.partialHash === aPartialHash; + }); + }, + + fillTableStatesBase64: function HCR_fillTableStatesBase64(aCallback) { + lazy.gDbService.getTables(aTableData => { + aTableData.split("\n").forEach(line => { + let p = line.indexOf(";"); + if (-1 === p) { + return; + } + // [tableName];[stateBase64]:[checksumBase64] + let tableName = line.substring(0, p); + if (this.tableNames.has(tableName)) { + let metadata = line.substring(p + 1).split(":"); + let stateBase64 = metadata[0]; + this.tableNames.set(tableName, stateBase64); + } + }); + + aCallback(); + }); + }, + + // This initiates the HTTP request. It can fail due to backoff timings and + // will notify all callbacks as necessary. We notify the backoff object on + // begin. + begin: function HCR_begin() { + if (!this._completer.canMakeRequest(this.gethashUrl)) { + log("Can't make request to " + this.gethashUrl + "\n"); + this.notifyFailure(Cr.NS_ERROR_ABORT); + return false; + } + + Services.obs.addObserver(this, "quit-application"); + + // V4 requires table states to build the request so we need + // a async call to retrieve the table states from disk. + // Note that |HCR_begin| is fine to be sync because + // it doesn't appear in a sync call chain. + this.fillTableStatesBase64(() => { + try { + this.openChannel(); + // Notify the RequestBackoff if opening the channel succeeded. At this + // point, finishRequest must be called. + this._completer.noteRequest(this.gethashUrl); + } catch (err) { + this._completer._ongoingRequests = + this._completer._ongoingRequests.filter(v => v != this); + this.notifyFailure(err); + throw err; + } + }); + + return true; + }, + + notify: function HCR_notify() { + // If we haven't gotten onStopRequest, just cancel. This will call us + // with onStopRequest since we implement nsIStreamListener on the + // channel. + if (this._channel && this._channel.isPending()) { + log("cancelling request to " + this.gethashUrl + " (timeout)\n"); + Services.telemetry + .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_TIMEOUT2") + .add(this.telemetryProvider, 1); + this._channel.cancel(Cr.NS_BINDING_ABORTED); + } + }, + + // Creates an nsIChannel for the request and fills the body. + // Enforce bypassing URL Classifier check because if the request is + // blocked, it means SafeBrowsing is malfunction. + openChannel: function HCR_openChannel() { + let loadFlags = + Ci.nsIChannel.INHIBIT_CACHING | + Ci.nsIChannel.LOAD_BYPASS_CACHE | + Ci.nsIChannel.LOAD_BYPASS_URL_CLASSIFIER; + + this.request = { + url: this.gethashUrl, + body: "", + }; + + if (this.isV4) { + // As per spec, we add the request payload to the gethash url. + this.request.url += "&$req=" + this.buildRequestV4(); + } + + log("actualGethashUrl: " + this.request.url); + + let channel = NetUtil.newChannel({ + uri: this.request.url, + loadUsingSystemPrincipal: true, + }); + channel.loadFlags = loadFlags; + channel.loadInfo.originAttributes = { + // The firstPartyDomain value should sync with NECKO_SAFEBROWSING_FIRST_PARTY_DOMAIN + // defined in nsNetUtil.h. + firstPartyDomain: + "safebrowsing.86868755-6b82-4842-b301-72671a0db32e.mozilla", + }; + + // Disable keepalive. + let httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + httpChannel.setRequestHeader("Connection", "close", false); + + this._channel = channel; + + if (this.isV4) { + httpChannel.setRequestHeader("X-HTTP-Method-Override", "POST", false); + } else { + let body = this.buildRequest(); + this.addRequestBody(body); + } + + // Set a timer that cancels the channel after timeout_ms in case we + // don't get a gethash response. + this.timer_ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + // Ask the timer to use nsITimerCallback (.notify()) when ready + let timeout = Services.prefs.getIntPref("urlclassifier.gethash.timeout_ms"); + this.timer_.initWithCallback(this, timeout, this.timer_.TYPE_ONE_SHOT); + channel.asyncOpen(this); + this.telemetryClockStart = Date.now(); + }, + + buildRequestV4: function HCR_buildRequestV4() { + // Convert the "name to state" mapping to two equal-length arrays. + let tableNameArray = []; + let stateArray = []; + this.tableNames.forEach((state, name) => { + // We skip the table which is not associated with a state. + if (state) { + tableNameArray.push(name); + stateArray.push(state); + } + }); + + // Build the "distinct" prefix array. + // The array is sorted to make sure the entries are arbitrary mixed in a + // deterministic way + let prefixSet = new Set(); + this._requests.forEach(r => prefixSet.add(btoa(r.partialHash))); + let prefixArray = Array.from(prefixSet).sort(); + + log( + "Build v4 gethash request with " + + JSON.stringify(tableNameArray) + + ", " + + JSON.stringify(stateArray) + + ", " + + JSON.stringify(prefixArray) + ); + + return lazy.gUrlUtil.makeFindFullHashRequestV4( + tableNameArray, + stateArray, + prefixArray + ); + }, + + // Returns a string for the request body based on the contents of + // this._requests. + buildRequest: function HCR_buildRequest() { + // Sometimes duplicate entries are sent to HashCompleter but we do not need + // to propagate these to the server. (bug 633644) + let prefixes = []; + + for (let i = 0; i < this._requests.length; i++) { + let request = this._requests[i]; + if (!prefixes.includes(request.partialHash)) { + prefixes.push(request.partialHash); + } + } + + // Sort to make sure the entries are arbitrary mixed in a deterministic way + prefixes.sort(); + + let body; + body = + PARTIAL_LENGTH + + ":" + + PARTIAL_LENGTH * prefixes.length + + "\n" + + prefixes.join(""); + + log( + "Requesting completions for " + + prefixes.length + + " " + + PARTIAL_LENGTH + + "-byte prefixes: " + + body + ); + return body; + }, + + // Sets the request body of this._channel. + addRequestBody: function HCR_addRequestBody(aBody) { + let inputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + inputStream.setData(aBody, aBody.length); + + let uploadChannel = this._channel.QueryInterface(Ci.nsIUploadChannel); + uploadChannel.setUploadStream(inputStream, "text/plain", -1); + + let httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel); + httpChannel.requestMethod = "POST"; + }, + + // Parses the response body and eventually adds items to the |response.matches| array + // for elements of |this._requests|. + handleResponse: function HCR_handleResponse() { + if (this._response == "") { + return; + } + + if (this.isV4) { + this.handleResponseV4(); + return; + } + + let start = 0; + + let length = this._response.length; + while (start != length) { + start = this.handleTable(start); + } + }, + + handleResponseV4: function HCR_handleResponseV4() { + let callback = { + // onCompleteHashFound will be called for each fullhash found in + // FullHashResponse. + onCompleteHashFound: ( + aCompleteHash, + aTableNames, + aPerHashCacheDuration + ) => { + log( + "V4 fullhash response complete hash found callback: " + + aTableNames + + ", CacheDuration(" + + aPerHashCacheDuration + + ")" + ); + + // Filter table names which we didn't requested. + let filteredTables = aTableNames.split(",").filter(name => { + return this.tableNames.get(name); + }); + if (0 === filteredTables.length) { + log("ERROR: Got complete hash which is from unknown table."); + return; + } + if (filteredTables.length > 1) { + log("WARNING: Got complete hash which has ambigious threat type."); + } + + this.handleItem({ + completeHash: aCompleteHash, + tableName: filteredTables[0], + cacheDuration: aPerHashCacheDuration, + }); + }, + + // onResponseParsed will be called no matter if there is match in + // FullHashResponse, the callback is mainly used to pass negative cache + // duration and minimum wait duration. + onResponseParsed: (aMinWaitDuration, aNegCacheDuration) => { + log( + "V4 fullhash response parsed callback: " + + "MinWaitDuration(" + + aMinWaitDuration + + "), " + + "NegativeCacheDuration(" + + aNegCacheDuration + + ")" + ); + + let minWaitDuration = aMinWaitDuration; + + if (aMinWaitDuration > MIN_WAIT_DURATION_MAX_VALUE) { + log( + "WARNING: Minimum wait duration too large, clamping it down " + + "to a reasonable value." + ); + minWaitDuration = MIN_WAIT_DURATION_MAX_VALUE; + } else if (aMinWaitDuration < 0) { + log("WARNING: Minimum wait duration is negative, reset it to 0"); + minWaitDuration = 0; + } + + this._completer._nextGethashTimeMs[this.gethashUrl] = + Date.now() + minWaitDuration; + + // A fullhash request may contain more than one prefix, so the negative + // cache duration should be set for all the prefixes in the request. + this._requests.forEach(request => { + request.response.negCacheDuration = aNegCacheDuration; + }); + }, + }; + + lazy.gUrlUtil.parseFindFullHashResponseV4(this._response, callback); + }, + + // This parses a table entry in the response body and calls |handleItem| + // for complete hash in the table entry. + handleTable: function HCR_handleTable(aStart) { + let body = this._response.substring(aStart); + + // deal with new line indexes as there could be + // new line characters in the data parts. + let newlineIndex = body.indexOf("\n"); + if (newlineIndex == -1) { + throw errorWithStack(); + } + let header = body.substring(0, newlineIndex); + let entries = header.split(":"); + if (entries.length != 3) { + throw errorWithStack(); + } + + let list = entries[0]; + let addChunk = parseInt(entries[1]); + let dataLength = parseInt(entries[2]); + + log("Response includes add chunks for " + list + ": " + addChunk); + if ( + dataLength % COMPLETE_LENGTH != 0 || + dataLength == 0 || + dataLength > body.length - (newlineIndex + 1) + ) { + throw errorWithStack(); + } + + let data = body.substr(newlineIndex + 1, dataLength); + for (let i = 0; i < dataLength / COMPLETE_LENGTH; i++) { + this.handleItem({ + completeHash: data.substr(i * COMPLETE_LENGTH, COMPLETE_LENGTH), + tableName: list, + chunkId: addChunk, + }); + } + + return aStart + newlineIndex + 1 + dataLength; + }, + + // This adds a complete hash to any entry in |this._requests| that matches + // the hash. + handleItem: function HCR_handleItem(aData) { + let provider = lazy.gUrlUtil.getProvider(aData.tableName); + if (provider != this.provider) { + log( + "Ignoring table " + + aData.tableName + + " since it belongs to " + + provider + + " while the response came from " + + this.provider + + "." + ); + return; + } + + for (let i = 0; i < this._requests.length; i++) { + let request = this._requests[i]; + if (aData.completeHash.startsWith(request.partialHash)) { + request.response.matches.push(aData); + } + } + }, + + // notifySuccess and notifyFailure are used to alert the callbacks with + // results. notifySuccess makes |completion| and |completionFinished| calls + // while notifyFailure only makes a |completionFinished| call with the error + // code. + notifySuccess: function HCR_notifySuccess() { + // V2 completion handler + let completionV2 = req => { + req.response.matches.forEach(m => { + req.callback.completionV2(m.completeHash, m.tableName, m.chunkId); + }); + + req.callback.completionFinished(Cr.NS_OK); + }; + + // V4 completion handler + let completionV4 = req => { + let matches = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + + req.response.matches.forEach(m => { + matches.appendElement( + new FullHashMatch(m.tableName, m.completeHash, m.cacheDuration) + ); + }); + + req.callback.completionV4( + req.partialHash, + req.tableName, + req.response.negCacheDuration, + matches + ); + + req.callback.completionFinished(Cr.NS_OK); + }; + + let completion = this.isV4 ? completionV4 : completionV2; + this._requests.forEach(req => { + completion(req); + }); + }, + + notifyFailure: function HCR_notifyFailure(aStatus) { + log("notifying failure\n"); + for (let i = 0; i < this._requests.length; i++) { + let request = this._requests[i]; + request.callback.completionFinished(aStatus); + } + }, + + onDataAvailable: function HCR_onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ) { + let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(aInputStream); + this._response += sis.readBytes(aCount); + }, + + onStartRequest: function HCR_onStartRequest(aRequest) { + // At this point no data is available for us and we have no reason to + // terminate the connection, so we do nothing until |onStopRequest|. + this._completer._nextGethashTimeMs[this.gethashUrl] = 0; + + if (this.telemetryClockStart > 0) { + let msecs = Date.now() - this.telemetryClockStart; + Services.telemetry + .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_SERVER_RESPONSE_TIME") + .add(this.telemetryProvider, msecs); + } + }, + + onStopRequest: function HCR_onStopRequest(aRequest, aStatusCode) { + Services.obs.removeObserver(this, "quit-application"); + + if (this.timer_) { + this.timer_.cancel(); + this.timer_ = null; + } + + this.telemetryClockStart = 0; + + if (this._shuttingDown) { + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + + // Default HTTP status to service unavailable, in case we can't retrieve + // the true status from the channel. + let httpStatus = 503; + if (Components.isSuccessCode(aStatusCode)) { + let channel = aRequest.QueryInterface(Ci.nsIHttpChannel); + let success = channel.requestSucceeded; + httpStatus = channel.responseStatus; + if (!success) { + aStatusCode = Cr.NS_ERROR_ABORT; + } + } + let success = Components.isSuccessCode(aStatusCode); + log( + "Received a " + + httpStatus + + " status code from the " + + this.provider + + " gethash server (success=" + + success + + "): " + + btoa(this._response) + ); + + Services.telemetry + .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_REMOTE_STATUS2") + .add(this.telemetryProvider, httpStatusToBucket(httpStatus)); + if (httpStatus == 400) { + dump( + "Safe Browsing server returned a 400 during completion: request= " + + this.request.url + + ",payload= " + + this.request.body + + "\n" + ); + } + + Services.telemetry + .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_TIMEOUT2") + .add(this.telemetryProvider, 0); + + // Notify the RequestBackoff once a response is received. + this._completer.finishRequest(this, httpStatus); + + if (success) { + try { + this.handleResponse(); + } catch (err) { + log(err.stack); + aStatusCode = err.value; + success = false; + } + } + + if (success) { + this.notifySuccess(); + } else { + this.notifyFailure(aStatusCode); + } + }, + + observe: function HCR_observe(aSubject, aTopic, aData) { + if (aTopic == "quit-application") { + this._shuttingDown = true; + if (this._channel) { + this._channel.cancel(Cr.NS_ERROR_ABORT); + this.telemetryClockStart = 0; + } + + Services.obs.removeObserver(this, "quit-application"); + } + }, +}; + +function errorWithStack() { + let err = new Error(); + err.value = Cr.NS_ERROR_FAILURE; + return err; +} -- cgit v1.2.3