For a web developer considering a switch to HTTPS, the biggest obstacle
is often Mixed Content, especially from resources external to your site
(ads, CDN, analytics, etc). Before spending any money and time on a
certificate, you'd like to know what your dependencies are and whether
it's even possible to move them to HTTPS.

The idea, in this demo, is that a web developer would
  (1) Install HTTPS Everywhere and enable Switch Planner mode
  (2) Navigate to all the corners of their existing HTTP web site
(example.com)
  (3) Click the HTTPS Everywhere icon to get a list of all the
third-party domains used by example.com and not rewritable by HTTPS
Everywhere.

If the list is empty: You're good to go! You can deploy a certificate
today, get added to HTTPS Everywhere, and users with the plugin will
start getting a secure experience right away. You can then begin the
slow process of rewriting all the references in your site's source code,
so non-plugin users can be secure too.

If the list is non-empty: Go through it in priority order and see if (a)
the third-party domain is actually available under HTTPS, but not yet
added to HTTPS Everywhere, (b) you can convince the third-party operator
to implement HTTPS, or (c) you can remove or replace the resource.

This is also a useful tool for savvy users to see what would block their
favorite sites from switching, and try to get some of the dependencies
fixed.

Patches (Chrome-only) attached, and also at
https://github.com/jsha/https-everywhere/compare/switchplanner

Ready-built .crx file to try out:
https://jacob.hoffman-andrews.com/hacks/https-everywhere-jsha-switch-planner-demo-v1.crx
(and .asc for sig). Try it on any HTTP site, e.g
http://www.theguardian.com or http://www.washingtonpost.com/. For most
accurate results turn off any ad blockers first.

What do you all think? Is this a feature you'd like to see land in
master? If so I'll work on a cleaner and more usable patch.
From d22c1c51266e6cfa604669645ae31972ab58b692 Mon Sep 17 00:00:00 2001
From: Jacob Hoffman-Andrews <[email protected]>
Date: Mon, 30 Dec 2013 12:34:21 -0500
Subject: [PATCH 2/2] Add details page

---
 chromium/_locales/en/messages.json |  4 +-
 chromium/background.js             | 78 +++++++++++++++++++++++++++++---------
 chromium/popup.html                |  1 +
 chromium/popup.js                  | 14 +++++--
 chromium/switch-planner.html       |  7 ++++
 chromium/switch-planner.js         |  7 ++++
 6 files changed, 89 insertions(+), 22 deletions(-)
 create mode 100644 chromium/switch-planner.html
 create mode 100644 chromium/switch-planner.js

diff --git a/chromium/_locales/en/messages.json b/chromium/_locales/en/messages.json
index fb3a047..f5ff0c1 100644
--- a/chromium/_locales/en/messages.json
+++ b/chromium/_locales/en/messages.json
@@ -99,8 +99,8 @@
         "message": "Switch Planner"
     },
     "switch_planner_description": {
-        "message": "This hostname loaded HTTP resources that couldn't be translated to HTTPS, from these other hostnames: "
-    }
+        "message": "Unrewritten HTTP resources:"
+    },
     "chrome_stable_rules": {
         "message": "Stable rules"
     },
diff --git a/chromium/background.js b/chromium/background.js
index c007bb5..9bee5cc 100644
--- a/chromium/background.js
+++ b/chromium/background.js
@@ -1,5 +1,6 @@
+// TODO: This keeps around history across "clear history" events. Fix that.
 var switchPlannerMode = true;
-var switchPlanner = {};
+var switchPlannerInfo = {};
   console.log("XXX TESTING XXX");
 
 var all_rules = new RuleSets();
@@ -209,14 +210,14 @@ function writeToSwitchPlanner(type, tab_host, resource_host, resource_url, rewri
   // TODO: Maybe also count rewritten URLs separately.
   if (rewritten_url != null) return;
 
-  if (!switchPlanner[tab_host])
-    switchPlanner[tab_host] = {};
-  if (!switchPlanner[tab_host][resource_host])
-    switchPlanner[tab_host][resource_host] = {};
-  if (!switchPlanner[tab_host][resource_host][active_content])
-    switchPlanner[tab_host][resource_host][active_content] = {};
+  if (!switchPlannerInfo[tab_host])
+    switchPlannerInfo[tab_host] = {};
+  if (!switchPlannerInfo[tab_host][resource_host])
+    switchPlannerInfo[tab_host][resource_host] = {};
+  if (!switchPlannerInfo[tab_host][resource_host][active_content])
+    switchPlannerInfo[tab_host][resource_host][active_content] = {};
 
-  switchPlanner[tab_host][resource_host][active_content][resource_url] = 1;
+  switchPlannerInfo[tab_host][resource_host][active_content][resource_url] = 1;
 }
 
 // Return the number of properties in an object. For associative maps, this is
@@ -230,27 +231,36 @@ function objSize(obj) {
   return size;
 }
 
-// Format the switch planner output for presentation to a user.
+// Make an array of asset hosts by score so we can sort them,
+// presenting the most important ones first.
 function sortSwitchPlanner(tab_host) {
-  var output = "";
-
   var asset_host_list = [];
-  var parentInfo = switchPlanner[tab_host];
-  // Make an array of asset hosts by score so we can sort them,
-  // presenting the most important ones first.
+  var parentInfo = switchPlannerInfo[tab_host];
   for (var asset_host in parentInfo) {
     var ah = parentInfo[asset_host];
     var activeCount = objSize(ah[1]);
     var passiveCount = objSize(ah[0]);
-    asset_host_list.push([activeCount * 100 + passiveCount, activeCount, passiveCount, asset_host]);
+    var score = activeCount * 100 + passiveCount;
+    asset_host_list.push([score, activeCount, passiveCount, asset_host]);
   }
   asset_host_list.sort(function(a,b){return a[0]-b[0]});
+  return asset_host_list;
+}
+
+// Format the switch planner output for presentation to a user.
+function switchPlannerSmallHtml(tab_host) {
+  var asset_host_list = sortSwitchPlanner(tab_host);
+  if (asset_host_list.length == 0) {
+    return "<b>none</b>";
+  }
+
+  var output = "";
   for (var i = asset_host_list.length - 1; i >= 0; i--) {
     var host = asset_host_list[i][3];
     var activeCount = asset_host_list[i][1];
     var passiveCount = asset_host_list[i][2];
 
-    output += host + ": ";
+    output += "<b>" + host + "</b>: ";
     if (activeCount > 0) {
       output += activeCount + " active";
       if (passiveCount > 0)
@@ -259,7 +269,41 @@ function sortSwitchPlanner(tab_host) {
     if (passiveCount > 0) {
       output += passiveCount + " passive";
     }
-    output += "\n";
+    output += "<br/>";
+  }
+  return output;
+}
+
+function linksFromKeys(map) {
+  if (typeof map == 'undefined') return "";
+  var output = "";
+  for (var key in map) {
+    if (map.hasOwnProperty(key)) {
+      output += "<a href='" + key + "'>" + key + "</a><br/>";
+    }
+  }
+  return output;
+}
+
+function switchPlannerDetailsHtml(tab_host) {
+  var asset_host_list = sortSwitchPlanner(tab_host);
+  var output = "";
+
+  for (var i = asset_host_list.length - 1; i >= 0; i--) {
+    var host = asset_host_list[i][3];
+    var activeCount = asset_host_list[i][1];
+    var passiveCount = asset_host_list[i][2];
+
+    output += "<b>" + host + "</b>: ";
+    if (activeCount > 0) {
+      output += activeCount + " active<br/>";
+      output += linksFromKeys(switchPlannerInfo[tab_host][host][1]);
+    }
+    if (passiveCount > 0) {
+      output += "<br/>" + passiveCount + " passive<br/>";
+      output += linksFromKeys(switchPlannerInfo[tab_host][host][0]);
+    }
+    output += "<br/>";
   }
   return output;
 }
diff --git a/chromium/popup.html b/chromium/popup.html
index 3b57585..e72fdd1 100755
--- a/chromium/popup.html
+++ b/chromium/popup.html
@@ -15,6 +15,7 @@
 <section id="SwitchPlanner" class="switchplanner">
   <h3 i18n="switch_planner">Switch Planner</h3>
   <p class="description" i18n="switch_planner_description"></p>
+  <a id=SwitchPlannerDetails href="javascript:void(0);">details</a>
 </section>
 <section id="StableRules" class="rules">
   <h3 i18n="chrome_stable_rules"></h3>
diff --git a/chromium/popup.js b/chromium/popup.js
index 12d8efc..a7df63f 100644
--- a/chromium/popup.js
+++ b/chromium/popup.js
@@ -62,13 +62,21 @@ function createRuleLine(ruleset) {
 function gotTab(tab) {
   if (backgroundPage.switchPlannerMode) {
     // XXX: Call URI here, but it's not in-scope. Need to make it in-scope.
-    var tab_host = tab.url.match(/https?:\/\/([^\/]*)/)[1];
+    var tab_hostname = tab.url.match(/https?:\/\/([^\/]*)/)[1];
+
+    var detailsLink = document.getElementById("SwitchPlannerDetails");
+    detailsLink.onclick = function() {
+      window.open("switch-planner.html?host=" + tab_hostname);
+    };
+
     var switchPlannerTextDiv = document.createElement("div");
-    var switchPlannerText = backgroundPage.sortSwitchPlanner(tab_host);
+    var switchPlannerText = backgroundPage.switchPlannerSmallHtml(tab_hostname);
     switchPlannerTextDiv.innerHTML = switchPlannerText;
-    switchPlannerDiv.appendChild(switchPlannerTextDiv);
+    switchPlannerDiv.className = "switch_planner_info";
     switchPlannerDiv.style.position = "static";
     switchPlannerDiv.style.visibility = "visible";
+
+    switchPlannerDiv.insertBefore(switchPlannerTextDiv, detailsLink);
   }
 
   var rulesets = backgroundPage.activeRulesets.getRulesets(tab.id);
diff --git a/chromium/switch-planner.html b/chromium/switch-planner.html
new file mode 100644
index 0000000..87fe4b5
--- /dev/null
+++ b/chromium/switch-planner.html
@@ -0,0 +1,7 @@
+<head>
+<title></title>
+<script src="switch-planner.js"></script>
+</head>
+<body>
+<div id=content />
+</body>
diff --git a/chromium/switch-planner.js b/chromium/switch-planner.js
new file mode 100644
index 0000000..c2e8999
--- /dev/null
+++ b/chromium/switch-planner.js
@@ -0,0 +1,7 @@
+window.onload = function() {
+  var backgroundPage = chrome.extension.getBackgroundPage(); 
+  var hostname = document.location.search.match(/host=([^&]*)/)[1];
+  document.getElementById("content").innerHTML =
+    backgroundPage.switchPlannerDetailsHtml(hostname);
+};
+
-- 
1.8.3.2

From 11aa454edf501523acb64eb91480b2160bcbb8ec Mon Sep 17 00:00:00 2001
From: Jacob Hoffman-Andrews <[email protected]>
Date: Sat, 28 Dec 2013 16:30:37 -0500
Subject: [PATCH 1/2] First working version of switch planner

---
 chromium/_locales/en/messages.json |   6 ++
 chromium/background.js             | 157 +++++++++++++++++++++++++++++++------
 chromium/popup.html                |   4 +
 chromium/popup.js                  |  13 +++
 4 files changed, 158 insertions(+), 22 deletions(-)

diff --git a/chromium/_locales/en/messages.json b/chromium/_locales/en/messages.json
index b67838d..fb3a047 100644
--- a/chromium/_locales/en/messages.json
+++ b/chromium/_locales/en/messages.json
@@ -95,6 +95,12 @@
     "source_unable_to_download": {
         "message": "Unable to download source."
     },
+    "switch_planner": {
+        "message": "Switch Planner"
+    },
+    "switch_planner_description": {
+        "message": "This hostname loaded HTTP resources that couldn't be translated to HTTPS, from these other hostnames: "
+    }
     "chrome_stable_rules": {
         "message": "Stable rules"
     },
diff --git a/chromium/background.js b/chromium/background.js
index c5a54d0..c007bb5 100644
--- a/chromium/background.js
+++ b/chromium/background.js
@@ -1,3 +1,6 @@
+var switchPlannerMode = true;
+var switchPlanner = {};
+  console.log("XXX TESTING XXX");
 
 var all_rules = new RuleSets();
 var wr = chrome.webRequest;
@@ -81,33 +84,30 @@ var redirectCounter = {};
 function onBeforeRequest(details) {
   // get URL into canonical format
   // todo: check that this is enough
-  var tmpuri = new URI(details.url);
+  var uri = new URI(details.url);
 
   // Normalise hosts such as "www.example.com."
-  var tmphost = tmpuri.hostname();
-  if (tmphost.charAt(tmphost.length - 1) == ".") {
-    while (tmphost.charAt(tmphost.length - 1) == ".")
-      tmphost = tmphost.slice(0,-1);
+  var canonical_host = uri.hostname();
+  if (canonical_host.charAt(canonical_host.length - 1) == ".") {
+    while (canonical_host.charAt(canonical_host.length - 1) == ".")
+      canonical_host = canonical_host.slice(0,-1);
   }
-  tmpuri.hostname(tmphost);
+  uri.hostname(canonical_host);
 
   // If there is a username / password, put them aside during the ruleset
   // analysis process
-  var tmpuserinfo = tmpuri.userinfo();
-  tmpuri.userinfo('');
+  var tmpuserinfo = uri.userinfo();
+  uri.userinfo('');
 
-  var canonical_url = tmpuri.toString();
+  var canonical_url = uri.toString();
   if (details.url != canonical_url && tmpuserinfo === '') {
     log(INFO, "Original url " + details.url + 
         " changed before processing to " + canonical_url);
   }
   if (canonical_url in urlBlacklist) {
-    return;
+    return null;
   }
 
-  var a = document.createElement("a");
-  a.href = canonical_url;
-
   if (details.type == "main_frame") {
     activeRulesets.removeTab(details.tabId);
   }
@@ -123,15 +123,14 @@ function onBeforeRequest(details) {
   if (redirectCounter[details.requestId] > 9) {
     log(NOTE, "Redirect counter hit for "+canonical_url);
     urlBlacklist[canonical_url] = true;
-    var hostname = tmpuri.hostname();
-    domainBlacklist[hostname] = true;
-    log(WARN, "Domain blacklisted " + hostname);
-    return;
+    domainBlacklist[canonical_host] = true;
+    log(WARN, "Domain blacklisted " + canonical_host);
+    return null;
   }
 
   var newuristr = null;
 
-  var rs = all_rules.potentiallyApplicableRulesets(a.hostname);
+  var rs = all_rules.potentiallyApplicableRulesets(canonical_host);
   for(var i = 0; i < rs.length; ++i) {
     activeRulesets.addRulesetToTab(details.tabId, rs[i]);
     if (rs[i].active && !newuristr)
@@ -140,15 +139,129 @@ function onBeforeRequest(details) {
 
   displayPageAction(details.tabId);
 
-  if (newuristr) {
+  if (newuristr && tmpuserinfo != "") {
     // re-insert userpass info which was stripped temporarily
     // while rules were applied
     var finaluri = new URI(newuristr);
     finaluri.userinfo(tmpuserinfo);
-    var finaluristr = finaluri.toString();
-    log(DBUG, "Redirecting from "+a.href+" to "+finaluristr);
-    return {redirectUrl: finaluristr};
+    newuristr = finaluri.toString();
+  }
+
+  // In Switch Planner Mode, record any non-rewriteable
+  // HTTP URIs by parent hostname, along with the resource type.
+  if (switchPlannerMode && uri.protocol() !== "https") {
+    // In order to figure out the document requesting this resource,
+    // have to get the tab. TODO: any cheaper way?
+    // XXX: Because this is async it's actually inaccurate during quick page
+    // switches. Maybe it only matters when you're switching domains though?
+    chrome.tabs.get(details.tabId, function(tab) {
+      var tab_host = new URI(tab.url).hostname();
+      if (tab_host !== canonical_host) {
+        writeToSwitchPlanner(details.type,
+                             tab_host,
+                             canonical_host,
+                             details.url,
+                             newuristr);
+      }
+    });
+  }
+
+  if (newuristr) {
+    log(DBUG, "Redirecting from "+details.url+" to "+newuristr);
+    return {redirectUrl: newuristr};
+  } else {
+    return null;
+  }
+}
+
+
+// Map of which values for the `type' enum denote active vs passive content.
+// https://developer.chrome.com/extensions/webRequest.html#event-onBeforeRequest
+var activeTypes = { stylesheet: 1, script: 1, object: 1, other: 1};
+// We consider sub_frame to be passive even though it can contain JS or Flash.
+// This is because code running the sub_frame cannot access the main frame's
+// content, by same-origin policy. This is true even if the sub_frame is on the
+// same domain but different protocol - i.e. HTTP while the parent is HTTPS -
+// because same-origin policy includes the protocol. This also mimics Chrome's
+// UI treatment of insecure subframes.
+var passiveTypes = { main_frame: 1, sub_frame: 1, image: 1, xmlhttprequest: 1};
+
+// Record a non-HTTPS URL loaded by a given hostname in the Switch Planner, for
+// use in determining which resources need to be ported to HTTPS.
+// TODO: Maybe unique by resource URL, so reloading a single page doesn't double
+// the counts?
+function writeToSwitchPlanner(type, tab_host, resource_host, resource_url, rewritten_url) {
+  var rw = "rw";
+  if (rewritten_url == null)
+    rw = "no";
+
+  var active_content = 0;
+  if (activeTypes[type]) {
+    active_content = 1;
+  } else if (passiveTypes[type]) {
+    active_content = 0;
+  } else {
+    log(WARN, "Unknown type from onBeforeRequest details: `" + type + "', assuming active");
+    active_content = 1;
+  }
+
+  // Only add if we were unable to rewrite this URL.
+  // TODO: Maybe also count rewritten URLs separately.
+  if (rewritten_url != null) return;
+
+  if (!switchPlanner[tab_host])
+    switchPlanner[tab_host] = {};
+  if (!switchPlanner[tab_host][resource_host])
+    switchPlanner[tab_host][resource_host] = {};
+  if (!switchPlanner[tab_host][resource_host][active_content])
+    switchPlanner[tab_host][resource_host][active_content] = {};
+
+  switchPlanner[tab_host][resource_host][active_content][resource_url] = 1;
+}
+
+// Return the number of properties in an object. For associative maps, this is
+// their size.
+function objSize(obj) {
+  if (typeof obj == 'undefined') return 0;
+  var size = 0, key;
+  for (key in obj) {
+    if (obj.hasOwnProperty(key)) size++;
+  }
+  return size;
+}
+
+// Format the switch planner output for presentation to a user.
+function sortSwitchPlanner(tab_host) {
+  var output = "";
+
+  var asset_host_list = [];
+  var parentInfo = switchPlanner[tab_host];
+  // Make an array of asset hosts by score so we can sort them,
+  // presenting the most important ones first.
+  for (var asset_host in parentInfo) {
+    var ah = parentInfo[asset_host];
+    var activeCount = objSize(ah[1]);
+    var passiveCount = objSize(ah[0]);
+    asset_host_list.push([activeCount * 100 + passiveCount, activeCount, passiveCount, asset_host]);
+  }
+  asset_host_list.sort(function(a,b){return a[0]-b[0]});
+  for (var i = asset_host_list.length - 1; i >= 0; i--) {
+    var host = asset_host_list[i][3];
+    var activeCount = asset_host_list[i][1];
+    var passiveCount = asset_host_list[i][2];
+
+    output += host + ": ";
+    if (activeCount > 0) {
+      output += activeCount + " active";
+      if (passiveCount > 0)
+        output += ", ";
+    }
+    if (passiveCount > 0) {
+      output += passiveCount + " passive";
+    }
+    output += "\n";
   }
+  return output;
 }
 
 function onCookieChanged(changeInfo) {
diff --git a/chromium/popup.html b/chromium/popup.html
index 7c9be67..3b57585 100755
--- a/chromium/popup.html
+++ b/chromium/popup.html
@@ -12,6 +12,10 @@
 <header>
   <h1 i18n="about_ext_name"></h1>
 </header>
+<section id="SwitchPlanner" class="switchplanner">
+  <h3 i18n="switch_planner">Switch Planner</h3>
+  <p class="description" i18n="switch_planner_description"></p>
+</section>
 <section id="StableRules" class="rules">
   <h3 i18n="chrome_stable_rules"></h3>
   <p class="description" i18n="chrome_stable_rules_description"></p>
diff --git a/chromium/popup.js b/chromium/popup.js
index 1f1475b..12d8efc 100644
--- a/chromium/popup.js
+++ b/chromium/popup.js
@@ -1,6 +1,7 @@
 var backgroundPage = null;
 var stableRules = null;
 var unstableRules = null;
+var switchPlannerDiv = null;
 var hostReg = /.*\/\/[^$/]*\//;
 
 function toggleRuleLine(checkbox, ruleset) {
@@ -59,6 +60,17 @@ function createRuleLine(ruleset) {
 }
 
 function gotTab(tab) {
+  if (backgroundPage.switchPlannerMode) {
+    // XXX: Call URI here, but it's not in-scope. Need to make it in-scope.
+    var tab_host = tab.url.match(/https?:\/\/([^\/]*)/)[1];
+    var switchPlannerTextDiv = document.createElement("div");
+    var switchPlannerText = backgroundPage.sortSwitchPlanner(tab_host);
+    switchPlannerTextDiv.innerHTML = switchPlannerText;
+    switchPlannerDiv.appendChild(switchPlannerTextDiv);
+    switchPlannerDiv.style.position = "static";
+    switchPlannerDiv.style.visibility = "visible";
+  }
+
   var rulesets = backgroundPage.activeRulesets.getRulesets(tab.id);
 
   for (var r in rulesets) {
@@ -76,6 +88,7 @@ document.addEventListener("DOMContentLoaded", function () {
   backgroundPage = chrome.extension.getBackgroundPage();
   stableRules = document.getElementById("StableRules");
   unstableRules = document.getElementById("UnstableRules");
+  switchPlannerDiv = document.getElementById("SwitchPlanner");
   chrome.tabs.getSelected(null, gotTab);
 
   // auto-translate all elements with i18n attributes
-- 
1.8.3.2

Attachment: signature.asc
Description: OpenPGP digital signature

_______________________________________________
HTTPS-Everywhere mailing list
[email protected]
https://lists.eff.org/mailman/listinfo/https-everywhere

Reply via email to