diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/module-cross-origin.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/module-cross-origin.adoc
new file mode 100644
index 000000000000..8bde6ba208bf
--- /dev/null
+++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/module-cross-origin.adoc
@@ -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]
+----
diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/modules-standard.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/modules-standard.adoc
index ae5bdb1f717e..c36e6a294fca 100644
--- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/modules-standard.adoc
+++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/modules-standard.adoc
@@ -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[]
diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc
index e7742c5a0a52..166d89687bc9 100644
--- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc
+++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc
@@ -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
diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-filter.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-filter.adoc
index f04ba63862c9..a4acf64bfc93 100644
--- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-filter.adoc
+++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/websocket/server-websocket-filter.adoc
@@ -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">
My WebSocket WebApp
-
+
- cross-origin
- org.eclipse.jetty.{ee-current}.servlets.CrossOriginFilter
+ security
+ com.acme.SecurityFilter
true
- cross-origin
+ security
/*
@@ -69,7 +69,7 @@ For example:
----
-<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.
diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
index 591f586b2f03..96254ab73817 100644
--- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
+++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
@@ -18,12 +18,11 @@
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 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;
@@ -72,6 +69,7 @@
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;
@@ -1004,22 +1002,21 @@ public void servletContextHandler() throws Exception
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[]
}
@@ -1401,6 +1398,15 @@ public void securedHandler() throws Exception
// 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[]
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java
index 9f364575c435..0329464ac41a 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java
@@ -21,7 +21,6 @@
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"),
diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-cross-origin.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-cross-origin.xml
new file mode 100644
index 000000000000..7bc59d951156
--- /dev/null
+++ b/jetty-core/jetty-server/src/main/config/etc/jetty-cross-origin.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-core/jetty-server/src/main/config/modules/cross-origin.mod b/jetty-core/jetty-server/src/main/config/modules/cross-origin.mod
new file mode 100644
index 000000000000..24d6356a6c7d
--- /dev/null
+++ b/jetty-core/jetty-server/src/main/config/modules/cross-origin.mod
@@ -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[]
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CrossOriginHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CrossOriginHandler.java
new file mode 100644
index 000000000000..e5e9a703deb4
--- /dev/null
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CrossOriginHandler.java
@@ -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;
+
+/**
+ *
Implementation of the CORS protocol defined by the
+ * fetch standard.
+ * This {@link Handler} should be present in the {@link Handler} tree to prevent
+ * cross site request forgery attacks.
+ * 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:
+ * {@code
+ * GET / HTTP/1.1
+ * Host: cross.domain.com
+ * Origin: http://domain.com
+ * }
+ * 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}.
+ */
+@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 allowedHeaders = Set.of("Content-Type");
+ private Set allowedMethods = Set.of("GET", "POST", "HEAD");
+ private Set allowedOrigins = Set.of("*");
+ private Set allowedTimingOrigins = Set.of();
+ private boolean deliverPreflight = false;
+ private boolean deliverNonAllowedOrigin = true;
+ private boolean deliverNonAllowedOriginWebSocketUpgrade = false;
+ private Set exposedHeaders = Set.of();
+ private Duration preflightMaxAge = Duration.ofSeconds(60);
+ private boolean anyOriginAllowed;
+ private final Set allowedOriginPatterns = new LinkedHashSet<>();
+ private boolean anyTimingOriginAllowed;
+ private final Set 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;
+ }
+
+ /**
+ * Sets whether the cross server allows cross-origin requests to include credentials
+ * such as cookies or authentication headers.
+ * For example, when the cross server allows credentials to be included, cross-origin
+ * requests will contain cookies, otherwise they will not.
+ * The default is {@code true}.
+ *
+ * @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 getAllowedHeaders()
+ {
+ return allowedHeaders;
+ }
+
+ /**
+ * Sets the set of allowed headers in a cross-origin request.
+ * 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.
+ * The headers can be either the character {@code *} to indicate any
+ * header, or actual header names.
+ *
+ * @param headers the set of allowed headers in a cross-origin request
+ */
+ public void setAllowedHeaders(Set 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 getAllowedMethods()
+ {
+ return allowedMethods;
+ }
+
+ /**
+ * Sets the set of allowed methods in a cross-origin request.
+ * 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.
+ *
+ * @param methods the set of allowed methods in a cross-origin request
+ */
+ public void setAllowedMethods(Set 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 getAllowedOriginPatterns()
+ {
+ return allowedOrigins;
+ }
+
+ /**
+ * Sets the set of allowed origin regex strings in a cross-origin request.
+ * 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).
+ * The origins are either the character {@code *}, or regular expressions,
+ * so dot characters separating domain segments must be escaped:
+ * {@code
+ * crossOriginHandler.setAllowedOriginPatterns(Set.of("https://.*\\.domain\\.com"));
+ * }
+ * The default value is {@code *}.
+ *
+ * @param origins the set of allowed origin regex strings in a cross-origin request
+ */
+ public void setAllowedOriginPatterns(Set 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 getAllowedTimingOriginPatterns()
+ {
+ return allowedTimingOrigins;
+ }
+
+ /**
+ * Sets the set of allowed timing origin regex strings in a cross-origin request.
+ *
+ * @param origins the set of allowed timing origin regex strings in a cross-origin request
+ */
+ public void setAllowedTimingOriginPatterns(Set 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;
+ }
+
+ /**
+ * Sets whether preflight requests are delivered to the child {@link Handler}.
+ * Default value is {@code false}.
+ *
+ * @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;
+ }
+
+ /**
+ * Sets whether requests whose origin is not allowed are delivered to the child Handler.
+ * Default value is {@code true}.
+ *
+ * @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;
+ }
+
+ /**
+ * Sets whether WebSocket upgrade requests whose origin is not allowed are delivered to the child Handler.
+ * Default value is {@code false}.
+ *
+ * @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 getExposedHeaders()
+ {
+ return exposedHeaders;
+ }
+
+ /**
+ * Sets the set of headers exposed in a cross-origin response.
+ * The cross server receives a cross-origin request and indicates
+ * which response headers are exposed to scripts running in the browser.
+ *
+ * @param headers the set of headers exposed in a cross-origin response
+ */
+ public void setExposedHeaders(Set 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 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 allowedMethods = getAllowedMethods();
+ if (!allowedMethods.isEmpty())
+ headers.put(accessControlAllowMethodsField);
+ Set 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 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");
+ }
+}
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/CrossOriginHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/CrossOriginHandlerTest.java
new file mode 100644
index 000000000000..e641b67b36c3
--- /dev/null
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/CrossOriginHandlerTest.java
@@ -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 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;
+ }
+ }
+}
diff --git a/jetty-ee10/jetty-ee10-servlets/src/main/java/org/eclipse/jetty/ee10/servlets/CrossOriginFilter.java b/jetty-ee10/jetty-ee10-servlets/src/main/java/org/eclipse/jetty/ee10/servlets/CrossOriginFilter.java
index b576a7aa2c8a..6615beb62a91 100644
--- a/jetty-ee10/jetty-ee10-servlets/src/main/java/org/eclipse/jetty/ee10/servlets/CrossOriginFilter.java
+++ b/jetty-ee10/jetty-ee10-servlets/src/main/java/org/eclipse/jetty/ee10/servlets/CrossOriginFilter.java
@@ -116,7 +116,10 @@
* ...
* </web-app>
*
+ *
+ * @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);
diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
index cf5171f7b246..c624c1811291 100644
--- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
+++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
@@ -1725,4 +1725,58 @@ public void testInetAccessHandler() throws Exception
}
}
}
+
+ @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 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));
+ }
+ }
+ }
}