toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

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

mercurial