From c5743888357ea7f4e9da654225ae3185fafe254c Mon Sep 17 00:00:00 2001 From: Jared Wein Date: Sun, 21 Oct 2012 17:26:11 -0700 Subject: [PATCH] Bug 787767 - Implement runtime performance monitoring for Worker API abuse. r=felipe --HG-- extra : rebase_source : 93e027809cc3f6ceb2939cb2e3919c3516d05be7 --- browser/base/content/test/Makefile.in | 1 + .../test/browser_social_usageMonitor.js | 121 ++++++++++++++++++ browser/base/content/test/social_worker.js | 4 + modules/libpref/src/init/all.js | 2 + toolkit/components/social/WorkerAPI.jsm | 37 ++++++ .../en-US/chrome/global/social.properties | 6 + toolkit/locales/jar.mn | 1 + 7 files changed, 172 insertions(+) create mode 100644 browser/base/content/test/browser_social_usageMonitor.js create mode 100644 toolkit/locales/en-US/chrome/global/social.properties diff --git a/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in index 98ee9c76772..618f64052c8 100644 --- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -277,6 +277,7 @@ _BROWSER_FILES = \ browser_social_mozSocial_API.js \ browser_social_isVisible.js \ browser_social_chatwindow.js \ + browser_social_usageMonitor.js \ social_panel.html \ social_share_image.png \ social_sidebar.html \ diff --git a/browser/base/content/test/browser_social_usageMonitor.js b/browser/base/content/test/browser_social_usageMonitor.js new file mode 100644 index 00000000000..05ef0fcad4c --- /dev/null +++ b/browser/base/content/test/browser_social_usageMonitor.js @@ -0,0 +1,121 @@ +/* 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/. */ + +// A mock notifications server. Based on: +// dom/tests/mochitest/notification/notification_common.js +const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator).generateUUID(); + +const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1"; +const ALERTS_SERVICE_CID = Components.ID(Cc[ALERTS_SERVICE_CONTRACT_ID].number); + +function MockAlertsService() {} + +MockAlertsService.prototype = { + + showAlertNotification: function(imageUrl, title, text, textClickable, + cookie, alertListener, name) { + let obData = JSON.stringify({ + imageUrl: imageUrl, + title: title, + text: text, + textClickable: textClickable, + cookie: cookie, + name: name + }); + Services.obs.notifyObservers(null, "social-test:notification-alert", obData); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsISupports) || + aIID.equals(Ci.nsIAlertsService)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +var factory = { + createInstance: function(aOuter, aIID) { + if (aOuter != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return new MockAlertsService().QueryInterface(aIID); + } +}; + +function replacePromptService() { + Components.manager.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(FAKE_CID, "", + ALERTS_SERVICE_CONTRACT_ID, + factory) +} + +function restorePromptService() { + Components.manager.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(ALERTS_SERVICE_CID, "", + ALERTS_SERVICE_CONTRACT_ID, + null); +} +// end of alerts service mock. + +function test() { + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" + }; + Services.prefs.setBoolPref("social.debug.monitorUsage", true); + Services.prefs.setIntPref("social.debug.monitorUsageTimeLimitMS", 1000); + replacePromptService(); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("social.debug.monitorUsage"); + Services.prefs.clearUserPref("social.debug.monitorUsageTimeLimitMS"); + restorePromptService(); + }); + + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +var tests = { + testWorkerAPIAbuse: function(next) { + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + Services.obs.addObserver(function abuseObserver(subject, topic, data) { + Services.obs.removeObserver(abuseObserver, "social-test:notification-alert"); + data = JSON.parse(data); + is(data.title, "provider 1", "Abusive provider name should match"); + is(data.text, + "Social API performance warning: More than 10 calls to social.cookies-get in less than 10 seconds.", + "Usage warning should mention social.cookies-get"); + next(); + }, "social-test:notification-alert", false); + + for (let i = 0; i < 15; i++) + port.postMessage({topic: "test-worker-spam-message"}); + }, + testTimeBetweenFirstAndLastMoreThanLimit: function(next) { + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + Services.obs.addObserver(function abuseObserver(subject, topic, data) { + Services.obs.removeObserver(abuseObserver, "social-test:notification-alert"); + data = JSON.parse(data); + is(data.title, "provider 1", "Abusive provider name should match"); + is(data.text, + "Social API performance warning: More than 10 calls to social.cookies-get in less than 10 seconds.", + "Usage warning should mention social.cookies-get"); + next(); + }, "social-test:notification-alert", false); + + port.postMessage({topic: "test-worker-spam-message"}); + setTimeout(function() { + for (let i = 0; i < 15; i++) + port.postMessage({topic: "test-worker-spam-message"}); + }, 2000); + } +} diff --git a/browser/base/content/test/social_worker.js b/browser/base/content/test/social_worker.js index 9d1a4abed5f..637892a83c4 100644 --- a/browser/base/content/test/social_worker.js +++ b/browser/base/content/test/social_worker.js @@ -76,6 +76,10 @@ onconnect = function(e) { case "test-worker-chat": apiPort.postMessage({topic: "social.request-chat", data: event.data.data }); break; + case "test-worker-spam-message": + // Just use a random api message, but one that has little side-effects. + apiPort.postMessage({topic: "social.cookies-get"}); + break; case "social.initialize": // This is the workerAPI port, respond and set up a notification icon. apiPort = port; diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 74ed5a57de5..0cbe25cbaae 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -3791,6 +3791,8 @@ pref("memory.low_memory_notification_interval_ms", 10000); pref("memory.ghost_window_timeout_seconds", 60); pref("social.enabled", false); +pref("social.debug.monitorUsage", false); +pref("social.debug.monitorUsageTimeThresholdMS", 10000); // Disable idle observer fuzz, because only privileged content can access idle // observers (bug 780507). diff --git a/toolkit/components/social/WorkerAPI.jsm b/toolkit/components/social/WorkerAPI.jsm index 5d443e6260a..bb91f0f020a 100644 --- a/toolkit/components/social/WorkerAPI.jsm +++ b/toolkit/components/social/WorkerAPI.jsm @@ -22,6 +22,9 @@ function WorkerAPI(provider, port) { this._provider = provider; this._port = port; this._port.onmessage = this._handleMessage.bind(this); + this._usageMonitor = Services.prefs.getBoolPref("social.debug.monitorUsage") ? + new WorkerAPIUsageMonitor(provider) : + null; // Send an "intro" message so the worker knows this is the port // used for the api. @@ -42,6 +45,8 @@ WorkerAPI.prototype = { return; } try { + if (this._usageMonitor) + this._usageMonitor.logMessage(topic); handler.call(this, data); } catch (ex) { Cu.reportError("WorkerAPI: failed to handle message '" + topic + "': " + ex); @@ -130,3 +135,35 @@ WorkerAPI.prototype = { }, } } + +function WorkerAPIUsageMonitor(provider) { + if (!provider) + throw new Error("Can't initialize WorkerAPIUsageMonitor with a null provider"); + this._providerName = provider.name; + this.TIME_THRESHOLD_MS = Services.prefs.getIntPref("social.debug.monitorUsageTimeThresholdMS"); + this._messages = {}; +} + +WorkerAPIUsageMonitor.prototype = { + logMessage: function WorkerAPIUsage_logMessage(aMessage) { + if (!(aMessage in this._messages)) { + this._messages[aMessage] = []; + } + let messageList = this._messages[aMessage]; + messageList.push(Date.now()); + if (messageList.length > 10) { + if (messageList[9] - messageList[0] < this.TIME_THRESHOLD_MS) { + let alertsService = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService); + const SOCIAL_BUNDLE = "chrome://global/locale/social.properties"; + let socialBundle = Services.strings.createBundle(SOCIAL_BUNDLE); + let seconds = (this.TIME_THRESHOLD_MS / 1000).toString(); + let text = socialBundle.formatStringFromName("social.usageAbuse", + [aMessage, seconds], 2); + alertsService.showAlertNotification("chrome://branding/content/icon48.png", + this._providerName, text); + } + messageList.shift(); + } + } +}; diff --git a/toolkit/locales/en-US/chrome/global/social.properties b/toolkit/locales/en-US/chrome/global/social.properties new file mode 100644 index 00000000000..e066ccb24b7 --- /dev/null +++ b/toolkit/locales/en-US/chrome/global/social.properties @@ -0,0 +1,6 @@ +# 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/. + +# LOCALIZATION NOTE (social.usage): This is only used when debugging. +social.usageAbuse=Social API performance warning: More than 10 calls to %S in less than %S seconds. diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn index 39fc48b7964..039f20e9045 100644 --- a/toolkit/locales/jar.mn +++ b/toolkit/locales/jar.mn @@ -58,6 +58,7 @@ locale/@AB_CD@/global/printProgress.dtd (%chrome/global/printProgress.dtd) locale/@AB_CD@/global/regionNames.properties (%chrome/global/regionNames.properties) locale/@AB_CD@/global/resetProfile.dtd (%chrome/global/resetProfile.dtd) + locale/@AB_CD@/global/social.properties (%chrome/global/social.properties) locale/@AB_CD@/global/dialog.properties (%chrome/global/dialog.properties) locale/@AB_CD@/global/tree.dtd (%chrome/global/tree.dtd) locale/@AB_CD@/global/textcontext.dtd (%chrome/global/textcontext.dtd)