Bug 476567: Clear Access-Control preflight cache when spec says to. Also rewrite cache test-runner to make it more sane. r/sr=jst
authorJonas Sicking <jonas@sicking.cc>
Tue, 17 Feb 2009 14:10:50 -0800
changeset 25081 81a70b157a1cb9e93393aa1d5a244b9e66eae464
parent 25080 79ef13e126a5f11831809a2b289e1a97e3f1e2b4
child 25082 870843e43abe55aaf9fb73f36d171f8acd59aba5
push idunknown
push userunknown
push dateunknown
bugs476567
milestone1.9.2a1pre
Bug 476567: Clear Access-Control preflight cache when spec says to. Also rewrite cache test-runner to make it more sane. r/sr=jst
content/base/src/nsCrossSiteListenerProxy.cpp
content/base/src/nsXMLHttpRequest.cpp
content/base/src/nsXMLHttpRequest.h
content/base/test/file_CrossSiteXHR_cache_server.sjs
content/base/test/test_CrossSiteXHR_cache.html
--- a/content/base/src/nsCrossSiteListenerProxy.cpp
+++ b/content/base/src/nsCrossSiteListenerProxy.cpp
@@ -48,16 +48,17 @@
 #include "nsMimeTypes.h"
 #include "nsIStreamConverterService.h"
 #include "nsStringStream.h"
 #include "nsParserUtils.h"
 #include "nsGkAtoms.h"
 #include "nsWhitespaceTokenizer.h"
 #include "nsIChannelEventSink.h"
 #include "nsCommaSeparatedTokenizer.h"
+#include "nsXMLHttpRequest.h"
 
 static NS_DEFINE_CID(kCParserCID, NS_PARSER_CID);
 
 NS_IMPL_ISUPPORTS4(nsCrossSiteListenerProxy, nsIStreamListener,
                    nsIRequestObserver, nsIChannelEventSink,
                    nsIInterfaceRequestor)
 
 nsCrossSiteListenerProxy::nsCrossSiteListenerProxy(nsIStreamListener* aOuter,
@@ -112,16 +113,28 @@ nsCrossSiteListenerProxy::nsCrossSiteLis
 }
 
 NS_IMETHODIMP
 nsCrossSiteListenerProxy::OnStartRequest(nsIRequest* aRequest,
                                          nsISupports* aContext)
 {
   mRequestApproved = NS_SUCCEEDED(CheckRequestApproved(aRequest, PR_FALSE));
   if (!mRequestApproved) {
+    if (nsXMLHttpRequest::sAccessControlCache) {
+      nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+      if (channel) {
+      nsCOMPtr<nsIURI> uri;
+        channel->GetURI(getter_AddRefs(uri));
+        if (uri) {
+          nsXMLHttpRequest::sAccessControlCache->
+            RemoveEntries(uri, mRequestingPrincipal);
+        }
+      }
+    }
+
     aRequest->Cancel(NS_ERROR_DOM_BAD_URI);
     mOuterListener->OnStartRequest(aRequest, aContext);
 
     return NS_ERROR_DOM_BAD_URI;
   }
 
   return mOuterListener->OnStartRequest(aRequest, aContext);
 }
@@ -312,17 +325,27 @@ nsCrossSiteListenerProxy::GetInterface(c
 NS_IMETHODIMP
 nsCrossSiteListenerProxy::OnChannelRedirect(nsIChannel *aOldChannel,
                                             nsIChannel *aNewChannel,
                                             PRUint32    aFlags)
 {
   nsresult rv;
   if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags)) {
     rv = CheckRequestApproved(aOldChannel, PR_TRUE);
-    NS_ENSURE_SUCCESS(rv, rv);
+    if (NS_FAILED(rv)) {
+      if (nsXMLHttpRequest::sAccessControlCache) {
+        nsCOMPtr<nsIURI> oldURI;
+        aOldChannel->GetURI(getter_AddRefs(oldURI));
+        if (oldURI) {
+          nsXMLHttpRequest::sAccessControlCache->
+            RemoveEntries(oldURI, mRequestingPrincipal);
+        }
+      }
+      return NS_ERROR_DOM_BAD_URI;
+    }
   }
 
   nsCOMPtr<nsIChannelEventSink> outer =
     do_GetInterface(mOuterNotificationCallbacks);
   if (outer) {
     rv = outer->OnChannelRedirect(aOldChannel, aNewChannel, aFlags);
     NS_ENSURE_SUCCESS(rv, rv);
   }
--- a/content/base/src/nsXMLHttpRequest.cpp
+++ b/content/base/src/nsXMLHttpRequest.cpp
@@ -969,25 +969,43 @@ nsAccessControlLRUCache::GetEntry(nsIURI
     // entry.
     if (mTable.Count() > ACCESS_CONTROL_CACHE_SIZE) {
       CacheEntry* lruEntry = static_cast<CacheEntry*>(PR_LIST_TAIL(&mList));
       PR_REMOVE_LINK(lruEntry);
 
       // This will delete 'lruEntry'.
       mTable.Remove(lruEntry->mKey);
 
-      NS_ASSERTION(mTable.Count() >= ACCESS_CONTROL_CACHE_SIZE,
+      NS_ASSERTION(mTable.Count() == ACCESS_CONTROL_CACHE_SIZE,
                    "Somehow tried to remove an entry that was never added!");
     }
   }
   
   return entry;
 }
 
 void
+nsAccessControlLRUCache::RemoveEntries(nsIURI* aURI, nsIPrincipal* aPrincipal)
+{
+  CacheEntry* entry;
+  nsCString key;
+  if (GetCacheKey(aURI, aPrincipal, PR_TRUE, key) &&
+      mTable.Get(key, &entry)) {
+    PR_REMOVE_LINK(entry);
+    mTable.Remove(key);
+  }
+
+  if (GetCacheKey(aURI, aPrincipal, PR_FALSE, key) &&
+      mTable.Get(key, &entry)) {
+    PR_REMOVE_LINK(entry);
+    mTable.Remove(key);
+  }
+}
+
+void
 nsAccessControlLRUCache::Clear()
 {
   PR_INIT_CLIST(&mList);
   mTable.Clear();
 }
 
 /* static */ PLDHashOperator
 nsAccessControlLRUCache::RemoveExpiredEntries(const nsACString& aKey,
--- a/content/base/src/nsXMLHttpRequest.h
+++ b/content/base/src/nsXMLHttpRequest.h
@@ -122,16 +122,17 @@ public:
 
   PRBool Initialize()
   {
     return mTable.Init();
   }
 
   CacheEntry* GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal,
                        PRBool aWithCredentials, PRBool aCreate);
+  void RemoveEntries(nsIURI* aURI, nsIPrincipal* aPrincipal);
 
   void Clear();
 
 private:
   static PLDHashOperator
     RemoveExpiredEntries(const nsACString& aKey, nsAutoPtr<CacheEntry>& aValue,
                          void* aUserData);
 
--- a/content/base/test/file_CrossSiteXHR_cache_server.sjs
+++ b/content/base/test/file_CrossSiteXHR_cache_server.sjs
@@ -1,43 +1,46 @@
 function handleRequest(request, response)
 {
   var query = {};
   request.queryString.split('&').forEach(function (val) {
     var [name, value] = val.split('=');
     query[name] = unescape(value);
   });
 
+  if ("setState" in query) {
+    setState("test/content/base/test_CrossSiteXHR_cache:secData",
+             query.setState);
+
+    response.setHeader("Cache-Control", "no-cache", false);
+    response.setHeader("Content-Type", "text/plain", false);
+    response.write("hi");
+
+    return;
+  }
+
   var isPreflight = request.method == "OPTIONS";
 
   // Send response
 
-  response.setHeader("Access-Control-Allow-Origin", query.allowOrigin);
+  secData =
+    eval(getState("test/content/base/test_CrossSiteXHR_cache:secData"));
+
+  if (secData.allowOrigin)
+    response.setHeader("Access-Control-Allow-Origin", secData.allowOrigin);
 
   if (isPreflight) {
-    var secData = {};
-
-    if (request.hasHeader("Access-Control-Request-Headers")) {
-      var magicHeader =
-        request.getHeader("Access-Control-Request-Headers").split(",").
-        filter(function(name) /^magic-/.test(name))[0];
-    }
-
-    if (magicHeader) {
-      secData = eval(unescape(magicHeader.substr(6)));
-      secData.allowHeaders = (secData.allowHeaders || "") + "," + magicHeader;
-    }
-
     if (secData.allowHeaders)
       response.setHeader("Access-Control-Allow-Headers", secData.allowHeaders);
 
     if (secData.allowMethods)
       response.setHeader("Access-Control-Allow-Methods", secData.allowMethods);
 
     if (secData.cacheTime)
       response.setHeader("Access-Control-Max-Age", secData.cacheTime.toString());
 
     return;
   }
 
+  response.setHeader("Cache-Control", "no-cache", false);
   response.setHeader("Content-Type", "application/xml", false);
   response.write("<res>hello pass</res>\n");
 }
--- a/content/base/test/test_CrossSiteXHR_cache.html
+++ b/content/base/test/test_CrossSiteXHR_cache.html
@@ -64,17 +64,17 @@ function runTest() {
            },
            { pass: 0,
              method: "GET",
              headers: { "y-my-header": "hello" },
            },
            { pass: 1,
              method: "GET",
              headers: { "y-my-header": "hello" },
-             allowHeaders: "y-my-header",
+             allowHeaders: "y-my-header,x-my-header",
              cacheTime: 3600,
            },
            { pass: 1,
              method: "GET",
              headers: { "x-my-header": "myValue",
                         "y-my-header": "second" },
            },
            { newTest: "*******" },
@@ -153,31 +153,51 @@ function runTest() {
              cacheTime: 2,
            },
            { pass: 1,
              method: "GET",
              headers: { "second-header": "myValue" },
              allowHeaders: "second-header",
              cacheTime: 3600,
            },
+           { pass: 1,
+             method: "GET",
+             headers: { "third-header": "myValue" },
+             allowHeaders: "third-header",
+             cacheTime: 2,
+           },
+           { pause: 2.1 },
+           { pass: 1,
+             method: "GET",
+             headers: { "second-header": "myValue" },
+           },
            { pass: 0,
              method: "GET",
-             headers: { "third-header": "myValue" },
+             headers: { "first-header": "myValue" },
+           },
+           { newTest: "*******" },
+           { pass: 1,
+             method: "GET",
+             headers: { "first-header": "myValue" },
+             allowHeaders: "first-header",
+             cacheTime: 2,
+           },
+           { pass: 1,
+             method: "GET",
+             headers: { "second-header": "myValue" },
+             allowHeaders: "second-header",
+             cacheTime: 3600,
            },
            { pass: 1,
              method: "GET",
              headers: { "third-header": "myValue" },
              allowHeaders: "third-header",
              cacheTime: 2,
            },
            { pause: 2.1 },
-           { pass: 0,
-             method: "GET",
-             headers: { "first-header": "myValue" },
-           },
            { pass: 1,
              method: "GET",
              headers: { "second-header": "myValue" },
            },
            { pass: 0,
              method: "GET",
              headers: { "third-header": "myValue" },
            },
@@ -209,17 +229,17 @@ function runTest() {
            { pass: 1,
              method: "PATCH",
              allowMethods: "PATCH",
              cacheTime: 3600,
            },
            { pass: 1,
              method: "PATCH",
            },
-           { pass: 1,
+           { pass: 0,
              method: "XXDELETE",
            },
            { pass: 0,
              method: "XXPUT",
            },
            { newTest: "*******" },
            { pass: 0,
              method: "XXDELETE",
@@ -254,65 +274,185 @@ function runTest() {
              allowMethods: "FIRST",
              cacheTime: 2,
            },
            { pass: 1,
              method: "SECOND",
              allowMethods: "SECOND",
              cacheTime: 3600,
            },
+           { pass: 1,
+             method: "THIRD",
+             allowMethods: "THIRD",
+             cacheTime: 2,
+           },
+           { pause: 2.1 },
+           { pass: 1,
+             method: "SECOND",
+           },
            { pass: 0,
-             method: "THIRD",
+             method: "FIRST",
+           },
+           { newTest: "*******" },
+           { pass: 1,
+             method: "FIRST",
+             allowMethods: "FIRST",
+             cacheTime: 2,
+           },
+           { pass: 1,
+             method: "SECOND",
+             allowMethods: "SECOND",
+             cacheTime: 3600,
            },
            { pass: 1,
              method: "THIRD",
              allowMethods: "THIRD",
              cacheTime: 2,
            },
            { pause: 2.1 },
-           { pass: 0,
-             method: "FIRST",
-           },
            { pass: 1,
              method: "SECOND",
            },
            { pass: 0,
              method: "THIRD",
            },
+           { newTest: "*******" },
+           { pass: 1,
+             method: "GET",
+             headers: { "x-my-header": "x-value" },
+             allowHeaders: "x-my-header",
+             cacheTime: 3600,
+           },
+           { pass: 1,
+             method: "GET",
+             headers: { "x-my-header": "x-value" }
+           },
+           { pass: 0,
+             method: "GET",
+             headers: { "y-my-header": "y-value" }
+           },
+           { pass: 0,
+             method: "GET",
+             headers: { "x-my-header": "x-value" }
+           },
+           { newTest: "*******" },
+           { pass: 1,
+             method: "GET",
+             headers: { "x-my-header": "x-value" },
+             allowHeaders: "x-my-header",
+             cacheTime: 3600,
+           },
+           { pass: 1,
+             method: "GET",
+             headers: { "x-my-header": "x-value" },
+           },
+           { pass: 0,
+             method: "XXPUT",
+           },
+           { pass: 0,
+             method: "GET",
+             headers: { "x-my-header": "x-value" },
+           },
+           { newTest: "*******" },
+           { pass: 1,
+             method: "GET",
+             headers: { "x-my-header": "x-value" },
+             allowHeaders: "x-my-header",
+             cacheTime: 3600,
+           },
+           { pass: 1,
+             method: "GET",
+             headers: { "x-my-header": "x-value" },
+           },
+           { pass: 0,
+             method: "GET",
+             noOrigin: 1,
+           },
+           { pass: 0,
+             method: "GET",
+             headers: { "x-my-header": "x-value" },
+           },
+           { newTest: "*******" },
+           { pass: 1,
+             method: "XXDELETE",
+             allowMethods: "XXDELETE",
+             cacheTime: 3600,
+           },
+           { pass: 1,
+             method: "XXDELETE"
+           },
+           { pass: 0,
+             method: "XXPUT"
+           },
+           { pass: 0,
+             method: "XXDELETE"
+           },
+           { newTest: "*******" },
+           { pass: 1,
+             method: "XXDELETE",
+             allowMethods: "XXDELETE",
+             cacheTime: 3600,
+           },
+           { pass: 1,
+             method: "XXDELETE"
+           },
+           { pass: 0,
+             method: "XXDELETE",
+             headers: { "my-header": "value" },
+           },
+           { pass: 0,
+             method: "XXDELETE"
+           },
+           { newTest: "*******" },
+           { pass: 1,
+             method: "XXDELETE",
+             allowMethods: "XXDELETE",
+             cacheTime: 3600,
+           },
+           { pass: 1,
+             method: "XXDELETE"
+           },
+           { pass: 0,
+             method: "GET",
+             noOrigin: 1,
+           },
+           { pass: 0,
+             method: "XXDELETE"
+           },
            ];
 
   baseURL = "http://localhost:8888/tests/content/base/test/" +
              "file_CrossSiteXHR_cache_server.sjs?";
+  setStateURL = baseURL + "setState=";
 
   var unique = Date.now();
   for each (test in tests) {
     if (test.newTest) {
       unique++;
       continue;
     }
     if (test.pause) {
       setTimeout(function() { gen.next() }, test.pause * 1000);
       yield;
       continue;
     }
 
     req = {
-      url: baseURL + "c=" + unique +
-           "&allowOrigin=" + escape(origin),
+      url: baseURL + "c=" + unique,
       method: test.method,
       headers: test.headers,
     };
 
-    if (test.cacheTime || test.allowHeaders || test.allowMethods) {
-      sec = { allowHeaders: test.allowHeaders,
-              allowMethods: test.allowMethods,
-              cacheTime: test.cacheTime };
-      req.headers = req.headers || {};
-      req.headers["magic-" + escape(sec.toSource())] = "";
-    }
+    sec = { allowOrigin: test.noOrigin ? "" : origin,
+            allowHeaders: test.allowHeaders,
+            allowMethods: test.allowMethods,
+            cacheTime: test.cacheTime };
+    xhr = new XMLHttpRequest();
+    xhr.open("POST", setStateURL + escape(sec.toSource()), false);
+    xhr.send();
 
     loaderWindow.postMessage(req.toSource(), origin);
 
     res = eval(yield);
 
     testName = test.toSource() + " (index " + tests.indexOf(test) + ")";
 
     if (test.pass) {