!import
1 //@line 37 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/toolkit/components/url-classifier/src/nsUrlClassifierListManager.js"
2
3 const Cc = Components.classes;
4 const Ci = Components.interfaces;
5
6 //@line 37 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/toolkit/components/url-classifier/content/listmanager.js"
7
8
9 // A class that manages lists, namely white and black lists for
10 // phishing or malware protection. The ListManager knows how to fetch,
11 // update, and store lists.
12 //
13 // There is a single listmanager for the whole application.
14 //
15 // TODO more comprehensive update tests, for example add unittest check
16 // that the listmanagers tables are properly written on updates
17
18 // How frequently we check for updates (30 minutes)
19 const kUpdateInterval = 30 * 60 * 1000;
20
QueryAdapter
21 function QueryAdapter(callback) {
22 this.callback_ = callback;
23 };
24
handleResponse
25 QueryAdapter.prototype.handleResponse = function(value) {
26 this.callback_.handleEvent(value);
27 }
28
29 /**
30 * A ListManager keeps track of black and white lists and knows
31 * how to update them.
32 *
33 * @constructor
34 */
PROT_ListManager
35 function PROT_ListManager() {
36 this.debugZone = "listmanager";
37 G_debugService.enableZone(this.debugZone);
38
39 this.currentUpdateChecker_ = null; // set when we toggle updates
40 this.prefs_ = new G_Preferences();
41
42 this.updateserverURL_ = null;
43 this.gethashURL_ = null;
44
45 this.isTesting_ = false;
46
47 this.tablesData = {};
48
49 this.observerServiceObserver_ = new G_ObserverServiceObserver(
50 'xpcom-shutdown',
51 BindToObject(this.shutdown_, this),
52 true /*only once*/);
53
54 // Lazily create the key manager (to avoid fetching keys when they
55 // aren't needed).
56 this.keyManager_ = null;
57
58 this.rekeyObserver_ = new G_ObserverServiceObserver(
59 'url-classifier-rekey-requested',
60 BindToObject(this.rekey_, this),
61 false);
62 this.updateWaitingForKey_ = false;
63
64 this.cookieObserver_ = new G_ObserverServiceObserver(
65 'cookie-changed',
66 BindToObject(this.cookieChanged_, this),
67 false);
68
69 this.requestBackoff_ = new RequestBackoff(3 /* num errors */,
70 10*60*1000 /* error time, 10min */,
71 4 /* num requests */,
72 60*60*1000 /* request time, 60 min */,
73 60*60*1000 /* backoff interval, 60min */,
74 6*60*60*1000 /* max backoff, 6hr */);
75
76 this.dbService_ = Cc["@mozilla.org/url-classifier/dbservice;1"]
77 .getService(Ci.nsIUrlClassifierDBService);
78
79 this.hashCompleter_ = Cc["@mozilla.org/url-classifier/hashcompleter;1"]
80 .getService(Ci.nsIUrlClassifierHashCompleter);
81 }
82
83 /**
84 * xpcom-shutdown callback
85 * Delete all of our data tables which seem to leak otherwise.
86 */
shutdown_
87 PROT_ListManager.prototype.shutdown_ = function() {
88 for (var name in this.tablesData) {
89 delete this.tablesData[name];
90 }
91 }
92
93 /**
94 * Set the url we check for updates. If the new url is valid and different,
95 * update our table list.
96 *
97 * After setting the update url, the caller is responsible for registering
98 * tables and then toggling update checking. All the code for this logic is
99 * currently in browser/components/safebrowsing. Maybe it should be part of
100 * the listmanger?
101 */
setUpdateUrl
102 PROT_ListManager.prototype.setUpdateUrl = function(url) {
103 G_Debug(this, "Set update url: " + url);
104 if (url != this.updateserverURL_) {
105 this.updateserverURL_ = url;
106 this.requestBackoff_.reset();
107
108 // Remove old tables which probably aren't valid for the new provider.
109 for (var name in this.tablesData) {
110 delete this.tablesData[name];
111 }
112 }
113 }
114
115 /**
116 * Set the gethash url.
117 */
setGethashUrl
118 PROT_ListManager.prototype.setGethashUrl = function(url) {
119 G_Debug(this, "Set gethash url: " + url);
120 if (url != this.gethashURL_) {
121 this.gethashURL_ = url;
122 this.hashCompleter_.gethashUrl = url;
123 }
124 }
125
126 /**
127 * Set the crypto key url.
128 * @param url String
129 */
setKeyUrl
130 PROT_ListManager.prototype.setKeyUrl = function(url) {
131 G_Debug(this, "Set key url: " + url);
132 if (!this.keyManager_) {
133 this.keyManager_ = new PROT_UrlCryptoKeyManager();
134 this.keyManager_.onNewKey(BindToObject(this.newKey_, this));
135
136 this.hashCompleter_.setKeys(this.keyManager_.getClientKey(),
137 this.keyManager_.getWrappedKey());
138 }
139
140 this.keyManager_.setKeyUrl(url);
141 }
142
143 /**
144 * Register a new table table
145 * @param tableName - the name of the table
146 * @param opt_requireMac true if a mac is required on update, false otherwise
147 * @returns true if the table could be created; false otherwise
148 */
registerTable
149 PROT_ListManager.prototype.registerTable = function(tableName,
150 opt_requireMac) {
151 this.tablesData[tableName] = {};
152 this.tablesData[tableName].needsUpdate = false;
153
154 return true;
155 }
156
157 /**
158 * Enable updates for some tables
159 * @param tables - an array of table names that need updating
160 */
enableUpdate
161 PROT_ListManager.prototype.enableUpdate = function(tableName) {
162 var changed = false;
163 var table = this.tablesData[tableName];
164 if (table) {
165 G_Debug(this, "Enabling table updates for " + tableName);
166 table.needsUpdate = true;
167 changed = true;
168 }
169
170 if (changed === true)
171 this.maybeToggleUpdateChecking();
172 }
173
174 /**
175 * Disables updates for some tables
176 * @param tables - an array of table names that no longer need updating
177 */
disableUpdate
178 PROT_ListManager.prototype.disableUpdate = function(tableName) {
179 var changed = false;
180 var table = this.tablesData[tableName];
181 if (table) {
182 G_Debug(this, "Disabling table updates for " + tableName);
183 table.needsUpdate = false;
184 changed = true;
185 }
186
187 if (changed === true)
188 this.maybeToggleUpdateChecking();
189 }
190
191 /**
192 * Determine if we have some tables that need updating.
193 */
requireTableUpdates
194 PROT_ListManager.prototype.requireTableUpdates = function() {
195 for (var type in this.tablesData) {
196 // Tables that need updating even if other tables dont require it
197 if (this.tablesData[type].needsUpdate)
198 return true;
199 }
200
201 return false;
202 }
203
204 /**
205 * Start managing the lists we know about. We don't do this automatically
206 * when the listmanager is instantiated because their profile directory
207 * (where we store the lists) might not be available.
208 */
maybeStartManagingUpdates
209 PROT_ListManager.prototype.maybeStartManagingUpdates = function() {
210 if (this.isTesting_)
211 return;
212
213 // We might have been told about tables already, so see if we should be
214 // actually updating.
215 this.maybeToggleUpdateChecking();
216 }
217
kickoffUpdate_
218 PROT_ListManager.prototype.kickoffUpdate_ = function (tableData)
219 {
220 this.startingUpdate_ = false;
221 // If the user has never downloaded tables, do the check now.
222 // If the user has tables, add a fuzz of a few minutes.
223 var initialUpdateDelay = 3000;
224 if (tableData != "") {
225 // Add a fuzz of 0-5 minutes.
226 initialUpdateDelay += Math.floor(Math.random() * (5 * 60 * 1000));
227 }
228
229 this.currentUpdateChecker_ =
230 new G_Alarm(BindToObject(this.checkForUpdates, this),
231 initialUpdateDelay);
232 }
233
234 /**
235 * Determine if we have any tables that require updating. Different
236 * Wardens may call us with new tables that need to be updated.
237 */
maybeToggleUpdateChecking
238 PROT_ListManager.prototype.maybeToggleUpdateChecking = function() {
239 // If we are testing or dont have an application directory yet, we should
240 // not start reading tables from disk or schedule remote updates
241 if (this.isTesting_)
242 return;
243
244 // We update tables if we have some tables that want updates. If there
245 // are no tables that want to be updated - we dont need to check anything.
246 if (this.requireTableUpdates() === true) {
247 G_Debug(this, "Starting managing lists");
248 this.startUpdateChecker();
249
250 // Multiple warden can ask us to reenable updates at the same time, but we
251 // really just need to schedule a single update.
252 if (!this.currentUpdateChecker && !this.startingUpdate_) {
253 this.startingUpdate_ = true;
254 // check the current state of tables in the database
255 this.dbService_.getTables(BindToObject(this.kickoffUpdate_, this));
256 }
257 } else {
258 G_Debug(this, "Stopping managing lists (if currently active)");
259 this.stopUpdateChecker(); // Cancel pending updates
260 }
261 }
262
263 /**
264 * Start periodic checks for updates. Idempotent.
265 * We want to distribute update checks evenly across the update period (an
266 * hour). To do this, we pick a random number of time between 0 and 30
267 * minutes. The client first checks at 15 + rand, then every 30 minutes after
268 * that.
269 */
startUpdateChecker
270 PROT_ListManager.prototype.startUpdateChecker = function() {
271 this.stopUpdateChecker();
272
273 // Schedule the first check for between 15 and 45 minutes.
274 var repeatingUpdateDelay = kUpdateInterval / 2;
275 repeatingUpdateDelay += Math.floor(Math.random() * kUpdateInterval);
276 this.updateChecker_ = new G_Alarm(BindToObject(this.initialUpdateCheck_,
277 this),
278 repeatingUpdateDelay);
279 }
280
281 /**
282 * Callback for the first update check.
283 * We go ahead and check for table updates, then start a regular timer (once
284 * every 30 minutes).
285 */
initialUpdateCheck_
286 PROT_ListManager.prototype.initialUpdateCheck_ = function() {
287 this.checkForUpdates();
288 this.updateChecker_ = new G_Alarm(BindToObject(this.checkForUpdates, this),
289 kUpdateInterval, true /* repeat */);
290 }
291
292 /**
293 * Stop checking for updates. Idempotent.
294 */
stopUpdateChecker
295 PROT_ListManager.prototype.stopUpdateChecker = function() {
296 if (this.updateChecker_) {
297 this.updateChecker_.cancel();
298 this.updateChecker_ = null;
299 }
300 // Cancel the oneoff check from maybeToggleUpdateChecking.
301 if (this.currentUpdateChecker_) {
302 this.currentUpdateChecker_.cancel();
303 this.currentUpdateChecker_ = null;
304 }
305 }
306
307 /**
308 * Provides an exception free way to look up the data in a table. We
309 * use this because at certain points our tables might not be loaded,
310 * and querying them could throw.
311 *
312 * @param table String Name of the table that we want to consult
313 * @param key String Key for table lookup
314 * @param callback nsIUrlListManagerCallback (ie., Function) given false or the
315 * value in the table corresponding to key. If the table name does not
316 * exist, we return false, too.
317 */
safeLookup
318 PROT_ListManager.prototype.safeLookup = function(key, callback) {
319 try {
320 G_Debug(this, "safeLookup: " + key);
321 var cb = new QueryAdapter(callback);
322 this.dbService_.lookup(key,
323 BindToObject(cb.handleResponse, cb),
324 true);
325 } catch(e) {
326 G_Debug(this, "safeLookup masked failure for key " + key + ": " + e);
327 callback.handleEvent("");
328 }
329 }
330
331 /**
332 * Updates our internal tables from the update server
333 *
334 * @returns true when a new request was scheduled, false if an old request
335 * was still pending.
336 */
checkForUpdates
337 PROT_ListManager.prototype.checkForUpdates = function() {
338 // Allow new updates to be scheduled from maybeToggleUpdateChecking()
339 this.currentUpdateChecker_ = null;
340
341 if (!this.updateserverURL_) {
342 G_Debug(this, 'checkForUpdates: no update server url');
343 return false;
344 }
345
346 // See if we've triggered the request backoff logic.
347 if (!this.requestBackoff_.canMakeRequest())
348 return false;
349
350 // Grab the current state of the tables from the database
351 this.dbService_.getTables(BindToObject(this.makeUpdateRequest_, this));
352 return true;
353 }
354
355 /**
356 * Method that fires the actual HTTP update request.
357 * First we reset any tables that have disappeared.
358 * @param tableData List of table data already in the database, in the form
359 * tablename;<chunk ranges>\n
360 */
makeUpdateRequest_
361 PROT_ListManager.prototype.makeUpdateRequest_ = function(tableData) {
362 if (!this.keyManager_)
363 return;
364
365 if (!this.keyManager_.hasKey()) {
366 // We don't have a client key yet. Schedule a rekey, and rerequest
367 // when we have one.
368
369 // If there's already an update waiting for a new key, don't bother.
370 if (this.updateWaitingForKey_)
371 return;
372
373 // If maybeReKey() returns false we have asked for too many keys,
374 // and won't be getting a new one. Since we don't want to do
375 // updates without a client key, we'll skip this update if maybeReKey()
376 // fails.
377 if (this.keyManager_.maybeReKey())
378 this.updateWaitingForKey_ = true;
379
380 return;
381 }
382
383 var tableList;
384 var tableNames = {};
385 for (var tableName in this.tablesData) {
386 if (this.tablesData[tableName].needsUpdate)
387 tableNames[tableName] = true;
388 if (!tableList) {
389 tableList = tableName;
390 } else {
391 tableList += "," + tableName;
392 }
393 }
394
395 var request = "";
396
397 // For each table already in the database, include the chunk data from
398 // the database
399 var lines = tableData.split("\n");
400 for (var i = 0; i < lines.length; i++) {
401 var fields = lines[i].split(";");
402 if (tableNames[fields[0]]) {
403 request += lines[i] + ":mac\n";
404 delete tableNames[fields[0]];
405 }
406 }
407
408 // For each requested table that didn't have chunk data in the database,
409 // request it fresh
410 for (var tableName in tableNames) {
411 request += tableName + ";:mac\n";
412 }
413
414 G_Debug(this, 'checkForUpdates: scheduling request..');
415 var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"]
416 .getService(Ci.nsIUrlClassifierStreamUpdater);
417 try {
418 streamer.updateUrl = this.updateserverURL_ +
419 "&wrkey=" + this.keyManager_.getWrappedKey();
420 } catch (e) {
421 G_Debug(this, 'invalid url');
422 return;
423 }
424
425 this.requestBackoff_.noteRequest();
426
427 if (!streamer.downloadUpdates(tableList,
428 request,
429 this.keyManager_.getClientKey(),
430 BindToObject(this.updateSuccess_, this),
431 BindToObject(this.updateError_, this),
432 BindToObject(this.downloadError_, this))) {
433 G_Debug(this, "pending update, wait until later");
434 }
435 }
436
437 /**
438 * Callback function if the update request succeeded.
439 * @param waitForUpdate String The number of seconds that the client should
440 * wait before requesting again.
441 */
updateSuccess_
442 PROT_ListManager.prototype.updateSuccess_ = function(waitForUpdate) {
443 G_Debug(this, "update success: " + waitForUpdate);
444 if (waitForUpdate) {
445 var delay = parseInt(waitForUpdate, 10);
446 // As long as the delay is something sane (5 minutes or more), update
447 // our delay time for requesting updates
448 if (delay >= (5 * 60) && this.updateChecker_)
449 this.updateChecker_.setDelay(delay * 1000);
450 }
451
452 // Let the backoff object know that we completed successfully.
453 this.requestBackoff_.noteServerResponse(200);
454 }
455
456 /**
457 * Callback function if the update request succeeded.
458 * @param result String The error code of the failure
459 */
updateError_
460 PROT_ListManager.prototype.updateError_ = function(result) {
461 G_Debug(this, "update error: " + result);
462 // XXX: there was some trouble applying the updates.
463 }
464
465 /**
466 * Callback function when the download failed
467 * @param status String http status or an empty string if connection refused.
468 */
downloadError_
469 PROT_ListManager.prototype.downloadError_ = function(status) {
470 G_Debug(this, "download error: " + status);
471 // If status is empty, then we assume that we got an NS_CONNECTION_REFUSED
472 // error. In this case, we treat this is a http 500 error.
473 if (!status) {
474 status = 500;
475 }
476 status = parseInt(status, 10);
477 this.requestBackoff_.noteServerResponse(status);
478
479 if (this.requestBackoff_.isErrorStatus(status)) {
480 // Try again in a minute
481 this.currentUpdateChecker_ =
482 new G_Alarm(BindToObject(this.checkForUpdates, this), 60000);
483 }
484 }
485
486 /**
487 * Called when either the update process or a gethash request signals
488 * that the server requested a rekey.
489 */
rekey_
490 PROT_ListManager.prototype.rekey_ = function() {
491 G_Debug(this, "rekey requested");
492
493 // The current key is no good anymore.
494 this.keyManager_.dropKey();
495 this.keyManager_.maybeReKey();
496 }
497
498 /**
499 * Called when cookies are cleared - clears the current MAC keys.
500 */
cookieChanged_
501 PROT_ListManager.prototype.cookieChanged_ = function(subject, topic, data) {
502 if (data != "cleared")
503 return;
504
505 G_Debug(this, "cookies cleared");
506 this.keyManager_.dropKey();
507 }
508
509 /**
510 * Called when we've received a new key from the server.
511 */
newKey_
512 PROT_ListManager.prototype.newKey_ = function() {
513 G_Debug(this, "got a new MAC key");
514
515 this.hashCompleter_.setKeys(this.keyManager_.getClientKey(),
516 this.keyManager_.getWrappedKey());
517
518 if (this.keyManager_.hasKey()) {
519 if (this.updateWaitingForKey_) {
520 this.updateWaitingForKey_ = false;
521 this.checkForUpdates();
522 }
523 }
524 }
525
QueryInterface
526 PROT_ListManager.prototype.QueryInterface = function(iid) {
527 if (iid.equals(Ci.nsISupports) ||
528 iid.equals(Ci.nsIUrlListManager) ||
529 iid.equals(Ci.nsITimerCallback))
530 return this;
531
532 Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
533 return null;
534 }
535 //@line 42 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/toolkit/components/url-classifier/src/nsUrlClassifierListManager.js"
536
537 var modScope = this;
Init
538 function Init() {
539 // Pull the library in.
540 var jslib = Cc["@mozilla.org/url-classifier/jslib;1"]
541 .getService().wrappedJSObject;
542 Function.prototype.inherits = jslib.Function.prototype.inherits;
543 modScope.G_Preferences = jslib.G_Preferences;
544 modScope.G_PreferenceObserver = jslib.G_PreferenceObserver;
545 modScope.G_ObserverServiceObserver = jslib.G_ObserverServiceObserver;
546 modScope.G_Debug = jslib.G_Debug;
547 modScope.G_Assert = jslib.G_Assert;
548 modScope.G_debugService = jslib.G_debugService;
549 modScope.G_Alarm = jslib.G_Alarm;
550 modScope.BindToObject = jslib.BindToObject;
551 modScope.PROT_XMLFetcher = jslib.PROT_XMLFetcher;
552 modScope.PROT_UrlCryptoKeyManager = jslib.PROT_UrlCryptoKeyManager;
553 modScope.RequestBackoff = jslib.RequestBackoff;
554
555 // We only need to call Init once.
anon:556:18
556 modScope.Init = function() {};
557 }
558
559 // Module object
UrlClassifierListManagerMod
560 function UrlClassifierListManagerMod() {
561 this.firstTime = true;
562 this.cid = Components.ID("{ca168834-cc00-48f9-b83c-fd018e58cae3}");
563 this.progid = "@mozilla.org/url-classifier/listmanager;1";
564 }
565
registerSelf
566 UrlClassifierListManagerMod.prototype.registerSelf = function(compMgr, fileSpec, loc, type) {
567 if (this.firstTime) {
568 this.firstTime = false;
569 throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
570 }
571 compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
572 compMgr.registerFactoryLocation(this.cid,
573 "UrlClassifier List Manager Module",
574 this.progid,
575 fileSpec,
576 loc,
577 type);
578 };
579
getClassObject
580 UrlClassifierListManagerMod.prototype.getClassObject = function(compMgr, cid, iid) {
581 if (!cid.equals(this.cid))
582 throw Components.results.NS_ERROR_NO_INTERFACE;
583 if (!iid.equals(Ci.nsIFactory))
584 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
585
586 return this.factory;
587 }
588
canUnload
589 UrlClassifierListManagerMod.prototype.canUnload = function(compMgr) {
590 return true;
591 }
592
593 UrlClassifierListManagerMod.prototype.factory = {
createInstance
594 createInstance: function(outer, iid) {
595 if (outer != null)
596 throw Components.results.NS_ERROR_NO_AGGREGATION;
597 Init();
598 return (new PROT_ListManager()).QueryInterface(iid);
599 }
600 };
601
602 var ListManagerModInst = new UrlClassifierListManagerMod();
603
NSGetModule
604 function NSGetModule(compMgr, fileSpec) {
605 return ListManagerModInst;
606 }