michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: michael@0: // This is the only implementation of nsIUrlListManager. michael@0: // A class that manages lists, namely white and black lists for michael@0: // phishing or malware protection. The ListManager knows how to fetch, michael@0: // update, and store lists. michael@0: // michael@0: // There is a single listmanager for the whole application. michael@0: // michael@0: // TODO more comprehensive update tests, for example add unittest check michael@0: // that the listmanagers tables are properly written on updates michael@0: michael@0: function QueryAdapter(callback) { michael@0: this.callback_ = callback; michael@0: }; michael@0: michael@0: QueryAdapter.prototype.handleResponse = function(value) { michael@0: this.callback_.handleEvent(value); michael@0: } michael@0: michael@0: /** michael@0: * A ListManager keeps track of black and white lists and knows michael@0: * how to update them. michael@0: * michael@0: * @constructor michael@0: */ michael@0: function PROT_ListManager() { michael@0: this.debugZone = "listmanager"; michael@0: G_debugService.enableZone(this.debugZone); michael@0: michael@0: this.currentUpdateChecker_ = null; // set when we toggle updates michael@0: this.prefs_ = new G_Preferences(); michael@0: this.updateInterval = this.prefs_.getPref("urlclassifier.updateinterval", 30 * 60) * 1000; michael@0: michael@0: this.updateserverURL_ = null; michael@0: this.gethashURL_ = null; michael@0: michael@0: this.isTesting_ = false; michael@0: michael@0: this.tablesData = {}; michael@0: michael@0: this.observerServiceObserver_ = new G_ObserverServiceObserver( michael@0: 'quit-application', michael@0: BindToObject(this.shutdown_, this), michael@0: true /*only once*/); michael@0: michael@0: this.cookieObserver_ = new G_ObserverServiceObserver( michael@0: 'cookie-changed', michael@0: BindToObject(this.cookieChanged_, this), michael@0: false); michael@0: michael@0: /* Backoff interval should be between 30 and 60 minutes. */ michael@0: var backoffInterval = 30 * 60 * 1000; michael@0: backoffInterval += Math.floor(Math.random() * (30 * 60 * 1000)); michael@0: michael@0: this.requestBackoff_ = new RequestBackoff(2 /* max errors */, michael@0: 60*1000 /* retry interval, 1 min */, michael@0: 4 /* num requests */, michael@0: 60*60*1000 /* request time, 60 min */, michael@0: backoffInterval /* backoff interval, 60 min */, michael@0: 8*60*60*1000 /* max backoff, 8hr */); michael@0: michael@0: this.dbService_ = Cc["@mozilla.org/url-classifier/dbservice;1"] michael@0: .getService(Ci.nsIUrlClassifierDBService); michael@0: michael@0: this.hashCompleter_ = Cc["@mozilla.org/url-classifier/hashcompleter;1"] michael@0: .getService(Ci.nsIUrlClassifierHashCompleter); michael@0: } michael@0: michael@0: /** michael@0: * xpcom-shutdown callback michael@0: * Delete all of our data tables which seem to leak otherwise. michael@0: */ michael@0: PROT_ListManager.prototype.shutdown_ = function() { michael@0: for (var name in this.tablesData) { michael@0: delete this.tablesData[name]; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Set the url we check for updates. If the new url is valid and different, michael@0: * update our table list. michael@0: * michael@0: * After setting the update url, the caller is responsible for registering michael@0: * tables and then toggling update checking. All the code for this logic is michael@0: * currently in browser/components/safebrowsing. Maybe it should be part of michael@0: * the listmanger? michael@0: */ michael@0: PROT_ListManager.prototype.setUpdateUrl = function(url) { michael@0: G_Debug(this, "Set update url: " + url); michael@0: if (url != this.updateserverURL_) { michael@0: this.updateserverURL_ = url; michael@0: this.requestBackoff_.reset(); michael@0: michael@0: // Remove old tables which probably aren't valid for the new provider. michael@0: for (var name in this.tablesData) { michael@0: delete this.tablesData[name]; michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Set the gethash url. michael@0: */ michael@0: PROT_ListManager.prototype.setGethashUrl = function(url) { michael@0: G_Debug(this, "Set gethash url: " + url); michael@0: if (url != this.gethashURL_) { michael@0: this.gethashURL_ = url; michael@0: this.hashCompleter_.gethashUrl = url; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Register a new table table michael@0: * @param tableName - the name of the table michael@0: * @param opt_requireMac true if a mac is required on update, false otherwise michael@0: * @returns true if the table could be created; false otherwise michael@0: */ michael@0: PROT_ListManager.prototype.registerTable = function(tableName, michael@0: opt_requireMac) { michael@0: this.tablesData[tableName] = {}; michael@0: this.tablesData[tableName].needsUpdate = false; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: /** michael@0: * Enable updates for some tables michael@0: * @param tables - an array of table names that need updating michael@0: */ michael@0: PROT_ListManager.prototype.enableUpdate = function(tableName) { michael@0: var changed = false; michael@0: var table = this.tablesData[tableName]; michael@0: if (table) { michael@0: G_Debug(this, "Enabling table updates for " + tableName); michael@0: table.needsUpdate = true; michael@0: changed = true; michael@0: } michael@0: michael@0: if (changed === true) michael@0: this.maybeToggleUpdateChecking(); michael@0: } michael@0: michael@0: /** michael@0: * Disables updates for some tables michael@0: * @param tables - an array of table names that no longer need updating michael@0: */ michael@0: PROT_ListManager.prototype.disableUpdate = function(tableName) { michael@0: var changed = false; michael@0: var table = this.tablesData[tableName]; michael@0: if (table) { michael@0: G_Debug(this, "Disabling table updates for " + tableName); michael@0: table.needsUpdate = false; michael@0: changed = true; michael@0: } michael@0: michael@0: if (changed === true) michael@0: this.maybeToggleUpdateChecking(); michael@0: } michael@0: michael@0: /** michael@0: * Determine if we have some tables that need updating. michael@0: */ michael@0: PROT_ListManager.prototype.requireTableUpdates = function() { michael@0: for (var type in this.tablesData) { michael@0: // Tables that need updating even if other tables dont require it michael@0: if (this.tablesData[type].needsUpdate) michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Start managing the lists we know about. We don't do this automatically michael@0: * when the listmanager is instantiated because their profile directory michael@0: * (where we store the lists) might not be available. michael@0: */ michael@0: PROT_ListManager.prototype.maybeStartManagingUpdates = function() { michael@0: if (this.isTesting_) michael@0: return; michael@0: michael@0: // We might have been told about tables already, so see if we should be michael@0: // actually updating. michael@0: this.maybeToggleUpdateChecking(); michael@0: } michael@0: michael@0: /** michael@0: * Acts as a nsIUrlClassifierCallback for getTables. michael@0: */ michael@0: PROT_ListManager.prototype.kickoffUpdate_ = function (onDiskTableData) michael@0: { michael@0: this.startingUpdate_ = false; michael@0: var initialUpdateDelay = 3000; michael@0: michael@0: // Check if any table registered for updates has ever been downloaded. michael@0: var diskTablesAreUpdating = false; michael@0: for (var tableName in this.tablesData) { michael@0: if (this.tablesData[tableName].needsUpdate) { michael@0: if (onDiskTableData.indexOf(tableName) != -1) { michael@0: diskTablesAreUpdating = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // If the user has never downloaded tables, do the check now. michael@0: // If the user has tables, add a fuzz of a few minutes. michael@0: if (diskTablesAreUpdating) { michael@0: // Add a fuzz of 0-5 minutes. michael@0: initialUpdateDelay += Math.floor(Math.random() * (5 * 60 * 1000)); michael@0: } michael@0: michael@0: this.currentUpdateChecker_ = michael@0: new G_Alarm(BindToObject(this.checkForUpdates, this), michael@0: initialUpdateDelay); michael@0: } michael@0: michael@0: /** michael@0: * Determine if we have any tables that require updating. Different michael@0: * Wardens may call us with new tables that need to be updated. michael@0: */ michael@0: PROT_ListManager.prototype.maybeToggleUpdateChecking = function() { michael@0: // If we are testing or dont have an application directory yet, we should michael@0: // not start reading tables from disk or schedule remote updates michael@0: if (this.isTesting_) michael@0: return; michael@0: michael@0: // We update tables if we have some tables that want updates. If there michael@0: // are no tables that want to be updated - we dont need to check anything. michael@0: if (this.requireTableUpdates() === true) { michael@0: G_Debug(this, "Starting managing lists"); michael@0: this.startUpdateChecker(); michael@0: michael@0: // Multiple warden can ask us to reenable updates at the same time, but we michael@0: // really just need to schedule a single update. michael@0: if (!this.currentUpdateChecker && !this.startingUpdate_) { michael@0: this.startingUpdate_ = true; michael@0: // check the current state of tables in the database michael@0: this.dbService_.getTables(BindToObject(this.kickoffUpdate_, this)); michael@0: } michael@0: } else { michael@0: G_Debug(this, "Stopping managing lists (if currently active)"); michael@0: this.stopUpdateChecker(); // Cancel pending updates michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Start periodic checks for updates. Idempotent. michael@0: * We want to distribute update checks evenly across the update period (an michael@0: * hour). The first update is scheduled for a random time between 0.5 and 1.5 michael@0: * times the update interval. michael@0: */ michael@0: PROT_ListManager.prototype.startUpdateChecker = function() { michael@0: this.stopUpdateChecker(); michael@0: michael@0: // Schedule the first check for between 15 and 45 minutes. michael@0: var repeatingUpdateDelay = this.updateInterval / 2; michael@0: repeatingUpdateDelay += Math.floor(Math.random() * this.updateInterval); michael@0: this.updateChecker_ = new G_Alarm(BindToObject(this.initialUpdateCheck_, michael@0: this), michael@0: repeatingUpdateDelay); michael@0: } michael@0: michael@0: /** michael@0: * Callback for the first update check. michael@0: * We go ahead and check for table updates, then start a regular timer (once michael@0: * every update interval). michael@0: */ michael@0: PROT_ListManager.prototype.initialUpdateCheck_ = function() { michael@0: this.checkForUpdates(); michael@0: this.updateChecker_ = new G_Alarm(BindToObject(this.checkForUpdates, this), michael@0: this.updateInterval, true /* repeat */); michael@0: } michael@0: michael@0: /** michael@0: * Stop checking for updates. Idempotent. michael@0: */ michael@0: PROT_ListManager.prototype.stopUpdateChecker = function() { michael@0: if (this.updateChecker_) { michael@0: this.updateChecker_.cancel(); michael@0: this.updateChecker_ = null; michael@0: } michael@0: // Cancel the oneoff check from maybeToggleUpdateChecking. michael@0: if (this.currentUpdateChecker_) { michael@0: this.currentUpdateChecker_.cancel(); michael@0: this.currentUpdateChecker_ = null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Provides an exception free way to look up the data in a table. We michael@0: * use this because at certain points our tables might not be loaded, michael@0: * and querying them could throw. michael@0: * michael@0: * @param table String Name of the table that we want to consult michael@0: * @param key Principal being used to lookup the database michael@0: * @param callback nsIUrlListManagerCallback (ie., Function) given false or the michael@0: * value in the table corresponding to key. If the table name does not michael@0: * exist, we return false, too. michael@0: */ michael@0: PROT_ListManager.prototype.safeLookup = function(key, callback) { michael@0: try { michael@0: G_Debug(this, "safeLookup: " + key); michael@0: var cb = new QueryAdapter(callback); michael@0: this.dbService_.lookup(key, michael@0: BindToObject(cb.handleResponse, cb), michael@0: true); michael@0: } catch(e) { michael@0: G_Debug(this, "safeLookup masked failure for key " + key + ": " + e); michael@0: callback.handleEvent(""); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Updates our internal tables from the update server michael@0: * michael@0: * @returns true when a new request was scheduled, false if an old request michael@0: * was still pending. michael@0: */ michael@0: PROT_ListManager.prototype.checkForUpdates = function() { michael@0: // Allow new updates to be scheduled from maybeToggleUpdateChecking() michael@0: this.currentUpdateChecker_ = null; michael@0: michael@0: if (!this.updateserverURL_) { michael@0: G_Debug(this, 'checkForUpdates: no update server url'); michael@0: return false; michael@0: } michael@0: michael@0: // See if we've triggered the request backoff logic. michael@0: if (!this.requestBackoff_.canMakeRequest()) michael@0: return false; michael@0: michael@0: // Grab the current state of the tables from the database michael@0: this.dbService_.getTables(BindToObject(this.makeUpdateRequest_, this)); michael@0: return true; michael@0: } michael@0: michael@0: /** michael@0: * Method that fires the actual HTTP update request. michael@0: * First we reset any tables that have disappeared. michael@0: * @param tableData List of table data already in the database, in the form michael@0: * tablename;\n michael@0: */ michael@0: PROT_ListManager.prototype.makeUpdateRequest_ = function(tableData) { michael@0: var tableList; michael@0: var tableNames = {}; michael@0: for (var tableName in this.tablesData) { michael@0: if (this.tablesData[tableName].needsUpdate) michael@0: tableNames[tableName] = true; michael@0: if (!tableList) { michael@0: tableList = tableName; michael@0: } else { michael@0: tableList += "," + tableName; michael@0: } michael@0: } michael@0: michael@0: var request = ""; michael@0: michael@0: // For each table already in the database, include the chunk data from michael@0: // the database michael@0: var lines = tableData.split("\n"); michael@0: for (var i = 0; i < lines.length; i++) { michael@0: var fields = lines[i].split(";"); michael@0: if (tableNames[fields[0]]) { michael@0: request += lines[i] + "\n"; michael@0: delete tableNames[fields[0]]; michael@0: } michael@0: } michael@0: michael@0: // For each requested table that didn't have chunk data in the database, michael@0: // request it fresh michael@0: for (var tableName in tableNames) { michael@0: request += tableName + ";\n"; michael@0: } michael@0: michael@0: G_Debug(this, 'checkForUpdates: scheduling request..'); michael@0: var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"] michael@0: .getService(Ci.nsIUrlClassifierStreamUpdater); michael@0: try { michael@0: streamer.updateUrl = this.updateserverURL_; michael@0: } catch (e) { michael@0: G_Debug(this, 'invalid url'); michael@0: return; michael@0: } michael@0: michael@0: this.requestBackoff_.noteRequest(); michael@0: michael@0: if (!streamer.downloadUpdates(tableList, michael@0: request, michael@0: BindToObject(this.updateSuccess_, this), michael@0: BindToObject(this.updateError_, this), michael@0: BindToObject(this.downloadError_, this))) { michael@0: G_Debug(this, "pending update, wait until later"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Callback function if the update request succeeded. michael@0: * @param waitForUpdate String The number of seconds that the client should michael@0: * wait before requesting again. michael@0: */ michael@0: PROT_ListManager.prototype.updateSuccess_ = function(waitForUpdate) { michael@0: G_Debug(this, "update success: " + waitForUpdate); michael@0: if (waitForUpdate) { michael@0: var delay = parseInt(waitForUpdate, 10); michael@0: // As long as the delay is something sane (5 minutes or more), update michael@0: // our delay time for requesting updates michael@0: if (delay >= (5 * 60) && this.updateChecker_) michael@0: this.updateChecker_.setDelay(delay * 1000); michael@0: } michael@0: michael@0: // Let the backoff object know that we completed successfully. michael@0: this.requestBackoff_.noteServerResponse(200); michael@0: } michael@0: michael@0: /** michael@0: * Callback function if the update request succeeded. michael@0: * @param result String The error code of the failure michael@0: */ michael@0: PROT_ListManager.prototype.updateError_ = function(result) { michael@0: G_Debug(this, "update error: " + result); michael@0: // XXX: there was some trouble applying the updates. michael@0: } michael@0: michael@0: /** michael@0: * Callback function when the download failed michael@0: * @param status String http status or an empty string if connection refused. michael@0: */ michael@0: PROT_ListManager.prototype.downloadError_ = function(status) { michael@0: G_Debug(this, "download error: " + status); michael@0: // If status is empty, then we assume that we got an NS_CONNECTION_REFUSED michael@0: // error. In this case, we treat this is a http 500 error. michael@0: if (!status) { michael@0: status = 500; michael@0: } michael@0: status = parseInt(status, 10); michael@0: this.requestBackoff_.noteServerResponse(status); michael@0: michael@0: if (this.requestBackoff_.isErrorStatus(status)) { michael@0: // Schedule an update for when our backoff is complete michael@0: this.currentUpdateChecker_ = michael@0: new G_Alarm(BindToObject(this.checkForUpdates, this), michael@0: this.requestBackoff_.nextRequestDelay()); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Called when cookies are cleared michael@0: */ michael@0: PROT_ListManager.prototype.cookieChanged_ = function(subject, topic, data) { michael@0: if (data != "cleared") michael@0: return; michael@0: michael@0: G_Debug(this, "cookies cleared"); michael@0: } michael@0: michael@0: PROT_ListManager.prototype.QueryInterface = function(iid) { michael@0: if (iid.equals(Ci.nsISupports) || michael@0: iid.equals(Ci.nsIUrlListManager) || michael@0: iid.equals(Ci.nsITimerCallback)) michael@0: return this; michael@0: michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }