Merge branch 'jetty-12.0.x' of github.com:jetty/jetty.project into jetty-12.0.x

This commit is contained in:
Joakim Erdfelt 2024-01-15 06:39:45 -06:00
commit 090287db5e
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
21 changed files with 1586 additions and 95 deletions

View File

@ -0,0 +1,27 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
[[og-module-cross-origin]]
===== Module `cross-origin`
The `cross-origin` module provides support for the link:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[CORS protocol] implemented by browsers when performing cross-origin requests.
This module installs the xref:{prog-guide}#pg-server-http-handler-use-cross-origin[`CrossOriginHandler`] in the `Handler` tree; `CrossOriginHandler` inspects cross-origin requests and adds the relevant CORS response headers.
`CrossOriginHandler` should be used when an application performs cross-origin requests to your server, to protect from link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery] attacks.
The module properties are:
----
include::{jetty-home}/modules/cross-origin.mod[tags=documentation]
----

View File

@ -18,6 +18,7 @@ include::module-alpn.adoc[]
include::module-bytebufferpool.adoc[]
include::module-console-capture.adoc[]
include::module-core-deploy.adoc[]
include::module-cross-origin.adoc[]
include::module-eeN-deploy.adoc[]
include::module-http.adoc[]
include::module-http2.adoc[]

View File

@ -404,6 +404,53 @@ Server applications must configure a `HttpConfiguration` object with the secure
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=securedHandler]
----
[[pg-server-http-handler-use-cross-origin]]
====== CrossOriginHandler
`CrossOriginHandler` supports the server-side requirements of the link:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[CORS protocol] implemented by browsers when performing cross-origin requests.
An example of a cross-origin request is when a script downloaded from the origin domain `+http://domain.com+` uses `fetch()` or `XMLHttpRequest` to make a request to a cross domain such as `+http://cross.domain.com+` (a subdomain of the origin domain) or to `+http://example.com+` (a completely different domain).
This is common, for example, when you embed reusable components such as a chat component into a web page: the web page and the chat component files are downloaded from `+http://domain.com+`, but the chat server is at `+http://chat.domain.com+`, so the chat component must make cross-origin requests to the chat server.
This kind of setup exposes to link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery attacks], and the CORS protocol has been established to protect against this kind of attacks.
For security reasons, browsers by default do not allow cross-origin requests, unless the response from the cross domain contains the right CORS headers.
`CrossOriginHandler` relieves server-side web applications from handling CORS headers explicitly.
You can set up your `Handler` tree with the `CrossOriginHandler`, configure it, and it will take care of the CORS headers separately from your application, where you can concentrate on the business logic.
The `Handler` tree structure looks like the following:
[source,screen]
----
Server
└── CrossOriginHandler
└── ContextHandler /app
└── AppHandler
----
The most important `CrossOriginHandler` configuration parameter is `allowedOrigins`, which by default is `*`, allowing any origin.
You may want to restrict your server to only origins you trust.
From the chat example above, the chat server at `+http://chat.domain.com+` knows that the chat component is downloaded from the origin server at `+http://domain.com+`, so the `CrossOriginHandler` is configured in this way:
[source,java,indent=0]
----
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=crossOriginAllowedOrigins]
----
Browsers send cross-origin request in two ways:
* Directly, if the cross-origin request meets some simple criteria.
* By issuing a hidden _preflight_ request before the actual cross-origin request, to verify with the server if it is willing to reply properly to the actual cross-origin request.
Both preflight requests and cross-origin requests will be handled by `CrossOriginHandler`, which will analyze the request and possibly add appropriate CORS response headers.
By default, preflight requests are not delivered to the `CrossOriginHandler` child `Handler`, but it is possible to configure `CrossOriginHandler` by setting `deliverPreflightRequests=true` so that the web application can fine-tune the CORS response headers.
For more `CrossOriginHandler` configuration options, refer to the link:{javadoc-url}/org/eclipse/jetty/server/handler/CrossOriginHandler.html[`CrossOriginHandler` javadocs].
[[pg-server-http-handler-use-default]]
====== DefaultHandler

View File

@ -22,7 +22,7 @@ However, if the `WebSocketUpgradeFilter` is already present in `web.xml` under t
This allows you to customize:
* The filter order; for example, by configuring the `CrossOriginFilter` (or other filters) for increased security or authentication _before_ the `WebSocketUpgradeFilter`.
* The filter order; for example, by configuring filters for increased security or authentication _before_ the `WebSocketUpgradeFilter`.
* The `WebSocketUpgradeFilter` configuration via ``init-param``s, that affects all `Session` instances created by this filter.
* The `WebSocketUpgradeFilter` path mapping. Rather than the default mapping of `+/*+`, you can map the `WebSocketUpgradeFilter` to a more specific path such as `+/ws/*+`.
* The possibility to have multiple ``WebSocketUpgradeFilter``s, mapped to different paths, each with its own configuration.
@ -38,14 +38,14 @@ For example:
version="5.0">
<display-name>My WebSocket WebApp</display-name>
<!-- The CrossOriginFilter *must* be the first --> <!--1-->
<!-- The SecurityFilter *must* be the first --> <!--1-->
<filter>
<filter-name>cross-origin</filter-name>
<filter-class>org.eclipse.jetty.{ee-current}.servlets.CrossOriginFilter</filter-class>
<filter-name>security</filter-name>
<filter-class>com.acme.SecurityFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>cross-origin</filter-name>
<filter-name>security</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
@ -69,7 +69,7 @@ For example:
</web-app>
----
<1> The `CrossOriginFilter` is the first to protect against link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery attacks].
<1> The custom `SecurityFilter` is the first, to apply custom security.
<2> The configuration for the _default_ `WebSocketUpgradeFilter`.
<3> Note the use of the _default_ `WebSocketUpgradeFilter` name.
<4> Specific configuration for `WebSocketUpgradeFilter` parameters.

View File

@ -18,12 +18,11 @@ import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.security.Security;
import java.time.Duration;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.CompletableFuture;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
@ -31,10 +30,8 @@ import jakarta.servlet.http.HttpServletResponse;
import org.conscrypt.OpenSSLProvider;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.servlets.CrossOriginFilter;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpFields;
@ -74,6 +71,7 @@ import org.eclipse.jetty.server.Slf4jRequestLogWriter;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.CrossOriginHandler;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.EventsHandler;
import org.eclipse.jetty.server.handler.QoSHandler;
@ -1066,22 +1064,21 @@ public class HTTPServerDocs
Connector connector = new ServerConnector(server);
server.addConnector(connector);
// Add the CrossOriginHandler to protect from CSRF attacks.
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
server.setHandler(crossOriginHandler);
// Create a ServletContextHandler with contextPath.
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/shop");
// Link the context to the server.
server.setHandler(context);
crossOriginHandler.setHandler(context);
// Add the Servlet implementing the cart functionality to the context.
ServletHolder servletHolder = context.addServlet(ShopCartServlet.class, "/cart/*");
// Configure the Servlet with init-parameters.
servletHolder.setInitParameter("maxItems", "128");
// Add the CrossOriginFilter to protect from CSRF attacks.
FilterHolder filterHolder = context.addFilter(CrossOriginFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Configure the filter.
filterHolder.setAsyncSupported(true);
server.start();
// end::servletContextHandler-setup[]
}
@ -1463,6 +1460,15 @@ public class HTTPServerDocs
// end::securedHandler[]
}
public void crossOriginAllowedOrigins()
{
// tag::crossOriginAllowedOrigins[]
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
// The allowed origins are regex patterns.
crossOriginHandler.setAllowedOriginPatterns(Set.of("http://domain\\.com"));
// end::crossOriginAllowedOrigins[]
}
public void defaultHandler() throws Exception
{
// tag::defaultHandler[]

View File

@ -30,7 +30,7 @@ import org.eclipse.jetty.io.Content;
* multiPart.close();
* ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
* .method(HttpMethod.POST)
* .content(multiPart)
* .body(multiPart)
* .send();
* </pre>
* <p>The above example would be the equivalent of submitting this form:</p>

View File

@ -34,7 +34,7 @@ import org.eclipse.jetty.io.content.OutputStreamContentSource;
* try (OutputStream output = content.getOutputStream())
* {
* httpClient.newRequest("localhost", 8080)
* .content(content)
* .body(content)
* .send(new Response.CompleteListener()
* {
* &#64;Override

View File

@ -26,6 +26,8 @@ public interface CookieParser
{
/**
* <p>A factory method to create a new parser suitable for the compliance mode.</p>
*
* @param handler the handler for Cookie Parsing events
* @param compliance The compliance mode to use for parsing.
* @param complianceListener A listener for compliance violations or null.
* @return A CookieParser instance.

View File

@ -21,7 +21,6 @@ import org.eclipse.jetty.util.StringUtil;
public enum HttpHeader
{
/**
* General Fields.
*/
@ -59,6 +58,8 @@ public enum HttpHeader
ACCEPT_CHARSET("Accept-Charset"),
ACCEPT_ENCODING("Accept-Encoding"),
ACCEPT_LANGUAGE("Accept-Language"),
ACCESS_CONTROL_REQUEST_HEADERS("Access-Control-Request-Headers"),
ACCESS_CONTROL_REQUEST_METHOD("Access-Control-Request-Method"),
AUTHORIZATION("Authorization"),
EXPECT("Expect"),
FORWARDED("Forwarded"),
@ -87,6 +88,12 @@ public enum HttpHeader
* Response Fields.
*/
ACCEPT_RANGES("Accept-Ranges"),
ACCESS_CONTROL_ALLOW_ORIGIN("Access-Control-Allow-Origin"),
ACCESS_CONTROL_ALLOW_METHODS("Access-Control-Allow-Methods"),
ACCESS_CONTROL_ALLOW_HEADERS("Access-Control-Allow-Headers"),
ACCESS_CONTROL_MAX_AGE("Access-Control-Max-Age"),
ACCESS_CONTROL_ALLOW_CREDENTIALS("Access-Control-Allow-Credentials"),
ACCESS_CONTROL_EXPOSE_HEADERS("Access-Control-Expose-Headers"),
AGE("Age"),
ALT_SVC("Alt-Svc"),
ETAG("ETag"),
@ -96,6 +103,7 @@ public enum HttpHeader
RETRY_AFTER("Retry-After"),
SERVER("Server"),
SERVLET_ENGINE("Servlet-Engine"),
TIMING_ALLOW_ORIGIN("Timing-Allow-Origin"),
VARY("Vary"),
WWW_AUTHENTICATE("WWW-Authenticate"),

View File

@ -45,7 +45,7 @@ import org.eclipse.jetty.util.thread.AutoLock;
* Content.Source content = ...;
*
* // Create and configure MultiPartByteRanges.
* MultiPartByteRanges byteRanges = new MultiPartByteRanges(boundary);
* MultiPartByteRanges.Parser byteRanges = new MultiPartByteRanges.Parser(boundary);
*
* // Parse the content.
* byteRanges.parse(content)

View File

@ -55,7 +55,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
* Content.Source content = ...;
*
* // Create and configure MultiPartFormData.
* MultiPartFormData formData = new MultiPartFormData(boundary);
* MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
* // Where to store the files.
* formData.setFilesDirectory(Path.of("/tmp"));
* // Max 1 MiB files.

View File

@ -0,0 +1,71 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="insertHandler">
<Arg>
<New id="CrossOriginHandler" class="org.eclipse.jetty.server.handler.CrossOriginHandler">
<Set name="allowCredentials">
<Property name="jetty.crossorigin.allowCredentials" default="true" />
</Set>
<Call name="setAllowedHeaders">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedHeaders" default="Content-Type" />
</Arg>
</Call>
</Arg>
</Call>
<Call name="setAllowedMethods">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedMethods" default="GET,POST,HEAD" />
</Arg>
</Call>
</Arg>
</Call>
<Call name="setAllowedOriginPatterns">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedOriginPatterns" default="*" />
</Arg>
</Call>
</Arg>
</Call>
<Call name="setAllowedTimingOriginPatterns">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedTimingOriginPatterns" default="" />
</Arg>
</Call>
</Arg>
</Call>
<Set name="deliverPreflightRequests" property="jetty.crossorigin.deliverPreflightRequests" />
<Set name="deliverNonAllowedOriginRequests" property="jetty.crossorigin.deliverNonAllowedOriginRequests" />
<Set name="deliverNonAllowedOriginWebSocketUpgradeRequests" property="jetty.crossorigin.deliverNonAllowedOriginWebSocketUpgradeRequests" />
<Call name="setExposedHeaders">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.exposedHeaders" default="" />
</Arg>
</Call>
</Arg>
</Call>
<Call name="setPreflightMaxAge">
<Arg>
<Call class="java.time.Duration" name="ofSeconds">
<Arg type="long">
<Property name="jetty.crossorigin.preflightMaxAge" default="60" />
</Arg>
</Call>
</Arg>
</Call>
</New>
</Arg>
</Call>
</Configure>

View File

@ -0,0 +1,48 @@
# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
[description]
Enables CrossOriginHandler to support the CORS protocol and protect from cross-site request forgery (CSRF) attacks.
[tags]
server
handler
csrf
[depend]
server
[xml]
etc/jetty-cross-origin.xml
[ini-template]
#tag::documentation[]
## Whether cross-origin requests can include credentials such as cookies or authentication headers.
# jetty.crossorigin.allowCredentials=true
## A comma-separated list of headers allowed in cross-origin requests.
# jetty.crossorigin.allowedHeaders=Content-Type
## A comma-separated list of HTTP methods allowed in cross-origin requests.
# jetty.crossorigin.allowedMethods=GET,POST,HEAD
## A comma-separated list of origins regex patterns allowed in cross-origin requests.
# jetty.crossorigin.allowedOriginPatterns=*
## A comma-separated list of timing origins regex patterns allowed in cross-origin requests.
# jetty.crossorigin.allowedTimingOriginPatterns=
## Whether preflight requests are delivered to the child Handler of CrossOriginHandler.
# jetty.crossorigin.deliverPreflightRequests=false
## Whether requests whose origin is not allowed are delivered to the child Handler of CrossOriginHandler.
# jetty.crossorigin.deliverNonAllowedOriginRequests=true
## Whether WebSocket upgrade requests whose origin is not allowed are delivered to the child Handler of CrossOriginHandler.
# jetty.crossorigin.deliverNonAllowedOriginWebSocketUpgradeRequests=false
## A comma-separated list of headers allowed in cross-origin responses.
# jetty.crossorigin.exposedHeaders=
## How long the preflight results can be cached by browsers, in seconds.
# jetty.crossorigin.preflightMaxAge=60
#end::documentation[]

View File

@ -32,8 +32,9 @@ import org.eclipse.jetty.util.StringUtil;
import static org.eclipse.jetty.util.UrlEncoded.decodeHexByte;
/**
* A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded}
* content has been parsed asynchronously from the {@link Content.Source}.
* <p>A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded}
* content has been parsed asynchronously from the {@link Content.Source}.</p>
* <p><a href="https://url.spec.whatwg.org/#application/x-www-form-urlencoded">Specification</a>.</p>
*/
public class FormFields extends ContentSourceCompletableFuture<Fields>
{
@ -209,98 +210,103 @@ public class FormFields extends ContentSourceCompletableFuture<Fields>
@Override
protected Fields parse(Content.Chunk chunk) throws CharacterCodingException
{
String value = null;
ByteBuffer buffer = chunk.getByteBuffer();
do
while (BufferUtil.hasContent(buffer))
{
loop:
while (BufferUtil.hasContent(buffer))
byte b = buffer.get();
switch (_percent)
{
byte b = buffer.get();
switch (_percent)
case 1 ->
{
case 1 ->
{
_percentCode = b;
_percent++;
continue;
}
case 2 ->
{
_builder.append(decodeHexByte((char)_percentCode, (char)b));
_percent = 0;
continue;
}
_percentCode = b;
_percent++;
continue;
}
if (_name == null)
case 2 ->
{
switch (b)
{
case '=' ->
{
_name = _builder.build();
checkLength(_name);
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
}
}
else
{
switch (b)
{
case '&' ->
{
value = _builder.build();
checkLength(value);
break loop;
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
}
_percent = 0;
_builder.append(decodeHexByte((char)_percentCode, (char)b));
continue;
}
}
if (_name != null)
if (_name == null)
{
if (value == null && chunk.isLast())
switch (b)
{
if (_percent > 0)
case '&' ->
{
_builder.append((byte)'%');
_builder.append(_percentCode);
String name = _builder.build();
checkMaxLength(name);
onNewField(name, "");
}
value = _builder.build();
checkLength(value);
case '=' ->
{
_name = _builder.build();
checkMaxLength(_name);
}
case '+' -> _builder.append(' ');
case '%' -> _percent++;
default -> _builder.append(b);
}
if (value != null)
}
else
{
switch (b)
{
Fields.Field field = new Fields.Field(_name, value);
_name = null;
value = null;
if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
throw new IllegalStateException("form with too many fields > " + _maxFields);
_fields.add(field);
case '&' ->
{
String value = _builder.build();
checkMaxLength(value);
onNewField(_name, value);
_name = null;
}
case '+' -> _builder.append(' ');
case '%' -> _percent++;
default -> _builder.append(b);
}
}
}
while (BufferUtil.hasContent(buffer));
return chunk.isLast() ? _fields : null;
if (!chunk.isLast())
return null;
// Append any remaining %x.
if (_percent > 0)
throw new IllegalStateException("invalid percent encoding");
String value = _builder.build();
if (_name == null)
{
if (!value.isEmpty())
{
checkMaxLength(value);
onNewField(value, "");
}
return _fields;
}
checkMaxLength(value);
onNewField(_name, value);
return _fields;
}
private void checkLength(String nameOrValue)
private void checkMaxLength(String nameOrValue)
{
if (_maxLength >= 0)
{
_length += nameOrValue.length();
if (_length > _maxLength)
throw new IllegalStateException("form too large");
throw new IllegalStateException("form too large > " + _maxLength);
}
}
private void onNewField(String name, String value)
{
Fields.Field field = new Fields.Field(name, value);
_fields.add(field);
if (_maxFields >= 0 && _fields.getSize() > _maxFields)
throw new IllegalStateException("form with too many fields > " + _maxFields);
}
}

View File

@ -0,0 +1,521 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.handler;
import java.time.Duration;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>Implementation of the CORS protocol defined by the
* <a href="https://fetch.spec.whatwg.org/">fetch standard</a>.</p>
* <p>This {@link Handler} should be present in the {@link Handler} tree to prevent
* <a href="https://owasp.org/www-community/attacks/csrf">cross site request forgery</a> attacks.</p>
* <p>A typical case is a web page containing a script downloaded from the origin server at
* {@code domain.com}, where the script makes requests to the cross server at {@code cross.domain.com}.
* The cross server at {@code cross.domain.com} has the {@link CrossOriginHandler} installed and will
* see requests such as:</p>
* <pre>{@code
* GET / HTTP/1.1
* Host: cross.domain.com
* Origin: http://domain.com
* }</pre>
* <p>The cross server at {@code cross.domain.com} must decide whether these cross-origin requests
* are allowed or not, and it may easily do so by configuring the {@link CrossOriginHandler},
* for example configuring the {@link #setAllowedOriginPatterns(Set) allowed origins} to contain only
* the origin server with origin {@code http://domain.com}.</p>
*/
@ManagedObject
public class CrossOriginHandler extends Handler.Wrapper
{
private static final Logger LOG = LoggerFactory.getLogger(CrossOriginHandler.class);
private static final PreEncodedHttpField ACCESS_CONTROL_ALLOW_CREDENTIALS_TRUE = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
private static final PreEncodedHttpField VARY_ORIGIN = new PreEncodedHttpField(HttpHeader.VARY, HttpHeader.ORIGIN.asString());
private boolean allowCredentials = true;
private Set<String> allowedHeaders = Set.of("Content-Type");
private Set<String> allowedMethods = Set.of("GET", "POST", "HEAD");
private Set<String> allowedOrigins = Set.of("*");
private Set<String> allowedTimingOrigins = Set.of();
private boolean deliverPreflight = false;
private boolean deliverNonAllowedOrigin = true;
private boolean deliverNonAllowedOriginWebSocketUpgrade = false;
private Set<String> exposedHeaders = Set.of();
private Duration preflightMaxAge = Duration.ofSeconds(60);
private boolean anyOriginAllowed;
private final Set<Pattern> allowedOriginPatterns = new LinkedHashSet<>();
private boolean anyTimingOriginAllowed;
private final Set<Pattern> allowedTimingOriginPatterns = new LinkedHashSet<>();
private PreEncodedHttpField accessControlAllowMethodsField;
private PreEncodedHttpField accessControlAllowHeadersField;
private PreEncodedHttpField accessControlExposeHeadersField;
private PreEncodedHttpField accessControlMaxAge;
/**
* @return whether the cross server allows cross-origin requests to include credentials
*/
@ManagedAttribute("Whether the server allows cross-origin requests to include credentials (cookies, authentication headers, etc.)")
public boolean isAllowCredentials()
{
return allowCredentials;
}
/**
* <p>Sets whether the cross server allows cross-origin requests to include credentials
* such as cookies or authentication headers.</p>
* <p>For example, when the cross server allows credentials to be included, cross-origin
* requests will contain cookies, otherwise they will not.</p>
* <p>The default is {@code true}.</p>
*
* @param allow whether the cross server allows cross-origin requests to include credentials
*/
public void setAllowCredentials(boolean allow)
{
throwIfStarted();
allowCredentials = allow;
}
/**
* @return the immutable set of allowed headers in a cross-origin request
*/
@ManagedAttribute("The set of allowed headers in a cross-origin request")
public Set<String> getAllowedHeaders()
{
return allowedHeaders;
}
/**
* <p>Sets the set of allowed headers in a cross-origin request.</p>
* <p>The cross server receives a preflight request that specifies the headers
* of the cross-origin request, and the cross server replies to the preflight
* request with the set of allowed headers.
* Browsers are responsible to check whether the headers of the cross-origin
* request are allowed, and if they are not produce an error.</p>
* <p>The headers can be either the character {@code *} to indicate any
* header, or actual header names.</p>
*
* @param headers the set of allowed headers in a cross-origin request
*/
public void setAllowedHeaders(Set<String> headers)
{
throwIfStarted();
allowedHeaders = Set.copyOf(headers);
}
/**
* @return the immutable set of allowed methods in a cross-origin request
*/
@ManagedAttribute("The set of allowed methods in a cross-origin request")
public Set<String> getAllowedMethods()
{
return allowedMethods;
}
/**
* <p>Sets the set of allowed methods in a cross-origin request.</p>
* <p>The cross server receives a preflight request that specifies the method
* of the cross-origin request, and the cross server replies to the preflight
* request with the set of allowed methods.
* Browsers are responsible to check whether the method of the cross-origin
* request is allowed, and if it is not produce an error.</p>
*
* @param methods the set of allowed methods in a cross-origin request
*/
public void setAllowedMethods(Set<String> methods)
{
throwIfStarted();
allowedMethods = Set.copyOf(methods);
}
/**
* @return the immutable set of allowed origin regex strings in a cross-origin request
*/
@ManagedAttribute("The set of allowed origin regex strings in a cross-origin request")
public Set<String> getAllowedOriginPatterns()
{
return allowedOrigins;
}
/**
* <p>Sets the set of allowed origin regex strings in a cross-origin request.</p>
* <p>The cross server receives a preflight or a cross-origin request
* specifying the {@link HttpHeader#ORIGIN}, and replies with the
* same origin if allowed, otherwise the {@link HttpHeader#ACCESS_CONTROL_ALLOW_ORIGIN}
* is not added to the response (and the client should fail the
* cross-origin or preflight request).</p>
* <p>The origins are either the character {@code *}, or regular expressions,
* so dot characters separating domain segments must be escaped:</p>
* <pre>{@code
* crossOriginHandler.setAllowedOriginPatterns(Set.of("https://.*\\.domain\\.com"));
* }</pre>
* <p>The default value is {@code *}.</p>
*
* @param origins the set of allowed origin regex strings in a cross-origin request
*/
public void setAllowedOriginPatterns(Set<String> origins)
{
throwIfStarted();
allowedOrigins = Set.copyOf(origins);
}
/**
* @return the immutable set of allowed timing origin regex strings in a cross-origin request
*/
@ManagedAttribute("The set of allowed timing origin regex strings in a cross-origin request")
public Set<String> getAllowedTimingOriginPatterns()
{
return allowedTimingOrigins;
}
/**
* <p>Sets the set of allowed timing origin regex strings in a cross-origin request.</p>
*
* @param origins the set of allowed timing origin regex strings in a cross-origin request
*/
public void setAllowedTimingOriginPatterns(Set<String> origins)
{
throwIfStarted();
allowedTimingOrigins = Set.copyOf(origins);
}
/**
* @return whether preflight requests are delivered to the child Handler
*/
@ManagedAttribute("whether preflight requests are delivered to the child Handler")
public boolean isDeliverPreflightRequests()
{
return deliverPreflight;
}
/**
* <p>Sets whether preflight requests are delivered to the child {@link Handler}.</p>
* <p>Default value is {@code false}.</p>
*
* @param deliver whether preflight requests are delivered to the child Handler
*/
public void setDeliverPreflightRequests(boolean deliver)
{
throwIfStarted();
deliverPreflight = deliver;
}
/**
* @return whether requests whose origin is not allowed are delivered to the child Handler
*/
@ManagedAttribute("whether requests whose origin is not allowed are delivered to the child Handler")
public boolean isDeliverNonAllowedOriginRequests()
{
return deliverNonAllowedOrigin;
}
/**
* <p>Sets whether requests whose origin is not allowed are delivered to the child Handler.</p>
* <p>Default value is {@code true}.</p>
*
* @param deliverNonAllowedOrigin whether requests whose origin is not allowed are delivered to the child Handler
*/
public void setDeliverNonAllowedOriginRequests(boolean deliverNonAllowedOrigin)
{
this.deliverNonAllowedOrigin = deliverNonAllowedOrigin;
}
/**
* @return whether WebSocket upgrade requests whose origin is not allowed are delivered to the child Handler
*/
@ManagedAttribute("whether WebSocket upgrade requests whose origin is not allowed are delivered to the child Handler")
public boolean isDeliverNonAllowedOriginWebSocketUpgradeRequests()
{
return deliverNonAllowedOriginWebSocketUpgrade;
}
/**
* <p>Sets whether WebSocket upgrade requests whose origin is not allowed are delivered to the child Handler.</p>
* <p>Default value is {@code false}.</p>
*
* @param deliverNonAllowedOriginWebSocketUpgrade whether WebSocket upgrade requests whose origin is not allowed are delivered to the child Handler
*/
public void setDeliverNonAllowedOriginWebSocketUpgradeRequests(boolean deliverNonAllowedOriginWebSocketUpgrade)
{
this.deliverNonAllowedOriginWebSocketUpgrade = deliverNonAllowedOriginWebSocketUpgrade;
}
/**
* @return the immutable set of headers exposed in a cross-origin response
*/
@ManagedAttribute("The set of headers exposed in a cross-origin response")
public Set<String> getExposedHeaders()
{
return exposedHeaders;
}
/**
* <p>Sets the set of headers exposed in a cross-origin response.</p>
* <p>The cross server receives a cross-origin request and indicates
* which response headers are exposed to scripts running in the browser.</p>
*
* @param headers the set of headers exposed in a cross-origin response
*/
public void setExposedHeaders(Set<String> headers)
{
throwIfStarted();
exposedHeaders = Set.copyOf(headers);
}
/**
* @return how long the preflight results can be cached by browsers
*/
@ManagedAttribute("How long the preflight results can be cached by browsers")
public Duration getPreflightMaxAge()
{
return preflightMaxAge;
}
/**
* @param duration how long the preflight results can be cached by browsers
*/
public void setPreflightMaxAge(Duration duration)
{
throwIfStarted();
preflightMaxAge = duration;
}
@Override
protected void doStart() throws Exception
{
resolveAllowedOrigins();
resolveAllowedTimingOrigins();
accessControlAllowMethodsField = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_ALLOW_METHODS, String.join(",", getAllowedMethods()));
accessControlAllowHeadersField = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS, String.join(",", getAllowedHeaders()));
accessControlExposeHeadersField = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_EXPOSE_HEADERS, String.join(",", getExposedHeaders()));
accessControlMaxAge = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_MAX_AGE, getPreflightMaxAge().toSeconds());
super.doStart();
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// The response may change if the Origin header is present, so always add Vary.
response.getHeaders().ensureField(VARY_ORIGIN);
String origins = request.getHeaders().get(HttpHeader.ORIGIN);
if (origins == null)
return super.handle(request, response, callback);
if (LOG.isDebugEnabled())
LOG.debug("handling cross-origin request {}", request);
boolean preflight = isPreflight(request);
if (originMatches(origins))
{
if (LOG.isDebugEnabled())
LOG.debug("cross-origin request matches allowed origins: {} {}", request, getAllowedOriginPatterns());
if (preflight)
{
if (LOG.isDebugEnabled())
LOG.debug("preflight cross-origin request {}", request);
handlePreflightResponse(origins, response);
if (!isDeliverPreflightRequests())
{
if (LOG.isDebugEnabled())
LOG.debug("preflight cross-origin request not delivered to child handler {}", request);
callback.succeeded();
return true;
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("simple cross-origin request {}", request);
handleSimpleResponse(origins, response);
}
if (timingOriginMatches(origins))
{
if (LOG.isDebugEnabled())
LOG.debug("cross-origin request matches allowed timing origins: {} {}", request, getAllowedTimingOriginPatterns());
response.getHeaders().put(HttpHeader.TIMING_ALLOW_ORIGIN, origins);
}
return super.handle(request, response, callback);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("cross-origin request does not match allowed origins: {} {}", request, getAllowedOriginPatterns());
if (isDeliverNonAllowedOriginRequests())
{
if (preflight)
{
if (!isDeliverPreflightRequests())
{
if (LOG.isDebugEnabled())
LOG.debug("preflight cross-origin request not delivered to child handler {}", request);
callback.succeeded();
return true;
}
}
else
{
if (isWebSocketUpgrade(request) && !isDeliverNonAllowedOriginWebSocketUpgradeRequests())
{
Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "origin not allowed");
return true;
}
}
if (LOG.isDebugEnabled())
LOG.debug("cross-origin request delivered to child handler {}", request);
return super.handle(request, response, callback);
}
else
{
Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "origin not allowed");
return true;
}
}
}
private boolean originMatches(String origins)
{
if (anyOriginAllowed)
return true;
if (allowedOriginPatterns.isEmpty())
return false;
return originMatches(origins, allowedOriginPatterns);
}
private boolean timingOriginMatches(String origins)
{
if (anyTimingOriginAllowed)
return true;
if (allowedTimingOriginPatterns.isEmpty())
return false;
return originMatches(origins, allowedTimingOriginPatterns);
}
private boolean originMatches(String origins, Set<Pattern> allowedOriginPatterns)
{
for (String origin : origins.split(" "))
{
origin = origin.trim();
if (origin.isEmpty())
continue;
for (Pattern pattern : allowedOriginPatterns)
{
if (pattern.matcher(origin).matches())
return true;
}
}
return false;
}
private boolean isPreflight(Request request)
{
return HttpMethod.OPTIONS.is(request.getMethod()) && request.getHeaders().contains(HttpHeader.ACCESS_CONTROL_REQUEST_METHOD);
}
private boolean isWebSocketUpgrade(Request request)
{
return request.getHeaders().contains(HttpHeader.SEC_WEBSOCKET_VERSION);
}
private void handlePreflightResponse(String origins, Response response)
{
HttpFields.Mutable headers = response.getHeaders();
headers.put(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN, origins);
if (isAllowCredentials())
headers.put(ACCESS_CONTROL_ALLOW_CREDENTIALS_TRUE);
Set<String> allowedMethods = getAllowedMethods();
if (!allowedMethods.isEmpty())
headers.put(accessControlAllowMethodsField);
Set<String> allowedHeaders = getAllowedHeaders();
if (!allowedHeaders.isEmpty())
headers.put(accessControlAllowHeadersField);
long seconds = getPreflightMaxAge().toSeconds();
if (seconds > 0)
headers.put(accessControlMaxAge);
}
private void handleSimpleResponse(String origin, Response response)
{
HttpFields.Mutable headers = response.getHeaders();
headers.put(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
if (isAllowCredentials())
headers.put(ACCESS_CONTROL_ALLOW_CREDENTIALS_TRUE);
Set<String> exposedHeaders = getExposedHeaders();
if (!exposedHeaders.isEmpty())
headers.put(accessControlExposeHeadersField);
}
private void resolveAllowedOrigins()
{
for (String allowedOrigin : getAllowedOriginPatterns())
{
allowedOrigin = allowedOrigin.trim();
if (allowedOrigin.isEmpty())
continue;
if ("*".equals(allowedOrigin))
{
anyOriginAllowed = true;
return;
}
allowedOriginPatterns.add(Pattern.compile(allowedOrigin, Pattern.CASE_INSENSITIVE));
}
}
private void resolveAllowedTimingOrigins()
{
for (String allowedTimingOrigin : getAllowedTimingOriginPatterns())
{
allowedTimingOrigin = allowedTimingOrigin.trim();
if (allowedTimingOrigin.isEmpty())
continue;
if ("*".equals(allowedTimingOrigin))
{
anyTimingOriginAllowed = true;
return;
}
allowedTimingOriginPatterns.add(Pattern.compile(allowedTimingOrigin, Pattern.CASE_INSENSITIVE));
}
}
private void throwIfStarted()
{
if (isStarted())
throw new IllegalStateException("Cannot configure after start");
}
}

View File

@ -13,11 +13,13 @@
package org.eclipse.jetty.server;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@ -32,31 +34,41 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FormFieldsTest
{
public static Stream<Arguments> tests()
public static Stream<Arguments> validData()
{
return Stream.of(
Arguments.of(List.of(""), UTF_8, -1, -1, Map.of()),
Arguments.of(List.of("name"), UTF_8, -1, -1, Map.of("name", "")),
Arguments.of(List.of("name&"), UTF_8, -1, -1, Map.of("name", "")),
Arguments.of(List.of("name="), UTF_8, -1, -1, Map.of("name", "")),
Arguments.of(List.of("name%00="), UTF_8, -1, -1, Map.of("name\u0000", "")),
Arguments.of(List.of("n1=v1&n2"), UTF_8, -1, -1, Map.of("n1", "v1", "n2", "")),
Arguments.of(List.of("n1=v1&n2&n3=v3&n4"), UTF_8, -1, -1, Map.of("n1", "v1", "n2", "", "n3", "v3", "n4", "")),
Arguments.of(List.of("name=value"), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("name=%0A"), UTF_8, -1, -1, Map.of("name", "\n")),
Arguments.of(List.of("name=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("name", "=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("n", "ame", "=", "value"), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("n=v&X=Y"), UTF_8, 2, 4, Map.of("n", "v", "X", "Y")),
Arguments.of(List.of("name=f¤¤&X=Y"), UTF_8, -1, -1, Map.of("name", "f¤¤", "X", "Y")),
Arguments.of(List.of("n=v&X=Y"), UTF_8, 1, -1, null),
Arguments.of(List.of("n=v&X=Y"), UTF_8, -1, 3, null)
Arguments.of(List.of("na+me=", "va", "+", "lue"), UTF_8, -1, -1, Map.of("na me", "va lue")),
Arguments.of(List.of("=v"), UTF_8, -1, -1, Map.of("", "v"))
);
}
@ParameterizedTest
@MethodSource("tests")
public void testFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Map<String, String> expected)
throws Exception
@MethodSource("validData")
public void testValidFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Map<String, String> expected)
{
AsyncContent source = new AsyncContent();
Attributes attributes = new Attributes.Mapped();
@ -66,8 +78,9 @@ public class FormFieldsTest
int last = chunks.size() - 1;
FutureCallback eof = new FutureCallback();
for (int i = 0; i <= last; i++)
{
source.write(i == last, BufferUtil.toBuffer(chunks.get(i), charset), i == last ? eof : Callback.NOOP);
}
try
{
@ -89,4 +102,45 @@ public class FormFieldsTest
assertNull(expected);
}
}
public static Stream<Arguments> invalidData()
{
return Stream.of(
Arguments.of(List.of("%A"), UTF_8, -1, -1, IllegalStateException.class),
Arguments.of(List.of("name%"), UTF_8, -1, -1, IllegalStateException.class),
Arguments.of(List.of("name%A"), UTF_8, -1, -1, IllegalStateException.class),
// TODO: these 2 should throw the same exception.
Arguments.of(List.of("name%A="), UTF_8, -1, -1, CharacterCodingException.class),
Arguments.of(List.of("name%A&"), UTF_8, -1, -1, IllegalArgumentException.class),
Arguments.of(List.of("name=%"), UTF_8, -1, -1, IllegalStateException.class),
Arguments.of(List.of("name=A%%A"), UTF_8, -1, -1, IllegalArgumentException.class),
Arguments.of(List.of("name=A%%3D"), UTF_8, -1, -1, IllegalArgumentException.class),
Arguments.of(List.of("%="), UTF_8, -1, -1, IllegalStateException.class),
Arguments.of(List.of("name=%A"), UTF_8, -1, -1, IllegalStateException.class),
Arguments.of(List.of("name=value%A"), UTF_8, -1, -1, IllegalStateException.class),
Arguments.of(List.of("n=v&X=Y"), UTF_8, 1, -1, IllegalStateException.class),
Arguments.of(List.of("n=v&X=Y"), UTF_8, -1, 3, IllegalStateException.class),
Arguments.of(List.of("n%AH=v"), UTF_8, -1, -1, IllegalArgumentException.class),
Arguments.of(List.of("n=v%AH"), UTF_8, -1, -1, IllegalArgumentException.class),
Arguments.of(List.of("n=v%FF"), UTF_8, -1, -1, CharacterCodingException.class)
);
}
@ParameterizedTest
@MethodSource("invalidData")
public void testInvalidFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Class<? extends Exception> expectedException)
{
AsyncContent source = new AsyncContent();
CompletableFuture<Fields> futureFields = FormFields.from(source, new Attributes.Mapped(), charset, maxFields, maxLength);
assertFalse(futureFields.isDone());
int last = chunks.size() - 1;
for (int i = 0; i <= last; i++)
{
source.write(i == last, BufferUtil.toBuffer(chunks.get(i)), Callback.NOOP);
}
Throwable cause = assertThrows(ExecutionException.class, futureFields::get).getCause();
assertThat(cause, instanceOf(expectedException));
}
}

View File

@ -0,0 +1,616 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.handler;
import java.util.List;
import java.util.Set;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class CrossOriginHandlerTest
{
private Server server;
private LocalConnector connector;
public void start(CrossOriginHandler crossOriginHandler) throws Exception
{
server = new Server();
connector = new LocalConnector(server);
server.addConnector(connector);
ContextHandler context = new ContextHandler("/");
server.setHandler(context);
context.setHandler(crossOriginHandler);
crossOriginHandler.setHandler(new ApplicationHandler());
server.start();
}
@AfterEach
public void destroy()
{
LifeCycle.stop(server);
}
@Test
public void testRequestWithNoOriginArrivesToApplication() throws Exception
{
start(new CrossOriginHandler());
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertThat(response.get(HttpHeader.VARY), is(HttpHeader.ORIGIN.asString()));
}
@Test
public void testSimpleRequestWithNonMatchingOrigin() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of("http://localhost"));
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: http://127.0.0.1\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testSimpleRequestWithNonMatchingOriginNotDelivered() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of("http://localhost"));
crossOriginHandler.setDeliverNonAllowedOriginRequests(false);
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: http://127.0.0.1\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.BAD_REQUEST_400));
assertFalse(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testSimpleRequestWithWildcardOrigin() throws Exception
{
String origin = "http://foo.example.com";
start(new CrossOriginHandler());
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: %s\r
\r
""".formatted(origin);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertTrue(response.contains(HttpHeader.VARY));
}
@Test
public void testSimpleRequestWithMatchingWildcardOrigin() throws Exception
{
String origin = "http://subdomain.example.com";
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of("http://.*\\.example\\.com"));
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: %s\r
\r
""".formatted(origin);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertTrue(response.contains(HttpHeader.VARY));
}
@Test
public void testSimpleRequestWithMatchingWildcardOriginAndMultipleSubdomains() throws Exception
{
String origin = "http://subdomain.subdomain.example.com";
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of("http://.*\\.example\\.com"));
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: %s\r
\r
""".formatted(origin);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertTrue(response.contains(HttpHeader.VARY));
}
@Test
public void testSimpleRequestWithMatchingOriginAndWithoutTimingOrigin() throws Exception
{
String origin = "http://localhost";
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of(origin));
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: %s\r
\r
""".formatted(origin);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertFalse(response.contains(HttpHeader.TIMING_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.VARY));
}
@Test
public void testSimpleRequestWithMatchingOriginAndNonMatchingTimingOrigin() throws Exception
{
String origin = "http://localhost";
String timingOrigin = "http://127.0.0.1";
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of(origin));
crossOriginHandler.setAllowedTimingOriginPatterns(Set.of(timingOrigin.replace(".", "\\.")));
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: %s\r
\r
""".formatted(origin);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertFalse(response.contains(HttpHeader.TIMING_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.VARY));
}
@Test
public void testSimpleRequestWithMatchingOriginAndMatchingTimingOrigin() throws Exception
{
String origin = "http://localhost";
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of(origin));
crossOriginHandler.setAllowedTimingOriginPatterns(Set.of(origin));
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: %s\r
\r
""".formatted(origin);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertTrue(response.contains(HttpHeader.TIMING_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.VARY));
}
@Test
public void testSimpleRequestWithMatchingMultipleOrigins() throws Exception
{
String origin = "http://localhost";
String otherOrigin = "http://127\\.0\\.0\\.1";
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of(origin, otherOrigin));
start(crossOriginHandler);
// Use 2 spaces as separator in the Origin header
// to test that the implementation does not fail.
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: %s %s\r
\r
""".formatted(otherOrigin, origin);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertTrue(response.contains(HttpHeader.VARY));
}
@Test
public void testSimpleRequestWithoutCredentials() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowCredentials(false);
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testNonSimpleRequestWithoutPreflight() throws Exception
{
// We cannot know if an actual request has performed the preflight before:
// we'll trust browsers to do it right, so responses to actual requests
// will contain the CORS response headers.
start(new CrossOriginHandler());
String request = """
PUT / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testOptionsRequestButNotPreflight() throws Exception
{
// We cannot know if an actual request has performed the preflight before:
// we'll trust browsers to do it right, so responses to actual requests
// will contain the CORS response headers.
start(new CrossOriginHandler());
String request = """
OPTIONS / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testPreflightWithWildcardCustomHeaders() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedHeaders(Set.of("*"));
start(crossOriginHandler);
String request = """
OPTIONS / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Access-Control-Request-Headers: X-Foo-Bar\r
Access-Control-Request-Method: GET\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertFalse(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testPUTRequestWithPreflight() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedMethods(Set.of("PUT"));
start(crossOriginHandler);
// Preflight request.
String request = """
OPTIONS / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Access-Control-Request-Method: PUT\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertFalse(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_MAX_AGE));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_METHODS));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS));
// Preflight request was ok, now make the actual request.
request = """
PUT / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: http://localhost\r
\r
""";
response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testDELETERequestWithPreflightAndAllowedCustomHeaders() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedMethods(Set.of("GET", "HEAD", "POST", "PUT", "DELETE"));
crossOriginHandler.setAllowedHeaders(Set.of("X-Requested-With"));
start(crossOriginHandler);
// Preflight request.
String request = """
OPTIONS / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Access-Control-Request-Method: DELETE\r
Access-Control-Request-Headers: origin,x-custom,x-requested-with\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertFalse(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_MAX_AGE));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_METHODS));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS));
// Preflight request was ok, now make the actual request.
request = """
DELETE / HTTP/1.1\r
Host: localhost\r
Connection: close\r
X-Custom: value\r
X-Requested-With: local\r
Origin: http://localhost\r
\r
""";
response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testDELETERequestWithPreflightAndNotAllowedCustomHeaders() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedMethods(Set.of("GET", "HEAD", "POST", "PUT", "DELETE"));
start(crossOriginHandler);
// Preflight request.
String request = """
OPTIONS / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Access-Control-Request-Method: DELETE\r
Access-Control-Request-Headers: origin, x-custom, x-requested-with\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertFalse(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
List<String> allowedHeaders = response.getValuesList(HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS);
assertFalse(allowedHeaders.contains("x-custom"));
// The preflight request failed because header X-Custom is not allowed, actual request not issued.
}
@Test
public void testSimpleRequestWithExposedHeaders() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setExposedHeaders(Set.of("Content-Length"));
start(crossOriginHandler);
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_EXPOSE_HEADERS));
}
@Test
public void testDoNotDeliverPreflightRequest() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setDeliverPreflightRequests(false);
start(crossOriginHandler);
// Preflight request.
String request = """
OPTIONS / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Access-Control-Request-Method: PUT\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertFalse(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_METHODS));
}
@Test
public void testDeliverWebSocketUpgradeRequest() throws Exception
{
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
start(crossOriginHandler);
// Preflight request.
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: Upgrade\r
Upgrade: websocket\r
Sec-WebSocket-Version: 13\r
Origin: http://localhost\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertThat(response.get(HttpHeader.VARY), is(HttpHeader.ORIGIN.asString()));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
@Test
public void testDoNotDeliverNonMatchingWebSocketUpgradeRequest() throws Exception
{
String origin = "http://localhost";
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
crossOriginHandler.setAllowedOriginPatterns(Set.of(origin));
start(crossOriginHandler);
// Preflight request.
String request = """
GET / HTTP/1.1\r
Host: localhost\r
Connection: Upgrade\r
Upgrade: websocket\r
Sec-WebSocket-Version: 13
Origin: http://127.0.0.1\r
\r
""";
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request));
assertThat(response.getStatus(), is(HttpStatus.BAD_REQUEST_400));
assertFalse(response.contains(ApplicationHandler.APPLICATION_HEADER));
assertThat(response.get(HttpHeader.VARY), is(HttpHeader.ORIGIN.asString()));
assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
public static class ApplicationHandler extends Handler.Abstract
{
private static final String APPLICATION_HEADER = "X-Application";
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put(APPLICATION_HEADER, "true");
callback.succeeded();
return true;
}
}
}

View File

@ -95,6 +95,14 @@ public interface CharsetStringBuilder
*/
String build() throws CharacterCodingException;
/**
* @return the length in characters
*/
int length();
/**
* <p>Resets this sequence to be empty.</p>
*/
void reset();
/**
@ -168,6 +176,12 @@ public interface CharsetStringBuilder
return s;
}
@Override
public int length()
{
return _builder.length();
}
@Override
public void reset()
{
@ -207,6 +221,12 @@ public interface CharsetStringBuilder
return s;
}
@Override
public int length()
{
return _builder.length();
}
@Override
public void reset()
{
@ -319,6 +339,12 @@ public interface CharsetStringBuilder
}
}
@Override
public int length()
{
return _stringBuilder.length();
}
@Override
public void reset()
{

View File

@ -92,6 +92,7 @@ public class Utf8StringBuilder implements CharsetStringBuilder
_buffer = buffer;
}
@Override
public int length()
{
return _buffer.length();

View File

@ -116,7 +116,10 @@ import org.slf4j.LoggerFactory;
* ...
* &lt;/web-app&gt;
* </pre>
*
* @deprecated Use {@link org.eclipse.jetty.server.handler.CrossOriginHandler} instead
*/
@Deprecated
public class CrossOriginFilter implements Filter
{
private static final Logger LOG = LoggerFactory.getLogger(CrossOriginFilter.class);

View File

@ -1725,4 +1725,58 @@ public class DistributionTests extends AbstractJettyHomeTest
}
}
}
@Test
public void testCrossOriginModule() throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");
JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.build();
try (JettyHomeTester.Run run1 = distribution.start("--add-modules=http,cross-origin,demo-handler"))
{
run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS);
assertThat(run1.getExitValue(), is(0));
int httpPort1 = distribution.freePort();
try (JettyHomeTester.Run run2 = distribution.start(List.of("jetty.http.port=" + httpPort1)))
{
assertThat(run2.awaitConsoleLogsFor("Started oejs.Server", START_TIMEOUT, TimeUnit.SECONDS), is(true));
startHttpClient();
ContentResponse response = client.newRequest("http://localhost:" + httpPort1 + "/demo-handler/")
.headers(headers -> headers.put(HttpHeader.ORIGIN, "http://localhost:" + httpPort1))
.timeout(15, TimeUnit.SECONDS)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getContentAsString(), containsString("Hello World"));
// Verify that the CORS headers are present.
assertTrue(response.getHeaders().contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
}
int httpPort2 = distribution.freePort();
List<String> args = List.of(
"jetty.http.port=" + httpPort2,
// Allow a different origin.
"jetty.crossorigin.allowedOriginPatterns=http://localhost"
);
try (JettyHomeTester.Run run2 = distribution.start(args))
{
assertThat(run2.awaitConsoleLogsFor("Started oejs.Server", START_TIMEOUT, TimeUnit.SECONDS), is(true));
startHttpClient();
ContentResponse response = client.newRequest("http://localhost:" + httpPort2 + "/demo-handler/")
.headers(headers -> headers.put(HttpHeader.ORIGIN, "http://localhost:" + httpPort2))
.timeout(15, TimeUnit.SECONDS)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getContentAsString(), containsString("Hello World"));
// Verify that the CORS headers are not present, as the allowed origin is different.
assertFalse(response.getHeaders().contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN));
}
}
}
}