1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5// COMPLETE_LENGTH and PARTIAL_LENGTH copied from nsUrlClassifierDBService.h,
6// they correspond to the length, in bytes, of a hash prefix and the total
7// hash.
8const COMPLETE_LENGTH = 32;
9const PARTIAL_LENGTH = 4;
10
11// Upper limit on the server response minimumWaitDuration
12const MIN_WAIT_DURATION_MAX_VALUE = 24 * 60 * 60 * 1000;
13const PREF_DEBUG_ENABLED = "browser.safebrowsing.debug";
14
15const { XPCOMUtils } = ChromeUtils.import(
16  "resource://gre/modules/XPCOMUtils.jsm"
17);
18const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
19const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
20
21XPCOMUtils.defineLazyServiceGetter(
22  this,
23  "gDbService",
24  "@mozilla.org/url-classifier/dbservice;1",
25  "nsIUrlClassifierDBService"
26);
27
28XPCOMUtils.defineLazyServiceGetter(
29  this,
30  "gUrlUtil",
31  "@mozilla.org/url-classifier/utils;1",
32  "nsIUrlClassifierUtils"
33);
34
35let loggingEnabled = false;
36
37// Log only if browser.safebrowsing.debug is true
38function log(...stuff) {
39  if (!loggingEnabled) {
40    return;
41  }
42
43  var d = new Date();
44  let msg = "hashcompleter: " + d.toTimeString() + ": " + stuff.join(" ");
45  dump(Services.urlFormatter.trimSensitiveURLs(msg) + "\n");
46}
47
48// Map the HTTP response code to a Telemetry bucket
49// https://developers.google.com/safe-browsing/developers_guide_v2?hl=en
50// eslint-disable-next-line complexity
51function httpStatusToBucket(httpStatus) {
52  var statusBucket;
53  switch (httpStatus) {
54    case 100:
55    case 101:
56      // Unexpected 1xx return code
57      statusBucket = 0;
58      break;
59    case 200:
60      // OK - Data is available in the HTTP response body.
61      statusBucket = 1;
62      break;
63    case 201:
64    case 202:
65    case 203:
66    case 205:
67    case 206:
68      // Unexpected 2xx return code
69      statusBucket = 2;
70      break;
71    case 204:
72      // No Content - There are no full-length hashes with the requested prefix.
73      statusBucket = 3;
74      break;
75    case 300:
76    case 301:
77    case 302:
78    case 303:
79    case 304:
80    case 305:
81    case 307:
82    case 308:
83      // Unexpected 3xx return code
84      statusBucket = 4;
85      break;
86    case 400:
87      // Bad Request - The HTTP request was not correctly formed.
88      // The client did not provide all required CGI parameters.
89      statusBucket = 5;
90      break;
91    case 401:
92    case 402:
93    case 405:
94    case 406:
95    case 407:
96    case 409:
97    case 410:
98    case 411:
99    case 412:
100    case 414:
101    case 415:
102    case 416:
103    case 417:
104    case 421:
105    case 426:
106    case 428:
107    case 429:
108    case 431:
109    case 451:
110      // Unexpected 4xx return code
111      statusBucket = 6;
112      break;
113    case 403:
114      // Forbidden - The client id is invalid.
115      statusBucket = 7;
116      break;
117    case 404:
118      // Not Found
119      statusBucket = 8;
120      break;
121    case 408:
122      // Request Timeout
123      statusBucket = 9;
124      break;
125    case 413:
126      // Request Entity Too Large - Bug 1150334
127      statusBucket = 10;
128      break;
129    case 500:
130    case 501:
131    case 510:
132      // Unexpected 5xx return code
133      statusBucket = 11;
134      break;
135    case 502:
136    case 504:
137    case 511:
138      // Local network errors, we'll ignore these.
139      statusBucket = 12;
140      break;
141    case 503:
142      // Service Unavailable - The server cannot handle the request.
143      // Clients MUST follow the backoff behavior specified in the
144      // Request Frequency section.
145      statusBucket = 13;
146      break;
147    case 505:
148      // HTTP Version Not Supported - The server CANNOT handle the requested
149      // protocol major version.
150      statusBucket = 14;
151      break;
152    default:
153      statusBucket = 15;
154  }
155  return statusBucket;
156}
157
158function FullHashMatch(table, hash, duration) {
159  this.tableName = table;
160  this.fullHash = hash;
161  this.cacheDuration = duration;
162}
163
164FullHashMatch.prototype = {
165  QueryInterface: ChromeUtils.generateQI([Ci.nsIFullHashMatch]),
166
167  tableName: null,
168  fullHash: null,
169  cacheDuration: null,
170};
171
172function HashCompleter() {
173  // The current HashCompleterRequest in flight. Once it is started, it is set
174  // to null. It may be used by multiple calls to |complete| in succession to
175  // avoid creating multiple requests to the same gethash URL.
176  this._currentRequest = null;
177  // An Array of ongoing gethash requests which is used to find requests for
178  // the same hash prefix.
179  this._ongoingRequests = [];
180  // A map of gethashUrls to HashCompleterRequests that haven't yet begun.
181  this._pendingRequests = {};
182
183  // A map of gethash URLs to RequestBackoff objects.
184  this._backoffs = {};
185
186  // Whether we have been informed of a shutdown by the shutdown event.
187  this._shuttingDown = false;
188
189  // A map of gethash URLs to next gethash time in miliseconds
190  this._nextGethashTimeMs = {};
191
192  Services.obs.addObserver(this, "quit-application");
193  Services.prefs.addObserver(PREF_DEBUG_ENABLED, this);
194
195  loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED);
196}
197
198HashCompleter.prototype = {
199  classID: Components.ID("{9111de73-9322-4bfc-8b65-2b727f3e6ec8}"),
200  QueryInterface: ChromeUtils.generateQI([
201    Ci.nsIUrlClassifierHashCompleter,
202    Ci.nsIRunnable,
203    Ci.nsIObserver,
204    Ci.nsISupportsWeakReference,
205    Ci.nsITimerCallback,
206  ]),
207
208  // This is mainly how the HashCompleter interacts with other components.
209  // Even though it only takes one partial hash and callback, subsequent
210  // calls are made into the same HTTP request by using a thread dispatch.
211  complete: function HC_complete(
212    aPartialHash,
213    aGethashUrl,
214    aTableName,
215    aCallback
216  ) {
217    if (!aGethashUrl) {
218      throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
219    }
220
221    // Check ongoing requests before creating a new HashCompleteRequest
222    for (let r of this._ongoingRequests) {
223      if (r.find(aPartialHash, aGethashUrl, aTableName)) {
224        log(
225          "Merge gethash request in " +
226            aTableName +
227            " for prefix : " +
228            btoa(aPartialHash)
229        );
230        r.add(aPartialHash, aCallback, aTableName);
231        return;
232      }
233    }
234
235    if (!this._currentRequest) {
236      this._currentRequest = new HashCompleterRequest(this, aGethashUrl);
237    }
238    if (this._currentRequest.gethashUrl == aGethashUrl) {
239      this._currentRequest.add(aPartialHash, aCallback, aTableName);
240    } else {
241      if (!this._pendingRequests[aGethashUrl]) {
242        this._pendingRequests[aGethashUrl] = new HashCompleterRequest(
243          this,
244          aGethashUrl
245        );
246      }
247      this._pendingRequests[aGethashUrl].add(
248        aPartialHash,
249        aCallback,
250        aTableName
251      );
252    }
253
254    if (!this._backoffs[aGethashUrl]) {
255      // Initialize request backoffs separately, since requests are deleted
256      // after they are dispatched.
257      var jslib = Cc["@mozilla.org/url-classifier/jslib;1"].getService()
258        .wrappedJSObject;
259
260      // Using the V4 backoff algorithm for both V2 and V4. See bug 1273398.
261      this._backoffs[aGethashUrl] = new jslib.RequestBackoffV4(
262        10 /* keep track of max requests */,
263        0 /* don't throttle on successful requests per time period */,
264        gUrlUtil.getProvider(aTableName) /* used by testcase */
265      );
266    }
267
268    if (!this._nextGethashTimeMs[aGethashUrl]) {
269      this._nextGethashTimeMs[aGethashUrl] = 0;
270    }
271
272    // Start off this request. Without dispatching to a thread, every call to
273    // complete makes an individual HTTP request.
274    Services.tm.dispatchToMainThread(this);
275  },
276
277  // This is called after several calls to |complete|, or after the
278  // currentRequest has finished.  It starts off the HTTP request by making a
279  // |begin| call to the HashCompleterRequest.
280  run() {
281    // Clear everything on shutdown
282    if (this._shuttingDown) {
283      this._currentRequest = null;
284      this._pendingRequests = null;
285      this._nextGethashTimeMs = null;
286
287      for (var url in this._backoffs) {
288        this._backoffs[url] = null;
289      }
290      throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
291    }
292
293    // If we don't have an in-flight request, make one
294    let pendingUrls = Object.keys(this._pendingRequests);
295    if (!this._currentRequest && pendingUrls.length) {
296      let nextUrl = pendingUrls[0];
297      this._currentRequest = this._pendingRequests[nextUrl];
298      delete this._pendingRequests[nextUrl];
299    }
300
301    if (this._currentRequest) {
302      try {
303        if (this._currentRequest.begin()) {
304          this._ongoingRequests.push(this._currentRequest);
305        }
306      } finally {
307        // If |begin| fails, we should get rid of our request.
308        this._currentRequest = null;
309      }
310    }
311  },
312
313  // Pass the server response status to the RequestBackoff for the given
314  // gethashUrl and fetch the next pending request, if there is one.
315  finishRequest(aRequest, aStatus) {
316    this._ongoingRequests = this._ongoingRequests.filter(v => v != aRequest);
317
318    this._backoffs[aRequest.gethashUrl].noteServerResponse(aStatus);
319    Services.tm.dispatchToMainThread(this);
320  },
321
322  // Returns true if we can make a request from the given url, false otherwise.
323  canMakeRequest(aGethashUrl) {
324    return (
325      this._backoffs[aGethashUrl].canMakeRequest() &&
326      Date.now() >= this._nextGethashTimeMs[aGethashUrl]
327    );
328  },
329
330  // Notifies the RequestBackoff of a new request so we can throttle based on
331  // max requests/time period. This must be called before a channel is opened,
332  // and finishRequest must be called once the response is received.
333  noteRequest(aGethashUrl) {
334    return this._backoffs[aGethashUrl].noteRequest();
335  },
336
337  observe: function HC_observe(aSubject, aTopic, aData) {
338    switch (aTopic) {
339      case "quit-application":
340        this._shuttingDown = true;
341        Services.obs.removeObserver(this, "quit-application");
342        break;
343      case "nsPref:changed":
344        if (aData == PREF_DEBUG_ENABLED) {
345          loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED);
346        }
347        break;
348    }
349  },
350};
351
352function HashCompleterRequest(aCompleter, aGethashUrl) {
353  // HashCompleter object that created this HashCompleterRequest.
354  this._completer = aCompleter;
355  // The internal set of hashes and callbacks that this request corresponds to.
356  this._requests = [];
357  // nsIChannel that the hash completion query is transmitted over.
358  this._channel = null;
359  // Response body of hash completion. Created in onDataAvailable.
360  this._response = "";
361  // Whether we have been informed of a shutdown by the quit-application event.
362  this._shuttingDown = false;
363  this.gethashUrl = aGethashUrl;
364
365  this.provider = "";
366  // Multiple partial hashes can be associated with the same tables
367  // so we use a map here.
368  this.tableNames = new Map();
369
370  this.telemetryProvider = "";
371  this.telemetryClockStart = 0;
372}
373HashCompleterRequest.prototype = {
374  QueryInterface: ChromeUtils.generateQI([
375    Ci.nsIRequestObserver,
376    Ci.nsIStreamListener,
377    Ci.nsIObserver,
378  ]),
379
380  // This is called by the HashCompleter to add a hash and callback to the
381  // HashCompleterRequest. It must be called before calling |begin|.
382  add: function HCR_add(aPartialHash, aCallback, aTableName) {
383    this._requests.push({
384      partialHash: aPartialHash,
385      callback: aCallback,
386      tableName: aTableName,
387      response: { matches: [] },
388    });
389
390    if (aTableName) {
391      let isTableNameV4 = aTableName.endsWith("-proto");
392      if (0 === this.tableNames.size) {
393        // Decide if this request is v4 by the first added partial hash.
394        this.isV4 = isTableNameV4;
395      } else if (this.isV4 !== isTableNameV4) {
396        log(
397          'ERROR: Cannot mix "proto" tables with other types within ' +
398            "the same gethash URL."
399        );
400      }
401      if (!this.tableNames.has(aTableName)) {
402        this.tableNames.set(aTableName);
403      }
404
405      // Assuming all tables with the same gethash URL have the same provider
406      if (this.provider == "") {
407        this.provider = gUrlUtil.getProvider(aTableName);
408      }
409
410      if (this.telemetryProvider == "") {
411        this.telemetryProvider = gUrlUtil.getTelemetryProvider(aTableName);
412      }
413    }
414  },
415
416  find: function HCR_find(aPartialHash, aGetHashUrl, aTableName) {
417    if (this.gethashUrl != aGetHashUrl || !this.tableNames.has(aTableName)) {
418      return false;
419    }
420
421    return this._requests.find(function(r) {
422      return r.partialHash === aPartialHash;
423    });
424  },
425
426  fillTableStatesBase64: function HCR_fillTableStatesBase64(aCallback) {
427    gDbService.getTables(aTableData => {
428      aTableData.split("\n").forEach(line => {
429        let p = line.indexOf(";");
430        if (-1 === p) {
431          return;
432        }
433        // [tableName];[stateBase64]:[checksumBase64]
434        let tableName = line.substring(0, p);
435        if (this.tableNames.has(tableName)) {
436          let metadata = line.substring(p + 1).split(":");
437          let stateBase64 = metadata[0];
438          this.tableNames.set(tableName, stateBase64);
439        }
440      });
441
442      aCallback();
443    });
444  },
445
446  // This initiates the HTTP request. It can fail due to backoff timings and
447  // will notify all callbacks as necessary. We notify the backoff object on
448  // begin.
449  begin: function HCR_begin() {
450    if (!this._completer.canMakeRequest(this.gethashUrl)) {
451      log("Can't make request to " + this.gethashUrl + "\n");
452      this.notifyFailure(Cr.NS_ERROR_ABORT);
453      return false;
454    }
455
456    Services.obs.addObserver(this, "quit-application");
457
458    // V4 requires table states to build the request so we need
459    // a async call to retrieve the table states from disk.
460    // Note that |HCR_begin| is fine to be sync because
461    // it doesn't appear in a sync call chain.
462    this.fillTableStatesBase64(() => {
463      try {
464        this.openChannel();
465        // Notify the RequestBackoff if opening the channel succeeded. At this
466        // point, finishRequest must be called.
467        this._completer.noteRequest(this.gethashUrl);
468      } catch (err) {
469        this._completer._ongoingRequests = this._completer._ongoingRequests.filter(
470          v => v != this
471        );
472        this.notifyFailure(err);
473        throw err;
474      }
475    });
476
477    return true;
478  },
479
480  notify: function HCR_notify() {
481    // If we haven't gotten onStopRequest, just cancel. This will call us
482    // with onStopRequest since we implement nsIStreamListener on the
483    // channel.
484    if (this._channel && this._channel.isPending()) {
485      log("cancelling request to " + this.gethashUrl + " (timeout)\n");
486      Services.telemetry
487        .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_TIMEOUT2")
488        .add(this.telemetryProvider, 1);
489      this._channel.cancel(Cr.NS_BINDING_ABORTED);
490    }
491  },
492
493  // Creates an nsIChannel for the request and fills the body.
494  // Enforce bypassing URL Classifier check because if the request is
495  // blocked, it means SafeBrowsing is malfunction.
496  openChannel: function HCR_openChannel() {
497    let loadFlags =
498      Ci.nsIChannel.INHIBIT_CACHING |
499      Ci.nsIChannel.LOAD_BYPASS_CACHE |
500      Ci.nsIChannel.LOAD_BYPASS_URL_CLASSIFIER;
501
502    this.request = {
503      url: this.gethashUrl,
504      body: "",
505    };
506
507    if (this.isV4) {
508      // As per spec, we add the request payload to the gethash url.
509      this.request.url += "&$req=" + this.buildRequestV4();
510    }
511
512    log("actualGethashUrl: " + this.request.url);
513
514    let channel = NetUtil.newChannel({
515      uri: this.request.url,
516      loadUsingSystemPrincipal: true,
517    });
518    channel.loadFlags = loadFlags;
519    channel.loadInfo.originAttributes = {
520      // The firstPartyDomain value should sync with NECKO_SAFEBROWSING_FIRST_PARTY_DOMAIN
521      // defined in nsNetUtil.h.
522      firstPartyDomain:
523        "safebrowsing.86868755-6b82-4842-b301-72671a0db32e.mozilla",
524    };
525
526    // Disable keepalive.
527    let httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
528    httpChannel.setRequestHeader("Connection", "close", false);
529
530    this._channel = channel;
531
532    if (this.isV4) {
533      httpChannel.setRequestHeader("X-HTTP-Method-Override", "POST", false);
534    } else {
535      let body = this.buildRequest();
536      this.addRequestBody(body);
537    }
538
539    // Set a timer that cancels the channel after timeout_ms in case we
540    // don't get a gethash response.
541    this.timer_ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
542    // Ask the timer to use nsITimerCallback (.notify()) when ready
543    let timeout = Services.prefs.getIntPref("urlclassifier.gethash.timeout_ms");
544    this.timer_.initWithCallback(this, timeout, this.timer_.TYPE_ONE_SHOT);
545    channel.asyncOpen(this);
546    this.telemetryClockStart = Date.now();
547  },
548
549  buildRequestV4: function HCR_buildRequestV4() {
550    // Convert the "name to state" mapping to two equal-length arrays.
551    let tableNameArray = [];
552    let stateArray = [];
553    this.tableNames.forEach((state, name) => {
554      // We skip the table which is not associated with a state.
555      if (state) {
556        tableNameArray.push(name);
557        stateArray.push(state);
558      }
559    });
560
561    // Build the "distinct" prefix array.
562    // The array is sorted to make sure the entries are arbitrary mixed in a
563    // deterministic way
564    let prefixSet = new Set();
565    this._requests.forEach(r => prefixSet.add(btoa(r.partialHash)));
566    let prefixArray = Array.from(prefixSet).sort();
567
568    log(
569      "Build v4 gethash request with " +
570        JSON.stringify(tableNameArray) +
571        ", " +
572        JSON.stringify(stateArray) +
573        ", " +
574        JSON.stringify(prefixArray)
575    );
576
577    return gUrlUtil.makeFindFullHashRequestV4(
578      tableNameArray,
579      stateArray,
580      prefixArray
581    );
582  },
583
584  // Returns a string for the request body based on the contents of
585  // this._requests.
586  buildRequest: function HCR_buildRequest() {
587    // Sometimes duplicate entries are sent to HashCompleter but we do not need
588    // to propagate these to the server. (bug 633644)
589    let prefixes = [];
590
591    for (let i = 0; i < this._requests.length; i++) {
592      let request = this._requests[i];
593      if (!prefixes.includes(request.partialHash)) {
594        prefixes.push(request.partialHash);
595      }
596    }
597
598    // Sort to make sure the entries are arbitrary mixed in a deterministic way
599    prefixes.sort();
600
601    let body;
602    body =
603      PARTIAL_LENGTH +
604      ":" +
605      PARTIAL_LENGTH * prefixes.length +
606      "\n" +
607      prefixes.join("");
608
609    log(
610      "Requesting completions for " +
611        prefixes.length +
612        " " +
613        PARTIAL_LENGTH +
614        "-byte prefixes: " +
615        body
616    );
617    return body;
618  },
619
620  // Sets the request body of this._channel.
621  addRequestBody: function HCR_addRequestBody(aBody) {
622    let inputStream = Cc[
623      "@mozilla.org/io/string-input-stream;1"
624    ].createInstance(Ci.nsIStringInputStream);
625
626    inputStream.setData(aBody, aBody.length);
627
628    let uploadChannel = this._channel.QueryInterface(Ci.nsIUploadChannel);
629    uploadChannel.setUploadStream(inputStream, "text/plain", -1);
630
631    let httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel);
632    httpChannel.requestMethod = "POST";
633  },
634
635  // Parses the response body and eventually adds items to the |response.matches| array
636  // for elements of |this._requests|.
637  handleResponse: function HCR_handleResponse() {
638    if (this._response == "") {
639      return;
640    }
641
642    if (this.isV4) {
643      this.handleResponseV4();
644      return;
645    }
646
647    let start = 0;
648
649    let length = this._response.length;
650    while (start != length) {
651      start = this.handleTable(start);
652    }
653  },
654
655  handleResponseV4: function HCR_handleResponseV4() {
656    let callback = {
657      // onCompleteHashFound will be called for each fullhash found in
658      // FullHashResponse.
659      onCompleteHashFound: (
660        aCompleteHash,
661        aTableNames,
662        aPerHashCacheDuration
663      ) => {
664        log(
665          "V4 fullhash response complete hash found callback: " +
666            aTableNames +
667            ", CacheDuration(" +
668            aPerHashCacheDuration +
669            ")"
670        );
671
672        // Filter table names which we didn't requested.
673        let filteredTables = aTableNames.split(",").filter(name => {
674          return this.tableNames.get(name);
675        });
676        if (0 === filteredTables.length) {
677          log("ERROR: Got complete hash which is from unknown table.");
678          return;
679        }
680        if (filteredTables.length > 1) {
681          log("WARNING: Got complete hash which has ambigious threat type.");
682        }
683
684        this.handleItem({
685          completeHash: aCompleteHash,
686          tableName: filteredTables[0],
687          cacheDuration: aPerHashCacheDuration,
688        });
689      },
690
691      // onResponseParsed will be called no matter if there is match in
692      // FullHashResponse, the callback is mainly used to pass negative cache
693      // duration and minimum wait duration.
694      onResponseParsed: (aMinWaitDuration, aNegCacheDuration) => {
695        log(
696          "V4 fullhash response parsed callback: " +
697            "MinWaitDuration(" +
698            aMinWaitDuration +
699            "), " +
700            "NegativeCacheDuration(" +
701            aNegCacheDuration +
702            ")"
703        );
704
705        let minWaitDuration = aMinWaitDuration;
706
707        if (aMinWaitDuration > MIN_WAIT_DURATION_MAX_VALUE) {
708          log(
709            "WARNING: Minimum wait duration too large, clamping it down " +
710              "to a reasonable value."
711          );
712          minWaitDuration = MIN_WAIT_DURATION_MAX_VALUE;
713        } else if (aMinWaitDuration < 0) {
714          log("WARNING: Minimum wait duration is negative, reset it to 0");
715          minWaitDuration = 0;
716        }
717
718        this._completer._nextGethashTimeMs[this.gethashUrl] =
719          Date.now() + minWaitDuration;
720
721        // A fullhash request may contain more than one prefix, so the negative
722        // cache duration should be set for all the prefixes in the request.
723        this._requests.forEach(request => {
724          request.response.negCacheDuration = aNegCacheDuration;
725        });
726      },
727    };
728
729    gUrlUtil.parseFindFullHashResponseV4(this._response, callback);
730  },
731
732  // This parses a table entry in the response body and calls |handleItem|
733  // for complete hash in the table entry.
734  handleTable: function HCR_handleTable(aStart) {
735    let body = this._response.substring(aStart);
736
737    // deal with new line indexes as there could be
738    // new line characters in the data parts.
739    let newlineIndex = body.indexOf("\n");
740    if (newlineIndex == -1) {
741      throw errorWithStack();
742    }
743    let header = body.substring(0, newlineIndex);
744    let entries = header.split(":");
745    if (entries.length != 3) {
746      throw errorWithStack();
747    }
748
749    let list = entries[0];
750    let addChunk = parseInt(entries[1]);
751    let dataLength = parseInt(entries[2]);
752
753    log("Response includes add chunks for " + list + ": " + addChunk);
754    if (
755      dataLength % COMPLETE_LENGTH != 0 ||
756      dataLength == 0 ||
757      dataLength > body.length - (newlineIndex + 1)
758    ) {
759      throw errorWithStack();
760    }
761
762    let data = body.substr(newlineIndex + 1, dataLength);
763    for (let i = 0; i < dataLength / COMPLETE_LENGTH; i++) {
764      this.handleItem({
765        completeHash: data.substr(i * COMPLETE_LENGTH, COMPLETE_LENGTH),
766        tableName: list,
767        chunkId: addChunk,
768      });
769    }
770
771    return aStart + newlineIndex + 1 + dataLength;
772  },
773
774  // This adds a complete hash to any entry in |this._requests| that matches
775  // the hash.
776  handleItem: function HCR_handleItem(aData) {
777    let provider = gUrlUtil.getProvider(aData.tableName);
778    if (provider != this.provider) {
779      log(
780        "Ignoring table " +
781          aData.tableName +
782          " since it belongs to " +
783          provider +
784          " while the response came from " +
785          this.provider +
786          "."
787      );
788      return;
789    }
790
791    for (let i = 0; i < this._requests.length; i++) {
792      let request = this._requests[i];
793      if (aData.completeHash.startsWith(request.partialHash)) {
794        request.response.matches.push(aData);
795      }
796    }
797  },
798
799  // notifySuccess and notifyFailure are used to alert the callbacks with
800  // results. notifySuccess makes |completion| and |completionFinished| calls
801  // while notifyFailure only makes a |completionFinished| call with the error
802  // code.
803  notifySuccess: function HCR_notifySuccess() {
804    // V2 completion handler
805    let completionV2 = req => {
806      req.response.matches.forEach(m => {
807        req.callback.completionV2(m.completeHash, m.tableName, m.chunkId);
808      });
809
810      req.callback.completionFinished(Cr.NS_OK);
811    };
812
813    // V4 completion handler
814    let completionV4 = req => {
815      let matches = Cc["@mozilla.org/array;1"].createInstance(
816        Ci.nsIMutableArray
817      );
818
819      req.response.matches.forEach(m => {
820        matches.appendElement(
821          new FullHashMatch(m.tableName, m.completeHash, m.cacheDuration)
822        );
823      });
824
825      req.callback.completionV4(
826        req.partialHash,
827        req.tableName,
828        req.response.negCacheDuration,
829        matches
830      );
831
832      req.callback.completionFinished(Cr.NS_OK);
833    };
834
835    let completion = this.isV4 ? completionV4 : completionV2;
836    this._requests.forEach(req => {
837      completion(req);
838    });
839  },
840
841  notifyFailure: function HCR_notifyFailure(aStatus) {
842    log("notifying failure\n");
843    for (let i = 0; i < this._requests.length; i++) {
844      let request = this._requests[i];
845      request.callback.completionFinished(aStatus);
846    }
847  },
848
849  onDataAvailable: function HCR_onDataAvailable(
850    aRequest,
851    aInputStream,
852    aOffset,
853    aCount
854  ) {
855    let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
856      Ci.nsIScriptableInputStream
857    );
858    sis.init(aInputStream);
859    this._response += sis.readBytes(aCount);
860  },
861
862  onStartRequest: function HCR_onStartRequest(aRequest) {
863    // At this point no data is available for us and we have no reason to
864    // terminate the connection, so we do nothing until |onStopRequest|.
865    this._completer._nextGethashTimeMs[this.gethashUrl] = 0;
866
867    if (this.telemetryClockStart > 0) {
868      let msecs = Date.now() - this.telemetryClockStart;
869      Services.telemetry
870        .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_SERVER_RESPONSE_TIME")
871        .add(this.telemetryProvider, msecs);
872    }
873  },
874
875  onStopRequest: function HCR_onStopRequest(aRequest, aStatusCode) {
876    Services.obs.removeObserver(this, "quit-application");
877
878    if (this.timer_) {
879      this.timer_.cancel();
880      this.timer_ = null;
881    }
882
883    this.telemetryClockStart = 0;
884
885    if (this._shuttingDown) {
886      throw Components.Exception("", Cr.NS_ERROR_ABORT);
887    }
888
889    // Default HTTP status to service unavailable, in case we can't retrieve
890    // the true status from the channel.
891    let httpStatus = 503;
892    if (Components.isSuccessCode(aStatusCode)) {
893      let channel = aRequest.QueryInterface(Ci.nsIHttpChannel);
894      let success = channel.requestSucceeded;
895      httpStatus = channel.responseStatus;
896      if (!success) {
897        aStatusCode = Cr.NS_ERROR_ABORT;
898      }
899    }
900    let success = Components.isSuccessCode(aStatusCode);
901    log(
902      "Received a " +
903        httpStatus +
904        " status code from the " +
905        this.provider +
906        " gethash server (success=" +
907        success +
908        "): " +
909        btoa(this._response)
910    );
911
912    Services.telemetry
913      .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_REMOTE_STATUS2")
914      .add(this.telemetryProvider, httpStatusToBucket(httpStatus));
915    if (httpStatus == 400) {
916      dump(
917        "Safe Browsing server returned a 400 during completion: request= " +
918          this.request.url +
919          ",payload= " +
920          this.request.body +
921          "\n"
922      );
923    }
924
925    Services.telemetry
926      .getKeyedHistogramById("URLCLASSIFIER_COMPLETE_TIMEOUT2")
927      .add(this.telemetryProvider, 0);
928
929    // Notify the RequestBackoff once a response is received.
930    this._completer.finishRequest(this, httpStatus);
931
932    if (success) {
933      try {
934        this.handleResponse();
935      } catch (err) {
936        log(err.stack);
937        aStatusCode = err.value;
938        success = false;
939      }
940    }
941
942    if (success) {
943      this.notifySuccess();
944    } else {
945      this.notifyFailure(aStatusCode);
946    }
947  },
948
949  observe: function HCR_observe(aSubject, aTopic, aData) {
950    if (aTopic == "quit-application") {
951      this._shuttingDown = true;
952      if (this._channel) {
953        this._channel.cancel(Cr.NS_ERROR_ABORT);
954        this.telemetryClockStart = 0;
955      }
956
957      Services.obs.removeObserver(this, "quit-application");
958    }
959  },
960};
961
962function errorWithStack() {
963  let err = new Error();
964  err.value = Cr.NS_ERROR_FAILURE;
965  return err;
966}
967
968var EXPORTED_SYMBOLS = ["HashCompleter"];
969