From: <rb...@us...> - 2013-08-25 13:29:11
|
Revision: 8447 http://sourceforge.net/p/htmlunit/code/8447 Author: rbri Date: 2013-08-25 13:29:07 +0000 (Sun, 25 Aug 2013) Log Message: ----------- use the correct separator for headers in the preflight CORS request Issue 1535 Modified Paths: -------------- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XMLHttpRequest.java trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XMLHttpRequestCORSTest.java Modified: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XMLHttpRequest.java =================================================================== --- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XMLHttpRequest.java 2013-08-25 08:04:14 UTC (rev 8446) +++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XMLHttpRequest.java 2013-08-25 13:29:07 UTC (rev 8447) @@ -106,6 +106,14 @@ /** All the data has been received; the complete data is available in responseBody and responseText. */ public static final int STATE_DONE = 4; + private static final String HEADER_ORIGIN = "Origin"; + private static final char REQUEST_HEADERS_SEPARATOR = ','; + + private static final String HEADER_ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + private static final String HEADER_ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + private static final String HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + private static final String HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + private static final String[] ALL_PROPERTIES_ = {"onreadystatechange", "readyState", "responseText", "responseXML", "status", "statusText", "abort", "getAllResponseHeaders", "getResponseHeader", "open", "send", "setRequestHeader"}; @@ -497,7 +505,7 @@ if (originUrl.getPort() != -1) { origin.append(':').append(originUrl.getPort()); } - request.setAdditionalHeader("Origin", origin.toString()); + request.setAdditionalHeader(HEADER_ORIGIN, origin.toString()); } request.setHttpMethod(HttpMethod.valueOf(method.toUpperCase(Locale.ENGLISH))); @@ -635,24 +643,33 @@ private void doSend(final Context context) { final WebClient wc = getWindow().getWebWindow().getWebClient(); try { - final boolean crossOriginResourceSharing = webRequest_.getAdditionalHeaders().get("Origin") != null; + final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN); + final boolean crossOriginResourceSharing = originHeaderValue != null; if (crossOriginResourceSharing && isPreflight()) { final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS); - preflightRequest.setAdditionalHeader("Origin", webRequest_.getAdditionalHeaders().get("Origin")); - preflightRequest.setAdditionalHeader("Access-Control-Request-Method", + + // header origin + preflightRequest.setAdditionalHeader(HEADER_ORIGIN, originHeaderValue); + + // header request-method + preflightRequest.setAdditionalHeader( + HEADER_ACCESS_CONTROL_REQUEST_METHOD, webRequest_.getHttpMethod().name()); + + // header request-headers final StringBuilder builder = new StringBuilder(); for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) { final String name = header.getKey().toLowerCase(Locale.ENGLISH); - final String value = header.getValue().toLowerCase(Locale.ENGLISH); - if (isPreflightHeader(name, value)) { + if (isPreflightHeader(name, header.getValue())) { if (builder.length() != 0) { - builder.append(' '); + builder.append(REQUEST_HEADERS_SEPARATOR); } builder.append(name); } } - preflightRequest.setAdditionalHeader("Access-Control-Request-Headers", builder.toString()); + preflightRequest.setAdditionalHeader(HEADER_ACCESS_CONTROL_REQUEST_HEADERS, builder.toString()); + + // do the preflight request final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest); if (!isPreflightAuthorized(preflightResponse)) { setState(STATE_HEADERS_RECEIVED, context); @@ -672,9 +689,8 @@ } boolean allowOriginResponse = true; if (crossOriginResourceSharing) { - final String value = webResponse.getResponseHeaderValue("Access-Control-Allow-Origin"); - allowOriginResponse = "*".equals(value) - || webRequest_.getAdditionalHeaders().get("Origin").equals(value); + final String value = webResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN); + allowOriginResponse = "*".equals(value) || originHeaderValue.equals(value); } if (allowOriginResponse) { if (overriddenMimeType_ == null) { @@ -722,7 +738,7 @@ } for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) { if (isPreflightHeader(header.getKey().toLowerCase(Locale.ENGLISH), - header.getValue().toLowerCase(Locale.ENGLISH))) { + header.getValue())) { return true; } } @@ -730,11 +746,11 @@ } private boolean isPreflightAuthorized(final WebResponse preflightResponse) { - final String originHeader = preflightResponse.getResponseHeaderValue("Access-Control-Allow-Origin"); - if (!"*".equals(originHeader) && !webRequest_.getAdditionalHeaders().get("Origin").equals(originHeader)) { + final String originHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN); + if (!"*".equals(originHeader) && !webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN).equals(originHeader)) { return false; } - String headersHeader = preflightResponse.getResponseHeaderValue("Access-Control-Allow-Headers"); + String headersHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_HEADERS); if (headersHeader == null) { headersHeader = ""; } @@ -743,7 +759,7 @@ } for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) { final String key = header.getKey().toLowerCase(Locale.ENGLISH); - if (isPreflightHeader(key, header.getValue().toLowerCase(Locale.ENGLISH)) + if (isPreflightHeader(key, header.getValue()) && !headersHeader.contains(key)) { return false; } @@ -752,15 +768,16 @@ } /** - * @param name header name (MUST be lower-case), for performance reasons - * @param value header value (MUST be lower-case), for performance reasons + * @param name header name (MUST be lower-case for performance reasons) + * @param value header value */ private boolean isPreflightHeader(final String name, final String value) { if ("content-type".equals(name)) { - if ("application/x-www-form-urlencoded".equals(value) - || "multipart/form-data".equals(value) - || "text/plain".equals(value) - || value.startsWith("text/plain;charset=")) { + final String lcValue = value.toLowerCase(Locale.ENGLISH); + if ("application/x-www-form-urlencoded".equals(lcValue) + || "multipart/form-data".equals(lcValue) + || "text/plain".equals(lcValue) + || lcValue.startsWith("text/plain;charset=")) { return false; } return true; Modified: trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XMLHttpRequestCORSTest.java =================================================================== --- trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XMLHttpRequestCORSTest.java 2013-08-25 08:04:14 UTC (rev 8446) +++ trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XMLHttpRequestCORSTest.java 2013-08-25 13:29:07 UTC (rev 8447) @@ -38,6 +38,7 @@ * @version $Revision$ * @author Ahmed Ashour * @author Marc Guillemot + * @author Ronald Brill */ @RunWith(BrowserRunner.class) public class XMLHttpRequestCORSTest extends WebDriverTestCase { @@ -328,6 +329,45 @@ * @throws Exception if the test fails. */ @Test + @Alerts(DEFAULT = { "4", "200", "options_headers", "x-ping,x-pong" }, + IE = { "4", "200", "options_headers", "null" }) + public void preflight_many_header_values() throws Exception { + expandExpectedAlertsVariables(new URL("http://localhost:" + PORT)); + + final String html = "<html><head>\n" + + "<script>\n" + + "var xhr = " + XHRInstantiation_ + ";\n" + + "function test() {\n" + + " try {\n" + + " var url = 'http://' + window.location.hostname + ':" + PORT2 + "/preflight2';\n" + + " xhr.open('GET', url, false);\n" + + " xhr.setRequestHeader('X-PING', 'ping');\n" + + " xhr.setRequestHeader('X-PONG', 'pong');\n" + + " xhr.send();\n" + + " } catch(e) { alert('exception') }\n" + + " alert(xhr.readyState);\n" + + " alert(xhr.status);\n" + + " alert(xhr.responseXML.firstChild.childNodes[3].tagName);" + + " alert(xhr.responseXML.firstChild.childNodes[3].firstChild.nodeValue);" + + "}\n" + + "</script>\n" + + "</head>\n" + + "<body onload='test()'></body></html>"; + + PreflightServerServlet.ACCESS_CONTROL_ALLOW_ORIGIN_ = "http://localhost:" + PORT; + PreflightServerServlet.ACCESS_CONTROL_ALLOW_METHODS_ = "POST, GET, OPTIONS"; + PreflightServerServlet.ACCESS_CONTROL_ALLOW_HEADERS_ = "X-PING, X-PONG"; + final Map<String, Class<? extends Servlet>> servlets2 = new HashMap<String, Class<? extends Servlet>>(); + servlets2.put("/preflight2", PreflightServerServlet.class); + startWebServer2(".", null, servlets2); + + loadPageWithAlerts2(html, new URL(getDefaultUrl(), "/preflight1")); + } + + /** + * @throws Exception if the test fails. + */ + @Test @Alerts({ "4", "200" }) public void withCredentials() throws Exception { testWithCredentials("true"); |