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

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/url-classifier/content/listmanager.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,467 @@
     1.4 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.7 +
     1.8 +
     1.9 +// This is the only implementation of nsIUrlListManager.
    1.10 +// A class that manages lists, namely white and black lists for
    1.11 +// phishing or malware protection. The ListManager knows how to fetch,
    1.12 +// update, and store lists.
    1.13 +//
    1.14 +// There is a single listmanager for the whole application.
    1.15 +//
    1.16 +// TODO more comprehensive update tests, for example add unittest check 
    1.17 +//      that the listmanagers tables are properly written on updates
    1.18 +
    1.19 +function QueryAdapter(callback) {
    1.20 +  this.callback_ = callback;
    1.21 +};
    1.22 +
    1.23 +QueryAdapter.prototype.handleResponse = function(value) {
    1.24 +  this.callback_.handleEvent(value);
    1.25 +}
    1.26 +
    1.27 +/**
    1.28 + * A ListManager keeps track of black and white lists and knows
    1.29 + * how to update them.
    1.30 + *
    1.31 + * @constructor
    1.32 + */
    1.33 +function PROT_ListManager() {
    1.34 +  this.debugZone = "listmanager";
    1.35 +  G_debugService.enableZone(this.debugZone);
    1.36 +
    1.37 +  this.currentUpdateChecker_ = null;   // set when we toggle updates
    1.38 +  this.prefs_ = new G_Preferences();
    1.39 +  this.updateInterval = this.prefs_.getPref("urlclassifier.updateinterval", 30 * 60) * 1000;
    1.40 +
    1.41 +  this.updateserverURL_ = null;
    1.42 +  this.gethashURL_ = null;
    1.43 +
    1.44 +  this.isTesting_ = false;
    1.45 +
    1.46 +  this.tablesData = {};
    1.47 +
    1.48 +  this.observerServiceObserver_ = new G_ObserverServiceObserver(
    1.49 +                                          'quit-application',
    1.50 +                                          BindToObject(this.shutdown_, this),
    1.51 +                                          true /*only once*/);
    1.52 +
    1.53 +  this.cookieObserver_ = new G_ObserverServiceObserver(
    1.54 +                                          'cookie-changed',
    1.55 +                                          BindToObject(this.cookieChanged_, this),
    1.56 +                                          false);
    1.57 +
    1.58 +  /* Backoff interval should be between 30 and 60 minutes. */
    1.59 +  var backoffInterval = 30 * 60 * 1000;
    1.60 +  backoffInterval += Math.floor(Math.random() * (30 * 60 * 1000));
    1.61 +
    1.62 +  this.requestBackoff_ = new RequestBackoff(2 /* max errors */,
    1.63 +                                      60*1000 /* retry interval, 1 min */,
    1.64 +                                            4 /* num requests */,
    1.65 +                                   60*60*1000 /* request time, 60 min */,
    1.66 +                              backoffInterval /* backoff interval, 60 min */,
    1.67 +                                 8*60*60*1000 /* max backoff, 8hr */);
    1.68 +
    1.69 +  this.dbService_ = Cc["@mozilla.org/url-classifier/dbservice;1"]
    1.70 +                   .getService(Ci.nsIUrlClassifierDBService);
    1.71 +
    1.72 +  this.hashCompleter_ = Cc["@mozilla.org/url-classifier/hashcompleter;1"]
    1.73 +                        .getService(Ci.nsIUrlClassifierHashCompleter);
    1.74 +}
    1.75 +
    1.76 +/**
    1.77 + * xpcom-shutdown callback
    1.78 + * Delete all of our data tables which seem to leak otherwise.
    1.79 + */
    1.80 +PROT_ListManager.prototype.shutdown_ = function() {
    1.81 +  for (var name in this.tablesData) {
    1.82 +    delete this.tablesData[name];
    1.83 +  }
    1.84 +}
    1.85 +
    1.86 +/**
    1.87 + * Set the url we check for updates.  If the new url is valid and different,
    1.88 + * update our table list.
    1.89 + * 
    1.90 + * After setting the update url, the caller is responsible for registering
    1.91 + * tables and then toggling update checking.  All the code for this logic is
    1.92 + * currently in browser/components/safebrowsing.  Maybe it should be part of
    1.93 + * the listmanger?
    1.94 + */
    1.95 +PROT_ListManager.prototype.setUpdateUrl = function(url) {
    1.96 +  G_Debug(this, "Set update url: " + url);
    1.97 +  if (url != this.updateserverURL_) {
    1.98 +    this.updateserverURL_ = url;
    1.99 +    this.requestBackoff_.reset();
   1.100 +    
   1.101 +    // Remove old tables which probably aren't valid for the new provider.
   1.102 +    for (var name in this.tablesData) {
   1.103 +      delete this.tablesData[name];
   1.104 +    }
   1.105 +  }
   1.106 +}
   1.107 +
   1.108 +/**
   1.109 + * Set the gethash url.
   1.110 + */
   1.111 +PROT_ListManager.prototype.setGethashUrl = function(url) {
   1.112 +  G_Debug(this, "Set gethash url: " + url);
   1.113 +  if (url != this.gethashURL_) {
   1.114 +    this.gethashURL_ = url;
   1.115 +    this.hashCompleter_.gethashUrl = url;
   1.116 +  }
   1.117 +}
   1.118 +
   1.119 +/**
   1.120 + * Register a new table table
   1.121 + * @param tableName - the name of the table
   1.122 + * @param opt_requireMac true if a mac is required on update, false otherwise
   1.123 + * @returns true if the table could be created; false otherwise
   1.124 + */
   1.125 +PROT_ListManager.prototype.registerTable = function(tableName, 
   1.126 +                                                    opt_requireMac) {
   1.127 +  this.tablesData[tableName] = {};
   1.128 +  this.tablesData[tableName].needsUpdate = false;
   1.129 +
   1.130 +  return true;
   1.131 +}
   1.132 +
   1.133 +/**
   1.134 + * Enable updates for some tables
   1.135 + * @param tables - an array of table names that need updating
   1.136 + */
   1.137 +PROT_ListManager.prototype.enableUpdate = function(tableName) {
   1.138 +  var changed = false;
   1.139 +  var table = this.tablesData[tableName];
   1.140 +  if (table) {
   1.141 +    G_Debug(this, "Enabling table updates for " + tableName);
   1.142 +    table.needsUpdate = true;
   1.143 +    changed = true;
   1.144 +  }
   1.145 +
   1.146 +  if (changed === true)
   1.147 +    this.maybeToggleUpdateChecking();
   1.148 +}
   1.149 +
   1.150 +/**
   1.151 + * Disables updates for some tables
   1.152 + * @param tables - an array of table names that no longer need updating
   1.153 + */
   1.154 +PROT_ListManager.prototype.disableUpdate = function(tableName) {
   1.155 +  var changed = false;
   1.156 +  var table = this.tablesData[tableName];
   1.157 +  if (table) {
   1.158 +    G_Debug(this, "Disabling table updates for " + tableName);
   1.159 +    table.needsUpdate = false;
   1.160 +    changed = true;
   1.161 +  }
   1.162 +
   1.163 +  if (changed === true)
   1.164 +    this.maybeToggleUpdateChecking();
   1.165 +}
   1.166 +
   1.167 +/**
   1.168 + * Determine if we have some tables that need updating.
   1.169 + */
   1.170 +PROT_ListManager.prototype.requireTableUpdates = function() {
   1.171 +  for (var type in this.tablesData) {
   1.172 +    // Tables that need updating even if other tables dont require it
   1.173 +    if (this.tablesData[type].needsUpdate)
   1.174 +      return true;
   1.175 +  }
   1.176 +
   1.177 +  return false;
   1.178 +}
   1.179 +
   1.180 +/**
   1.181 + * Start managing the lists we know about. We don't do this automatically
   1.182 + * when the listmanager is instantiated because their profile directory
   1.183 + * (where we store the lists) might not be available.
   1.184 + */
   1.185 +PROT_ListManager.prototype.maybeStartManagingUpdates = function() {
   1.186 +  if (this.isTesting_)
   1.187 +    return;
   1.188 +
   1.189 +  // We might have been told about tables already, so see if we should be
   1.190 +  // actually updating.
   1.191 +  this.maybeToggleUpdateChecking();
   1.192 +}
   1.193 +
   1.194 +/**
   1.195 + * Acts as a nsIUrlClassifierCallback for getTables.
   1.196 + */
   1.197 +PROT_ListManager.prototype.kickoffUpdate_ = function (onDiskTableData)
   1.198 +{
   1.199 +  this.startingUpdate_ = false;
   1.200 +  var initialUpdateDelay = 3000;
   1.201 +
   1.202 +  // Check if any table registered for updates has ever been downloaded.
   1.203 +  var diskTablesAreUpdating = false;
   1.204 +  for (var tableName in this.tablesData) {
   1.205 +    if (this.tablesData[tableName].needsUpdate) {
   1.206 +      if (onDiskTableData.indexOf(tableName) != -1) {
   1.207 +        diskTablesAreUpdating = true;
   1.208 +      }
   1.209 +    }
   1.210 +  }
   1.211 +
   1.212 +  // If the user has never downloaded tables, do the check now.
   1.213 +  // If the user has tables, add a fuzz of a few minutes.
   1.214 +  if (diskTablesAreUpdating) {
   1.215 +    // Add a fuzz of 0-5 minutes.
   1.216 +    initialUpdateDelay += Math.floor(Math.random() * (5 * 60 * 1000));
   1.217 +  }
   1.218 +
   1.219 +  this.currentUpdateChecker_ =
   1.220 +    new G_Alarm(BindToObject(this.checkForUpdates, this),
   1.221 +                initialUpdateDelay);
   1.222 +}
   1.223 +
   1.224 +/**
   1.225 + * Determine if we have any tables that require updating.  Different
   1.226 + * Wardens may call us with new tables that need to be updated.
   1.227 + */ 
   1.228 +PROT_ListManager.prototype.maybeToggleUpdateChecking = function() {
   1.229 +  // If we are testing or dont have an application directory yet, we should
   1.230 +  // not start reading tables from disk or schedule remote updates
   1.231 +  if (this.isTesting_)
   1.232 +    return;
   1.233 +
   1.234 +  // We update tables if we have some tables that want updates.  If there
   1.235 +  // are no tables that want to be updated - we dont need to check anything.
   1.236 +  if (this.requireTableUpdates() === true) {
   1.237 +    G_Debug(this, "Starting managing lists");
   1.238 +    this.startUpdateChecker();
   1.239 +
   1.240 +    // Multiple warden can ask us to reenable updates at the same time, but we
   1.241 +    // really just need to schedule a single update.
   1.242 +    if (!this.currentUpdateChecker && !this.startingUpdate_) {
   1.243 +      this.startingUpdate_ = true;
   1.244 +      // check the current state of tables in the database
   1.245 +      this.dbService_.getTables(BindToObject(this.kickoffUpdate_, this));
   1.246 +    }
   1.247 +  } else {
   1.248 +    G_Debug(this, "Stopping managing lists (if currently active)");
   1.249 +    this.stopUpdateChecker();                    // Cancel pending updates
   1.250 +  }
   1.251 +}
   1.252 +
   1.253 +/**
   1.254 + * Start periodic checks for updates. Idempotent.
   1.255 + * We want to distribute update checks evenly across the update period (an
   1.256 + * hour).  The first update is scheduled for a random time between 0.5 and 1.5
   1.257 + * times the update interval.
   1.258 + */
   1.259 +PROT_ListManager.prototype.startUpdateChecker = function() {
   1.260 +  this.stopUpdateChecker();
   1.261 +
   1.262 +  // Schedule the first check for between 15 and 45 minutes.
   1.263 +  var repeatingUpdateDelay = this.updateInterval / 2;
   1.264 +  repeatingUpdateDelay += Math.floor(Math.random() * this.updateInterval);
   1.265 +  this.updateChecker_ = new G_Alarm(BindToObject(this.initialUpdateCheck_,
   1.266 +                                                 this),
   1.267 +                                    repeatingUpdateDelay);
   1.268 +}
   1.269 +
   1.270 +/**
   1.271 + * Callback for the first update check.
   1.272 + * We go ahead and check for table updates, then start a regular timer (once
   1.273 + * every update interval).
   1.274 + */
   1.275 +PROT_ListManager.prototype.initialUpdateCheck_ = function() {
   1.276 +  this.checkForUpdates();
   1.277 +  this.updateChecker_ = new G_Alarm(BindToObject(this.checkForUpdates, this), 
   1.278 +                                    this.updateInterval, true /* repeat */);
   1.279 +}
   1.280 +
   1.281 +/**
   1.282 + * Stop checking for updates. Idempotent.
   1.283 + */
   1.284 +PROT_ListManager.prototype.stopUpdateChecker = function() {
   1.285 +  if (this.updateChecker_) {
   1.286 +    this.updateChecker_.cancel();
   1.287 +    this.updateChecker_ = null;
   1.288 +  }
   1.289 +  // Cancel the oneoff check from maybeToggleUpdateChecking.
   1.290 +  if (this.currentUpdateChecker_) {
   1.291 +    this.currentUpdateChecker_.cancel();
   1.292 +    this.currentUpdateChecker_ = null;
   1.293 +  }
   1.294 +}
   1.295 +
   1.296 +/**
   1.297 + * Provides an exception free way to look up the data in a table. We
   1.298 + * use this because at certain points our tables might not be loaded,
   1.299 + * and querying them could throw.
   1.300 + *
   1.301 + * @param table String Name of the table that we want to consult
   1.302 + * @param key Principal being used to lookup the database
   1.303 + * @param callback nsIUrlListManagerCallback (ie., Function) given false or the
   1.304 + *        value in the table corresponding to key.  If the table name does not
   1.305 + *        exist, we return false, too.
   1.306 + */
   1.307 +PROT_ListManager.prototype.safeLookup = function(key, callback) {
   1.308 +  try {
   1.309 +    G_Debug(this, "safeLookup: " + key);
   1.310 +    var cb = new QueryAdapter(callback);
   1.311 +    this.dbService_.lookup(key,
   1.312 +                           BindToObject(cb.handleResponse, cb),
   1.313 +                           true);
   1.314 +  } catch(e) {
   1.315 +    G_Debug(this, "safeLookup masked failure for key " + key + ": " + e);
   1.316 +    callback.handleEvent("");
   1.317 +  }
   1.318 +}
   1.319 +
   1.320 +/**
   1.321 + * Updates our internal tables from the update server
   1.322 + *
   1.323 + * @returns true when a new request was scheduled, false if an old request
   1.324 + *          was still pending.
   1.325 + */
   1.326 +PROT_ListManager.prototype.checkForUpdates = function() {
   1.327 +  // Allow new updates to be scheduled from maybeToggleUpdateChecking()
   1.328 +  this.currentUpdateChecker_ = null;
   1.329 +
   1.330 +  if (!this.updateserverURL_) {
   1.331 +    G_Debug(this, 'checkForUpdates: no update server url');
   1.332 +    return false;
   1.333 +  }
   1.334 +
   1.335 +  // See if we've triggered the request backoff logic.
   1.336 +  if (!this.requestBackoff_.canMakeRequest())
   1.337 +    return false;
   1.338 +
   1.339 +  // Grab the current state of the tables from the database
   1.340 +  this.dbService_.getTables(BindToObject(this.makeUpdateRequest_, this));
   1.341 +  return true;
   1.342 +}
   1.343 +
   1.344 +/**
   1.345 + * Method that fires the actual HTTP update request.
   1.346 + * First we reset any tables that have disappeared.
   1.347 + * @param tableData List of table data already in the database, in the form
   1.348 + *        tablename;<chunk ranges>\n
   1.349 + */
   1.350 +PROT_ListManager.prototype.makeUpdateRequest_ = function(tableData) {
   1.351 +  var tableList;
   1.352 +  var tableNames = {};
   1.353 +  for (var tableName in this.tablesData) {
   1.354 +    if (this.tablesData[tableName].needsUpdate)
   1.355 +      tableNames[tableName] = true;
   1.356 +    if (!tableList) {
   1.357 +      tableList = tableName;
   1.358 +    } else {
   1.359 +      tableList += "," + tableName;
   1.360 +    }
   1.361 +  }
   1.362 +
   1.363 +  var request = "";
   1.364 +
   1.365 +  // For each table already in the database, include the chunk data from
   1.366 +  // the database
   1.367 +  var lines = tableData.split("\n");
   1.368 +  for (var i = 0; i < lines.length; i++) {
   1.369 +    var fields = lines[i].split(";");
   1.370 +    if (tableNames[fields[0]]) {
   1.371 +      request += lines[i] + "\n";
   1.372 +      delete tableNames[fields[0]];
   1.373 +    }
   1.374 +  }
   1.375 +
   1.376 +  // For each requested table that didn't have chunk data in the database,
   1.377 +  // request it fresh
   1.378 +  for (var tableName in tableNames) {
   1.379 +    request += tableName + ";\n";
   1.380 +  }
   1.381 +
   1.382 +  G_Debug(this, 'checkForUpdates: scheduling request..');
   1.383 +  var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"]
   1.384 +                 .getService(Ci.nsIUrlClassifierStreamUpdater);
   1.385 +  try {
   1.386 +    streamer.updateUrl = this.updateserverURL_;
   1.387 +  } catch (e) {
   1.388 +    G_Debug(this, 'invalid url');
   1.389 +    return;
   1.390 +  }
   1.391 +
   1.392 +  this.requestBackoff_.noteRequest();
   1.393 +
   1.394 +  if (!streamer.downloadUpdates(tableList,
   1.395 +                                request,
   1.396 +                                BindToObject(this.updateSuccess_, this),
   1.397 +                                BindToObject(this.updateError_, this),
   1.398 +                                BindToObject(this.downloadError_, this))) {
   1.399 +    G_Debug(this, "pending update, wait until later");
   1.400 +  }
   1.401 +}
   1.402 +
   1.403 +/**
   1.404 + * Callback function if the update request succeeded.
   1.405 + * @param waitForUpdate String The number of seconds that the client should
   1.406 + *        wait before requesting again.
   1.407 + */
   1.408 +PROT_ListManager.prototype.updateSuccess_ = function(waitForUpdate) {
   1.409 +  G_Debug(this, "update success: " + waitForUpdate);
   1.410 +  if (waitForUpdate) {
   1.411 +    var delay = parseInt(waitForUpdate, 10);
   1.412 +    // As long as the delay is something sane (5 minutes or more), update
   1.413 +    // our delay time for requesting updates
   1.414 +    if (delay >= (5 * 60) && this.updateChecker_)
   1.415 +      this.updateChecker_.setDelay(delay * 1000);
   1.416 +  }
   1.417 +
   1.418 +  // Let the backoff object know that we completed successfully.
   1.419 +  this.requestBackoff_.noteServerResponse(200);
   1.420 +}
   1.421 +
   1.422 +/**
   1.423 + * Callback function if the update request succeeded.
   1.424 + * @param result String The error code of the failure
   1.425 + */
   1.426 +PROT_ListManager.prototype.updateError_ = function(result) {
   1.427 +  G_Debug(this, "update error: " + result);
   1.428 +  // XXX: there was some trouble applying the updates.
   1.429 +}
   1.430 +
   1.431 +/**
   1.432 + * Callback function when the download failed
   1.433 + * @param status String http status or an empty string if connection refused.
   1.434 + */
   1.435 +PROT_ListManager.prototype.downloadError_ = function(status) {
   1.436 +  G_Debug(this, "download error: " + status);
   1.437 +  // If status is empty, then we assume that we got an NS_CONNECTION_REFUSED
   1.438 +  // error.  In this case, we treat this is a http 500 error.
   1.439 +  if (!status) {
   1.440 +    status = 500;
   1.441 +  }
   1.442 +  status = parseInt(status, 10);
   1.443 +  this.requestBackoff_.noteServerResponse(status);
   1.444 +
   1.445 +  if (this.requestBackoff_.isErrorStatus(status)) {
   1.446 +    // Schedule an update for when our backoff is complete
   1.447 +    this.currentUpdateChecker_ =
   1.448 +      new G_Alarm(BindToObject(this.checkForUpdates, this),
   1.449 +                  this.requestBackoff_.nextRequestDelay());
   1.450 +  }
   1.451 +}
   1.452 +
   1.453 +/**
   1.454 + * Called when cookies are cleared
   1.455 + */
   1.456 +PROT_ListManager.prototype.cookieChanged_ = function(subject, topic, data) {
   1.457 +  if (data != "cleared")
   1.458 +    return;
   1.459 +
   1.460 +  G_Debug(this, "cookies cleared");
   1.461 +}
   1.462 +
   1.463 +PROT_ListManager.prototype.QueryInterface = function(iid) {
   1.464 +  if (iid.equals(Ci.nsISupports) ||
   1.465 +      iid.equals(Ci.nsIUrlListManager) ||
   1.466 +      iid.equals(Ci.nsITimerCallback))
   1.467 +    return this;
   1.468 +
   1.469 +  throw Components.results.NS_ERROR_NO_INTERFACE;
   1.470 +}

mercurial