toolkit/components/url-classifier/content/listmanager.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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
michael@0 6 // This is the only implementation of nsIUrlListManager.
michael@0 7 // A class that manages lists, namely white and black lists for
michael@0 8 // phishing or malware protection. The ListManager knows how to fetch,
michael@0 9 // update, and store lists.
michael@0 10 //
michael@0 11 // There is a single listmanager for the whole application.
michael@0 12 //
michael@0 13 // TODO more comprehensive update tests, for example add unittest check
michael@0 14 // that the listmanagers tables are properly written on updates
michael@0 15
michael@0 16 function QueryAdapter(callback) {
michael@0 17 this.callback_ = callback;
michael@0 18 };
michael@0 19
michael@0 20 QueryAdapter.prototype.handleResponse = function(value) {
michael@0 21 this.callback_.handleEvent(value);
michael@0 22 }
michael@0 23
michael@0 24 /**
michael@0 25 * A ListManager keeps track of black and white lists and knows
michael@0 26 * how to update them.
michael@0 27 *
michael@0 28 * @constructor
michael@0 29 */
michael@0 30 function PROT_ListManager() {
michael@0 31 this.debugZone = "listmanager";
michael@0 32 G_debugService.enableZone(this.debugZone);
michael@0 33
michael@0 34 this.currentUpdateChecker_ = null; // set when we toggle updates
michael@0 35 this.prefs_ = new G_Preferences();
michael@0 36 this.updateInterval = this.prefs_.getPref("urlclassifier.updateinterval", 30 * 60) * 1000;
michael@0 37
michael@0 38 this.updateserverURL_ = null;
michael@0 39 this.gethashURL_ = null;
michael@0 40
michael@0 41 this.isTesting_ = false;
michael@0 42
michael@0 43 this.tablesData = {};
michael@0 44
michael@0 45 this.observerServiceObserver_ = new G_ObserverServiceObserver(
michael@0 46 'quit-application',
michael@0 47 BindToObject(this.shutdown_, this),
michael@0 48 true /*only once*/);
michael@0 49
michael@0 50 this.cookieObserver_ = new G_ObserverServiceObserver(
michael@0 51 'cookie-changed',
michael@0 52 BindToObject(this.cookieChanged_, this),
michael@0 53 false);
michael@0 54
michael@0 55 /* Backoff interval should be between 30 and 60 minutes. */
michael@0 56 var backoffInterval = 30 * 60 * 1000;
michael@0 57 backoffInterval += Math.floor(Math.random() * (30 * 60 * 1000));
michael@0 58
michael@0 59 this.requestBackoff_ = new RequestBackoff(2 /* max errors */,
michael@0 60 60*1000 /* retry interval, 1 min */,
michael@0 61 4 /* num requests */,
michael@0 62 60*60*1000 /* request time, 60 min */,
michael@0 63 backoffInterval /* backoff interval, 60 min */,
michael@0 64 8*60*60*1000 /* max backoff, 8hr */);
michael@0 65
michael@0 66 this.dbService_ = Cc["@mozilla.org/url-classifier/dbservice;1"]
michael@0 67 .getService(Ci.nsIUrlClassifierDBService);
michael@0 68
michael@0 69 this.hashCompleter_ = Cc["@mozilla.org/url-classifier/hashcompleter;1"]
michael@0 70 .getService(Ci.nsIUrlClassifierHashCompleter);
michael@0 71 }
michael@0 72
michael@0 73 /**
michael@0 74 * xpcom-shutdown callback
michael@0 75 * Delete all of our data tables which seem to leak otherwise.
michael@0 76 */
michael@0 77 PROT_ListManager.prototype.shutdown_ = function() {
michael@0 78 for (var name in this.tablesData) {
michael@0 79 delete this.tablesData[name];
michael@0 80 }
michael@0 81 }
michael@0 82
michael@0 83 /**
michael@0 84 * Set the url we check for updates. If the new url is valid and different,
michael@0 85 * update our table list.
michael@0 86 *
michael@0 87 * After setting the update url, the caller is responsible for registering
michael@0 88 * tables and then toggling update checking. All the code for this logic is
michael@0 89 * currently in browser/components/safebrowsing. Maybe it should be part of
michael@0 90 * the listmanger?
michael@0 91 */
michael@0 92 PROT_ListManager.prototype.setUpdateUrl = function(url) {
michael@0 93 G_Debug(this, "Set update url: " + url);
michael@0 94 if (url != this.updateserverURL_) {
michael@0 95 this.updateserverURL_ = url;
michael@0 96 this.requestBackoff_.reset();
michael@0 97
michael@0 98 // Remove old tables which probably aren't valid for the new provider.
michael@0 99 for (var name in this.tablesData) {
michael@0 100 delete this.tablesData[name];
michael@0 101 }
michael@0 102 }
michael@0 103 }
michael@0 104
michael@0 105 /**
michael@0 106 * Set the gethash url.
michael@0 107 */
michael@0 108 PROT_ListManager.prototype.setGethashUrl = function(url) {
michael@0 109 G_Debug(this, "Set gethash url: " + url);
michael@0 110 if (url != this.gethashURL_) {
michael@0 111 this.gethashURL_ = url;
michael@0 112 this.hashCompleter_.gethashUrl = url;
michael@0 113 }
michael@0 114 }
michael@0 115
michael@0 116 /**
michael@0 117 * Register a new table table
michael@0 118 * @param tableName - the name of the table
michael@0 119 * @param opt_requireMac true if a mac is required on update, false otherwise
michael@0 120 * @returns true if the table could be created; false otherwise
michael@0 121 */
michael@0 122 PROT_ListManager.prototype.registerTable = function(tableName,
michael@0 123 opt_requireMac) {
michael@0 124 this.tablesData[tableName] = {};
michael@0 125 this.tablesData[tableName].needsUpdate = false;
michael@0 126
michael@0 127 return true;
michael@0 128 }
michael@0 129
michael@0 130 /**
michael@0 131 * Enable updates for some tables
michael@0 132 * @param tables - an array of table names that need updating
michael@0 133 */
michael@0 134 PROT_ListManager.prototype.enableUpdate = function(tableName) {
michael@0 135 var changed = false;
michael@0 136 var table = this.tablesData[tableName];
michael@0 137 if (table) {
michael@0 138 G_Debug(this, "Enabling table updates for " + tableName);
michael@0 139 table.needsUpdate = true;
michael@0 140 changed = true;
michael@0 141 }
michael@0 142
michael@0 143 if (changed === true)
michael@0 144 this.maybeToggleUpdateChecking();
michael@0 145 }
michael@0 146
michael@0 147 /**
michael@0 148 * Disables updates for some tables
michael@0 149 * @param tables - an array of table names that no longer need updating
michael@0 150 */
michael@0 151 PROT_ListManager.prototype.disableUpdate = function(tableName) {
michael@0 152 var changed = false;
michael@0 153 var table = this.tablesData[tableName];
michael@0 154 if (table) {
michael@0 155 G_Debug(this, "Disabling table updates for " + tableName);
michael@0 156 table.needsUpdate = false;
michael@0 157 changed = true;
michael@0 158 }
michael@0 159
michael@0 160 if (changed === true)
michael@0 161 this.maybeToggleUpdateChecking();
michael@0 162 }
michael@0 163
michael@0 164 /**
michael@0 165 * Determine if we have some tables that need updating.
michael@0 166 */
michael@0 167 PROT_ListManager.prototype.requireTableUpdates = function() {
michael@0 168 for (var type in this.tablesData) {
michael@0 169 // Tables that need updating even if other tables dont require it
michael@0 170 if (this.tablesData[type].needsUpdate)
michael@0 171 return true;
michael@0 172 }
michael@0 173
michael@0 174 return false;
michael@0 175 }
michael@0 176
michael@0 177 /**
michael@0 178 * Start managing the lists we know about. We don't do this automatically
michael@0 179 * when the listmanager is instantiated because their profile directory
michael@0 180 * (where we store the lists) might not be available.
michael@0 181 */
michael@0 182 PROT_ListManager.prototype.maybeStartManagingUpdates = function() {
michael@0 183 if (this.isTesting_)
michael@0 184 return;
michael@0 185
michael@0 186 // We might have been told about tables already, so see if we should be
michael@0 187 // actually updating.
michael@0 188 this.maybeToggleUpdateChecking();
michael@0 189 }
michael@0 190
michael@0 191 /**
michael@0 192 * Acts as a nsIUrlClassifierCallback for getTables.
michael@0 193 */
michael@0 194 PROT_ListManager.prototype.kickoffUpdate_ = function (onDiskTableData)
michael@0 195 {
michael@0 196 this.startingUpdate_ = false;
michael@0 197 var initialUpdateDelay = 3000;
michael@0 198
michael@0 199 // Check if any table registered for updates has ever been downloaded.
michael@0 200 var diskTablesAreUpdating = false;
michael@0 201 for (var tableName in this.tablesData) {
michael@0 202 if (this.tablesData[tableName].needsUpdate) {
michael@0 203 if (onDiskTableData.indexOf(tableName) != -1) {
michael@0 204 diskTablesAreUpdating = true;
michael@0 205 }
michael@0 206 }
michael@0 207 }
michael@0 208
michael@0 209 // If the user has never downloaded tables, do the check now.
michael@0 210 // If the user has tables, add a fuzz of a few minutes.
michael@0 211 if (diskTablesAreUpdating) {
michael@0 212 // Add a fuzz of 0-5 minutes.
michael@0 213 initialUpdateDelay += Math.floor(Math.random() * (5 * 60 * 1000));
michael@0 214 }
michael@0 215
michael@0 216 this.currentUpdateChecker_ =
michael@0 217 new G_Alarm(BindToObject(this.checkForUpdates, this),
michael@0 218 initialUpdateDelay);
michael@0 219 }
michael@0 220
michael@0 221 /**
michael@0 222 * Determine if we have any tables that require updating. Different
michael@0 223 * Wardens may call us with new tables that need to be updated.
michael@0 224 */
michael@0 225 PROT_ListManager.prototype.maybeToggleUpdateChecking = function() {
michael@0 226 // If we are testing or dont have an application directory yet, we should
michael@0 227 // not start reading tables from disk or schedule remote updates
michael@0 228 if (this.isTesting_)
michael@0 229 return;
michael@0 230
michael@0 231 // We update tables if we have some tables that want updates. If there
michael@0 232 // are no tables that want to be updated - we dont need to check anything.
michael@0 233 if (this.requireTableUpdates() === true) {
michael@0 234 G_Debug(this, "Starting managing lists");
michael@0 235 this.startUpdateChecker();
michael@0 236
michael@0 237 // Multiple warden can ask us to reenable updates at the same time, but we
michael@0 238 // really just need to schedule a single update.
michael@0 239 if (!this.currentUpdateChecker && !this.startingUpdate_) {
michael@0 240 this.startingUpdate_ = true;
michael@0 241 // check the current state of tables in the database
michael@0 242 this.dbService_.getTables(BindToObject(this.kickoffUpdate_, this));
michael@0 243 }
michael@0 244 } else {
michael@0 245 G_Debug(this, "Stopping managing lists (if currently active)");
michael@0 246 this.stopUpdateChecker(); // Cancel pending updates
michael@0 247 }
michael@0 248 }
michael@0 249
michael@0 250 /**
michael@0 251 * Start periodic checks for updates. Idempotent.
michael@0 252 * We want to distribute update checks evenly across the update period (an
michael@0 253 * hour). The first update is scheduled for a random time between 0.5 and 1.5
michael@0 254 * times the update interval.
michael@0 255 */
michael@0 256 PROT_ListManager.prototype.startUpdateChecker = function() {
michael@0 257 this.stopUpdateChecker();
michael@0 258
michael@0 259 // Schedule the first check for between 15 and 45 minutes.
michael@0 260 var repeatingUpdateDelay = this.updateInterval / 2;
michael@0 261 repeatingUpdateDelay += Math.floor(Math.random() * this.updateInterval);
michael@0 262 this.updateChecker_ = new G_Alarm(BindToObject(this.initialUpdateCheck_,
michael@0 263 this),
michael@0 264 repeatingUpdateDelay);
michael@0 265 }
michael@0 266
michael@0 267 /**
michael@0 268 * Callback for the first update check.
michael@0 269 * We go ahead and check for table updates, then start a regular timer (once
michael@0 270 * every update interval).
michael@0 271 */
michael@0 272 PROT_ListManager.prototype.initialUpdateCheck_ = function() {
michael@0 273 this.checkForUpdates();
michael@0 274 this.updateChecker_ = new G_Alarm(BindToObject(this.checkForUpdates, this),
michael@0 275 this.updateInterval, true /* repeat */);
michael@0 276 }
michael@0 277
michael@0 278 /**
michael@0 279 * Stop checking for updates. Idempotent.
michael@0 280 */
michael@0 281 PROT_ListManager.prototype.stopUpdateChecker = function() {
michael@0 282 if (this.updateChecker_) {
michael@0 283 this.updateChecker_.cancel();
michael@0 284 this.updateChecker_ = null;
michael@0 285 }
michael@0 286 // Cancel the oneoff check from maybeToggleUpdateChecking.
michael@0 287 if (this.currentUpdateChecker_) {
michael@0 288 this.currentUpdateChecker_.cancel();
michael@0 289 this.currentUpdateChecker_ = null;
michael@0 290 }
michael@0 291 }
michael@0 292
michael@0 293 /**
michael@0 294 * Provides an exception free way to look up the data in a table. We
michael@0 295 * use this because at certain points our tables might not be loaded,
michael@0 296 * and querying them could throw.
michael@0 297 *
michael@0 298 * @param table String Name of the table that we want to consult
michael@0 299 * @param key Principal being used to lookup the database
michael@0 300 * @param callback nsIUrlListManagerCallback (ie., Function) given false or the
michael@0 301 * value in the table corresponding to key. If the table name does not
michael@0 302 * exist, we return false, too.
michael@0 303 */
michael@0 304 PROT_ListManager.prototype.safeLookup = function(key, callback) {
michael@0 305 try {
michael@0 306 G_Debug(this, "safeLookup: " + key);
michael@0 307 var cb = new QueryAdapter(callback);
michael@0 308 this.dbService_.lookup(key,
michael@0 309 BindToObject(cb.handleResponse, cb),
michael@0 310 true);
michael@0 311 } catch(e) {
michael@0 312 G_Debug(this, "safeLookup masked failure for key " + key + ": " + e);
michael@0 313 callback.handleEvent("");
michael@0 314 }
michael@0 315 }
michael@0 316
michael@0 317 /**
michael@0 318 * Updates our internal tables from the update server
michael@0 319 *
michael@0 320 * @returns true when a new request was scheduled, false if an old request
michael@0 321 * was still pending.
michael@0 322 */
michael@0 323 PROT_ListManager.prototype.checkForUpdates = function() {
michael@0 324 // Allow new updates to be scheduled from maybeToggleUpdateChecking()
michael@0 325 this.currentUpdateChecker_ = null;
michael@0 326
michael@0 327 if (!this.updateserverURL_) {
michael@0 328 G_Debug(this, 'checkForUpdates: no update server url');
michael@0 329 return false;
michael@0 330 }
michael@0 331
michael@0 332 // See if we've triggered the request backoff logic.
michael@0 333 if (!this.requestBackoff_.canMakeRequest())
michael@0 334 return false;
michael@0 335
michael@0 336 // Grab the current state of the tables from the database
michael@0 337 this.dbService_.getTables(BindToObject(this.makeUpdateRequest_, this));
michael@0 338 return true;
michael@0 339 }
michael@0 340
michael@0 341 /**
michael@0 342 * Method that fires the actual HTTP update request.
michael@0 343 * First we reset any tables that have disappeared.
michael@0 344 * @param tableData List of table data already in the database, in the form
michael@0 345 * tablename;<chunk ranges>\n
michael@0 346 */
michael@0 347 PROT_ListManager.prototype.makeUpdateRequest_ = function(tableData) {
michael@0 348 var tableList;
michael@0 349 var tableNames = {};
michael@0 350 for (var tableName in this.tablesData) {
michael@0 351 if (this.tablesData[tableName].needsUpdate)
michael@0 352 tableNames[tableName] = true;
michael@0 353 if (!tableList) {
michael@0 354 tableList = tableName;
michael@0 355 } else {
michael@0 356 tableList += "," + tableName;
michael@0 357 }
michael@0 358 }
michael@0 359
michael@0 360 var request = "";
michael@0 361
michael@0 362 // For each table already in the database, include the chunk data from
michael@0 363 // the database
michael@0 364 var lines = tableData.split("\n");
michael@0 365 for (var i = 0; i < lines.length; i++) {
michael@0 366 var fields = lines[i].split(";");
michael@0 367 if (tableNames[fields[0]]) {
michael@0 368 request += lines[i] + "\n";
michael@0 369 delete tableNames[fields[0]];
michael@0 370 }
michael@0 371 }
michael@0 372
michael@0 373 // For each requested table that didn't have chunk data in the database,
michael@0 374 // request it fresh
michael@0 375 for (var tableName in tableNames) {
michael@0 376 request += tableName + ";\n";
michael@0 377 }
michael@0 378
michael@0 379 G_Debug(this, 'checkForUpdates: scheduling request..');
michael@0 380 var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"]
michael@0 381 .getService(Ci.nsIUrlClassifierStreamUpdater);
michael@0 382 try {
michael@0 383 streamer.updateUrl = this.updateserverURL_;
michael@0 384 } catch (e) {
michael@0 385 G_Debug(this, 'invalid url');
michael@0 386 return;
michael@0 387 }
michael@0 388
michael@0 389 this.requestBackoff_.noteRequest();
michael@0 390
michael@0 391 if (!streamer.downloadUpdates(tableList,
michael@0 392 request,
michael@0 393 BindToObject(this.updateSuccess_, this),
michael@0 394 BindToObject(this.updateError_, this),
michael@0 395 BindToObject(this.downloadError_, this))) {
michael@0 396 G_Debug(this, "pending update, wait until later");
michael@0 397 }
michael@0 398 }
michael@0 399
michael@0 400 /**
michael@0 401 * Callback function if the update request succeeded.
michael@0 402 * @param waitForUpdate String The number of seconds that the client should
michael@0 403 * wait before requesting again.
michael@0 404 */
michael@0 405 PROT_ListManager.prototype.updateSuccess_ = function(waitForUpdate) {
michael@0 406 G_Debug(this, "update success: " + waitForUpdate);
michael@0 407 if (waitForUpdate) {
michael@0 408 var delay = parseInt(waitForUpdate, 10);
michael@0 409 // As long as the delay is something sane (5 minutes or more), update
michael@0 410 // our delay time for requesting updates
michael@0 411 if (delay >= (5 * 60) && this.updateChecker_)
michael@0 412 this.updateChecker_.setDelay(delay * 1000);
michael@0 413 }
michael@0 414
michael@0 415 // Let the backoff object know that we completed successfully.
michael@0 416 this.requestBackoff_.noteServerResponse(200);
michael@0 417 }
michael@0 418
michael@0 419 /**
michael@0 420 * Callback function if the update request succeeded.
michael@0 421 * @param result String The error code of the failure
michael@0 422 */
michael@0 423 PROT_ListManager.prototype.updateError_ = function(result) {
michael@0 424 G_Debug(this, "update error: " + result);
michael@0 425 // XXX: there was some trouble applying the updates.
michael@0 426 }
michael@0 427
michael@0 428 /**
michael@0 429 * Callback function when the download failed
michael@0 430 * @param status String http status or an empty string if connection refused.
michael@0 431 */
michael@0 432 PROT_ListManager.prototype.downloadError_ = function(status) {
michael@0 433 G_Debug(this, "download error: " + status);
michael@0 434 // If status is empty, then we assume that we got an NS_CONNECTION_REFUSED
michael@0 435 // error. In this case, we treat this is a http 500 error.
michael@0 436 if (!status) {
michael@0 437 status = 500;
michael@0 438 }
michael@0 439 status = parseInt(status, 10);
michael@0 440 this.requestBackoff_.noteServerResponse(status);
michael@0 441
michael@0 442 if (this.requestBackoff_.isErrorStatus(status)) {
michael@0 443 // Schedule an update for when our backoff is complete
michael@0 444 this.currentUpdateChecker_ =
michael@0 445 new G_Alarm(BindToObject(this.checkForUpdates, this),
michael@0 446 this.requestBackoff_.nextRequestDelay());
michael@0 447 }
michael@0 448 }
michael@0 449
michael@0 450 /**
michael@0 451 * Called when cookies are cleared
michael@0 452 */
michael@0 453 PROT_ListManager.prototype.cookieChanged_ = function(subject, topic, data) {
michael@0 454 if (data != "cleared")
michael@0 455 return;
michael@0 456
michael@0 457 G_Debug(this, "cookies cleared");
michael@0 458 }
michael@0 459
michael@0 460 PROT_ListManager.prototype.QueryInterface = function(iid) {
michael@0 461 if (iid.equals(Ci.nsISupports) ||
michael@0 462 iid.equals(Ci.nsIUrlListManager) ||
michael@0 463 iid.equals(Ci.nsITimerCallback))
michael@0 464 return this;
michael@0 465
michael@0 466 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 467 }

mercurial