Merge branch 'jetty-12.0.x' of github.com:jetty/jetty.project into jetty-12.0.x
This commit is contained in:
commit
090287db5e
|
@ -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-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[]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ However, if the `WebSocketUpgradeFilter` is already present in `web.xml` under t
|
|||
|
||||
This allows you to customize:
|
||||
|
||||
* The filter order; for example, by configuring the `CrossOriginFilter` (or other filters) for increased security or authentication _before_ the `WebSocketUpgradeFilter`.
|
||||
* The filter order; for example, by configuring filters for increased security or authentication _before_ the `WebSocketUpgradeFilter`.
|
||||
* The `WebSocketUpgradeFilter` configuration via ``init-param``s, that affects all `Session` instances created by this filter.
|
||||
* The `WebSocketUpgradeFilter` path mapping. Rather than the default mapping of `+/*+`, you can map the `WebSocketUpgradeFilter` to a more specific path such as `+/ws/*+`.
|
||||
* The possibility to have multiple ``WebSocketUpgradeFilter``s, mapped to different paths, each with its own configuration.
|
||||
|
@ -38,14 +38,14 @@ For example:
|
|||
version="5.0">
|
||||
<display-name>My WebSocket WebApp</display-name>
|
||||
|
||||
<!-- The CrossOriginFilter *must* be the first --> <!--1-->
|
||||
<!-- The SecurityFilter *must* be the first --> <!--1-->
|
||||
<filter>
|
||||
<filter-name>cross-origin</filter-name>
|
||||
<filter-class>org.eclipse.jetty.{ee-current}.servlets.CrossOriginFilter</filter-class>
|
||||
<filter-name>security</filter-name>
|
||||
<filter-class>com.acme.SecurityFilter</filter-class>
|
||||
<async-supported>true</async-supported>
|
||||
</filter>
|
||||
<filter-mapping>
|
||||
<filter-name>cross-origin</filter-name>
|
||||
<filter-name>security</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
|
@ -69,7 +69,7 @@ For example:
|
|||
|
||||
</web-app>
|
||||
----
|
||||
<1> The `CrossOriginFilter` is the first to protect against link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery attacks].
|
||||
<1> The custom `SecurityFilter` is the first, to apply custom security.
|
||||
<2> The configuration for the _default_ `WebSocketUpgradeFilter`.
|
||||
<3> Note the use of the _default_ `WebSocketUpgradeFilter` name.
|
||||
<4> Specific configuration for `WebSocketUpgradeFilter` parameters.
|
||||
|
|
|
@ -18,12 +18,11 @@ import java.nio.ByteBuffer;
|
|||
import java.nio.file.Path;
|
||||
import java.security.Security;
|
||||
import java.time.Duration;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
@ -31,10 +30,8 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||
import org.conscrypt.OpenSSLProvider;
|
||||
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
|
||||
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
||||
import org.eclipse.jetty.ee10.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.ee10.servlets.CrossOriginFilter;
|
||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||
import org.eclipse.jetty.http.HttpCompliance;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
|
@ -74,6 +71,7 @@ import org.eclipse.jetty.server.Slf4jRequestLogWriter;
|
|||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
|
||||
import org.eclipse.jetty.server.handler.CrossOriginHandler;
|
||||
import org.eclipse.jetty.server.handler.DefaultHandler;
|
||||
import org.eclipse.jetty.server.handler.EventsHandler;
|
||||
import org.eclipse.jetty.server.handler.QoSHandler;
|
||||
|
@ -1066,22 +1064,21 @@ public class HTTPServerDocs
|
|||
Connector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
// Add the CrossOriginHandler to protect from CSRF attacks.
|
||||
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
|
||||
server.setHandler(crossOriginHandler);
|
||||
|
||||
// Create a ServletContextHandler with contextPath.
|
||||
ServletContextHandler context = new ServletContextHandler();
|
||||
context.setContextPath("/shop");
|
||||
// Link the context to the server.
|
||||
server.setHandler(context);
|
||||
crossOriginHandler.setHandler(context);
|
||||
|
||||
// Add the Servlet implementing the cart functionality to the context.
|
||||
ServletHolder servletHolder = context.addServlet(ShopCartServlet.class, "/cart/*");
|
||||
// Configure the Servlet with init-parameters.
|
||||
servletHolder.setInitParameter("maxItems", "128");
|
||||
|
||||
// Add the CrossOriginFilter to protect from CSRF attacks.
|
||||
FilterHolder filterHolder = context.addFilter(CrossOriginFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
|
||||
// Configure the filter.
|
||||
filterHolder.setAsyncSupported(true);
|
||||
|
||||
server.start();
|
||||
// end::servletContextHandler-setup[]
|
||||
}
|
||||
|
@ -1463,6 +1460,15 @@ public class HTTPServerDocs
|
|||
// end::securedHandler[]
|
||||
}
|
||||
|
||||
public void crossOriginAllowedOrigins()
|
||||
{
|
||||
// tag::crossOriginAllowedOrigins[]
|
||||
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
|
||||
// The allowed origins are regex patterns.
|
||||
crossOriginHandler.setAllowedOriginPatterns(Set.of("http://domain\\.com"));
|
||||
// end::crossOriginAllowedOrigins[]
|
||||
}
|
||||
|
||||
public void defaultHandler() throws Exception
|
||||
{
|
||||
// tag::defaultHandler[]
|
||||
|
|
|
@ -30,7 +30,7 @@ import org.eclipse.jetty.io.Content;
|
|||
* multiPart.close();
|
||||
* ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
|
||||
* .method(HttpMethod.POST)
|
||||
* .content(multiPart)
|
||||
* .body(multiPart)
|
||||
* .send();
|
||||
* </pre>
|
||||
* <p>The above example would be the equivalent of submitting this form:</p>
|
||||
|
|
|
@ -34,7 +34,7 @@ import org.eclipse.jetty.io.content.OutputStreamContentSource;
|
|||
* try (OutputStream output = content.getOutputStream())
|
||||
* {
|
||||
* httpClient.newRequest("localhost", 8080)
|
||||
* .content(content)
|
||||
* .body(content)
|
||||
* .send(new Response.CompleteListener()
|
||||
* {
|
||||
* @Override
|
||||
|
|
|
@ -26,6 +26,8 @@ public interface CookieParser
|
|||
{
|
||||
/**
|
||||
* <p>A factory method to create a new parser suitable for the compliance mode.</p>
|
||||
*
|
||||
* @param handler the handler for Cookie Parsing events
|
||||
* @param compliance The compliance mode to use for parsing.
|
||||
* @param complianceListener A listener for compliance violations or null.
|
||||
* @return A CookieParser instance.
|
||||
|
|
|
@ -21,7 +21,6 @@ import org.eclipse.jetty.util.StringUtil;
|
|||
|
||||
public enum HttpHeader
|
||||
{
|
||||
|
||||
/**
|
||||
* General Fields.
|
||||
*/
|
||||
|
@ -59,6 +58,8 @@ public enum HttpHeader
|
|||
ACCEPT_CHARSET("Accept-Charset"),
|
||||
ACCEPT_ENCODING("Accept-Encoding"),
|
||||
ACCEPT_LANGUAGE("Accept-Language"),
|
||||
ACCESS_CONTROL_REQUEST_HEADERS("Access-Control-Request-Headers"),
|
||||
ACCESS_CONTROL_REQUEST_METHOD("Access-Control-Request-Method"),
|
||||
AUTHORIZATION("Authorization"),
|
||||
EXPECT("Expect"),
|
||||
FORWARDED("Forwarded"),
|
||||
|
@ -87,6 +88,12 @@ public enum HttpHeader
|
|||
* Response Fields.
|
||||
*/
|
||||
ACCEPT_RANGES("Accept-Ranges"),
|
||||
ACCESS_CONTROL_ALLOW_ORIGIN("Access-Control-Allow-Origin"),
|
||||
ACCESS_CONTROL_ALLOW_METHODS("Access-Control-Allow-Methods"),
|
||||
ACCESS_CONTROL_ALLOW_HEADERS("Access-Control-Allow-Headers"),
|
||||
ACCESS_CONTROL_MAX_AGE("Access-Control-Max-Age"),
|
||||
ACCESS_CONTROL_ALLOW_CREDENTIALS("Access-Control-Allow-Credentials"),
|
||||
ACCESS_CONTROL_EXPOSE_HEADERS("Access-Control-Expose-Headers"),
|
||||
AGE("Age"),
|
||||
ALT_SVC("Alt-Svc"),
|
||||
ETAG("ETag"),
|
||||
|
@ -96,6 +103,7 @@ public enum HttpHeader
|
|||
RETRY_AFTER("Retry-After"),
|
||||
SERVER("Server"),
|
||||
SERVLET_ENGINE("Servlet-Engine"),
|
||||
TIMING_ALLOW_ORIGIN("Timing-Allow-Origin"),
|
||||
VARY("Vary"),
|
||||
WWW_AUTHENTICATE("WWW-Authenticate"),
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ import org.eclipse.jetty.util.thread.AutoLock;
|
|||
* Content.Source content = ...;
|
||||
*
|
||||
* // Create and configure MultiPartByteRanges.
|
||||
* MultiPartByteRanges byteRanges = new MultiPartByteRanges(boundary);
|
||||
* MultiPartByteRanges.Parser byteRanges = new MultiPartByteRanges.Parser(boundary);
|
||||
*
|
||||
* // Parse the content.
|
||||
* byteRanges.parse(content)
|
||||
|
|
|
@ -55,7 +55,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
|
|||
* Content.Source content = ...;
|
||||
*
|
||||
* // Create and configure MultiPartFormData.
|
||||
* MultiPartFormData formData = new MultiPartFormData(boundary);
|
||||
* MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
* // Where to store the files.
|
||||
* formData.setFilesDirectory(Path.of("/tmp"));
|
||||
* // Max 1 MiB files.
|
||||
|
|
|
@ -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[]
|
|
@ -32,8 +32,9 @@ import org.eclipse.jetty.util.StringUtil;
|
|||
import static org.eclipse.jetty.util.UrlEncoded.decodeHexByte;
|
||||
|
||||
/**
|
||||
* A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded}
|
||||
* content has been parsed asynchronously from the {@link Content.Source}.
|
||||
* <p>A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded}
|
||||
* content has been parsed asynchronously from the {@link Content.Source}.</p>
|
||||
* <p><a href="https://url.spec.whatwg.org/#application/x-www-form-urlencoded">Specification</a>.</p>
|
||||
*/
|
||||
public class FormFields extends ContentSourceCompletableFuture<Fields>
|
||||
{
|
||||
|
@ -209,98 +210,103 @@ public class FormFields extends ContentSourceCompletableFuture<Fields>
|
|||
@Override
|
||||
protected Fields parse(Content.Chunk chunk) throws CharacterCodingException
|
||||
{
|
||||
String value = null;
|
||||
ByteBuffer buffer = chunk.getByteBuffer();
|
||||
|
||||
do
|
||||
while (BufferUtil.hasContent(buffer))
|
||||
{
|
||||
loop:
|
||||
while (BufferUtil.hasContent(buffer))
|
||||
byte b = buffer.get();
|
||||
switch (_percent)
|
||||
{
|
||||
byte b = buffer.get();
|
||||
switch (_percent)
|
||||
case 1 ->
|
||||
{
|
||||
case 1 ->
|
||||
{
|
||||
_percentCode = b;
|
||||
_percent++;
|
||||
continue;
|
||||
}
|
||||
case 2 ->
|
||||
{
|
||||
_builder.append(decodeHexByte((char)_percentCode, (char)b));
|
||||
_percent = 0;
|
||||
continue;
|
||||
}
|
||||
_percentCode = b;
|
||||
_percent++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_name == null)
|
||||
case 2 ->
|
||||
{
|
||||
switch (b)
|
||||
{
|
||||
case '=' ->
|
||||
{
|
||||
_name = _builder.build();
|
||||
checkLength(_name);
|
||||
}
|
||||
case '+' -> _builder.append((byte)' ');
|
||||
case '%' -> _percent++;
|
||||
default -> _builder.append(b);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (b)
|
||||
{
|
||||
case '&' ->
|
||||
{
|
||||
value = _builder.build();
|
||||
checkLength(value);
|
||||
break loop;
|
||||
}
|
||||
case '+' -> _builder.append((byte)' ');
|
||||
case '%' -> _percent++;
|
||||
default -> _builder.append(b);
|
||||
}
|
||||
_percent = 0;
|
||||
_builder.append(decodeHexByte((char)_percentCode, (char)b));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (_name != null)
|
||||
if (_name == null)
|
||||
{
|
||||
if (value == null && chunk.isLast())
|
||||
switch (b)
|
||||
{
|
||||
if (_percent > 0)
|
||||
case '&' ->
|
||||
{
|
||||
_builder.append((byte)'%');
|
||||
_builder.append(_percentCode);
|
||||
String name = _builder.build();
|
||||
checkMaxLength(name);
|
||||
onNewField(name, "");
|
||||
}
|
||||
value = _builder.build();
|
||||
checkLength(value);
|
||||
case '=' ->
|
||||
{
|
||||
_name = _builder.build();
|
||||
checkMaxLength(_name);
|
||||
}
|
||||
case '+' -> _builder.append(' ');
|
||||
case '%' -> _percent++;
|
||||
default -> _builder.append(b);
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (b)
|
||||
{
|
||||
Fields.Field field = new Fields.Field(_name, value);
|
||||
_name = null;
|
||||
value = null;
|
||||
if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
|
||||
throw new IllegalStateException("form with too many fields > " + _maxFields);
|
||||
_fields.add(field);
|
||||
case '&' ->
|
||||
{
|
||||
String value = _builder.build();
|
||||
checkMaxLength(value);
|
||||
onNewField(_name, value);
|
||||
_name = null;
|
||||
}
|
||||
case '+' -> _builder.append(' ');
|
||||
case '%' -> _percent++;
|
||||
default -> _builder.append(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (BufferUtil.hasContent(buffer));
|
||||
|
||||
return chunk.isLast() ? _fields : null;
|
||||
if (!chunk.isLast())
|
||||
return null;
|
||||
|
||||
// Append any remaining %x.
|
||||
if (_percent > 0)
|
||||
throw new IllegalStateException("invalid percent encoding");
|
||||
String value = _builder.build();
|
||||
|
||||
if (_name == null)
|
||||
{
|
||||
if (!value.isEmpty())
|
||||
{
|
||||
checkMaxLength(value);
|
||||
onNewField(value, "");
|
||||
}
|
||||
return _fields;
|
||||
}
|
||||
|
||||
checkMaxLength(value);
|
||||
onNewField(_name, value);
|
||||
return _fields;
|
||||
}
|
||||
|
||||
private void checkLength(String nameOrValue)
|
||||
private void checkMaxLength(String nameOrValue)
|
||||
{
|
||||
if (_maxLength >= 0)
|
||||
{
|
||||
_length += nameOrValue.length();
|
||||
if (_length > _maxLength)
|
||||
throw new IllegalStateException("form too large");
|
||||
throw new IllegalStateException("form too large > " + _maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNewField(String name, String value)
|
||||
{
|
||||
Fields.Field field = new Fields.Field(name, value);
|
||||
_fields.add(field);
|
||||
if (_maxFields >= 0 && _fields.getSize() > _maxFields)
|
||||
throw new IllegalStateException("form with too many fields > " + _maxFields);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -13,11 +13,13 @@
|
|||
|
||||
package org.eclipse.jetty.server;
|
||||
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
@ -32,31 +34,41 @@ import org.junit.jupiter.params.provider.Arguments;
|
|||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class FormFieldsTest
|
||||
{
|
||||
public static Stream<Arguments> tests()
|
||||
public static Stream<Arguments> validData()
|
||||
{
|
||||
return Stream.of(
|
||||
Arguments.of(List.of(""), UTF_8, -1, -1, Map.of()),
|
||||
Arguments.of(List.of("name"), UTF_8, -1, -1, Map.of("name", "")),
|
||||
Arguments.of(List.of("name&"), UTF_8, -1, -1, Map.of("name", "")),
|
||||
Arguments.of(List.of("name="), UTF_8, -1, -1, Map.of("name", "")),
|
||||
Arguments.of(List.of("name%00="), UTF_8, -1, -1, Map.of("name\u0000", "")),
|
||||
Arguments.of(List.of("n1=v1&n2"), UTF_8, -1, -1, Map.of("n1", "v1", "n2", "")),
|
||||
Arguments.of(List.of("n1=v1&n2&n3=v3&n4"), UTF_8, -1, -1, Map.of("n1", "v1", "n2", "", "n3", "v3", "n4", "")),
|
||||
Arguments.of(List.of("name=value"), UTF_8, -1, -1, Map.of("name", "value")),
|
||||
Arguments.of(List.of("name=%0A"), UTF_8, -1, -1, Map.of("name", "\n")),
|
||||
Arguments.of(List.of("name=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
|
||||
Arguments.of(List.of("name", "=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
|
||||
Arguments.of(List.of("n", "ame", "=", "value"), UTF_8, -1, -1, Map.of("name", "value")),
|
||||
Arguments.of(List.of("n=v&X=Y"), UTF_8, 2, 4, Map.of("n", "v", "X", "Y")),
|
||||
Arguments.of(List.of("name=f¤¤&X=Y"), UTF_8, -1, -1, Map.of("name", "f¤¤", "X", "Y")),
|
||||
Arguments.of(List.of("n=v&X=Y"), UTF_8, 1, -1, null),
|
||||
Arguments.of(List.of("n=v&X=Y"), UTF_8, -1, 3, null)
|
||||
Arguments.of(List.of("na+me=", "va", "+", "lue"), UTF_8, -1, -1, Map.of("na me", "va lue")),
|
||||
Arguments.of(List.of("=v"), UTF_8, -1, -1, Map.of("", "v"))
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("tests")
|
||||
public void testFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Map<String, String> expected)
|
||||
throws Exception
|
||||
@MethodSource("validData")
|
||||
public void testValidFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Map<String, String> expected)
|
||||
{
|
||||
AsyncContent source = new AsyncContent();
|
||||
Attributes attributes = new Attributes.Mapped();
|
||||
|
@ -66,8 +78,9 @@ public class FormFieldsTest
|
|||
int last = chunks.size() - 1;
|
||||
FutureCallback eof = new FutureCallback();
|
||||
for (int i = 0; i <= last; i++)
|
||||
{
|
||||
source.write(i == last, BufferUtil.toBuffer(chunks.get(i), charset), i == last ? eof : Callback.NOOP);
|
||||
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -89,4 +102,45 @@ public class FormFieldsTest
|
|||
assertNull(expected);
|
||||
}
|
||||
}
|
||||
|
||||
public static Stream<Arguments> invalidData()
|
||||
{
|
||||
return Stream.of(
|
||||
Arguments.of(List.of("%A"), UTF_8, -1, -1, IllegalStateException.class),
|
||||
Arguments.of(List.of("name%"), UTF_8, -1, -1, IllegalStateException.class),
|
||||
Arguments.of(List.of("name%A"), UTF_8, -1, -1, IllegalStateException.class),
|
||||
|
||||
// TODO: these 2 should throw the same exception.
|
||||
Arguments.of(List.of("name%A="), UTF_8, -1, -1, CharacterCodingException.class),
|
||||
Arguments.of(List.of("name%A&"), UTF_8, -1, -1, IllegalArgumentException.class),
|
||||
|
||||
Arguments.of(List.of("name=%"), UTF_8, -1, -1, IllegalStateException.class),
|
||||
Arguments.of(List.of("name=A%%A"), UTF_8, -1, -1, IllegalArgumentException.class),
|
||||
Arguments.of(List.of("name=A%%3D"), UTF_8, -1, -1, IllegalArgumentException.class),
|
||||
Arguments.of(List.of("%="), UTF_8, -1, -1, IllegalStateException.class),
|
||||
Arguments.of(List.of("name=%A"), UTF_8, -1, -1, IllegalStateException.class),
|
||||
Arguments.of(List.of("name=value%A"), UTF_8, -1, -1, IllegalStateException.class),
|
||||
Arguments.of(List.of("n=v&X=Y"), UTF_8, 1, -1, IllegalStateException.class),
|
||||
Arguments.of(List.of("n=v&X=Y"), UTF_8, -1, 3, IllegalStateException.class),
|
||||
Arguments.of(List.of("n%AH=v"), UTF_8, -1, -1, IllegalArgumentException.class),
|
||||
Arguments.of(List.of("n=v%AH"), UTF_8, -1, -1, IllegalArgumentException.class),
|
||||
Arguments.of(List.of("n=v%FF"), UTF_8, -1, -1, CharacterCodingException.class)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("invalidData")
|
||||
public void testInvalidFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Class<? extends Exception> expectedException)
|
||||
{
|
||||
AsyncContent source = new AsyncContent();
|
||||
CompletableFuture<Fields> futureFields = FormFields.from(source, new Attributes.Mapped(), charset, maxFields, maxLength);
|
||||
assertFalse(futureFields.isDone());
|
||||
int last = chunks.size() - 1;
|
||||
for (int i = 0; i <= last; i++)
|
||||
{
|
||||
source.write(i == last, BufferUtil.toBuffer(chunks.get(i)), Callback.NOOP);
|
||||
}
|
||||
Throwable cause = assertThrows(ExecutionException.class, futureFields::get).getCause();
|
||||
assertThat(cause, instanceOf(expectedException));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -95,6 +95,14 @@ public interface CharsetStringBuilder
|
|||
*/
|
||||
String build() throws CharacterCodingException;
|
||||
|
||||
/**
|
||||
* @return the length in characters
|
||||
*/
|
||||
int length();
|
||||
|
||||
/**
|
||||
* <p>Resets this sequence to be empty.</p>
|
||||
*/
|
||||
void reset();
|
||||
|
||||
/**
|
||||
|
@ -168,6 +176,12 @@ public interface CharsetStringBuilder
|
|||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return _builder.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
|
@ -207,6 +221,12 @@ public interface CharsetStringBuilder
|
|||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return _builder.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
|
@ -319,6 +339,12 @@ public interface CharsetStringBuilder
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return _stringBuilder.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
|
|
|
@ -92,6 +92,7 @@ public class Utf8StringBuilder implements CharsetStringBuilder
|
|||
_buffer = buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return _buffer.length();
|
||||
|
|
|
@ -116,7 +116,10 @@ import org.slf4j.LoggerFactory;
|
|||
* ...
|
||||
* </web-app>
|
||||
* </pre>
|
||||
*
|
||||
* @deprecated Use {@link org.eclipse.jetty.server.handler.CrossOriginHandler} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public class CrossOriginFilter implements Filter
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CrossOriginFilter.class);
|
||||
|
|
|
@ -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