toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     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/. */
     5 const Cc = Components.classes;
     6 const Ci = Components.interfaces;
     7 const Cr = Components.results;
     8 const Cu = Components.utils;
    10 // COMPLETE_LENGTH and PARTIAL_LENGTH copied from nsUrlClassifierDBService.h,
    11 // they correspond to the length, in bytes, of a hash prefix and the total
    12 // hash.
    13 const COMPLETE_LENGTH = 32;
    14 const PARTIAL_LENGTH = 4;
    16 // These backoff related constants are taken from v2 of the Google Safe Browsing
    17 // API.
    18 // BACKOFF_ERRORS: the number of errors incurred until we start to back off.
    19 // BACKOFF_INTERVAL: the initial time, in seconds, to wait once we start backing
    20 //                   off.
    21 // BACKOFF_MAX: as the backoff time doubles after each failure, this is a
    22 //              ceiling on the time to wait, in seconds.
    23 // BACKOFF_TIME: length of the interval of time, in seconds, during which errors
    24 //               are taken into account.
    26 const BACKOFF_ERRORS = 2;
    27 const BACKOFF_INTERVAL = 30 * 60;
    28 const BACKOFF_MAX = 8 * 60 * 60;
    29 const BACKOFF_TIME = 5 * 60;
    31 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    32 Cu.import("resource://gre/modules/Services.jsm");
    34 function HashCompleter() {
    35   // This is a HashCompleterRequest and is used by multiple calls to |complete|
    36   // in succession to avoid unnecessarily creating requests. Once it has been
    37   // started, this is set to null again.
    38   this._currentRequest = null;
    40   // Whether we have been informed of a shutdown by the xpcom-shutdown event.
    41   this._shuttingDown = false;
    43   // All of these backoff properties are different per completer as the DB
    44   // service keeps one completer per table.
    45   //
    46   // _backoff tells us whether we are "backing off" from making requests.
    47   // It is set in |noteServerResponse| and set after a number of failures.
    48   this._backoff = false;
    49   // _backoffTime tells us how long we should wait (in seconds) before making
    50   // another request.
    51   this._backoffTime = 0;
    52   // _nextRequestTime is the earliest time at which we are allowed to make
    53   // another request by the backoff policy. It is measured in milliseconds.
    54   this._nextRequestTime = 0;
    55   // A list of the times at which a failed request was made, recorded in
    56   // |noteServerResponse|. Sorted by oldest to newest and its length is clamped
    57   // by BACKOFF_ERRORS.
    58   this._errorTimes = [];
    60   Services.obs.addObserver(this, "xpcom-shutdown", true);
    61 }
    62 HashCompleter.prototype = {
    63   classID: Components.ID("{9111de73-9322-4bfc-8b65-2b727f3e6ec8}"),
    64   QueryInterface: XPCOMUtils.generateQI([Ci.nsIUrlClassifierHashCompleter,
    65                                          Ci.nsIRunnable,
    66                                          Ci.nsIObserver,
    67                                          Ci.nsISupportsWeakReference,
    68                                          Ci.nsISupports]),
    70   // This is mainly how the HashCompleter interacts with other components.
    71   // Even though it only takes one partial hash and callback, subsequent
    72   // calls are made into the same HTTP request by using a thread dispatch.
    73   complete: function HC_complete(aPartialHash, aCallback) {
    74     if (!this._currentRequest) {
    75       this._currentRequest = new HashCompleterRequest(this);
    77       // It's possible for getHashUrl to not be set, usually at start-up.
    78       if (this._getHashUrl) {
    79         Services.tm.currentThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
    80       }
    81     }
    83     this._currentRequest.add(aPartialHash, aCallback);
    84   },
    86   // This is called when either the getHashUrl has been set or after a few calls
    87   // to |complete|. It starts off the HTTP request by making a |begin| call
    88   // to the HashCompleterRequest.
    89   run: function HC_run() {
    90     if (this._shuttingDown) {
    91       this._currentRequest = null;
    92       throw Cr.NS_ERROR_NOT_INITIALIZED;
    93     }
    95     if (!this._currentRequest) {
    96       return;
    97     }
    99     if (!this._getHashUrl) {
   100       throw Cr.NS_ERROR_NOT_INITIALIZED;
   101     }
   103     let url = this._getHashUrl;
   105     let uri = Services.io.newURI(url, null, null);
   106     this._currentRequest.setURI(uri);
   108     // If |begin| fails, we should get rid of our request.
   109     try {
   110       this._currentRequest.begin();
   111     }
   112     finally {
   113       this._currentRequest = null;
   114     }
   115   },
   117   get gethashUrl() {
   118     return this._getHashUrl;
   119   },
   120   // Because we hold off on making a request until we have a valid getHashUrl,
   121   // we kick off the process here.
   122   set gethashUrl(aNewUrl) {
   123     this._getHashUrl = aNewUrl;
   125     if (this._currentRequest) {
   126       Services.tm.currentThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
   127     }
   128   },
   130   // This handles all the logic about setting a back off time based on
   131   // server responses. It should only be called once in the life time of a
   132   // request.
   133   // The logic behind backoffs is documented in the Google Safe Browsing API,
   134   // the general idea is that a completer should go into backoff mode after
   135   // BACKOFF_ERRORS errors in the last BACKOFF_TIME seconds. From there,
   136   // we do not make a request for BACKOFF_INTERVAL seconds and for every failed
   137   // request after that we double how long to wait, to a maximum of BACKOFF_MAX.
   138   // Note that even in the case of a successful response we still keep a history
   139   // of past errors.
   140   noteServerResponse: function HC_noteServerResponse(aSuccess) {
   141     if (aSuccess) {
   142       this._backoff = false;
   143       this._nextRequestTime = 0;
   144       this._backoffTime = 0;
   145       return;
   146     }
   148     let now = Date.now();
   150     // We only alter the size of |_errorTimes| here, so we can guarantee that
   151     // its length is at most BACKOFF_ERRORS.
   152     this._errorTimes.push(now);
   153     if (this._errorTimes.length > BACKOFF_ERRORS) {
   154       this._errorTimes.shift();
   155     }
   157     if (this._backoff) {
   158       this._backoffTime *= 2;
   159     } else if (this._errorTimes.length == BACKOFF_ERRORS &&
   160                ((now - this._errorTimes[0]) / 1000) <= BACKOFF_TIME) {
   161       this._backoff = true;
   162       this._backoffTime = BACKOFF_INTERVAL;
   163     }
   165     if (this._backoff) {
   166       this._backoffTime = Math.min(this._backoffTime, BACKOFF_MAX);
   167       this._nextRequestTime = now + (this._backoffTime * 1000);
   168     }
   169   },
   171   // This is not included on the interface but is used to communicate to the
   172   // HashCompleterRequest. It returns a time in milliseconds.
   173   get nextRequestTime() {
   174     return this._nextRequestTime;
   175   },
   177   observe: function HC_observe(aSubject, aTopic, aData) {
   178     if (aTopic == "xpcom-shutdown") {
   179       this._shuttingDown = true;
   180     }
   181   },
   182 };
   184 function HashCompleterRequest(aCompleter) {
   185   // HashCompleter object that created this HashCompleterRequest.
   186   this._completer = aCompleter;
   187   // The internal set of hashes and callbacks that this request corresponds to.
   188   this._requests = [];
   189   // URI to query for hash completions. Largely comes from the
   190   // browser.safebrowsing.gethashURL pref.
   191   this._uri = null;
   192   // nsIChannel that the hash completion query is transmitted over.
   193   this._channel = null;
   194   // Response body of hash completion. Created in onDataAvailable.
   195   this._response = "";
   196   // Whether we have been informed of a shutdown by the xpcom-shutdown event.
   197   this._shuttingDown = false;
   198 }
   199 HashCompleterRequest.prototype = {
   200   QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
   201                                          Ci.nsIStreamListener,
   202                                          Ci.nsIObserver,
   203                                          Ci.nsISupports]),
   205   // This is called by the HashCompleter to add a hash and callback to the
   206   // HashCompleterRequest. It must be called before calling |begin|.
   207   add: function HCR_add(aPartialHash, aCallback) {
   208     this._requests.push({
   209       partialHash: aPartialHash,
   210       callback: aCallback,
   211       responses: [],
   212     });
   213   },
   215   // This initiates the HTTP request. It can fail due to backoff timings and
   216   // will notify all callbacks as necessary.
   217   begin: function HCR_begin() {
   218     if (Date.now() < this._completer.nextRequestTime) {
   219       this.notifyFailure(Cr.NS_ERROR_ABORT);
   220       return;
   221     }
   223     Services.obs.addObserver(this, "xpcom-shutdown", false);
   225     try {
   226       this.openChannel();
   227     }
   228     catch (err) {
   229       this.notifyFailure(err);
   230       throw err;
   231     }
   232   },
   234   setURI: function HCR_setURI(aURI) {
   235     this._uri = aURI;
   236   },
   238   // Creates an nsIChannel for the request and fills the body.
   239   openChannel: function HCR_openChannel() {
   240     let loadFlags = Ci.nsIChannel.INHIBIT_CACHING |
   241                     Ci.nsIChannel.LOAD_BYPASS_CACHE;
   243     let channel = Services.io.newChannelFromURI(this._uri);
   244     channel.loadFlags = loadFlags;
   246     this._channel = channel;
   248     let body = this.buildRequest();
   249     this.addRequestBody(body);
   251     channel.asyncOpen(this, null);
   252   },
   254   // Returns a string for the request body based on the contents of
   255   // this._requests.
   256   buildRequest: function HCR_buildRequest() {
   257     // Sometimes duplicate entries are sent to HashCompleter but we do not need
   258     // to propagate these to the server. (bug 633644)
   259     let prefixes = [];
   261     for (let i = 0; i < this._requests.length; i++) {
   262       let request = this._requests[i];
   263       if (prefixes.indexOf(request.partialHash) == -1) {
   264         prefixes.push(request.partialHash);
   265       }
   266     }
   268     // Randomize the order to obscure the original request from noise
   269     // unbiased Fisher-Yates shuffle
   270     let i = prefixes.length;
   271     while (i--) {
   272       let j = Math.floor(Math.random() * (i + 1));
   273       let temp = prefixes[i];
   274       prefixes[i] = prefixes[j];
   275       prefixes[j] = temp;
   276     }
   278     let body;
   279     body = PARTIAL_LENGTH + ":" + (PARTIAL_LENGTH * prefixes.length) +
   280            "\n" + prefixes.join("");
   282     return body;
   283   },
   285   // Sets the request body of this._channel.
   286   addRequestBody: function HCR_addRequestBody(aBody) {
   287     let inputStream = Cc["@mozilla.org/io/string-input-stream;1"].
   288                       createInstance(Ci.nsIStringInputStream);
   290     inputStream.setData(aBody, aBody.length);
   292     let uploadChannel = this._channel.QueryInterface(Ci.nsIUploadChannel);
   293     uploadChannel.setUploadStream(inputStream, "text/plain", -1);
   295     let httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel);
   296     httpChannel.requestMethod = "POST";
   297   },
   299   // Parses the response body and eventually adds items to the |responses| array
   300   // for elements of |this._requests|.
   301   handleResponse: function HCR_handleResponse() {
   302     if (this._response == "") {
   303       return;
   304     }
   306     let start = 0;
   308     let length = this._response.length;
   309     while (start != length)
   310       start = this.handleTable(start);
   311   },
   313   // This parses a table entry in the response body and calls |handleItem|
   314   // for complete hash in the table entry. 
   315   handleTable: function HCR_handleTable(aStart) {
   316     let body = this._response.substring(aStart);
   318     // deal with new line indexes as there could be
   319     // new line characters in the data parts.
   320     let newlineIndex = body.indexOf("\n");
   321     if (newlineIndex == -1) {
   322       throw errorWithStack();
   323     }
   324     let header = body.substring(0, newlineIndex);
   325     let entries = header.split(":");
   326     if (entries.length != 3) {
   327       throw errorWithStack();
   328     }
   330     let list = entries[0];
   331     let addChunk = parseInt(entries[1]);
   332     let dataLength = parseInt(entries[2]);
   334     if (dataLength % COMPLETE_LENGTH != 0 ||
   335         dataLength == 0 ||
   336         dataLength > body.length - (newlineIndex + 1)) {
   337       throw errorWithStack();
   338     }
   340     let data = body.substr(newlineIndex + 1, dataLength);
   341     for (let i = 0; i < (dataLength / COMPLETE_LENGTH); i++) {
   342       this.handleItem(data.substr(i * COMPLETE_LENGTH, COMPLETE_LENGTH), list,
   343                       addChunk);
   344     }
   346     return aStart + newlineIndex + 1 + dataLength;
   347   },
   349   // This adds a complete hash to any entry in |this._requests| that matches
   350   // the hash.
   351   handleItem: function HCR_handleItem(aData, aTableName, aChunkId) {
   352     for (let i = 0; i < this._requests.length; i++) {
   353       let request = this._requests[i];
   354       if (aData.substring(0,4) == request.partialHash) {
   355         request.responses.push({
   356           completeHash: aData,
   357           tableName: aTableName,
   358           chunkId: aChunkId,
   359         });
   360       }
   361     }
   362   },
   364   // notifySuccess and notifyFailure are used to alert the callbacks with
   365   // results. notifySuccess makes |completion| and |completionFinished| calls
   366   // while notifyFailure only makes a |completionFinished| call with the error
   367   // code.
   368   notifySuccess: function HCR_notifySuccess() {
   369     for (let i = 0; i < this._requests.length; i++) {
   370       let request = this._requests[i];
   371       for (let j = 0; j < request.responses.length; j++) {
   372         let response = request.responses[j];
   373         request.callback.completion(response.completeHash, response.tableName,
   374                                     response.chunkId);
   375       }
   377       request.callback.completionFinished(Cr.NS_OK);
   378     }
   379   },
   380   notifyFailure: function HCR_notifyFailure(aStatus) {
   381     for (let i = 0; i < this._requests; i++) {
   382       let request = this._requests[i];
   383       request.callback.completionFinished(aStatus);
   384     }
   385   },
   387   onDataAvailable: function HCR_onDataAvailable(aRequest, aContext,
   388                                                 aInputStream, aOffset, aCount) {
   389     let sis = Cc["@mozilla.org/scriptableinputstream;1"].
   390               createInstance(Ci.nsIScriptableInputStream);
   391     sis.init(aInputStream);
   392     this._response += sis.readBytes(aCount);
   393   },
   395   onStartRequest: function HCR_onStartRequest(aRequest, aContext) {
   396     // At this point no data is available for us and we have no reason to
   397     // terminate the connection, so we do nothing until |onStopRequest|.
   398   },
   400   onStopRequest: function HCR_onStopRequest(aRequest, aContext, aStatusCode) {
   401     Services.obs.removeObserver(this, "xpcom-shutdown");
   403     if (this._shuttingDown) {
   404       throw Cr.NS_ERROR_ABORT;
   405     }
   407     if (Components.isSuccessCode(aStatusCode)) {
   408       let channel = aRequest.QueryInterface(Ci.nsIHttpChannel);
   409       let success = channel.requestSucceeded;
   410       if (!success) {
   411         aStatusCode = Cr.NS_ERROR_ABORT;
   412       }
   413     }
   415     let success = Components.isSuccessCode(aStatusCode);
   416     this._completer.noteServerResponse(success);
   418     if (success) {
   419       try {
   420         this.handleResponse();
   421       }
   422       catch (err) {
   423         dump(err.stack);
   424         aStatusCode = err.value;
   425         success = false;
   426       }
   427     }
   429     if (success) {
   430       this.notifySuccess();
   431     } else {
   432       this.notifyFailure(aStatusCode);
   433     }
   434   },
   436   observe: function HCR_observe(aSubject, aTopic, aData) {
   437     if (aTopic != "xpcom-shutdown") {
   438       return;
   439     }
   441     this._shuttingDown = true;
   442     if (this._channel) {
   443       this._channel.cancel(Cr.NS_ERROR_ABORT);
   444     }
   445   },
   446 };
   448 // Converts a URL safe base64 string to a normal base64 string. Will not change
   449 // normal base64 strings. This is modelled after the same function in
   450 // nsUrlClassifierUtils.h.
   451 function unUrlsafeBase64(aStr) {
   452   return !aStr ? "" : aStr.replace(/-/g, "+")
   453                           .replace(/_/g, "/");
   454 }
   456 function errorWithStack() {
   457   let err = new Error();
   458   err.value = Cr.NS_ERROR_FAILURE;
   459   return err;
   460 }
   462 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HashCompleter]);

mercurial