WebCore:
authorweinig@apple.com <weinig@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 4 Aug 2008 20:21:19 +0000 (20:21 +0000)
committerweinig@apple.com <weinig@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 4 Aug 2008 20:21:19 +0000 (20:21 +0000)
2008-08-02  Sam Weinig  <sam@webkit.org>

        Reviewed by Dan Bernstein.

        Implement the Access-control for Cross Site requests
        preflight cache.

        Tests: http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header.html
               http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method.html
               http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout.html
               http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache.html

        * WebCore.vcproj/WebCore.vcproj:
        * WebCore.xcodeproj/project.pbxproj:
        * platform/KURL.h:
        (WTF::):
        * platform/KURLHash.h: Added.
        (WebCore::KURLHash::hash):
        (WebCore::KURLHash::equal):
        (WTF::):
        * xml/XMLHttpRequest.cpp:
        (WebCore::PreflightResultCacheItem::PreflightResultCacheItem):
        (WebCore::preflightResultCache):
        (WebCore::appendPreflightResultCacheEntry):
        (WebCore::canSkipPrelight):
        (WebCore::XMLHttpRequest::makeCrossSiteAccessRequestWithPreflight):
        (WebCore::parseAccessControlAllowList):
        (WebCore::parseAccessControlMaxAge):
        (WebCore::XMLHttpRequest::didReceiveResponsePreflight):

LayoutTests:

2008-08-04  Sam Weinig  <sam@webkit.org>

        Reviewed by Dan Bernstein.

        Tests for the Access-control for Cross Site requests
        preflight cache.

        * ChangeLog:
        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-expected.txt: Added.
        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header-expected.txt: Added.
        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header.html: Added.
        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method-expected.txt: Added.
        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method.html: Added.
        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout-expected.txt: Added.
        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout.html: Added.
        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache.html: Added.
        * http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php: Added.
        * http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-timeout.php: Added.
        * http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache.php: Added.
        * http/tests/xmlhttprequest/resources/portabilityLayer.php: Added.
        * http/tests/xmlhttprequest/resources/reset-temp-file.php: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@35540 268f45cc-cd09-0410-ab3c-d52691b4dbfc

20 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header.html [new file with mode: 0644]
LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method.html [new file with mode: 0644]
LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout.html [new file with mode: 0644]
LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache.html [new file with mode: 0644]
LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php [new file with mode: 0755]
LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-timeout.php [new file with mode: 0755]
LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache.php [new file with mode: 0755]
LayoutTests/http/tests/xmlhttprequest/resources/portabilityLayer.php [new file with mode: 0755]
LayoutTests/http/tests/xmlhttprequest/resources/reset-temp-file.php [new file with mode: 0755]
WebCore/ChangeLog
WebCore/WebCore.vcproj/WebCore.vcproj
WebCore/WebCore.xcodeproj/project.pbxproj
WebCore/platform/KURL.h
WebCore/platform/KURLHash.h [new file with mode: 0644]
WebCore/xml/XMLHttpRequest.cpp

index 3e25919..24b733d 100644 (file)
@@ -1,3 +1,25 @@
+2008-08-04  Sam Weinig  <sam@webkit.org>
+
+        Reviewed by Dan Bernstein.
+
+        Tests for the Access-control for Cross Site requests
+        preflight cache.
+
+        * ChangeLog:
+        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-expected.txt: Added.
+        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header-expected.txt: Added.
+        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header.html: Added.
+        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method-expected.txt: Added.
+        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method.html: Added.
+        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout-expected.txt: Added.
+        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout.html: Added.
+        * http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache.html: Added.
+        * http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php: Added.
+        * http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-timeout.php: Added.
+        * http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache.php: Added.
+        * http/tests/xmlhttprequest/resources/portabilityLayer.php: Added.
+        * http/tests/xmlhttprequest/resources/reset-temp-file.php: Added.
+
 2008-08-03  David D. Kilzer  <ddkilzer@apple.com>
 
         Future-proof fast/cookies/local-file-can-set-cookies.html
diff --git a/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-expected.txt b/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-expected.txt
new file mode 100644 (file)
index 0000000..40826ab
--- /dev/null
@@ -0,0 +1,5 @@
+PASS: First PUT request.
+PASS: First request complete
+PASS: Second PUT request.  Preflight worked
+PASS: Second request complete
+
diff --git a/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header-expected.txt b/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header-expected.txt
new file mode 100644 (file)
index 0000000..f81335a
--- /dev/null
@@ -0,0 +1,5 @@
+PASS: First PUT request.
+PASS: First request complete
+PASS: Second OPTIONS request was sent.
+PASS: Second request complete
+
diff --git a/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header.html b/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header.html
new file mode 100644 (file)
index 0000000..83ababa
--- /dev/null
@@ -0,0 +1,74 @@
+<html>
+<body>
+<pre id='console'></pre>
+<script type="text/javascript">
+function log(message)
+{
+    document.getElementById('console').appendChild(document.createTextNode(message + "\n"));
+}
+
+if (window.layoutTestController) {
+    layoutTestController.dumpAsText();
+    layoutTestController.waitUntilDone();
+}
+
+function errorHandler(event)
+{
+    log("FAIL: Network error. ");
+    if (window.layoutTestController)
+        layoutTestController.notifyDone();
+}
+
+var filename = "filename=preflightCacheInvalidationByHeader.txt";
+
+var xhr = new XMLHttpRequest;
+xhr.onerror = errorHandler;
+
+start = function()
+{
+    // Temp file removed.  We can start the test now.
+    if (xhr.readyState == xhr.DONE) {
+        firstRequest();
+    }
+}
+
+xhr.open("GET", "resources/reset-temp-file.php?" + filename, true);
+xhr.onreadystatechange = start;
+xhr.send();
+
+function firstRequest()
+{
+    xhr.onreadystatechange = function()
+    {
+        if (xhr.readyState == xhr.DONE) {
+            log(xhr.responseText);
+            log("PASS: First request complete");
+            secondRequest();
+        }
+    }
+
+    xhr.open("PUT", "http://localhost:8000/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php?" + filename, true);
+    xhr.send();
+}
+
+function secondRequest()
+{
+    xhr.onreadystatechange = function()
+    {
+        if (xhr.readyState == xhr.DONE) {
+            log(xhr.responseText);
+            log("PASS: Second request complete");
+            if (window.layoutTestController)
+                layoutTestController.notifyDone();
+        }
+    }
+
+    // Send a header not included in the inital cache. 
+    xhr.open("PUT", "http://localhost:8000/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php?" + filename, true);
+    xhr.setRequestHeader("x-webkit-test", "headerValue");
+    xhr.send();
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method-expected.txt b/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method-expected.txt
new file mode 100644 (file)
index 0000000..f81335a
--- /dev/null
@@ -0,0 +1,5 @@
+PASS: First PUT request.
+PASS: First request complete
+PASS: Second OPTIONS request was sent.
+PASS: Second request complete
+
diff --git a/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method.html b/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method.html
new file mode 100644 (file)
index 0000000..377f0bf
--- /dev/null
@@ -0,0 +1,73 @@
+<html>
+<body>
+<pre id='console'></pre>
+<script type="text/javascript">
+function log(message)
+{
+    document.getElementById('console').appendChild(document.createTextNode(message + "\n"));
+}
+
+if (window.layoutTestController) {
+    layoutTestController.dumpAsText();
+    layoutTestController.waitUntilDone();
+}
+
+function errorHandler(event)
+{
+    log("FAIL: Network error. ");
+    if (window.layoutTestController)
+        layoutTestController.notifyDone();
+}
+
+var filename = "filename=preflightCacheInvalidationByMethod.txt";
+
+var xhr = new XMLHttpRequest;
+xhr.onerror = errorHandler;
+
+start = function()
+{
+    // Temp file removed.  We can start the test now.
+    if (xhr.readyState == xhr.DONE) {
+        firstRequest();
+    }
+}
+
+xhr.open("GET", "resources/reset-temp-file.php?" + filename, true);
+xhr.onreadystatechange = start;
+xhr.send();
+
+function firstRequest()
+{
+    xhr.onreadystatechange = function()
+    {
+        if (xhr.readyState == xhr.DONE) {
+            log(xhr.responseText);
+            log("PASS: First request complete");
+            secondRequest();
+        }
+    }
+
+    xhr.open("PUT", "http://localhost:8000/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php?" + filename, true);
+    xhr.send();
+}
+
+function secondRequest()
+{
+    xhr.onreadystatechange = function()
+    {
+        if (xhr.readyState == xhr.DONE) {
+            log(xhr.responseText);
+            log("PASS: Second request complete");
+            if (window.layoutTestController)
+                layoutTestController.notifyDone();
+        }
+    }
+
+    // Send a method not included in the initial cache.
+    xhr.open("XMETHOD", "http://localhost:8000/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php?" + filename, true);
+    xhr.send();
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout-expected.txt b/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout-expected.txt
new file mode 100644 (file)
index 0000000..f81335a
--- /dev/null
@@ -0,0 +1,5 @@
+PASS: First PUT request.
+PASS: First request complete
+PASS: Second OPTIONS request was sent.
+PASS: Second request complete
+
diff --git a/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout.html b/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout.html
new file mode 100644 (file)
index 0000000..8150609
--- /dev/null
@@ -0,0 +1,72 @@
+<html>
+<body>
+<pre id='console'></pre>
+<script type="text/javascript">
+function log(message)
+{
+    document.getElementById('console').appendChild(document.createTextNode(message + "\n"));
+}
+
+if (window.layoutTestController) {
+    layoutTestController.dumpAsText();
+    layoutTestController.waitUntilDone();
+}
+
+function errorHandler(event)
+{
+    log("FAIL: Network error. ");
+    if (window.layoutTestController)
+        layoutTestController.notifyDone();
+}
+
+var filename = "filename=preflightCacheTimeout.txt";
+
+var xhr = new XMLHttpRequest;
+xhr.onerror = errorHandler;
+
+start = function()
+{
+    // Temp file removed.  We can start the test now.
+    if (xhr.readyState == xhr.DONE) {
+        firstRequest();
+    }
+}
+
+xhr.open("GET", "resources/reset-temp-file.php?" + filename, true);
+xhr.onreadystatechange = start;
+xhr.send();
+
+function firstRequest()
+{
+    xhr.onreadystatechange = function()
+    {
+        if (xhr.readyState == xhr.DONE) {
+            log(xhr.responseText);
+            log("PASS: First request complete");
+            setTimeout(secondRequest, 3000); // 5 seconds
+        }
+    }
+
+    xhr.open("PUT", "http://localhost:8000/xmlhttprequest/resources/access-control-basic-preflight-cache-timeout.php?" + filename, true);
+    xhr.send();
+}
+
+function secondRequest()
+{
+    xhr.onreadystatechange = function()
+    {
+        if (xhr.readyState == xhr.DONE) {
+            log(xhr.responseText);
+            log("PASS: Second request complete")
+            if (window.layoutTestController)
+                layoutTestController.notifyDone();
+        }
+    }
+
+    xhr.open("PUT", "http://localhost:8000/xmlhttprequest/resources/access-control-basic-preflight-cache-timeout.php?" + filename, true);
+    xhr.send();
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache.html b/LayoutTests/http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache.html
new file mode 100644 (file)
index 0000000..0fa26de
--- /dev/null
@@ -0,0 +1,72 @@
+<html>
+<body>
+<pre id='console'></pre>
+<script type="text/javascript">
+function log(message)
+{
+    document.getElementById('console').appendChild(document.createTextNode(message + "\n"));
+}
+
+if (window.layoutTestController) {
+    layoutTestController.dumpAsText();
+    layoutTestController.waitUntilDone();
+}
+
+function errorHandler(event)
+{
+    log("FAIL: Network error. ");
+    if (window.layoutTestController)
+        layoutTestController.notifyDone();
+}
+
+var filename = "filename=preflightCache.txt";
+
+var xhr = new XMLHttpRequest;
+xhr.onerror = errorHandler;
+
+start = function()
+{
+    // Temp file removed.  We can start the test now.
+    if (xhr.readyState == xhr.DONE) {
+        firstRequest();
+    }
+}
+
+xhr.open("GET", "resources/reset-temp-file.php?" + filename, true);
+xhr.onreadystatechange = start;
+xhr.send();
+
+function firstRequest()
+{
+    xhr.onreadystatechange = function()
+    {
+        if (xhr.readyState == xhr.DONE) {
+            log(xhr.responseText);
+            log("PASS: First request complete");
+            secondRequest();
+        }
+    }
+
+    xhr.open("PUT", "http://localhost:8000/xmlhttprequest/resources/access-control-basic-preflight-cache.php?" + filename, true);
+    xhr.send();
+}
+
+function secondRequest()
+{
+    xhr.onreadystatechange = function()
+    {
+        if (xhr.readyState == xhr.DONE) {
+            log(xhr.responseText);
+            log("PASS: Second request complete");
+            if (window.layoutTestController)
+                layoutTestController.notifyDone();
+        }
+    }
+
+    xhr.open("PUT", "http://localhost:8000/xmlhttprequest/resources/access-control-basic-preflight-cache.php?" + filename, true);
+    xhr.send();
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php b/LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-invalidation.php
new file mode 100755 (executable)
index 0000000..04e6610
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+require_once 'portabilityLayer.php';
+
+$tempDir = sys_get_temp_dir();
+$tmpFile = $tempDir . $_GET['filename'];
+
+function fail()
+{
+    header("Access-Control-Origin: http://127.0.0.1:8000");
+    header("Access-Control-Credentials: true");
+    header("Access-Control-Allow-Methods: PUT");
+    header("Access-Control-Allow-Headers: x-webkit-test");
+    echo "FAIL: " . $_SERVER['REQUEST_METHOD'] . "\n";
+    exit();
+}
+
+function setState($newState, $file)
+{
+    file_put_contents($file, $newState);
+}
+
+function getState($file)
+{
+    if (!file_exists($file)) {
+        return "Uninitialized";
+    }
+    return file_get_contents($file);
+}
+
+$state = getState($tmpFile);
+
+if ($state == "Uninitialized") {
+    if ($_SERVER['REQUEST_METHOD'] == "OPTIONS") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        header("Access-Control-Allow-Methods: PUT");
+        header("Access-Control-Max-Age: 10"); // 10 seconds
+        setState("OptionsSent", $tmpFile);
+    } else {
+        fail();
+    }
+} else if ($state == "OptionsSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "PUT") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "PASS: First PUT request.";
+        setState("FirstPUTSent", $tmpFile);
+    } else {
+        fail();
+    }
+} else if ($state == "FirstPUTSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "OPTIONS") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        header("Access-Control-Allow-Methods: PUT, XMETHOD");
+        header("Access-Control-Allow-Headers: x-webkit-test");
+        setState("SecondOPTIONSSent", $tmpFile);
+    } else if ($_SERVER['REQUEST_METHOD'] == "PUT") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "FAIL: Second PUT request sent without preflight";
+    }
+} else if ($state == "SecondOPTIONSSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "PUT" || $_SERVER['REQUEST_METHOD'] == "XMETHOD") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "PASS: Second OPTIONS request was sent.";
+    } else {
+        fail();
+    }
+} else {
+    fail();
+}
+?>
diff --git a/LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-timeout.php b/LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache-timeout.php
new file mode 100755 (executable)
index 0000000..96758a8
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+require_once 'portabilityLayer.php';
+
+$tempDir = sys_get_temp_dir();
+$tmpFile = $tempDir . $_GET['filename'];
+
+function fail()
+{
+    header("Access-Control-Origin: http://127.0.0.1:8000");
+    header("Access-Control-Credentials: true");
+    header("Access-Control-Allow-Methods: PUT");
+    header("Access-Control-Allow-Headers: x-webkit-test");
+    echo "FAIL: " . $_SERVER['REQUEST_METHOD'] . "\n";
+    exit();
+}
+
+function setState($newState, $file)
+{
+    file_put_contents($file, $newState);
+}
+
+function getState($file)
+{
+    if (!file_exists($file)) {
+        return "Uninitialized";
+    }
+    return file_get_contents($file);
+}
+
+$state = getState($tmpFile);
+
+if ($state == "Uninitialized") {
+    if ($_SERVER['REQUEST_METHOD'] == "OPTIONS") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        header("Access-Control-Allow-Methods: PUT");
+        header("Access-Control-Allow-Headers: x-webkit-test");
+        header("Access-Control-Max-Age: 1"); // 1 second
+        setState("OptionsSent", $tmpFile);
+    } else {
+        fail();
+    }
+} else if ($state == "OptionsSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "PUT") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "PASS: First PUT request.";
+        setState("FirstPUTSent", $tmpFile);
+    } else {
+        fail();
+    }
+} else if ($state == "FirstPUTSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "OPTIONS") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        header("Access-Control-Allow-Methods: PUT");
+        header("Access-Control-Allow-Headers: x-webkit-test");
+        setState("SecondOPTIONSSent", $tmpFile);
+    } else if ($_SERVER['REQUEST_METHOD'] == "PUT") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "FAIL: Second PUT request sent without preflight";
+    }
+} else if ($state == "SecondOPTIONSSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "PUT") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "PASS: Second OPTIONS request was sent.";
+    } else {
+        fail();
+    }
+} else {
+    fail();
+}
+?>
diff --git a/LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache.php b/LayoutTests/http/tests/xmlhttprequest/resources/access-control-basic-preflight-cache.php
new file mode 100755 (executable)
index 0000000..2cfbdae
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+require_once 'portabilityLayer.php';
+
+$tempDir = sys_get_temp_dir();
+$tmpFile = $tempDir . $_GET['filename'];
+
+function fail()
+{
+    header("Access-Control-Origin: http://127.0.0.1:8000");
+    header("Access-Control-Credentials: true");
+    header("Access-Control-Allow-Methods: PUT");
+    header("Access-Control-Allow-Headers: x-webkit-test");
+    echo "FAIL: " . $_SERVER['REQUEST_METHOD'] . "\n";
+    exit();
+}
+
+function setState($newState, $file)
+{
+    file_put_contents($file, $newState);
+}
+
+function getState($file)
+{
+    if (!file_exists($file)) {
+        return "Uninitialized";
+    }
+    return file_get_contents($file);
+}
+
+$state = getState($tmpFile);
+
+if ($state == "Uninitialized") {
+    if ($_SERVER['REQUEST_METHOD'] == "OPTIONS") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        header("Access-Control-Allow-Methods: PUT");
+        header("Access-Control-Allow-Headers: x-webkit-test");
+        header("Access-Control-Max-Age: 10"); // 10 seconds
+        setState("OptionsSent", $tmpFile);
+    } else {
+        fail();
+    }
+} else if ($state == "OptionsSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "PUT") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "PASS: First PUT request.";
+        setState("FirstPUTSent", $tmpFile);
+    } else {
+        fail();
+    }
+} else if ($state == "FirstPUTSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "PUT") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "PASS: Second PUT request.  Preflight worked";
+    } else if ($_SERVER['REQUEST_METHOD'] == "OPTIONS") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        header("Access-Control-Allow-Methods: PUT");
+        header("Access-Control-Allow-Headers: x-webkit-test");
+        setState("FAILSecondOPTIONSSent", $tmpFile);
+    }
+} else if ($state == "FAILSecondOPTIONSSent") {
+    if ($_SERVER['REQUEST_METHOD'] == "PUT") {
+        header("Access-Control-Origin: http://127.0.0.1:8000");
+        header("Access-Control-Credentials: true");
+        echo "FAIL: Second OPTIONS request was sent.  Preflight failed";
+    } else {
+        fail();
+    }
+} else {
+    fail();
+}
+?>
diff --git a/LayoutTests/http/tests/xmlhttprequest/resources/portabilityLayer.php b/LayoutTests/http/tests/xmlhttprequest/resources/portabilityLayer.php
new file mode 100755 (executable)
index 0000000..5ce4f80
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+if (!function_exists('sys_get_temp_dir')) {
+    // Based on http://www.phpit.net/article/creating-zip-tar-archives-dynamically-php/2/
+    function sys_get_temp_dir()
+    {
+        // Try to get from environment variable
+        if (!empty($_ENV['TMP'])) {
+            return realpath($_ENV['TMP']) . "/";
+        } else if (!empty($_ENV['TMPDIR']) ) {
+            return realpath($_ENV['TMPDIR']) . "/";
+        } else if ( !empty($_ENV['TEMP'])) {
+            return realpath( $_ENV['TEMP']) . "/";
+        }
+        return FALSE;
+    }
+}
+
+if (!function_exists('file_put_contents')) {
+    function file_put_contents($filename, $data)
+    {
+        $handle = fopen($filename, "w");
+        fwrite($handle, $data);
+        fclose($handle);
+    }
+}
+
+?>
diff --git a/LayoutTests/http/tests/xmlhttprequest/resources/reset-temp-file.php b/LayoutTests/http/tests/xmlhttprequest/resources/reset-temp-file.php
new file mode 100755 (executable)
index 0000000..097084a
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+require_once 'portabilityLayer.php';
+
+$tempDir = sys_get_temp_dir();
+$tmpFile = $tempDir . $_GET['filename'];
+unlink($tmpFile)
+?>
index c4dfffa..92c0f9e 100644 (file)
@@ -1,3 +1,33 @@
+2008-08-02  Sam Weinig  <sam@webkit.org>
+
+        Reviewed by Dan Bernstein.
+
+        Implement the Access-control for Cross Site requests
+        preflight cache.
+
+        Tests: http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-header.html
+               http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-invalidation-by-method.html
+               http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache-timeout.html
+               http/tests/xmlhttprequest/access-control-basic-allow-preflight-cache.html
+
+        * WebCore.vcproj/WebCore.vcproj:
+        * WebCore.xcodeproj/project.pbxproj:
+        * platform/KURL.h:
+        (WTF::):
+        * platform/KURLHash.h: Added.
+        (WebCore::KURLHash::hash):
+        (WebCore::KURLHash::equal):
+        (WTF::):
+        * xml/XMLHttpRequest.cpp:
+        (WebCore::PreflightResultCacheItem::PreflightResultCacheItem):
+        (WebCore::preflightResultCache):
+        (WebCore::appendPreflightResultCacheEntry):
+        (WebCore::canSkipPrelight):
+        (WebCore::XMLHttpRequest::makeCrossSiteAccessRequestWithPreflight):
+        (WebCore::parseAccessControlAllowList):
+        (WebCore::parseAccessControlMaxAge):
+        (WebCore::XMLHttpRequest::didReceiveResponsePreflight):
+
 2008-08-04  Eric Seidel  <eric@webkit.org>
 
         Reviewed by hyatt.
index acd577c..ec2c696 100644 (file)
                                >\r
                        </File>\r
                        <File\r
+                               RelativePath="..\platform\KURLHash.h"\r
+                               >\r
+                       </File>\r
+                       <File\r
                                RelativePath="..\platform\Language.h"\r
                                >\r
                        </File>\r
index 88ea89b..8b648b3 100644 (file)
                BCB773610C17853D00132BA4 /* JSNodeFilterCondition.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BCB7735E0C17853D00132BA4 /* JSNodeFilterCondition.cpp */; };
                BCB773620C17853D00132BA4 /* JSNodeFilterCondition.h in Headers */ = {isa = PBXBuildFile; fileRef = BCB7735F0C17853D00132BA4 /* JSNodeFilterCondition.h */; };
                BCB773630C17853D00132BA4 /* JSNodeFilterCustom.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BCB773600C17853D00132BA4 /* JSNodeFilterCustom.cpp */; };
+               BCBD21AB0E417AD400A070F2 /* KURLHash.h in Headers */ = {isa = PBXBuildFile; fileRef = BCBD21AA0E417AD400A070F2 /* KURLHash.h */; };
                BCBFB53C0DCD29CF0019B3E5 /* JSDOMWindowShell.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BCBFB53A0DCD29CF0019B3E5 /* JSDOMWindowShell.cpp */; };
                BCBFB53D0DCD29CF0019B3E5 /* JSDOMWindowShell.h in Headers */ = {isa = PBXBuildFile; fileRef = BCBFB53B0DCD29CF0019B3E5 /* JSDOMWindowShell.h */; settings = {ATTRIBUTES = (Private, ); }; };
                BCC573350D695BBE006EF517 /* DOMProgressEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = BCC573330D695BBE006EF517 /* DOMProgressEvent.h */; };
                BCB7735E0C17853D00132BA4 /* JSNodeFilterCondition.cpp */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = JSNodeFilterCondition.cpp; sourceTree = "<group>"; };
                BCB7735F0C17853D00132BA4 /* JSNodeFilterCondition.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = JSNodeFilterCondition.h; sourceTree = "<group>"; };
                BCB773600C17853D00132BA4 /* JSNodeFilterCustom.cpp */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = JSNodeFilterCustom.cpp; sourceTree = "<group>"; };
+               BCBD21AA0E417AD400A070F2 /* KURLHash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KURLHash.h; sourceTree = "<group>"; };
                BCBFB53A0DCD29CF0019B3E5 /* JSDOMWindowShell.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSDOMWindowShell.cpp; sourceTree = "<group>"; };
                BCBFB53B0DCD29CF0019B3E5 /* JSDOMWindowShell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSDOMWindowShell.h; sourceTree = "<group>"; };
                BCC573330D695BBE006EF517 /* DOMProgressEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DOMProgressEvent.h; sourceTree = "<group>"; };
                                BC073BA90C399B1F000F5979 /* FloatConversion.h */,
                                6593923509AE4346002C531F /* KURL.cpp */,
                                6593923609AE4346002C531F /* KURL.h */,
+                               BCBD21AA0E417AD400A070F2 /* KURLHash.h */,
                                85EC9AF90A71A2C600EEEAED /* Language.h */,
                                935207BD09BD410A00F2038D /* LocalizedStrings.h */,
                                A8239DFE09B3CF8A00B60641 /* Logging.cpp */,
                                A824B4650E2EF2EA0081A7B7 /* TextRun.h in Headers */,
                                BCDFD48E0E305290009D10AD /* XMLHttpRequestUpload.h in Headers */,
                                BCDFD4960E30592F009D10AD /* JSXMLHttpRequestUpload.h in Headers */,
+                               BCBD21AB0E417AD400A070F2 /* KURLHash.h in Headers */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
index 1889152..1ca5b11 100644 (file)
@@ -49,6 +49,7 @@ QT_END_NAMESPACE
 namespace WebCore {
 
 class TextEncoding;
+struct KURLHash;
 
 // FIXME: Our terminology here is a bit inconsistent. We refer to the part
 // after the "#" as the "fragment" in some places and the "ref" in others.
@@ -267,6 +268,16 @@ inline bool operator!=(const String& a, const KURL& b)
     return a != b.string();
 }
 
-}
+} // namespace WebCore
+
+namespace WTF {
+
+    // KURLHash is the default hash for String
+    template<typename T> struct DefaultHash;
+    template<> struct DefaultHash<WebCore::KURL> {
+        typedef WebCore::KURLHash Hash;
+    };
+
+} // namespace WTF
 
 #endif // KURL_h
diff --git a/WebCore/platform/KURLHash.h b/WebCore/platform/KURLHash.h
new file mode 100644 (file)
index 0000000..4deb078
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2008 Apple Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+ */
+
+#ifndef KURLHash_h
+#define KURLHash_h
+
+#include "KURL.h"
+#include "PlatformString.h"
+#include "StringHash.h"
+
+namespace WebCore {
+
+    struct KURLHash {
+        static unsigned hash(const KURL& key)
+        {
+            return key.string().impl()->hash();
+        }
+
+        static bool equal(const KURL& a, const KURL& b)
+        {
+            return StringHash::equal(a.string(), b.string());
+        }
+
+        static const bool safeToCompareToEmptyOrDeleted = false;
+    };
+
+} // namespace WebCore
+
+namespace WTF {
+
+    template<> struct HashTraits<WebCore::KURL> : GenericHashTraits<WebCore::KURL> {
+        static const bool emptyValueIsZero = true;
+        static void constructDeletedValue(WebCore::KURL& slot) { new (&slot) WebCore::KURL(WebCore::String(HashTableDeletedValue)); }
+        static bool isDeletedValue(const WebCore::KURL& slot) { return slot.string().isHashTableDeletedValue(); }
+    };
+
+} // namespace WTF
+
+#endif // KURLHash_h
index ebe7b2c..c1c5064 100644 (file)
 #include "HTTPParsers.h"
 #include "InspectorController.h"
 #include "JSDOMBinding.h"
+#include "KURL.h"
+#include "KURLHash.h"
 #include "Page.h"
 #include "Settings.h"
 #include "SubresourceLoader.h"
+#include "SystemTime.h"
 #include "TextResourceDecoder.h"
 #include "XMLHttpRequestException.h"
 #include "XMLHttpRequestProgressEvent.h"
@@ -49,6 +52,41 @@ namespace WebCore {
 
 using namespace EventNames;
 
+struct PreflightResultCacheItem {
+    PreflightResultCacheItem(unsigned expiryDelta, bool credentials, HashSet<String>* methods, HashSet<String, CaseFoldingHash>* headers)
+        : m_absoluteExpiryTime(currentTime() + expiryDelta)
+        , m_credentials(credentials)
+        , m_methods(methods)
+        , m_headers(headers)
+    {
+    }
+
+    // FIXME: A better solution to holding onto the absolute expiration time might be
+    // to start a timer for the expiration delta, that removes this from the cache when
+    // it fires.
+    double m_absoluteExpiryTime;
+    bool m_credentials;
+    OwnPtr<HashSet<String> > m_methods;
+    OwnPtr<HashSet<String, CaseFoldingHash> > m_headers;
+};
+
+typedef HashMap<std::pair<String, KURL>, PreflightResultCacheItem*> PreflightResultCache;
+
+static PreflightResultCache& preflightResultCache()
+{
+    static PreflightResultCache cache;
+    return cache;
+}
+
+static void appendPreflightResultCacheEntry(String origin, KURL url, unsigned expiryDelta, 
+                                            bool credentials, HashSet<String>* methods, HashSet<String, CaseFoldingHash>* headers)
+{
+    ASSERT(!preflightResultCache().contains(std::make_pair(origin, url)));
+
+    PreflightResultCacheItem* item = new PreflightResultCacheItem(expiryDelta, credentials, methods, headers);
+    preflightResultCache().set(std::make_pair(origin, url), item);
+}
+
 typedef HashSet<XMLHttpRequest*> RequestsSet;
 
 static HashMap<Document*, RequestsSet*>& requestsByDocument()
@@ -557,6 +595,24 @@ void XMLHttpRequest::makeSimpleCrossSiteAccessRequest(ExceptionCode& ec)
         loadRequestSynchronously(request, ec);
 }
 
+static bool canSkipPrelight(PreflightResultCache::iterator cacheIt, bool includeCredentials, const String& method, const HTTPHeaderMap& requestHeaders)
+{
+    PreflightResultCacheItem* item = cacheIt->second;
+    if (item->m_absoluteExpiryTime < currentTime())
+        return false;
+    if (includeCredentials && !item->m_credentials)
+        return false;
+    if (!item->m_methods->contains(method) && method != "GET" && method != "POST")
+        return false;
+    HTTPHeaderMap::const_iterator end = requestHeaders.end();
+    for (HTTPHeaderMap::const_iterator it = requestHeaders.begin(); it != end; ++it) {
+        if (!item->m_headers->contains(it->first) && !isOnAccessControlSimpleRequestHeaderWhitelist(it->first))
+            return false;
+    }
+
+    return true;
+}
+
 void XMLHttpRequest::makeCrossSiteAccessRequestWithPreflight(ExceptionCode& ec)
 {
     String origin = accessControlOrigin();
@@ -564,39 +620,50 @@ void XMLHttpRequest::makeCrossSiteAccessRequestWithPreflight(ExceptionCode& ec)
     url.setUser(String());
     url.setPass(String());
 
-    m_inPreflight = true;
-    ResourceRequest preflightRequest(url);
-    preflightRequest.setHTTPMethod("OPTIONS");
-    preflightRequest.setHTTPHeaderField("Origin", origin);
-    preflightRequest.setHTTPHeaderField("Access-Control-Request-Method", m_method);
-
-    if (m_requestHeaders.size() > 0) {
-        Vector<UChar> headerBuffer;
-        HTTPHeaderMap::const_iterator it = m_requestHeaders.begin();
-        append(headerBuffer, it->first);
-        ++it;
-
-        HTTPHeaderMap::const_iterator end = m_requestHeaders.end();
-        for (; it != end; ++it) {
-            headerBuffer.append(',');
-            headerBuffer.append(' ');
-            append(headerBuffer, it->first);
-        }
+    bool skipPreflight = false;
 
-        preflightRequest.setHTTPHeaderField("Access-Control-Request-Headers", String::adopt(headerBuffer));
-        preflightRequest.addHTTPHeaderFields(m_requestHeaders);
+    PreflightResultCache::iterator cacheIt = preflightResultCache().find(std::make_pair(origin, url));
+    if (cacheIt != preflightResultCache().end()) {
+        skipPreflight = canSkipPrelight(cacheIt, m_includeCredentials, m_method, m_requestHeaders);
+        if (!skipPreflight)
+            preflightResultCache().remove(cacheIt);
     }
 
-    if (m_async) {
-        loadRequestAsynchronously(preflightRequest);
-        return;
-    }
+    if (!skipPreflight) {
+        m_inPreflight = true;
+        ResourceRequest preflightRequest(url);
+        preflightRequest.setHTTPMethod("OPTIONS");
+        preflightRequest.setHTTPHeaderField("Origin", origin);
+        preflightRequest.setHTTPHeaderField("Access-Control-Request-Method", m_method);
 
-    loadRequestSynchronously(preflightRequest, ec);
-    m_inPreflight = false;
+        if (m_requestHeaders.size() > 0) {
+            Vector<UChar> headerBuffer;
+            HTTPHeaderMap::const_iterator it = m_requestHeaders.begin();
+            append(headerBuffer, it->first);
+            ++it;
 
-    if (ec)
-        return;
+            HTTPHeaderMap::const_iterator end = m_requestHeaders.end();
+            for (; it != end; ++it) {
+                headerBuffer.append(',');
+                headerBuffer.append(' ');
+                append(headerBuffer, it->first);
+            }
+
+            preflightRequest.setHTTPHeaderField("Access-Control-Request-Headers", String::adopt(headerBuffer));
+            preflightRequest.addHTTPHeaderFields(m_requestHeaders);
+        }
+
+        if (m_async) {
+            loadRequestAsynchronously(preflightRequest);
+            return;
+        }
+
+        loadRequestSynchronously(preflightRequest, ec);
+        m_inPreflight = false;
+
+        if (ec)
+            return;
+    }
 
     // Send the actual request.
     ResourceRequest request(url);
@@ -612,6 +679,11 @@ void XMLHttpRequest::makeCrossSiteAccessRequestWithPreflight(ExceptionCode& ec)
         request.setHTTPBody(m_requestEntityBody.release());
     }
 
+    if (m_async) {
+        loadRequestAsynchronously(request);
+        return;
+    }
+
     loadRequestSynchronously(request, ec);
 }
 
@@ -1082,7 +1154,7 @@ void XMLHttpRequest::didReceiveResponse(SubresourceLoader* loader, const Resourc
 }
 
 template<class HashType>
-static bool parseAccessControlAllowList(const String& string, HashSet<String, HashType> & set)
+static bool parseAccessControlAllowList(const String& string, HashSet<String, HashType>* set)
 {
     int start = 0;
     int end;
@@ -1091,15 +1163,23 @@ static bool parseAccessControlAllowList(const String& string, HashSet<String, Ha
             return false;
 
         // FIXME: this could be made more efficient by not not allocating twice.
-        set.add(string.substring(start, end - start).stripWhiteSpace());
+        set->add(string.substring(start, end - start).stripWhiteSpace());
         start = end + 1;
     }
     if (start != static_cast<int>(string.length()))
-        set.add(string.substring(start).stripWhiteSpace());
+        set->add(string.substring(start).stripWhiteSpace());
 
     return true;
 }
 
+static bool parseAccessControlMaxAge(const String& string, unsigned& expiryDelta)
+{
+    // FIXME: this will not do the correct thing for a number starting with a '+'
+    bool ok = false;
+    expiryDelta = string.toUIntStrict(&ok);
+    return ok;
+}
+
 void XMLHttpRequest::didReceiveResponsePreflight(SubresourceLoader*, const ResourceResponse& response)
 {
     ASSERT(m_inPreflight);
@@ -1110,30 +1190,36 @@ void XMLHttpRequest::didReceiveResponsePreflight(SubresourceLoader*, const Resou
         return;
     }
 
-    HashSet<String> methods;
-    if (!parseAccessControlAllowList(response.httpHeaderField("Access-Control-Allow-Methods"), methods)) {
+    OwnPtr<HashSet<String> > methods(new HashSet<String>);
+    if (!parseAccessControlAllowList(response.httpHeaderField("Access-Control-Allow-Methods"), methods.get())) {
         networkError();
         return;
     }
 
-    if (!methods.contains(m_method) && m_method != "GET" && m_method != "POST") {
+    if (!methods->contains(m_method) && m_method != "GET" && m_method != "POST") {
         networkError();
         return;
     }
 
-    HashSet<String, CaseFoldingHash> headers;
-    if (!parseAccessControlAllowList(response.httpHeaderField("Access-Control-Allow-Headers"), headers)) {
+    OwnPtr<HashSet<String, CaseFoldingHash> > headers(new HashSet<String, CaseFoldingHash>);
+    if (!parseAccessControlAllowList(response.httpHeaderField("Access-Control-Allow-Headers"), headers.get())) {
         networkError();
         return;
     }
 
     HTTPHeaderMap::const_iterator end = m_requestHeaders.end();
     for (HTTPHeaderMap::const_iterator it = m_requestHeaders.begin(); it != end; ++it) {
-        if (!headers.contains(it->first) && !isOnAccessControlSimpleRequestHeaderWhitelist(it->first)) {
+        if (!headers->contains(it->first) && !isOnAccessControlSimpleRequestHeaderWhitelist(it->first)) {
             networkError();
             return;
         }
     }
+
+    unsigned expiryDelta = 0;
+    if (!parseAccessControlMaxAge(response.httpHeaderField("Access-Control-Max-Age"), expiryDelta))
+        expiryDelta = 5;
+
+    appendPreflightResultCacheEntry(accessControlOrigin(), m_url, expiryDelta, m_includeCredentials, methods.release(), headers.release());
 }
 
 void XMLHttpRequest::receivedCancellation(SubresourceLoader*, const AuthenticationChallenge& challenge)