From faf749e67ea8f11ae14f8dedcd68d36613396691 Mon Sep 17 00:00:00 2001 From: Jared Wein Date: Thu, 18 Oct 2012 18:02:42 -0700 Subject: [PATCH] Bug 787767 - Implement runtime performance warnings for Worker API abuse. r=felipe --- 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 | 39 +++++- toolkit/locales/jar.mn | 1 + 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 browser/base/content/test/browser_social_usageMonitor.js diff --git a/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in index 71fc9cdedc2d..379cb17beea9 100644 --- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -275,6 +275,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 000000000000..05ef0fcad4cf --- /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 9d1a4abed5fa..637892a83c4e 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 ce1bbaedc4ee..db2ec155d19f 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -3780,6 +3780,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 c09d9e4712fd..bb91f0f020a3 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); @@ -69,7 +74,7 @@ WorkerAPI.prototype = { cookies.forEach(function(aCookie) { let [name, value] = aCookie.split("="); results.push({name: unescape(name.trim()), - value: unescape(value.trim())}); + value: value ? unescape(value.trim()) : ""}); }); this._port.postMessage({topic: "social.cookies-get-response", data: results}); @@ -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/jar.mn b/toolkit/locales/jar.mn index 39fc48b7964b..039f20e9045c 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)