forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			968 lines
		
	
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			968 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.import(
 | 
						|
  "resource://gre/modules/XPCOMUtils.jsm"
 | 
						|
);
 | 
						|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  this,
 | 
						|
  "gDbService",
 | 
						|
  "@mozilla.org/url-classifier/dbservice;1",
 | 
						|
  "nsIUrlClassifierDBService"
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  this,
 | 
						|
  "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 */,
 | 
						|
        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 = gUrlUtil.getProvider(aTableName);
 | 
						|
      }
 | 
						|
 | 
						|
      if (this.telemetryProvider == "") {
 | 
						|
        this.telemetryProvider = 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) {
 | 
						|
    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 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;
 | 
						|
        });
 | 
						|
      },
 | 
						|
    };
 | 
						|
 | 
						|
    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 = 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"];
 |