forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			969 lines
		
	
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			969 lines
		
	
	
	
		
			28 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 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";
 | |
| 
 | |
| const { XPCOMUtils } = ChromeUtils.importESModule(
 | |
|   "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,
 | |
| };
 | |
| 
 | |
| 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;
 | |
| }
 | |
| 
 | |
| var EXPORTED_SYMBOLS = ["HashCompleter"];
 | 
