864 lines
25 KiB
JavaScript
864 lines
25 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
// This is the only implementation of nsIUrlListManager.
|
|
// A class that manages lists, namely exception and block lists for
|
|
// phishing or malware protection. The ListManager knows how to fetch,
|
|
// update, and store lists.
|
|
//
|
|
// There is a single listmanager for the whole application.
|
|
//
|
|
// TODO more comprehensive update tests, for example add unittest check
|
|
// that the listmanagers tables are properly written on updates
|
|
|
|
// Lower and upper limits on the server-provided polling frequency
|
|
const minDelayMs = 5 * 60 * 1000;
|
|
const maxDelayMs = 24 * 60 * 60 * 1000;
|
|
const defaultUpdateIntervalMs = 30 * 60 * 1000;
|
|
// The threshold to check if the browser is idle. We will defer the update in
|
|
// order to save the power consumption if the browser has been idle for one hour
|
|
// because it's likely that the browser will keep idle for a longer period.
|
|
const browserIdleThresholdMs = 60 * 60 * 1000;
|
|
const PREF_DEBUG_ENABLED = "browser.safebrowsing.debug";
|
|
const PREF_TEST_NOTIFICATIONS =
|
|
"browser.safebrowsing.test-notifications.enabled";
|
|
|
|
let loggingEnabled = false;
|
|
|
|
// Variables imported from library.
|
|
let BindToObject, RequestBackoffV4;
|
|
|
|
// Log only if browser.safebrowsing.debug is true
|
|
function log(...stuff) {
|
|
if (!loggingEnabled) {
|
|
return;
|
|
}
|
|
|
|
var d = new Date();
|
|
let msg = "listmanager: " + d.toTimeString() + ": " + stuff.join(" ");
|
|
msg = Services.urlFormatter.trimSensitiveURLs(msg);
|
|
Services.console.logStringMessage(msg);
|
|
dump(msg + "\n");
|
|
}
|
|
|
|
/**
|
|
* A ListManager keeps track of exception and block lists and knows
|
|
* how to update them.
|
|
*
|
|
* @constructor
|
|
*/
|
|
function PROT_ListManager() {
|
|
loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED);
|
|
|
|
log("Initializing list manager");
|
|
|
|
// A map of tableNames to objects of type
|
|
// { updateUrl: <updateUrl>, gethashUrl: <gethashUrl> }
|
|
this.tablesData = {};
|
|
// A map of updateUrls to maps of tables requiring updates, e.g.
|
|
// { safebrowsing-update-url: { goog-phish-shavar: true,
|
|
// goog-malware-shavar: true }
|
|
this.needsUpdate_ = {};
|
|
|
|
// A map of updateUrls to single-use nsITimer. An entry exists if and only if
|
|
// there is at least one table with updates enabled for that url. nsITimers
|
|
// are reset when enabling/disabling updates or on update callbacks (update
|
|
// success, update failure, download error).
|
|
this.updateCheckers_ = {};
|
|
this.requestBackoffs_ = {};
|
|
|
|
// This is only used by testcases to ensure SafeBrowsing.sys.mjs is inited
|
|
this.registered = false;
|
|
|
|
this.dbService_ = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
|
|
Ci.nsIUrlClassifierDBService
|
|
);
|
|
|
|
this.idleService_ = Cc["@mozilla.org/widget/useridleservice;1"].getService(
|
|
Ci.nsIUserIdleService
|
|
);
|
|
|
|
Services.obs.addObserver(this, "quit-application");
|
|
Services.prefs.addObserver(PREF_DEBUG_ENABLED, this);
|
|
}
|
|
|
|
/**
|
|
* Register a new table table
|
|
* @param tableName - the name of the table
|
|
* @param updateUrl - the url for updating the table
|
|
* @param gethashUrl - the url for fetching hash completions
|
|
* @returns true if the table could be created; false otherwise
|
|
*/
|
|
PROT_ListManager.prototype.registerTable = function (
|
|
tableName,
|
|
providerName,
|
|
updateUrl,
|
|
gethashUrl
|
|
) {
|
|
this.registered = true;
|
|
|
|
this.tablesData[tableName] = {};
|
|
if (!updateUrl) {
|
|
log("Can't register table " + tableName + " without updateUrl");
|
|
return false;
|
|
}
|
|
log("registering " + tableName + " with " + updateUrl);
|
|
this.tablesData[tableName].updateUrl = updateUrl;
|
|
this.tablesData[tableName].gethashUrl = gethashUrl;
|
|
this.tablesData[tableName].provider = providerName;
|
|
|
|
// Keep track of all of our update URLs.
|
|
if (!this.needsUpdate_[updateUrl]) {
|
|
this.needsUpdate_[updateUrl] = {};
|
|
|
|
// Using the V4 backoff algorithm for both V2 and V4. See bug 1273398.
|
|
this.requestBackoffs_[updateUrl] = new RequestBackoffV4(
|
|
4 /* num requests */,
|
|
60 * 60 * 1000 /* request time, 60 min */,
|
|
providerName /* used by testcase */
|
|
);
|
|
}
|
|
this.needsUpdate_[updateUrl][tableName] = false;
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Unregister a table table from list
|
|
*/
|
|
PROT_ListManager.prototype.unregisterTable = function (tableName) {
|
|
log("unregistering " + tableName);
|
|
var table = this.tablesData[tableName];
|
|
if (table) {
|
|
if (
|
|
!this.updatesNeeded_(table.updateUrl) &&
|
|
this.updateCheckers_[table.updateUrl]
|
|
) {
|
|
this.updateCheckers_[table.updateUrl].cancel();
|
|
this.updateCheckers_[table.updateUrl] = null;
|
|
}
|
|
delete this.needsUpdate_[table.updateUrl][tableName];
|
|
}
|
|
delete this.tablesData[tableName];
|
|
};
|
|
|
|
/**
|
|
* Delete all of our data tables which seem to leak otherwise.
|
|
* Remove observers
|
|
*/
|
|
PROT_ListManager.prototype.shutdown_ = function () {
|
|
this.stopUpdateCheckers();
|
|
for (var name in this.tablesData) {
|
|
delete this.tablesData[name];
|
|
}
|
|
Services.obs.removeObserver(this, "quit-application");
|
|
Services.prefs.removeObserver(PREF_DEBUG_ENABLED, this);
|
|
};
|
|
|
|
/**
|
|
* xpcom-shutdown callback
|
|
*/
|
|
PROT_ListManager.prototype.observe = function (aSubject, aTopic, aData) {
|
|
switch (aTopic) {
|
|
case "quit-application":
|
|
this.shutdown_();
|
|
break;
|
|
case "nsPref:changed":
|
|
if (aData == PREF_DEBUG_ENABLED) {
|
|
loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED);
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
PROT_ListManager.prototype.getGethashUrl = function (tableName) {
|
|
if (this.tablesData[tableName] && this.tablesData[tableName].gethashUrl) {
|
|
return this.tablesData[tableName].gethashUrl;
|
|
}
|
|
return "";
|
|
};
|
|
|
|
PROT_ListManager.prototype.getUpdateUrl = function (tableName) {
|
|
if (this.tablesData[tableName] && this.tablesData[tableName].updateUrl) {
|
|
return this.tablesData[tableName].updateUrl;
|
|
}
|
|
return "";
|
|
};
|
|
|
|
/**
|
|
* Enable updates for a single table.
|
|
*/
|
|
PROT_ListManager.prototype.enableUpdate = function (tableName) {
|
|
var table = this.tablesData[tableName];
|
|
if (table) {
|
|
log("Enabling table updates for " + tableName);
|
|
this.needsUpdate_[table.updateUrl][tableName] = true;
|
|
}
|
|
};
|
|
|
|
PROT_ListManager.prototype.isRegistered = function () {
|
|
return this.registered;
|
|
};
|
|
|
|
/**
|
|
* Returns true if any table associated with the updateUrl requires updates.
|
|
* @param updateUrl - the updateUrl
|
|
*/
|
|
PROT_ListManager.prototype.updatesNeeded_ = function (updateUrl) {
|
|
let updatesNeeded = false;
|
|
for (var tableName in this.needsUpdate_[updateUrl]) {
|
|
if (this.needsUpdate_[updateUrl][tableName]) {
|
|
updatesNeeded = true;
|
|
}
|
|
}
|
|
return updatesNeeded;
|
|
};
|
|
|
|
/**
|
|
* Disable updates for all tables.
|
|
*/
|
|
PROT_ListManager.prototype.disableAllUpdates = function () {
|
|
for (const tableName of Object.keys(this.tablesData)) {
|
|
this.disableUpdate(tableName);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Disables updates for a single table. Avoid this internal function
|
|
* and use disableAllUpdates() instead.
|
|
*/
|
|
PROT_ListManager.prototype.disableUpdate = function (tableName) {
|
|
var table = this.tablesData[tableName];
|
|
if (table) {
|
|
log("Disabling table updates for " + tableName);
|
|
this.needsUpdate_[table.updateUrl][tableName] = false;
|
|
if (
|
|
!this.updatesNeeded_(table.updateUrl) &&
|
|
this.updateCheckers_[table.updateUrl]
|
|
) {
|
|
this.updateCheckers_[table.updateUrl].cancel();
|
|
this.updateCheckers_[table.updateUrl] = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Determine if we have some tables that need updating.
|
|
*/
|
|
PROT_ListManager.prototype.requireTableUpdates = function () {
|
|
for (var name in this.tablesData) {
|
|
// Tables that need updating even if other tables don't require it
|
|
if (this.needsUpdate_[this.tablesData[name].updateUrl][name]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Set timer to check update after delay
|
|
*/
|
|
PROT_ListManager.prototype.setUpdateCheckTimer = function (updateUrl, delay) {
|
|
this.updateCheckers_[updateUrl] = Cc["@mozilla.org/timer;1"].createInstance(
|
|
Ci.nsITimer
|
|
);
|
|
|
|
// A helper function to trigger the table update.
|
|
let update = function () {
|
|
if (!this.checkForUpdates(updateUrl)) {
|
|
// Make another attempt later.
|
|
this.setUpdateCheckTimer(updateUrl, defaultUpdateIntervalMs);
|
|
}
|
|
}.bind(this);
|
|
|
|
this.updateCheckers_[updateUrl].initWithCallback(
|
|
() => {
|
|
this.updateCheckers_[updateUrl] = null;
|
|
// Check if we are in the idle mode. We will stop the current update and
|
|
// defer it to the next user interaction active if the browser is
|
|
// considered in idle mode.
|
|
if (this.idleService_.idleTime > browserIdleThresholdMs) {
|
|
let observer = function () {
|
|
Services.obs.removeObserver(observer, "user-interaction-active");
|
|
update();
|
|
};
|
|
|
|
Services.obs.addObserver(observer, "user-interaction-active");
|
|
return;
|
|
}
|
|
|
|
update();
|
|
},
|
|
delay,
|
|
Ci.nsITimer.TYPE_ONE_SHOT
|
|
);
|
|
};
|
|
/**
|
|
* Acts as a nsIUrlClassifierCallback for getTables.
|
|
*/
|
|
PROT_ListManager.prototype.kickoffUpdate_ = function () {
|
|
this.startingUpdate_ = false;
|
|
var initialUpdateDelay = 3000;
|
|
// Add a fuzz of 0-1 minutes for both v2 and v4 according to Bug 1305478.
|
|
initialUpdateDelay += Math.floor(Math.random() * (1 * 60 * 1000));
|
|
|
|
// If the user has never downloaded tables, do the check now.
|
|
log("needsUpdate: " + JSON.stringify(this.needsUpdate_, undefined, 2));
|
|
for (var updateUrl in this.needsUpdate_) {
|
|
// If we haven't already kicked off updates for this updateUrl, set a
|
|
// non-repeating timer for it. The timer delay will be reset either on
|
|
// updateSuccess to the default update interval, or backed off on
|
|
// downloadError. Don't set the updateChecker unless at least one table has
|
|
// updates enabled.
|
|
if (this.updatesNeeded_(updateUrl) && !this.updateCheckers_[updateUrl]) {
|
|
let provider = null;
|
|
Object.keys(this.tablesData).forEach(function (table) {
|
|
if (this.tablesData[table].updateUrl === updateUrl) {
|
|
let newProvider = this.tablesData[table].provider;
|
|
if (provider) {
|
|
if (newProvider !== provider) {
|
|
log(
|
|
"Multiple tables for the same updateURL have a different provider?!"
|
|
);
|
|
}
|
|
} else {
|
|
provider = newProvider;
|
|
}
|
|
}
|
|
}, this);
|
|
log(
|
|
"Initializing update checker for " +
|
|
updateUrl +
|
|
" provided by " +
|
|
provider
|
|
);
|
|
|
|
// Use the initialUpdateDelay + fuzz unless we had previous updates
|
|
// and the server told us when to try again.
|
|
let updateDelay = initialUpdateDelay;
|
|
let nextUpdatePref =
|
|
"browser.safebrowsing.provider." + provider + ".nextupdatetime";
|
|
let nextUpdate = Services.prefs.getCharPref(nextUpdatePref, "");
|
|
|
|
if (nextUpdate) {
|
|
updateDelay = Math.min(
|
|
maxDelayMs,
|
|
Math.max(0, nextUpdate - Date.now())
|
|
);
|
|
log("Next update at " + nextUpdate);
|
|
}
|
|
log("Next update " + Math.round(updateDelay / 60000) + "min from now");
|
|
|
|
this.setUpdateCheckTimer(updateUrl, updateDelay);
|
|
} else {
|
|
log("No updates needed or already initialized for " + updateUrl);
|
|
}
|
|
}
|
|
};
|
|
|
|
PROT_ListManager.prototype.stopUpdateCheckers = function () {
|
|
log("Stopping updates");
|
|
for (var updateUrl in this.updateCheckers_) {
|
|
if (this.updateCheckers_[updateUrl]) {
|
|
this.updateCheckers_[updateUrl].cancel();
|
|
this.updateCheckers_[updateUrl] = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Determine if we have any tables that require updating. Different
|
|
* Wardens may call us with new tables that need to be updated.
|
|
*/
|
|
PROT_ListManager.prototype.maybeToggleUpdateChecking = function () {
|
|
// We update tables if we have some tables that want updates. If there
|
|
// are no tables that want to be updated - we dont need to check anything.
|
|
if (this.requireTableUpdates()) {
|
|
log("Starting managing lists");
|
|
|
|
// Get the list of existing tables from the DBService before making any
|
|
// update requests.
|
|
if (!this.startingUpdate_) {
|
|
this.startingUpdate_ = true;
|
|
// check the current state of tables in the database
|
|
this.kickoffUpdate_();
|
|
}
|
|
} else {
|
|
log("Stopping managing lists (if currently active)");
|
|
this.stopUpdateCheckers(); // Cancel pending updates
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Force updates for the given tables. This API may trigger more than one update
|
|
* if the table lists provided belong to multiple updateurl (multiple provider).
|
|
* Return false when any update is fail due to back-off algorithm.
|
|
*/
|
|
PROT_ListManager.prototype.forceUpdates = function (tables) {
|
|
log("forceUpdates with " + tables);
|
|
if (!tables) {
|
|
return false;
|
|
}
|
|
|
|
let updateUrls = new Set();
|
|
tables.split(",").forEach(table => {
|
|
if (this.tablesData[table]) {
|
|
updateUrls.add(this.tablesData[table].updateUrl);
|
|
}
|
|
});
|
|
|
|
let ret = true;
|
|
|
|
updateUrls.forEach(url => {
|
|
// Cancel current update timer for the url because we are forcing an update.
|
|
if (this.updateCheckers_[url]) {
|
|
this.updateCheckers_[url].cancel();
|
|
this.updateCheckers_[url] = null;
|
|
}
|
|
|
|
// Trigger an update for the given url.
|
|
if (!this.checkForUpdates(url, true)) {
|
|
ret = false;
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
};
|
|
|
|
/**
|
|
* Updates our internal tables from the update server
|
|
*
|
|
* @param updateUrl: request updates for tables associated with that url, or
|
|
* for all tables if the url is empty.
|
|
* @param manual: the update is triggered manually
|
|
*/
|
|
PROT_ListManager.prototype.checkForUpdates = function (
|
|
updateUrl,
|
|
manual = false
|
|
) {
|
|
log("checkForUpdates with " + updateUrl);
|
|
// See if we've triggered the request backoff logic.
|
|
if (!updateUrl) {
|
|
return false;
|
|
}
|
|
|
|
// Disable SafeBrowsing updates in Safe Mode, but still allow manually
|
|
// triggering an update for debugging.
|
|
if (Services.appinfo.inSafeMode && !manual) {
|
|
log("update is disabled in Safe Mode");
|
|
return false;
|
|
}
|
|
|
|
if (lazy.enableTestNotifications) {
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"safebrowsing-update-attempt",
|
|
updateUrl
|
|
);
|
|
}
|
|
|
|
if (
|
|
!this.requestBackoffs_[updateUrl] ||
|
|
!this.requestBackoffs_[updateUrl].canMakeRequest()
|
|
) {
|
|
log("Can't make update request");
|
|
return false;
|
|
}
|
|
// Grab the current state of the tables from the database
|
|
this.dbService_.getTables(
|
|
BindToObject(this.makeUpdateRequest_, this, updateUrl)
|
|
);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Method that fires the actual HTTP update request.
|
|
* First we reset any tables that have disappeared.
|
|
* @param tableData List of table data already in the database, in the form
|
|
* tablename;<chunk ranges>\n
|
|
*/
|
|
PROT_ListManager.prototype.makeUpdateRequest_ = function (
|
|
updateUrl,
|
|
tableData
|
|
) {
|
|
log("this.tablesData: " + JSON.stringify(this.tablesData, undefined, 2));
|
|
log("existing chunks: " + tableData + "\n");
|
|
// Disallow blank updateUrls
|
|
if (!updateUrl) {
|
|
return;
|
|
}
|
|
// An object of the form
|
|
// { tableList: comma-separated list of tables to request,
|
|
// tableNames: map of tables that need updating,
|
|
// request: list of tables and existing chunk ranges from tableData
|
|
// }
|
|
var streamerMap = {
|
|
tableList: null,
|
|
tableNames: {},
|
|
requestPayload: "",
|
|
isPostRequest: true,
|
|
};
|
|
|
|
let useProtobuf = false;
|
|
let onceThru = false;
|
|
for (var tableName in this.tablesData) {
|
|
// Skip tables not matching this update url
|
|
if (this.tablesData[tableName].updateUrl != updateUrl) {
|
|
continue;
|
|
}
|
|
|
|
// Check if |updateURL| is for 'proto'. (only v4 uses protobuf for now.)
|
|
// We use the table name 'goog-*-proto' and an additional provider "google4"
|
|
// to describe the v4 settings.
|
|
let isCurTableProto = tableName.endsWith("-proto");
|
|
if (!onceThru) {
|
|
useProtobuf = isCurTableProto;
|
|
onceThru = true;
|
|
} else if (useProtobuf !== isCurTableProto) {
|
|
log(
|
|
'ERROR: Cannot mix "proto" tables with other types ' +
|
|
"within the same provider."
|
|
);
|
|
}
|
|
|
|
if (this.needsUpdate_[this.tablesData[tableName].updateUrl][tableName]) {
|
|
streamerMap.tableNames[tableName] = true;
|
|
}
|
|
if (!streamerMap.tableList) {
|
|
streamerMap.tableList = tableName;
|
|
} else {
|
|
streamerMap.tableList += "," + tableName;
|
|
}
|
|
}
|
|
|
|
if (useProtobuf) {
|
|
let tableArray = [];
|
|
Object.keys(streamerMap.tableNames).forEach(aTableName => {
|
|
if (streamerMap.tableNames[aTableName]) {
|
|
tableArray.push(aTableName);
|
|
}
|
|
});
|
|
|
|
// Build the <tablename, stateBase64> mapping.
|
|
let tableState = {};
|
|
tableData.split("\n").forEach(line => {
|
|
let p = line.indexOf(";");
|
|
if (-1 === p) {
|
|
return;
|
|
}
|
|
let tableName = line.substring(0, p);
|
|
if (tableName in streamerMap.tableNames) {
|
|
let metadata = line.substring(p + 1).split(":");
|
|
let stateBase64 = metadata[0];
|
|
log(tableName + " ==> " + stateBase64);
|
|
tableState[tableName] = stateBase64;
|
|
}
|
|
});
|
|
|
|
// The state is a byte stream which server told us from the
|
|
// last table update. The state would be used to do the partial
|
|
// update and the empty string means the table has
|
|
// never been downloaded. See Bug 1287058 for supporting
|
|
// partial update.
|
|
let stateArray = [];
|
|
tableArray.forEach(listName => {
|
|
stateArray.push(tableState[listName] || "");
|
|
});
|
|
|
|
log("stateArray: " + stateArray);
|
|
|
|
let urlUtils = Cc["@mozilla.org/url-classifier/utils;1"].getService(
|
|
Ci.nsIUrlClassifierUtils
|
|
);
|
|
|
|
streamerMap.requestPayload = urlUtils.makeUpdateRequestV4(
|
|
tableArray,
|
|
stateArray
|
|
);
|
|
streamerMap.isPostRequest = false;
|
|
} else {
|
|
// Build the request. For each table already in the database, include the
|
|
// chunk data from the database
|
|
var lines = tableData.split("\n");
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var fields = lines[i].split(";");
|
|
var name = fields[0];
|
|
if (streamerMap.tableNames[name]) {
|
|
streamerMap.requestPayload += lines[i] + "\n";
|
|
delete streamerMap.tableNames[name];
|
|
}
|
|
}
|
|
// For each requested table that didn't have chunk data in the database,
|
|
// request it fresh
|
|
for (let tableName in streamerMap.tableNames) {
|
|
streamerMap.requestPayload += tableName + ";\n";
|
|
}
|
|
|
|
streamerMap.isPostRequest = true;
|
|
}
|
|
|
|
log("update request: " + JSON.stringify(streamerMap, undefined, 2) + "\n");
|
|
|
|
// Don't send an empty request.
|
|
if (streamerMap.requestPayload.length) {
|
|
this.makeUpdateRequestForEntry_(
|
|
updateUrl,
|
|
streamerMap.tableList,
|
|
streamerMap.requestPayload,
|
|
streamerMap.isPostRequest
|
|
);
|
|
} else {
|
|
// We were disabled between kicking off getTables and now.
|
|
log("Not sending empty request");
|
|
}
|
|
};
|
|
|
|
PROT_ListManager.prototype.makeUpdateRequestForEntry_ = function (
|
|
updateUrl,
|
|
tableList,
|
|
requestPayload,
|
|
isPostRequest
|
|
) {
|
|
log(
|
|
"makeUpdateRequestForEntry_: requestPayload " +
|
|
requestPayload +
|
|
" update: " +
|
|
updateUrl +
|
|
" tablelist: " +
|
|
tableList +
|
|
"\n"
|
|
);
|
|
var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"].getService(
|
|
Ci.nsIUrlClassifierStreamUpdater
|
|
);
|
|
|
|
this.requestBackoffs_[updateUrl].noteRequest();
|
|
|
|
if (
|
|
!streamer.downloadUpdates(
|
|
tableList,
|
|
requestPayload,
|
|
isPostRequest,
|
|
updateUrl,
|
|
BindToObject(this.updateSuccess_, this, tableList, updateUrl),
|
|
BindToObject(this.updateError_, this, tableList, updateUrl),
|
|
BindToObject(this.downloadError_, this, tableList, updateUrl)
|
|
)
|
|
) {
|
|
// Our alarm gets reset in one of the 3 callbacks.
|
|
log("pending update, queued request until later");
|
|
} else {
|
|
let table = Object.keys(this.tablesData).find(key => {
|
|
return this.tablesData[key].updateUrl === updateUrl;
|
|
});
|
|
let provider = this.tablesData[table].provider;
|
|
Services.obs.notifyObservers(null, "safebrowsing-update-begin", provider);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Callback function if the update request succeeded.
|
|
* @param waitForUpdate String The number of seconds that the client should
|
|
* wait before requesting again.
|
|
*/
|
|
PROT_ListManager.prototype.updateSuccess_ = function (
|
|
tableList,
|
|
updateUrl,
|
|
waitForUpdateSec
|
|
) {
|
|
log(
|
|
"update success for " +
|
|
tableList +
|
|
" from " +
|
|
updateUrl +
|
|
": " +
|
|
waitForUpdateSec +
|
|
"\n"
|
|
);
|
|
|
|
// The time unit below are all milliseconds if not specified.
|
|
|
|
var delay = 0;
|
|
if (waitForUpdateSec) {
|
|
delay = parseInt(waitForUpdateSec, 10) * 1000;
|
|
}
|
|
// As long as the delay is something sane (5 min to 1 day), update
|
|
// our delay time for requesting updates. We always use a non-repeating
|
|
// timer since the delay is set differently at every callback.
|
|
if (delay > maxDelayMs) {
|
|
log(
|
|
"Ignoring delay from server (too long), waiting " +
|
|
Math.round(maxDelayMs / 60000) +
|
|
"min"
|
|
);
|
|
delay = maxDelayMs;
|
|
} else if (delay < minDelayMs) {
|
|
log(
|
|
"Ignoring delay from server (too short), waiting " +
|
|
Math.round(defaultUpdateIntervalMs / 60000) +
|
|
"min"
|
|
);
|
|
delay = defaultUpdateIntervalMs;
|
|
} else {
|
|
log("Waiting " + Math.round(delay / 60000) + "min");
|
|
}
|
|
|
|
this.setUpdateCheckTimer(updateUrl, delay);
|
|
|
|
// Let the backoff object know that we completed successfully.
|
|
this.requestBackoffs_[updateUrl].noteServerResponse(200);
|
|
|
|
// Set last update time for provider
|
|
// Get the provider for these tables, check for consistency
|
|
let tables = tableList.split(",");
|
|
let provider = null;
|
|
for (let table of tables) {
|
|
let newProvider = this.tablesData[table].provider;
|
|
if (provider) {
|
|
if (newProvider !== provider) {
|
|
log(
|
|
"Multiple tables for the same updateURL have a different provider?!"
|
|
);
|
|
}
|
|
} else {
|
|
provider = newProvider;
|
|
}
|
|
}
|
|
|
|
// Store the last update time (needed to know if the table is "fresh")
|
|
// and the next update time (to know when to update next).
|
|
let lastUpdatePref =
|
|
"browser.safebrowsing.provider." + provider + ".lastupdatetime";
|
|
let now = Date.now();
|
|
log("Setting last update of " + provider + " to " + now);
|
|
Services.prefs.setCharPref(lastUpdatePref, now.toString());
|
|
|
|
let nextUpdatePref =
|
|
"browser.safebrowsing.provider." + provider + ".nextupdatetime";
|
|
let targetTime = now + delay;
|
|
log(
|
|
"Setting next update of " +
|
|
provider +
|
|
" to " +
|
|
targetTime +
|
|
" (" +
|
|
Math.round(delay / 60000) +
|
|
"min from now)"
|
|
);
|
|
Services.prefs.setCharPref(nextUpdatePref, targetTime.toString());
|
|
|
|
Services.obs.notifyObservers(null, "safebrowsing-update-finished", "success");
|
|
};
|
|
|
|
/**
|
|
* Callback function if the update request succeeded.
|
|
* @param result String The error code of the failure
|
|
*/
|
|
PROT_ListManager.prototype.updateError_ = function (table, updateUrl, result) {
|
|
log(
|
|
"update error for " + table + " from " + updateUrl + ": " + result + "\n"
|
|
);
|
|
// There was some trouble applying the updates. Don't try again for at least
|
|
// updateInterval milliseconds.
|
|
this.setUpdateCheckTimer(updateUrl, defaultUpdateIntervalMs);
|
|
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"safebrowsing-update-finished",
|
|
"update error: " + result
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Callback function when the download failed
|
|
* @param status String http status or an empty string if connection refused.
|
|
*/
|
|
PROT_ListManager.prototype.downloadError_ = function (
|
|
table,
|
|
updateUrl,
|
|
status
|
|
) {
|
|
log("download error for " + table + ": " + status + "\n");
|
|
// If status is empty, then we assume that we got an NS_CONNECTION_REFUSED
|
|
// error. In this case, we treat this is a http 500 error.
|
|
if (!status) {
|
|
status = 500;
|
|
}
|
|
status = parseInt(status, 10);
|
|
this.requestBackoffs_[updateUrl].noteServerResponse(status);
|
|
let delay = defaultUpdateIntervalMs;
|
|
if (this.requestBackoffs_[updateUrl].isErrorStatus(status)) {
|
|
// Schedule an update for when our backoff is complete
|
|
delay = this.requestBackoffs_[updateUrl].nextRequestDelay();
|
|
} else {
|
|
log("Got non error status for error callback?!");
|
|
}
|
|
|
|
this.setUpdateCheckTimer(updateUrl, delay);
|
|
|
|
Services.obs.notifyObservers(
|
|
null,
|
|
"safebrowsing-update-finished",
|
|
"download error: " + status
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Get back-off time for the given provider.
|
|
* Return 0 if we are not in back-off mode.
|
|
*/
|
|
PROT_ListManager.prototype.getBackOffTime = function (provider) {
|
|
let updateUrl = "";
|
|
for (var table in this.tablesData) {
|
|
if (this.tablesData[table].provider == provider) {
|
|
updateUrl = this.tablesData[table].updateUrl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!updateUrl || !this.requestBackoffs_[updateUrl]) {
|
|
return 0;
|
|
}
|
|
|
|
let delay = this.requestBackoffs_[updateUrl].nextRequestDelay();
|
|
return delay == 0 ? 0 : Date.now() + delay;
|
|
};
|
|
|
|
PROT_ListManager.prototype.QueryInterface = ChromeUtils.generateQI([
|
|
"nsIUrlListManager",
|
|
"nsIObserver",
|
|
"nsITimerCallback",
|
|
]);
|
|
|
|
let initialized = false;
|
|
function Init() {
|
|
if (initialized) {
|
|
return;
|
|
}
|
|
|
|
// Pull the library in.
|
|
var jslib =
|
|
Cc["@mozilla.org/url-classifier/jslib;1"].getService().wrappedJSObject;
|
|
BindToObject = jslib.BindToObject;
|
|
RequestBackoffV4 = jslib.RequestBackoffV4;
|
|
|
|
initialized = true;
|
|
}
|
|
|
|
export function RegistrationData() {
|
|
Init();
|
|
return new PROT_ListManager();
|
|
}
|
|
|
|
const lazy = {};
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"enableTestNotifications",
|
|
PREF_TEST_NOTIFICATIONS,
|
|
false
|
|
);
|