bug 557598 - Support strict-transport-security (STS) in private browsing mode; r=ehsan,dveditz a=blocking-betaN+
authorSid Stamm <sstamm@mozilla.com>
Wed, 06 Oct 2010 10:07:39 -0700
changeset 61189 57db6ae6d83266e95f8d462520e0bc7d7868a9fc
parent 61188 2221636653f7704f5a53c36aa7195b4adfaa3374
child 61190 07e249383f3f61d1908d369c27aba94afc55c2be
push idunknown
push userunknown
push dateunknown
reviewersehsan, dveditz, blocking-betaN
bugs557598
milestone2.0b10pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 557598 - Support strict-transport-security (STS) in private browsing mode; r=ehsan,dveditz a=blocking-betaN+
netwerk/base/public/nsIStrictTransportSecurityService.idl
netwerk/protocol/http/nsHttpChannel.cpp
security/manager/boot/src/nsStrictTransportSecurityService.cpp
security/manager/boot/src/nsStrictTransportSecurityService.h
security/manager/ssl/tests/mochitest/stricttransportsecurity/Makefile.in
security/manager/ssl/tests/mochitest/stricttransportsecurity/nosts_bootstrap.html
security/manager/ssl/tests/mochitest/stricttransportsecurity/nosts_bootstrap.html^headers^
security/manager/ssl/tests/mochitest/stricttransportsecurity/test_sts_privatebrowsing.html
--- a/netwerk/base/public/nsIStrictTransportSecurityService.idl
+++ b/netwerk/base/public/nsIStrictTransportSecurityService.idl
@@ -33,16 +33,17 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
 
 interface nsIURI;
+interface nsIObserver;
 interface nsIHttpChannel;
 
 [scriptable, uuid(16955eee-6c48-4152-9309-c42a465138a1)]
 interface nsIStrictTransportSecurityService : nsISupports
 {
     /**
      * Parses a given HTTP header and records the results internally.
      * The format of the STS header is defined by the STS specification:
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -905,23 +905,16 @@ nsHttpChannel::ShouldSSLProxyResponseCon
  *
  * @return NS_ERROR_FAILURE if there's security information missing even though
  *             it's an HTTPS connection.
  */
 nsresult
 nsHttpChannel::ProcessSTSHeader()
 {
     nsresult rv;
-
-    // We need to check private browsing mode here since some permissions are
-    // allowed to be tweaked when private browsing mode is enabled, but STS is
-    // not allowed to operate at all in PBM.
-    if (gHttpHandler->InPrivateBrowsingMode())
-        return NS_OK;
-
     PRBool isHttps = PR_FALSE;
     rv = mURI->SchemeIs("https", &isHttps);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // If this channel is not loading securely, STS doesn't do anything.
     // The upgrade to HTTPS takes place earlier in the channel load process.
     if (!isHttps)
         return NS_OK;
--- a/security/manager/boot/src/nsStrictTransportSecurityService.cpp
+++ b/security/manager/boot/src/nsStrictTransportSecurityService.cpp
@@ -35,16 +35,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "plstr.h"
 #include "prlog.h"
 #include "prprf.h"
 #include "nsCRTGlue.h"
 #include "nsIPermissionManager.h"
+#include "nsIPrivateBrowsingService.h"
 #include "nsISSLStatus.h"
 #include "nsISSLStatusProvider.h"
 #include "nsStrictTransportSecurityService.h"
 #include "nsIURI.h"
 #include "nsInt64.h"
 #include "nsNetUtil.h"
 #include "nsThreadUtils.h"
 #include "nsStringGlue.h"
@@ -56,35 +57,72 @@ PRLogModuleInfo *gSTSLog = PR_NewLogModu
 #define STSLOG(args) PR_LOG(gSTSLog, 4, args)
 
 #define STS_PARSER_FAIL_IF(test,args) \
   if (test) { \
     STSLOG(args); \
     return NS_ERROR_FAILURE; \
   }
 
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsSTSHostEntry::nsSTSHostEntry(const char* aHost)
+  : mHost(aHost)
+  , mExpireTime(0)
+  , mDeleted(PR_FALSE)
+  , mIncludeSubdomains(PR_FALSE)
+{
+}
+
+nsSTSHostEntry::nsSTSHostEntry(const nsSTSHostEntry& toCopy)
+  : mHost(toCopy.mHost)
+  , mExpireTime(toCopy.mExpireTime)
+  , mDeleted(toCopy.mDeleted)
+  , mIncludeSubdomains(toCopy.mIncludeSubdomains)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+
 nsStrictTransportSecurityService::nsStrictTransportSecurityService()
+  : mInPrivateMode(PR_FALSE)
 {
 }
 
 nsStrictTransportSecurityService::~nsStrictTransportSecurityService()
 {
 }
 
-NS_IMPL_THREADSAFE_ISUPPORTS1(nsStrictTransportSecurityService,
+NS_IMPL_THREADSAFE_ISUPPORTS2(nsStrictTransportSecurityService,
+                              nsIObserver,
                               nsIStrictTransportSecurityService)
 
 nsresult
 nsStrictTransportSecurityService::Init()
 {
    nsresult rv;
 
    mPermMgr = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
    NS_ENSURE_SUCCESS(rv, rv);
 
+   // figure out if we're starting in private browsing mode
+   nsCOMPtr<nsIPrivateBrowsingService> pbs =
+     do_GetService(NS_PRIVATE_BROWSING_SERVICE_CONTRACTID);
+   if (pbs)
+     pbs->GetPrivateBrowsingEnabled(&mInPrivateMode);
+
+   mObserverService = mozilla::services::GetObserverService();
+   if (mObserverService)
+     mObserverService->AddObserver(this, NS_PRIVATE_BROWSING_SWITCH_TOPIC, PR_FALSE);
+
+   if (mInPrivateMode && !mPrivateModeHostTable.Init())
+     return NS_ERROR_OUT_OF_MEMORY;
+
    return NS_OK;
 }
 
 nsresult
 nsStrictTransportSecurityService::GetHost(nsIURI *aURI, nsACString &aResult)
 {
   nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aURI);
   if (!innerURI) return NS_ERROR_FAILURE;
@@ -107,54 +145,62 @@ nsStrictTransportSecurityService::SetSts
   if (!maxage)
     return RemoveStsState(aSourceURI);
 
   // Expire time is millis from now.  Since STS max-age is in seconds, and
   // PR_Now() is in micros, must equalize the units at milliseconds.
   PRInt64 expiretime = (PR_Now() / 1000) + (maxage * 1000);
 
   // record entry for this host with max-age in the permissions manager
-  mPermMgr->Add(aSourceURI, STS_PERMISSION,
+  STSLOG(("STS: maxage permission SET, adding permission\n"));
+  nsresult rv = AddPermission(aSourceURI,
+                              STS_PERMISSION,
                               (PRUint32) nsIPermissionManager::ALLOW_ACTION,
                               (PRUint32) nsIPermissionManager::EXPIRE_TIME,
                               expiretime);
-  STSLOG(("STS: set maxage permission\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
 
   if (includeSubdomains) {
     // record entry for this host with include subdomains in the permissions manager
-    mPermMgr->Add(aSourceURI, STS_SUBDOMAIN_PERMISSION,
-                                (PRUint32) nsIPermissionManager::ALLOW_ACTION,
-                                (PRUint32) nsIPermissionManager::EXPIRE_TIME,
-                                expiretime);
-    STSLOG(("STS: set subdomains permission\n"));
+    STSLOG(("STS: subdomains permission SET, adding permission\n"));
+    rv = AddPermission(aSourceURI,
+                       STS_SUBDOMAIN_PERMISSION,
+                       (PRUint32) nsIPermissionManager::ALLOW_ACTION,
+                       (PRUint32) nsIPermissionManager::EXPIRE_TIME,
+                       expiretime);
+    NS_ENSURE_SUCCESS(rv, rv);
   } else { // !includeSubdomains
     nsCAutoString hostname;
-    nsresult rv = GetHost(aSourceURI, hostname);
+    rv = GetHost(aSourceURI, hostname);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    mPermMgr->Remove(hostname, STS_SUBDOMAIN_PERMISSION);
+    STSLOG(("STS: subdomains permission UNSET, removing any existing ones\n"));
+    rv = RemovePermission(hostname, STS_SUBDOMAIN_PERMISSION);
+    NS_ENSURE_SUCCESS(rv, rv);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsStrictTransportSecurityService::RemoveStsState(nsIURI* aURI)
 {
   // Should be called on the main thread (or via proxy) since the permission
   // manager is used and it's not threadsafe.
   NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
 
   nsCAutoString hostname;
   nsresult rv = GetHost(aURI, hostname);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  mPermMgr->Remove(hostname, STS_PERMISSION);
+  rv = RemovePermission(hostname, STS_PERMISSION);
+  NS_ENSURE_SUCCESS(rv, rv);
   STSLOG(("STS: deleted maxage permission\n"));
 
-  mPermMgr->Remove(hostname, STS_SUBDOMAIN_PERMISSION);
+  rv = RemovePermission(hostname, STS_SUBDOMAIN_PERMISSION);
+  NS_ENSURE_SUCCESS(rv, rv);
   STSLOG(("STS: deleted subdomains permission\n"));
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsStrictTransportSecurityService::ProcessStsHeader(nsIURI* aSourceURI,
                                                    const char* aHeader)
@@ -290,21 +336,22 @@ nsStrictTransportSecurityService::IsStsU
 {
   // Should be called on the main thread (or via proxy) since the permission
   // manager is used and it's not threadsafe.
   NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
 
   nsresult rv;
   PRUint32 permExact, permGeneral;
   // If this domain has the forcehttps permission, this is an STS host.
-  rv = mPermMgr->TestExactPermission(aURI, STS_PERMISSION, &permExact);
+  rv = TestPermission(aURI, STS_PERMISSION, &permExact, PR_TRUE);
   NS_ENSURE_SUCCESS(rv, rv);
+
   // If any super-domain has the includeSubdomains permission, this is an
   // STS host.
-  rv = mPermMgr->TestPermission(aURI, STS_SUBDOMAIN_PERMISSION, &permGeneral);
+  rv = TestPermission(aURI, STS_SUBDOMAIN_PERMISSION, &permGeneral, PR_FALSE);
   NS_ENSURE_SUCCESS(rv, rv);
 
   *aResult = ((permExact   == nsIPermissionManager::ALLOW_ACTION) ||
               (permGeneral == nsIPermissionManager::ALLOW_ACTION));
   return NS_OK;
 }
 
 
@@ -336,8 +383,244 @@ nsStrictTransportSecurityService::Should
 
   rv = sslstat->GetIsUntrusted(&trustcheck);
   NS_ENSURE_SUCCESS(rv, rv);
   tlsIsBroken = tlsIsBroken || trustcheck;
 
   *aResult = tlsIsBroken;
   return NS_OK;
 }
+
+//------------------------------------------------------------
+// nsStrictTransportSecurityService::nsIObserver
+//------------------------------------------------------------
+
+NS_IMETHODIMP
+nsStrictTransportSecurityService::Observe(nsISupports *subject,
+                                          const char *topic,
+                                          const PRUnichar *data)
+{
+  if (strcmp(topic, NS_PRIVATE_BROWSING_SWITCH_TOPIC) == 0) {
+    if(NS_LITERAL_STRING(NS_PRIVATE_BROWSING_ENTER).Equals(data)) {
+      // Indication to start recording stuff locally and not writing changes
+      // out to the permission manager.
+
+      if (!mPrivateModeHostTable.IsInitialized()
+          && !mPrivateModeHostTable.Init()) {
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+      mInPrivateMode = PR_TRUE;
+    }
+    else if (NS_LITERAL_STRING(NS_PRIVATE_BROWSING_LEAVE).Equals(data)) {
+      mPrivateModeHostTable.Clear();
+      mInPrivateMode = PR_FALSE;
+    }
+  }
+
+  return NS_OK;
+}
+
+//------------------------------------------------------------
+// Functions to overlay the permission manager calls in case
+// we're in private browsing mode.
+//------------------------------------------------------------
+nsresult
+nsStrictTransportSecurityService::AddPermission(nsIURI     *aURI,
+                                                const char *aType,
+                                                PRUint32   aPermission,
+                                                PRUint32   aExpireType,
+                                                PRInt64    aExpireTime)
+{
+    // Private mode doesn't address user-set (EXPIRE_NEVER) permissions: let
+    // those be stored persistently.
+    if (!mInPrivateMode || aExpireType == nsIPermissionManager::EXPIRE_NEVER) {
+      // Not in private mode, or manually-set permission
+      return mPermMgr->Add(aURI, aType, aPermission, aExpireType, aExpireTime);
+    }
+
+    nsCAutoString host;
+    nsresult rv = GetHost(aURI, host);
+    NS_ENSURE_SUCCESS(rv, rv);
+    STSLOG(("AddPermission for entry for for %s", host.get()));
+
+    // Update in mPrivateModeHostTable only, so any changes will be rolled
+    // back when exiting private mode.
+
+    // Note: EXPIRE_NEVER permissions should trump anything that shows up in
+    // the HTTP header, so if there's an EXPIRE_NEVER permission already
+    // don't store anything new.
+    // Currently there's no way to get the type of expiry out of the
+    // permission manager, but that's okay since there's nothing that stores
+    // EXPIRE_NEVER permissions.
+
+    // PutEntry returns an existing entry if there already is one, or it
+    // creates a new one if there isn't.
+    nsSTSHostEntry* entry = mPrivateModeHostTable.PutEntry(host.get());
+    STSLOG(("Created private mode entry for for %s", host.get()));
+
+    // AddPermission() will be called twice if the STS header encountered has
+    // includeSubdomains (first for the main permission and second for the
+    // subdomains permission). If AddPermission() gets called a second time
+    // with the STS_SUBDOMAIN_PERMISSION, we just have to flip that bit in
+    // the nsSTSHostEntry.
+    if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) {
+      entry->mIncludeSubdomains = PR_TRUE;
+    }
+    // for the case where PutEntry() returned an existing host entry, make
+    // sure it's not set as deleted (which might have happened in the past).
+    entry->mDeleted = PR_FALSE;
+
+    // Also refresh the expiration time.
+    entry->mExpireTime = aExpireTime;
+    return NS_OK;
+
+}
+
+nsresult
+nsStrictTransportSecurityService::RemovePermission(const nsCString  &aHost,
+                                                   const char       *aType)
+{
+    if (!mInPrivateMode) {
+      // Not in private mode: remove permissions persistently.
+      return mPermMgr->Remove(aHost, aType);
+    }
+
+    // Make changes in mPrivateModeHostTable only, so any changes will be
+    // rolled back when exiting private mode.
+    nsSTSHostEntry* entry = mPrivateModeHostTable.GetEntry(aHost.get());
+
+    // Build up an nsIURI for use with the permission manager.
+    nsCOMPtr<nsIURI> uri;
+    nsresult rv = NS_NewURI(getter_AddRefs(uri),
+                            NS_LITERAL_CSTRING("http://") + aHost);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Check to see if there's STS data stored for this host in the
+    // permission manager (probably set outside private mode).
+    PRUint32 permmgrValue;
+    rv = mPermMgr->TestExactPermission(uri, aType, &permmgrValue);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // If there is STS data in the permission manager, store a "deleted" mask
+    // for the permission in mPrivateModeHostTable (either update
+    // mPrivateModeHostTable to have the deleted mask, or add one).
+    // This is because we don't want removals that happen in private mode to
+    // be reflected when private mode is exited -- but while in private mode
+    // we still want the effect of the removal.
+    if (permmgrValue != nsIPermissionManager::UNKNOWN_ACTION) {
+      // if there's no entry in mPrivateModeHostTable, we have to make one.
+      if (!entry) {
+        entry = mPrivateModeHostTable.PutEntry(aHost.get());
+        STSLOG(("Created private mode deleted mask for for %s", aHost.get()));
+      }
+      entry->mDeleted = PR_TRUE;
+      entry->mIncludeSubdomains = PR_FALSE;
+      return NS_OK;
+    }
+
+    // Otherwise, permission doesn't exist in the real permission manager, so
+    // there's nothing to "pretend" to delete.  I'ts ok to delete any copy in
+    // mPrivateModeHostTable.
+    if (entry) mPrivateModeHostTable.RawRemoveEntry(entry);
+    return NS_OK;
+}
+
+nsresult
+nsStrictTransportSecurityService::TestPermission(nsIURI     *aURI,
+                                                 const char *aType,
+                                                 PRUint32   *aPermission,
+                                                 PRBool     testExact)
+{
+    // set default for if we can't find any STS information
+    *aPermission = nsIPermissionManager::UNKNOWN_ACTION;
+
+    if (!mInPrivateMode) {
+      // if not in private mode, just delegate to the permission manager.
+      if (testExact)
+        return mPermMgr->TestExactPermission(aURI, aType, aPermission);
+      else
+        return mPermMgr->TestPermission(aURI, aType, aPermission);
+    }
+
+    nsCAutoString host;
+    nsresult rv = GetHost(aURI, host);
+    if (NS_FAILED(rv)) return NS_OK;
+
+    nsSTSHostEntry *entry;
+    PRUint32 actualExactPermission;
+    PRUint32 offset = 0;
+    PRInt64 now = PR_Now() / 1000;
+
+    // Used for testing permissions as we walk up the domain tree.
+    nsCOMPtr<nsIURI> domainWalkURI;
+
+    // In parallel, loop over private mode cache and also the real permission
+    // manager--ignoring any masked as "deleted" in the local cache. We have
+    // to do this here since the most specific permission in *either* the
+    // permission manager or mPrivateModeHostTable should be used.
+    do {
+      entry = mPrivateModeHostTable.GetEntry(host.get() + offset);
+      STSLOG(("Checking PM Table entry and permmgr for %s", host.get()+offset));
+
+      // flag as deleted any entries encountered that have expired.  We only
+      // flag the nsSTSHostEntry because there could be some data in the
+      // permission manager that -- if not in private mode -- would have been
+      // overwritten by newly encountered STS data.
+      if (entry && (now > entry->mExpireTime)) {
+        STSLOG(("Deleting expired PM Table entry for %s", host.get()+offset));
+        entry->mDeleted = PR_TRUE;
+        entry->mIncludeSubdomains = PR_FALSE;
+      }
+
+      rv = NS_NewURI(getter_AddRefs(domainWalkURI),
+                      NS_LITERAL_CSTRING("http://") + Substring(host, offset));
+      NS_ENSURE_SUCCESS(rv, rv);
+
+      rv = mPermMgr->TestExactPermission(domainWalkURI,
+                                          aType,
+                                          &actualExactPermission);
+      NS_ENSURE_SUCCESS(rv, rv);
+
+      // There are three cases as we walk up the hostname testing
+      // permissions:
+      // 1. There's no entry in mPrivateModeHostTable for this host; rely
+      // on data in the permission manager
+      if (!entry) {
+        if (actualExactPermission != nsIPermissionManager::UNKNOWN_ACTION) {
+          // no cached data but a permission in the permission manager so use
+          // it and stop looking.
+          *aPermission = actualExactPermission;
+          STSLOG(("no PM Table entry for %s, using permmgr", host.get()+offset));
+          break;
+        }
+      }
+      // 2. There's a "deleted" mask in mPrivateModeHostTable for this host
+      // or we're looking for includeSubdomain information and it's not set:
+      // any data in the permission manager must be ignored, since the
+      // permission would have been deleted if not in private mode.
+      else if (entry->mDeleted || (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0
+                                  && !entry->mIncludeSubdomains)) {
+        STSLOG(("no entry at all for %s, walking up", host.get()+offset));
+        // keep looking
+      }
+      // 3. There's a non-deleted entry in mPrivateModeHostTable for this
+      // host, so it should be used.
+      else {
+        // All STS permissions' values are ALLOW_ACTION or they are not
+        // known (as in, not set or turned off).
+        *aPermission = nsIPermissionManager::ALLOW_ACTION;
+        STSLOG(("PM Table entry for %s: forcing", host.get()+offset));
+        break;
+      }
+
+      // Don't continue walking up the host segments if the test was for an
+      // exact match only.
+      if (testExact) break;
+
+      STSLOG(("no PM Table entry or permmgr data for %s, walking up domain",
+              host.get()+offset));
+      // walk up the host segments
+      offset = host.FindChar('.', offset) + 1;
+    } while (offset > 0);
+
+    // Use whatever we ended up with, which defaults to UNKNOWN_ACTION.
+    return NS_OK;
+}
--- a/security/manager/boot/src/nsStrictTransportSecurityService.h
+++ b/security/manager/boot/src/nsStrictTransportSecurityService.h
@@ -38,36 +38,128 @@
 /**
  * This wraps nsSimpleURI so that all calls to it are done on the main thread.
  */
 
 #ifndef __nsStrictTransportSecurityService_h__
 #define __nsStrictTransportSecurityService_h__
 
 #include "nsIStrictTransportSecurityService.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
 #include "nsIPermissionManager.h"
 #include "nsCOMPtr.h"
 #include "nsIURI.h"
 #include "nsString.h"
+#include "nsTHashtable.h"
 
 // {16955eee-6c48-4152-9309-c42a465138a1}
 #define NS_STRICT_TRANSPORT_SECURITY_CID \
   {0x16955eee, 0x6c48, 0x4152, \
     {0x93, 0x09, 0xc4, 0x2a, 0x46, 0x51, 0x38, 0xa1} }
 
+////////////////////////////////////////////////////////////////////////////////
+// nsSTSHostEntry - similar to the nsHostEntry class in
+// nsPermissionManager.cpp, but specific to private-mode caching of STS
+// permissions.
+//
+// Each nsSTSHostEntry contains:
+//  - Expiry time
+//  - Deleted flag (boolean, default PR_FALSE)
+//  - Subdomains flag (boolean, default PR_FALSE)
+//
+// The existence of the nsSTSHostEntry implies STS state is set for the given
+// host -- unless the deleted flag is set, in which case not only is the STS
+// state not set for the host, but any permission actually present in the
+// permission manager should be ignored.
+//
+// Note: Only one expiry time is stored since the subdomains and STS
+// permissions are both encountered at the same time in the HTTP header; if the
+// includeSubdomains directive isn't present in the header, it means to delete
+// the permission, so the subdomains flag in the nsSTSHostEntry means both that
+// the permission doesn't exist and any permission in the real permission
+// manager should be ignored since newer information about it has been
+// encountered in private browsing mode.
+//
+// Note: If there's a permission set by the user (EXPIRE_NEVER), STS is not set
+// for the host (including the subdomains permission) when the header is
+// encountered.  Furthermore, any user-set permissions are stored persistently
+// and can't be shadowed.
+
+class nsSTSHostEntry : public PLDHashEntryHdr
+{
+  public:
+    explicit nsSTSHostEntry(const char* aHost);
+    explicit nsSTSHostEntry(const nsSTSHostEntry& toCopy);
+
+    nsCString    mHost;
+    PRInt64      mExpireTime;
+    PRPackedBool mDeleted;
+    PRPackedBool mIncludeSubdomains;
+
+    // Hash methods
+    typedef const char* KeyType;
+    typedef const char* KeyTypePointer;
+
+    KeyType GetKey() const
+    {
+      return mHost.get();
+    }
+
+    PRBool KeyEquals(KeyTypePointer aKey) const
+    {
+      return !strcmp(mHost.get(), aKey);
+    }
+
+    static KeyTypePointer KeyToPointer(KeyType aKey)
+    {
+      return aKey;
+    }
+
+    static PLDHashNumber HashKey(KeyTypePointer aKey)
+    {
+      return PL_DHashStringKey(nsnull, aKey);
+    }
+
+    // force the hashtable to use the copy constructor.
+    enum { ALLOW_MEMMOVE = PR_FALSE };
+};
+////////////////////////////////////////////////////////////////////////////////
+
 class nsStrictTransportSecurityService : public nsIStrictTransportSecurityService
+                                       , public nsIObserver
 {
 public:
   NS_DECL_ISUPPORTS
+  NS_DECL_NSIOBSERVER
   NS_DECL_NSISTRICTTRANSPORTSECURITYSERVICE
 
   nsStrictTransportSecurityService();
   nsresult Init();
   virtual ~nsStrictTransportSecurityService();
 
 private:
   nsresult GetHost(nsIURI *aURI, nsACString &aResult);
   nsresult SetStsState(nsIURI* aSourceURI, PRInt64 maxage, PRBool includeSubdomains);
   nsresult ProcessStsHeaderMutating(nsIURI* aSourceURI, char* aHeader);
+
+  // private-mode-preserving permission manager overlay functions
+  nsresult AddPermission(nsIURI     *aURI,
+                         const char *aType,
+                         PRUint32   aPermission,
+                         PRUint32   aExpireType,
+                         PRInt64    aExpireTime);
+  nsresult RemovePermission(const nsCString  &aHost,
+                            const char       *aType);
+  nsresult TestPermission(nsIURI     *aURI,
+                          const char *aType,
+                          PRUint32   *aPermission,
+                          PRBool     testExact);
+
+  // cached services
   nsCOMPtr<nsIPermissionManager> mPermMgr;
+  nsCOMPtr<nsIObserverService> mObserverService;
+
+  PRBool mInPrivateMode;
+  nsTHashtable<nsSTSHostEntry> mPrivateModeHostTable;
 };
 
 #endif // __nsStrictTransportSecurityService_h__
--- a/security/manager/ssl/tests/mochitest/stricttransportsecurity/Makefile.in
+++ b/security/manager/ssl/tests/mochitest/stricttransportsecurity/Makefile.in
@@ -44,14 +44,17 @@ relativesrcdir	= security/ssl/stricttran
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES = \
 	plain_bootstrap.html \
 	plain_bootstrap.html^headers^ \
 	subdom_bootstrap.html \
 	subdom_bootstrap.html^headers^ \
+	nosts_bootstrap.html \
+	nosts_bootstrap.html^headers^ \
 	verify.sjs \
 	test_stricttransportsecurity.html \
+	test_sts_privatebrowsing.html \
 	$(NULL)
 
 libs:: $(_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/mochitest/stricttransportsecurity/nosts_bootstrap.html
@@ -0,0 +1,57 @@
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (the "License"); you may not use this file except in compliance with
+   - the License. You may obtain a copy of the License at
+   - http://www.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is Strict-Transport-Security.
+   -
+   - The Initial Developer of the Original Code is
+   - Mozilla Foundation.
+   - Portions created by the Initial Developer are Copyright (C) 2010
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -  Sid Stamm <sid@mozilla.com>
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the LGPL or the GPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
+
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>STS test iframe</title>
+    <script>
+      var self = window;
+      window.addEventListener("load", function() {
+          self.parent.postMessage("BOOTSTRAP plain", "http://mochi.test:8888");
+        }, false);
+    </script>
+  </head>
+  <body>
+    <!-- This frame should be loaded over HTTPS to set the STS header. -->
+    This frame was loaded using 
+    <script>
+      document.write(document.location.protocol);
+    </script>
+    and set the STS header to force this site and allow subdomain upgrading.
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/mochitest/stricttransportsecurity/nosts_bootstrap.html^headers^
@@ -0,0 +1,1 @@
+Cache-Control: no-cache
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_sts_privatebrowsing.html
@@ -0,0 +1,269 @@
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (the "License"); you may not use this file except in compliance with
+   - the License. You may obtain a copy of the License at
+   - http://www.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is Strict-Transport-Security.
+   -
+   - The Initial Developer of the Original Code is
+   - Mozilla Foundation.
+   - Portions created by the Initial Developer are Copyright (C) 2010
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -  Sid Stamm <sid@mozilla.com>
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the LGPL or the GPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
+
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>opens additional content that should be converted to https</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+  <script class="testbody" type="text/javascript">
+  SimpleTest.waitForExplicitFinish();
+
+  const STSPATH = "/tests/security/ssl/stricttransportsecurity";
+
+  const NUM_TEST_FRAMES = 4;
+  var testframes = {
+    'samedom':
+      {'url':     "http://example.com" + STSPATH + "/verify.sjs",
+        'expected': {'plain': 'SECURE',
+                     'subdom': 'SECURE',
+                     'nosts': 'INSECURE'}},
+    'subdom':
+      {'url':     "http://test1.example.com" + STSPATH + "/verify.sjs",
+        'expected': {'plain': 'INSECURE',
+                     'subdom': 'SECURE',
+                     'nosts': 'INSECURE'}},
+    'otherdom':
+      {'url':     "http://example.org" + STSPATH + "/verify.sjs",
+        'expected': {'plain': 'INSECURE',
+                     'subdom': 'INSECURE',
+                     'nosts': 'INSECURE'}},
+    'alreadysecure':
+      {'url':     "https://test2.example.com" + STSPATH + "/verify.sjs",
+        'expected': {'plain': 'SECURE',
+                     'subdom': 'SECURE',
+                     'nosts': 'SECURE'}},
+  };
+
+  // This is how many sub-tests (testframes) in each round.
+  // When the round begins, this will be initialized.
+  var testsleftinround = 0;
+  var currentround = "";
+
+  var _PBSvc = null;
+  var _PrefSvc = null;
+
+  function _getPBService() {
+    if (_PBSvc)
+      return _PBSvc;
+
+    // not all apps will have the private browsing service.
+    try {
+      netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+      _PBSvc = Components.classes["@mozilla.org/privatebrowsing;1"]
+        .getService(Components.interfaces.nsIPrivateBrowsingService);
+      return _PBSvc;
+    } catch (e) {}
+    return null;
+  }
+  function _getPrefService() {
+    if (_PrefSvc)
+      return _PrefSvc;
+
+    // not all apps will have the private browsing service.
+    try {
+      netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+      _PrefSvc = Components.classes["@mozilla.org/preferences-service;1"]
+        .getService(Components.interfaces.nsIPrefService)
+        .QueryInterface(Components.interfaces.nsIPrefBranch2);
+      return _PrefSvc;
+    } catch (e) {}
+    return null;
+  }
+
+  function startRound(round) {
+    currentround = round;
+    testsleftinround = NUM_TEST_FRAMES;
+    dump("TESTS LEFT IN ROUND: " + testsleftinround + "\n");
+    var frame = document.createElement("iframe");
+    frame.setAttribute('id', 'ifr_bootstrap');
+    frame.setAttribute('src', "https://example.com" + STSPATH +
+                              "/" + round + "_bootstrap.html");
+    document.body.appendChild(frame);
+  }
+
+  function loadVerifyFrames(round) {
+    for (var test in testframes) {
+      var frame = document.createElement("iframe");
+      frame.setAttribute('id', 'ifr_' + test);
+      frame.setAttribute('src', testframes[test].url + '?id=' + test);
+      document.body.appendChild(frame);
+    }
+  }
+
+  /* Messages received are in this format:
+   *  (BOOTSTRAP|SECURE|INSECURE) testid
+   * For example: "BOOTSTRAP subdom"
+   *          or: "INSECURE otherdom"
+   */
+  function onMessageReceived(event) {
+
+    // otherwise, it's a test result
+    var result = event.data.split(/\s+/);
+    if (result.length != 2) {
+      SimpleTest.ok(false, event.data);
+      return;
+    }
+
+    if (result[0] === "BOOTSTRAP") {
+      loadVerifyFrames(currentround);
+      return;
+    }
+
+    // check if the result (SECURE/INSECURE) is expected for this round/test
+    // combo
+    dump_STSState();
+    dump( "*** in ROUND " + currentround +
+          ", test " + result[1] +
+          " is " + result[0] + "\n");
+    SimpleTest.is(result[0], testframes[result[1]].expected[currentround],
+                             "in ROUND " + currentround +
+                             ", test " + result[1]);
+    testsleftinround--;
+
+    // if this round is complete...
+    if (testsleftinround < 1) {
+      dump("DONE WITH ROUND " + currentround + "\n");
+      // remove all the iframes in the document
+      document.body.removeChild(document.getElementById('ifr_bootstrap'));
+      for (var test in testframes)
+        document.body.removeChild(document.getElementById('ifr_' + test));
+      currentround = "";
+
+      // And advance to the next test.
+      // Defer this so it doesn't muck with the stack too much.
+      SimpleTest.executeSoon(nextTest);
+    }
+  }
+
+  function test_sts_before_private_mode() {
+    dump_STSState();
+    dump("*** not in private browsing mode\n");
+    startRound('plain');
+  }
+
+  function test_sts_in_private_mode() {
+    dump_STSState();
+    dump("*** Entering private browsing mode\n");
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+    _getPrefService().setBoolPref("browser.privatebrowsing.keep_current_session",
+                               true);
+    _getPBService().privateBrowsingEnabled = true;
+    dump("*** ... done\n");
+    dump_STSState();
+    startRound('subdom');
+  }
+
+  function test_sts_after_exiting_private_mode() {
+    dump_STSState();
+    dump("*** Exiting private browsing mode\n");
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+    _getPBService().privateBrowsingEnabled = false;
+    _getPrefService().clearUserPref("browser.privatebrowsing.keep_current_session");
+    dump("*** ... done\n");
+    dump_STSState();
+    startRound('nosts');
+  }
+
+  function clean_up_sts_state() {
+    // erase all signs that this test ran.
+    dump("*** Cleaning up STS data.\n");
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+    const Cc = Components.classes;
+    const Ci = Components.interfaces;
+    var ios = Cc["@mozilla.org/network/io-service;1"]
+                .getService(Ci.nsIIOService);
+    var thehost = ios.newURI("http://example.com", null, null);
+    var stss = Cc["@mozilla.org/stsservice;1"]
+                 .getService(Ci.nsIStrictTransportSecurityService);
+    stss.removeStsState(thehost);
+    dump_STSState();
+    SimpleTest.executeSoon(nextTest);
+  }
+
+function dump_STSState() {
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+    var stss = Components.classes["@mozilla.org/stsservice;1"]
+          .getService(Components.interfaces.nsIStrictTransportSecurityService);
+    dump("*** State of example.com: " + stss.isStsHost("example.com") + "\n");
+}
+
+  // these are executed in the order presented.
+  // 0.  test that STS works before entering private browsing mode.
+  //     (load sts-bootstrapped "plain" tests)
+  //  ... clear any STS data ...
+  // 1.  test that STS works in private browsing mode
+  //     (load sts-bootstrapped "subdomain" tests)
+  // 2.  test that after exiting private browsing, STS data is forgotten
+  //     (verified with non-sts-bootstrapped pages)
+  var tests = [];
+  { // skip these tests if there's no private mode support
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+    if ("@mozilla.org/privatebrowsing;1" in Components.classes) {
+      tests = [
+        test_sts_before_private_mode,
+        clean_up_sts_state,
+        test_sts_in_private_mode,
+        test_sts_after_exiting_private_mode,
+        clean_up_sts_state,
+      ];
+    }
+  }
+
+  function nextTest() {
+    if (tests.length)
+      SimpleTest.executeSoon(tests.shift());
+    else
+      SimpleTest.executeSoon(SimpleTest.finish);
+  }
+
+  // listen for calls back from the sts-setting iframe and then
+  // the verification frames.
+  window.addEventListener("message", onMessageReceived, false);
+  window.addEventListener('load', nextTest, false);
+
+  </script>
+</head>
+
+<body>
+  This test will load some iframes and do some tests.
+
+</body>
+</html>