Introduced CrossOriginHandler. Added cross-origin Jetty module. Added CrossOriginHandler documentation to the programming guide. Added CrossOriginHandler documentation to the operations guide. Added cross-origin headers to the HttpHeader enum. Added test cases. Deprecated ee10 CrossOriginFilter. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
581f9ae9f4
commit
a9e564ad8c
|
@ -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]
|
||||||
|
----
|
|
@ -18,6 +18,7 @@ include::module-alpn.adoc[]
|
||||||
include::module-bytebufferpool.adoc[]
|
include::module-bytebufferpool.adoc[]
|
||||||
include::module-console-capture.adoc[]
|
include::module-console-capture.adoc[]
|
||||||
include::module-core-deploy.adoc[]
|
include::module-core-deploy.adoc[]
|
||||||
|
include::module-cross-origin.adoc[]
|
||||||
include::module-eeN-deploy.adoc[]
|
include::module-eeN-deploy.adoc[]
|
||||||
include::module-http.adoc[]
|
include::module-http.adoc[]
|
||||||
include::module-http2.adoc[]
|
include::module-http2.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]
|
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]]
|
[[pg-server-http-handler-use-default]]
|
||||||
====== DefaultHandler
|
====== DefaultHandler
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ However, if the `WebSocketUpgradeFilter` is already present in `web.xml` under t
|
||||||
|
|
||||||
This allows you to customize:
|
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` 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 `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.
|
* 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">
|
version="5.0">
|
||||||
<display-name>My WebSocket WebApp</display-name>
|
<display-name>My WebSocket WebApp</display-name>
|
||||||
|
|
||||||
<!-- The CrossOriginFilter *must* be the first --> <!--1-->
|
<!-- The SecurityFilter *must* be the first --> <!--1-->
|
||||||
<filter>
|
<filter>
|
||||||
<filter-name>cross-origin</filter-name>
|
<filter-name>security</filter-name>
|
||||||
<filter-class>org.eclipse.jetty.{ee-current}.servlets.CrossOriginFilter</filter-class>
|
<filter-class>com.acme.SecurityFilter</filter-class>
|
||||||
<async-supported>true</async-supported>
|
<async-supported>true</async-supported>
|
||||||
</filter>
|
</filter>
|
||||||
<filter-mapping>
|
<filter-mapping>
|
||||||
<filter-name>cross-origin</filter-name>
|
<filter-name>security</filter-name>
|
||||||
<url-pattern>/*</url-pattern>
|
<url-pattern>/*</url-pattern>
|
||||||
</filter-mapping>
|
</filter-mapping>
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ For example:
|
||||||
|
|
||||||
</web-app>
|
</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`.
|
<2> The configuration for the _default_ `WebSocketUpgradeFilter`.
|
||||||
<3> Note the use of the _default_ `WebSocketUpgradeFilter` name.
|
<3> Note the use of the _default_ `WebSocketUpgradeFilter` name.
|
||||||
<4> Specific configuration for `WebSocketUpgradeFilter` parameters.
|
<4> Specific configuration for `WebSocketUpgradeFilter` parameters.
|
||||||
|
|
|
@ -18,12 +18,11 @@ import java.nio.ByteBuffer;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import jakarta.servlet.DispatcherType;
|
|
||||||
import jakarta.servlet.ServletInputStream;
|
import jakarta.servlet.ServletInputStream;
|
||||||
import jakarta.servlet.http.HttpServlet;
|
import jakarta.servlet.http.HttpServlet;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
@ -31,10 +30,8 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.conscrypt.OpenSSLProvider;
|
import org.conscrypt.OpenSSLProvider;
|
||||||
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
|
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
|
||||||
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
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.ServletContextHandler;
|
||||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
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.ee10.webapp.WebAppContext;
|
||||||
import org.eclipse.jetty.http.HttpCompliance;
|
import org.eclipse.jetty.http.HttpCompliance;
|
||||||
import org.eclipse.jetty.http.HttpFields;
|
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.SslConnectionFactory;
|
||||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||||
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
|
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.DefaultHandler;
|
||||||
import org.eclipse.jetty.server.handler.EventsHandler;
|
import org.eclipse.jetty.server.handler.EventsHandler;
|
||||||
import org.eclipse.jetty.server.handler.QoSHandler;
|
import org.eclipse.jetty.server.handler.QoSHandler;
|
||||||
|
@ -1066,22 +1064,21 @@ public class HTTPServerDocs
|
||||||
Connector connector = new ServerConnector(server);
|
Connector connector = new ServerConnector(server);
|
||||||
server.addConnector(connector);
|
server.addConnector(connector);
|
||||||
|
|
||||||
|
// Add the CrossOriginHandler to protect from CSRF attacks.
|
||||||
|
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
|
||||||
|
server.setHandler(crossOriginHandler);
|
||||||
|
|
||||||
// Create a ServletContextHandler with contextPath.
|
// Create a ServletContextHandler with contextPath.
|
||||||
ServletContextHandler context = new ServletContextHandler();
|
ServletContextHandler context = new ServletContextHandler();
|
||||||
context.setContextPath("/shop");
|
context.setContextPath("/shop");
|
||||||
// Link the context to the server.
|
// Link the context to the server.
|
||||||
server.setHandler(context);
|
crossOriginHandler.setHandler(context);
|
||||||
|
|
||||||
// Add the Servlet implementing the cart functionality to the context.
|
// Add the Servlet implementing the cart functionality to the context.
|
||||||
ServletHolder servletHolder = context.addServlet(ShopCartServlet.class, "/cart/*");
|
ServletHolder servletHolder = context.addServlet(ShopCartServlet.class, "/cart/*");
|
||||||
// Configure the Servlet with init-parameters.
|
// Configure the Servlet with init-parameters.
|
||||||
servletHolder.setInitParameter("maxItems", "128");
|
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();
|
server.start();
|
||||||
// end::servletContextHandler-setup[]
|
// end::servletContextHandler-setup[]
|
||||||
}
|
}
|
||||||
|
@ -1463,6 +1460,15 @@ public class HTTPServerDocs
|
||||||
// end::securedHandler[]
|
// 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
|
public void defaultHandler() throws Exception
|
||||||
{
|
{
|
||||||
// tag::defaultHandler[]
|
// tag::defaultHandler[]
|
||||||
|
|
|
@ -21,7 +21,6 @@ import org.eclipse.jetty.util.StringUtil;
|
||||||
|
|
||||||
public enum HttpHeader
|
public enum HttpHeader
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General Fields.
|
* General Fields.
|
||||||
*/
|
*/
|
||||||
|
@ -59,6 +58,8 @@ public enum HttpHeader
|
||||||
ACCEPT_CHARSET("Accept-Charset"),
|
ACCEPT_CHARSET("Accept-Charset"),
|
||||||
ACCEPT_ENCODING("Accept-Encoding"),
|
ACCEPT_ENCODING("Accept-Encoding"),
|
||||||
ACCEPT_LANGUAGE("Accept-Language"),
|
ACCEPT_LANGUAGE("Accept-Language"),
|
||||||
|
ACCESS_CONTROL_REQUEST_HEADERS("Access-Control-Request-Headers"),
|
||||||
|
ACCESS_CONTROL_REQUEST_METHOD("Access-Control-Request-Method"),
|
||||||
AUTHORIZATION("Authorization"),
|
AUTHORIZATION("Authorization"),
|
||||||
EXPECT("Expect"),
|
EXPECT("Expect"),
|
||||||
FORWARDED("Forwarded"),
|
FORWARDED("Forwarded"),
|
||||||
|
@ -87,6 +88,12 @@ public enum HttpHeader
|
||||||
* Response Fields.
|
* Response Fields.
|
||||||
*/
|
*/
|
||||||
ACCEPT_RANGES("Accept-Ranges"),
|
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"),
|
AGE("Age"),
|
||||||
ALT_SVC("Alt-Svc"),
|
ALT_SVC("Alt-Svc"),
|
||||||
ETAG("ETag"),
|
ETAG("ETag"),
|
||||||
|
@ -96,6 +103,7 @@ public enum HttpHeader
|
||||||
RETRY_AFTER("Retry-After"),
|
RETRY_AFTER("Retry-After"),
|
||||||
SERVER("Server"),
|
SERVER("Server"),
|
||||||
SERVLET_ENGINE("Servlet-Engine"),
|
SERVLET_ENGINE("Servlet-Engine"),
|
||||||
|
TIMING_ALLOW_ORIGIN("Timing-Allow-Origin"),
|
||||||
VARY("Vary"),
|
VARY("Vary"),
|
||||||
WWW_AUTHENTICATE("WWW-Authenticate"),
|
WWW_AUTHENTICATE("WWW-Authenticate"),
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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[]
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -116,7 +116,10 @@ import org.slf4j.LoggerFactory;
|
||||||
* ...
|
* ...
|
||||||
* </web-app>
|
* </web-app>
|
||||||
* </pre>
|
* </pre>
|
||||||
|
*
|
||||||
|
* @deprecated Use {@link org.eclipse.jetty.server.handler.CrossOriginHandler} instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public class CrossOriginFilter implements Filter
|
public class CrossOriginFilter implements Filter
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CrossOriginFilter.class);
|
private static final Logger LOG = LoggerFactory.getLogger(CrossOriginFilter.class);
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue