This is an automated email from the ASF dual-hosted git repository.
thiagohp pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
The following commit(s) were added to refs/heads/master by this push:
new 8ce6cb788 TAP5-2720: Tapestry support for CORS
8ce6cb788 is described below
commit 8ce6cb788f1da9a9ff7b36b2861c29330f8aa6c9
Author: Thiago H. de Paula Figueiredo <[email protected]>
AuthorDate: Fri May 27 14:05:08 2022 -0300
TAP5-2720: Tapestry support for CORS
---
.../java/org/apache/tapestry5/SymbolConstants.java | 68 +++
.../integration/app1/services/AppModule.java | 7 +
.../apache/tapestry5/http/CorsHandlerResult.java | 46 ++
.../http/TapestryHttpSymbolConstants.java | 62 +++
.../internal/services/CorsHandlerHelperImpl.java | 189 ++++++++
.../http/internal/services/DefaultCorsHandler.java | 77 ++++
.../tapestry5/http/modules/TapestryHttpModule.java | 27 ++
.../tapestry5/http/services/CorsHandler.java | 39 ++
.../tapestry5/http/services/CorsHandlerHelper.java | 186 ++++++++
.../services/CorsHttpServletRequestFilter.java | 71 +++
.../services/CorsHandlerHelperImplTest.java | 502 +++++++++++++++++++++
.../services/CorsHttpServletRequestFilterTest.java | 80 ++++
.../internal/services/DefaultCorsHandlerTest.java | 78 ++++
13 files changed, 1432 insertions(+)
diff --git
a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
index 6d08af38a..e30bfa417 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -22,6 +22,9 @@ import org.apache.tapestry5.corelib.mixins.FormGroup;
import org.apache.tapestry5.http.TapestryHttpConstants;
import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
import org.apache.tapestry5.http.services.BaseURLSource;
+import org.apache.tapestry5.http.services.CorsHandler;
+import org.apache.tapestry5.http.services.CorsHandlerHelper;
+import org.apache.tapestry5.http.services.CorsHttpServletRequestFilter;
import org.apache.tapestry5.internal.services.AssetDispatcher;
import
org.apache.tapestry5.internal.services.rest.DefaultOpenApiDescriptionGenerator;
import org.apache.tapestry5.modules.NoBootstrapModule;
@@ -713,4 +716,69 @@ public class SymbolConstants
*/
public static final String OPENAPI_BASE_PATH =
"tapestry.openapi-base-path";
+ /**
+ * Defines whether the CORS (Cross-Origing Resource Sharing) support
+ * should be enabled or not. Default value is "false". If you set this to
"true",
+ * you should also set {@link #CORS_ALLOWED_ORIGINS}.
+ * This is an alias for {@link TapestryHttpSymbolConstants#CORS_ENABLED}.
+ * @see CorsHandler
+ * @see CorsHttpServletRequestFilter
+ * @since 5.8.2
+ */
+ public static final String CORS_ENABLED =
TapestryHttpSymbolConstants.CORS_ENABLED;
+
+ /**
+ * Comma-delimited of origins allowed for CORS. The special value "*"
means allowing all origins.
+ * This is used by {@link
CorsHandlerHelper#getAllowedOrigin(javax.servlet.http.HttpServletRequest)}.
+ * Default value is the empty string (i.e. no origins allowed and CORS
actually disabled).
+ * This is an alias for {@link
TapestryHttpSymbolConstants#CORS_ALLOWED_ORIGINS}.
+ * @since 5.8.2
+ */
+ public static final String CORS_ALLOWED_ORIGINS =
TapestryHttpSymbolConstants.CORS_ALLOWED_ORIGINS;
+
+ /**
+ * Boolean value defining whether the Access-Control-Allow-Credentials
HTTP header
+ * should be set automatically in the response for CORS requests. Default
value is
+ * <code>false</code>. This is used in {@link
CorsHandlerHelper#configureCredentials(javax.servlet.http.HttpServletResponse)}.
+ * This is an alias for {@link
TapestryHttpSymbolConstants#CORS_ALLOW_CREDENTIALS}.
+ * @since 5.8.2
+ */
+ public static final String CORS_ALLOW_CREDENTIALS =
TapestryHttpSymbolConstants.CORS_ALLOW_CREDENTIALS;
+
+ /**
+ * Value to be used in the Access-Control-Allow-Methods in CORS preflight
request responses.
+ * This is used by {@link
CorsHandlerHelper#configureMethods(javax.servlet.http.HttpServletResponse)}.
+ * Default value is <code>GET,HEAD,PUT,PATCH,POST,DELETE</code>.
+ * This is an alias for {@link
TapestryHttpSymbolConstants#CORS_ALLOW_METHODS}.
+ * @since 5.8.2
+ */
+ public static final String CORS_ALLOW_METHODS =
TapestryHttpSymbolConstants.CORS_ALLOW_METHODS;
+
+ /**
+ * Value to be used in the Access-Control-Allow-Methods in CORS preflight
request responses.
+ * This is used by {@link
CorsHandlerHelper#configureAllowedHeaders(javax.servlet.http.HttpServletResponse)}.
+ * Default value is the empty string.
+ * This is an alias for {@link
TapestryHttpSymbolConstants#CORS_ALLOWED_HEADERS}.
+ * @since 5.8.2
+ */
+ public static final String CORS_ALLOWED_HEADERS =
TapestryHttpSymbolConstants.CORS_ALLOWED_HEADERS;
+
+ /**
+ * Value to be used in the Access-Control-Expose-Headers in CORS request
responses.
+ * This is used by {@link
CorsHandlerHelper#configureExposeHeaders(javax.servlet.http.HttpServletResponse)}.
+ * Default value is the empty string.
+ * This is an alias for {@link
TapestryHttpSymbolConstants#CORS_EXPOSE_HEADERS}.
+ * @since 5.8.2
+ */
+ public static final String CORS_EXPOSE_HEADERS =
TapestryHttpSymbolConstants.CORS_EXPOSE_HEADERS;
+
+ /**
+ * Value to be used in the Access-Control-Max-Age in responses to
preflight CORS requests.
+ * This is used by {@link
CorsHandlerHelper#configureMaxAge(javax.servlet.http.HttpServletResponse)}.
+ * Default value is the empty string.
+ * This is an alias for {@link TapestryHttpSymbolConstants#CORS_MAX_AGE}.
+ * @since 5.8.2
+ */
+ public static final String CORS_MAX_AGE =
TapestryHttpSymbolConstants.CORS_MAX_AGE;
+
}
diff --git
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
index d41dcf6a4..5f57836bf 100644
---
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
+++
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
@@ -186,6 +186,13 @@ public class AppModule
configuration.add(SymbolConstants.OPENAPI_BASE_PATH, "/rest/");
// configuration.add(SymbolConstants.ERROR_CSS_CLASS, "yyyy");
// configuration.add(SymbolConstants.DEFAULT_STYLESHEET,
"classpath:/org/apache/tapestry5/integration/app1/app1.css");
+// configuration.add(SymbolConstants.CORS_ENABLED, true);
+// configuration.add(SymbolConstants.CORS_ALLOWED_ORIGINS,
"localhost,localtest.machina.com.br");
+// configuration.add(SymbolConstants.CORS_ALLOW_METHODS, "GET,PUT");
+// configuration.add(SymbolConstants.CORS_ALLOWED_HEADERS, "X,Y,Z");
+// configuration.add(SymbolConstants.CORS_MAX_AGE, "42");
+// configuration.add(SymbolConstants.CORS_ALLOW_CREDENTIALS, "true");
+// configuration.add(SymbolConstants.CORS_EXPOSE_HEADERS, "A,B,C");
}
public static void contributeIgnoredPathsFilter(Configuration<String>
configuration)
diff --git
a/tapestry-http/src/main/java/org/apache/tapestry5/http/CorsHandlerResult.java
b/tapestry-http/src/main/java/org/apache/tapestry5/http/CorsHandlerResult.java
new file mode 100644
index 000000000..0f7c3f967
--- /dev/null
+++
b/tapestry-http/src/main/java/org/apache/tapestry5/http/CorsHandlerResult.java
@@ -0,0 +1,46 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.http;
+
+import org.apache.tapestry5.http.services.CorsHandler;
+import org.apache.tapestry5.http.services.CorsHttpServletRequestFilter;
+import org.apache.tapestry5.http.services.HttpServletRequestFilter;
+
+/**
+ * Enumeration that defines the possible outcomes of
+ * {@link CorsHandler#handle(javax.servlet.http.HttpServletRequest,
javax.servlet.http.HttpServletResponse)}
+ * @see CorsHandler
+ * @see CorsHttpServletRequestFilter
+ * @since 5.8.2
+ */
+public enum CorsHandlerResult
+{
+ /**
+ * The next {@linkplain CorsHandler} should be called.
+ * Usually returned when the CORS handler didn't actually processed the
request.
+ */
+ CONTINUE_CORS_PROCESSING,
+
+ /**
+ * No other {@linkplain CorsHandler} should be called and request
processing
+ * should resume (i.e. {@linkplain CorsHttpServletRequestFilter} calls
+ * {@link
HttpServletRequestFilter#service(javax.servlet.http.HttpServletRequest,
javax.servlet.http.HttpServletResponse,
org.apache.tapestry5.http.services.HttpServletRequestHandler)}.
+ * Usually returned when the handler actually processed the request.
+ */
+ CONTINUE_REQUEST_PROCESSING,
+
+ /**
+ * The request processing should stop immediately. Usually used with CORS
preflight requests.
+ */
+ STOP_REQUEST_PROCESSING
+}
diff --git
a/tapestry-http/src/main/java/org/apache/tapestry5/http/TapestryHttpSymbolConstants.java
b/tapestry-http/src/main/java/org/apache/tapestry5/http/TapestryHttpSymbolConstants.java
index e570a8359..f7784ac37 100644
---
a/tapestry-http/src/main/java/org/apache/tapestry5/http/TapestryHttpSymbolConstants.java
+++
b/tapestry-http/src/main/java/org/apache/tapestry5/http/TapestryHttpSymbolConstants.java
@@ -11,6 +11,10 @@
// limitations under the License.
package org.apache.tapestry5.http;
+import org.apache.tapestry5.http.services.CorsHandler;
+import org.apache.tapestry5.http.services.CorsHandlerHelper;
+import org.apache.tapestry5.http.services.CorsHttpServletRequestFilter;
+
/**
* Class defining constants for Tapestry HTTP symbols.
*/
@@ -132,5 +136,63 @@ final public class TapestryHttpSymbolConstants {
* @since 5.1.0.0
*/
public static final String GZIP_COMPRESSION_ENABLED =
"tapestry.gzip-compression-enabled";
+ /**
+ * Defines whether the CORS (Cross-Origing Resource Sharing) support
+ * should be enabled or not. Default value is "false". If you set this to
"true",
+ * you should also set {@link #CORS_ALLOWED_ORIGINS}.
+ * @see CorsHandler
+ * @see CorsHttpServletRequestFilter
+ * @since 5.8.2
+ */
+ public static final String CORS_ENABLED = "tapestry.cors-enabled";
+
+ /**
+ * Comma-delimited of origins allowed for CORS. The special value "*"
means allowing all origins.
+ * This is used by {@link
CorsHandlerHelper#getAllowedOrigin(javax.servlet.http.HttpServletRequest)}.
+ * Default value is the empty string (i.e. no origins allowed and CORS
actually disabled).
+ * @since 5.8.2
+ */
+ public static final String CORS_ALLOWED_ORIGINS =
"tapestry.cors-allowed-origins";
+
+ /**
+ * Boolean value defining whether the Access-Control-Allow-Credentials
HTTP header
+ * should be set automatically in the response for CORS requests. Default
value is
+ * <code>false</code>. This is used in {@link
CorsHandlerHelper#configureCredentials(javax.servlet.http.HttpServletResponse)}.
+ * @since 5.8.2
+ */
+ public static final String CORS_ALLOW_CREDENTIALS =
"tapestry.cors-allow-credentials";
+
+ /**
+ * Value to be used in the Access-Control-Allow-Methods in CORS preflight
request responses.
+ * This is used by {@link
CorsHandlerHelper#configureMethods(javax.servlet.http.HttpServletResponse)}.
+ * Default value is <code>GET,HEAD,PUT,PATCH,POST,DELETE</code>.
+ * @since 5.8.2
+ */
+ public static final String CORS_ALLOW_METHODS =
"tapestry.cors-allow-methods";
+
+ /**
+ * Value to be used in the Access-Control-Allow-Methods in CORS preflight
request responses.
+ * This is used by {@link
CorsHandlerHelper#configureAllowedHeaders(javax.servlet.http.HttpServletResponse)}.
+ * Default value is the empty string.
+ * @since 5.8.2
+ */
+ public static final String CORS_ALLOWED_HEADERS =
"tapestry.cors-allowed-headers";
+
+ /**
+ * Value to be used in the Access-Control-Expose-Headers in CORS preflight
request responses.
+ * This is used by {@link
CorsHandlerHelper#configureExposeHeaders(javax.servlet.http.HttpServletResponse)}.
+ * Default value is the empty string.
+ * @since 5.8.2
+ */
+ public static final String CORS_EXPOSE_HEADERS =
"tapestry.cors-expose-headers";
+
+ /**
+ * Value to be used in the Access-Control-Max-Age in responses to
preflight CORS requests.
+ * This is used by {@link
CorsHandlerHelper#configureMaxAge(javax.servlet.http.HttpServletResponse)}.
+ * Default value is the empty string.
+ * This is an alias for {@link TapestryHttpSymbolConstants#CORS_MAX_AGE}.
+ * @since 5.8.2
+ */
+ public static final String CORS_MAX_AGE = "tapestry.cors-max-age";
}
diff --git
a/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/CorsHandlerHelperImpl.java
b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/CorsHandlerHelperImpl.java
new file mode 100644
index 000000000..c1bab731a
--- /dev/null
+++
b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/CorsHandlerHelperImpl.java
@@ -0,0 +1,189 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.http.internal.services;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
+import org.apache.tapestry5.http.services.CorsHandler;
+import org.apache.tapestry5.http.services.CorsHandlerHelper;
+import org.apache.tapestry5.ioc.annotations.Symbol;
+
+/**
+ * Default {@link CorsHandlerHelper} implementation.
+ *
+ * @see CorsHandler
+ * @see TapestryHttpSymbolConstants#CORS_ALLOWED_ORIGINS.
+ * @since 5.8.2
+ */
+public class CorsHandlerHelperImpl implements CorsHandlerHelper
+{
+
+ private final List<String> allowedOrigins;
+
+ private final boolean allowAllOrigins;
+ private final boolean allowCredentials;
+ private final String allowMethods;
+ private final String allowedHeaders;
+ private final String exposeHeaders;
+ private final String maxAge;
+
+ public CorsHandlerHelperImpl(
+ @Symbol(TapestryHttpSymbolConstants.CORS_ALLOWED_ORIGINS) String
allowedOrigins,
+ @Symbol(TapestryHttpSymbolConstants.CORS_ALLOW_CREDENTIALS)
boolean allowCredentials,
+ @Symbol(TapestryHttpSymbolConstants.CORS_ALLOW_METHODS) String
allowMethods,
+ @Symbol(TapestryHttpSymbolConstants.CORS_ALLOWED_HEADERS) String
allowedHeaders,
+ @Symbol(TapestryHttpSymbolConstants.CORS_EXPOSE_HEADERS) String
exposeHeaders,
+ @Symbol(TapestryHttpSymbolConstants.CORS_MAX_AGE) String maxAge)
+ {
+ allowAllOrigins = ORIGIN_WILDCARD.equals(allowedOrigins);
+ this.allowedOrigins = !allowAllOrigins ?
Arrays.asList(trim(allowedOrigins.split(","))) : null;
+ this.allowCredentials = allowCredentials;
+ this.allowMethods = allowMethods.trim();
+ this.allowedHeaders = allowedHeaders.trim();
+ this.exposeHeaders = exposeHeaders.trim();
+ this.maxAge = maxAge.trim();
+ }
+
+ @Override
+ public String getPath(HttpServletRequest request)
+ {
+
+ // Copied from RequestImpl.getRequest()
+ String pathInfo = request.getPathInfo();
+
+ if (pathInfo == null)
+ {
+ return request.getServletPath();
+ }
+
+ // Websphere 6.1 is a bit wonky (see TAPESTRY-1713), and tends to
return the empty string
+ // for the servlet path, and return the true path in pathInfo.
+
+ return pathInfo.length() == 0 ? "/" : pathInfo;
+ }
+
+ @Override
+ public Optional<String> getAllowedOrigin(HttpServletRequest request)
+ {
+ final Optional<String> allowedOrigin;
+ final Optional<String> origin = getOrigin(request);
+
+ if (allowAllOrigins)
+ {
+ allowedOrigin = Optional.of(ORIGIN_WILDCARD);
+ }
+ else if (origin.isPresent() && allowedOrigins.contains(origin.get()))
+ {
+ allowedOrigin = origin;
+ }
+ else
+ {
+ allowedOrigin = Optional.empty();
+ }
+
+ return allowedOrigin;
+ }
+
+ @Override
+ public Optional<String> getOrigin(HttpServletRequest request)
+ {
+ return Optional.ofNullable(request.getHeader(ORIGIN_HEADER));
+ }
+
+ @Override
+ public boolean isPreflight(HttpServletRequest request)
+ {
+ boolean preflight = false;
+ if (OPTIONS_METHOD.equals(request.getMethod()))
+ {
+ final Optional<String> origin = getAllowedOrigin(request);
+ preflight = origin.isPresent() && !origin.get().trim().isEmpty();
+ }
+ return preflight;
+ }
+
+ @Override
+ public void configureOrigin(HttpServletResponse response, String value)
+ {
+ response.setHeader(ALLOW_ORIGIN_HEADER, value);
+ addValueToVaryHeader(response, ORIGIN_HEADER);
+ }
+
+ @Override
+ public void configureCredentials(HttpServletResponse response)
+ {
+ if (allowCredentials)
+ {
+ response.setHeader(ALLOW_CREDENTIALS_HEADER, "true");
+ }
+ }
+
+ @Override
+ public void configureMethods(HttpServletResponse response)
+ {
+ response.setHeader(ALLOW_METHODS_HEADER, allowMethods);
+ }
+
+ @Override
+ public void configureAllowedHeaders(HttpServletResponse response,
HttpServletRequest request)
+ {
+ String value;
+ if (allowedHeaders.isEmpty())
+ {
+ value = request.getHeader(REQUEST_HEADERS_HEADER);
+ addValueToVaryHeader(response, REQUEST_HEADERS_HEADER);
+ }
+ else
+ {
+ value = allowedHeaders;
+ }
+ if (value != null && !value.isEmpty())
+ {
+ response.setHeader(ALLOW_HEADERS_HEADER, value);
+ }
+ }
+
+
+ @Override
+ public void configureExposeHeaders(HttpServletResponse response)
+ {
+ if (!exposeHeaders.isEmpty())
+ {
+ response.setHeader(EXPOSE_HEADERS_HEADER, exposeHeaders);
+ }
+ }
+
+ @Override
+ public void configureMaxAge(HttpServletResponse response)
+ {
+ if (!maxAge.isEmpty())
+ {
+ response.setHeader(MAX_AGE_HEADER, maxAge);
+ }
+ }
+
+
+ private static String[] trim(String[] strings) {
+ for (int i = 0; i < strings.length; i++)
+ {
+ strings[i] = strings[i].trim();
+ }
+ return strings;
+ }
+
+}
diff --git
a/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/DefaultCorsHandler.java
b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/DefaultCorsHandler.java
new file mode 100644
index 000000000..b14f9a284
--- /dev/null
+++
b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/DefaultCorsHandler.java
@@ -0,0 +1,77 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.http.internal.services;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.CorsHandlerResult;
+import org.apache.tapestry5.http.services.CorsHandler;
+import org.apache.tapestry5.http.services.CorsHandlerHelper;
+
+/**
+ * <p>
+ * Default {@link CorsHandler} implementation. It will process all requests
with an Origin HTTP header,
+ * regardless of path. It will also perform preflight requests if
+ * {@link CorsHandlerHelper#isPreflight(HttpServletRequest)}
+ * returns <code>true</code>. Most logic is delegated is {@link
CorsHandlerHelper}.
+ * <p>
+ * <p>
+ * This implementation is inspired by the cors NPM module.
+ * </p>
+ * @see CorsHandlerHelper
+ * @since 5.8.2
+ */
+public class DefaultCorsHandler implements CorsHandler
+{
+
+ private final CorsHandlerHelper helper;
+
+ public DefaultCorsHandler(CorsHandlerHelper helper)
+ {
+ this.helper = helper;
+ }
+
+ @Override
+ public CorsHandlerResult handle(HttpServletRequest request,
HttpServletResponse response) throws IOException
+ {
+
+ CorsHandlerResult result = CorsHandlerResult.CONTINUE_CORS_PROCESSING;
+ final Optional<String> allowedOrigin =
helper.getAllowedOrigin(request);
+ if (helper.isPreflight(request))
+ {
+ if (allowedOrigin.isPresent())
+ {
+ helper.configureOrigin(response, allowedOrigin.get());
+ helper.configureCredentials(response);
+ helper.configureExposeHeaders(response);
+ helper.configureMethods(response);
+ helper.configureAllowedHeaders(response, request);
+ helper.configureMaxAge(response);
+ response.setContentLength(0);
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ result = CorsHandlerResult.STOP_REQUEST_PROCESSING;
+ }
+ }
+ else if (allowedOrigin.isPresent())
+ {
+ helper.configureOrigin(response, allowedOrigin.get());
+ helper.configureCredentials(response);
+ helper.configureExposeHeaders(response);
+ }
+ return result;
+ }
+
+}
diff --git
a/tapestry-http/src/main/java/org/apache/tapestry5/http/modules/TapestryHttpModule.java
b/tapestry-http/src/main/java/org/apache/tapestry5/http/modules/TapestryHttpModule.java
index c01ae6366..8b4d5a83e 100644
---
a/tapestry-http/src/main/java/org/apache/tapestry5/http/modules/TapestryHttpModule.java
+++
b/tapestry-http/src/main/java/org/apache/tapestry5/http/modules/TapestryHttpModule.java
@@ -39,6 +39,8 @@ import
org.apache.tapestry5.http.internal.services.ApplicationGlobalsImpl;
import org.apache.tapestry5.http.internal.services.AsyncRequestServiceImpl;
import org.apache.tapestry5.http.internal.services.BaseURLSourceImpl;
import org.apache.tapestry5.http.internal.services.ContextImpl;
+import org.apache.tapestry5.http.internal.services.CorsHandlerHelperImpl;
+import org.apache.tapestry5.http.internal.services.DefaultCorsHandler;
import
org.apache.tapestry5.http.internal.services.DefaultSessionPersistedObjectAnalyzer;
import
org.apache.tapestry5.http.internal.services.OptimizedSessionPersistedObjectAnalyzer;
import org.apache.tapestry5.http.internal.services.RequestGlobalsImpl;
@@ -53,6 +55,9 @@ import
org.apache.tapestry5.http.services.ApplicationInitializer;
import org.apache.tapestry5.http.services.ApplicationInitializerFilter;
import org.apache.tapestry5.http.services.BaseURLSource;
import org.apache.tapestry5.http.services.Context;
+import org.apache.tapestry5.http.services.CorsHandler;
+import org.apache.tapestry5.http.services.CorsHandlerHelper;
+import org.apache.tapestry5.http.services.CorsHttpServletRequestFilter;
import org.apache.tapestry5.http.services.Dispatcher;
import org.apache.tapestry5.http.services.HttpRequestBodyConverter;
import org.apache.tapestry5.http.services.HttpServletRequestFilter;
@@ -107,6 +112,8 @@ public final class TapestryHttpModule {
binder.bind(ResponseCompressionAnalyzer.class,
ResponseCompressionAnalyzerImpl.class);
binder.bind(RestSupport.class, RestSupportImpl.class);
binder.bind(AsyncRequestService.class, AsyncRequestServiceImpl.class);
+ binder.bind(CorsHandlerHelper.class, CorsHandlerHelperImpl.class);
+ binder.bind(CorsHttpServletRequestFilter.class);
}
/**
@@ -123,6 +130,13 @@ public final class TapestryHttpModule {
configuration.add(TapestryHttpConstants.TAPESTRY_VERSION,
VersionUtils.readVersionNumber("META-INF/gradle/org.apache.tapestry/tapestry-http/project.properties"));
+ configuration.add(TapestryHttpSymbolConstants.CORS_ENABLED, "false");
+ configuration.add(TapestryHttpSymbolConstants.CORS_ALLOWED_ORIGINS,
"");
+ configuration.add(TapestryHttpSymbolConstants.CORS_ALLOW_CREDENTIALS,
"false");
+ configuration.add(TapestryHttpSymbolConstants.CORS_ALLOW_METHODS,
"GET,HEAD,PUT,PATCH,POST,DELETE");
+ configuration.add(TapestryHttpSymbolConstants.CORS_ALLOWED_HEADERS,
"");
+ configuration.add(TapestryHttpSymbolConstants.CORS_EXPOSE_HEADERS, "");
+
// The default values denote "use values from request"
configuration.add(TapestryHttpSymbolConstants.HOSTNAME, "");
configuration.add(TapestryHttpSymbolConstants.HOSTPORT, 0);
@@ -290,6 +304,8 @@ public final class TapestryHttpModule {
*/
public void
contributeHttpServletRequestHandler(OrderedConfiguration<HttpServletRequestFilter>
configuration,
@Symbol(TapestryHttpSymbolConstants.GZIP_COMPRESSION_ENABLED)
boolean gzipCompressionEnabled,
+ @Symbol(TapestryHttpSymbolConstants.CORS_ENABLED) boolean
corsEnabled,
+ CorsHttpServletRequestFilter corsHttpServletRequestFilter,
@Autobuild GZipFilter gzipFilter)
{
@@ -303,6 +319,12 @@ public final class TapestryHttpModule {
return handler.service(request, response);
}
};
+
+ if (corsEnabled)
+ {
+ configuration.add("CORS", corsHttpServletRequestFilter,
+ "after:StoreIntoGlobals", "before:GZIP");
+ }
configuration.add("StoreIntoGlobals", storeIntoGlobals, "before:*");
@@ -333,6 +355,11 @@ public final class TapestryHttpModule {
CoercionTuple.add(configuration, HttpServletRequest.class,
BufferedReader.class, TapestryHttpModule::toBufferedReader);
}
+ public static void
contributeCorsHttpServletRequestFilter(OrderedConfiguration<CorsHandler>
configuration)
+ {
+ configuration.addInstance("Default", DefaultCorsHandler.class,
"after:*");
+ }
+
private final static InputStream toInputStream(HttpServletRequest request)
{
try
diff --git
a/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHandler.java
b/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHandler.java
new file mode 100644
index 000000000..6d8b790fa
--- /dev/null
+++
b/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHandler.java
@@ -0,0 +1,39 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.http.services;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.CorsHandlerResult;
+
+/**
+ * Interface implemented by classes handling CORS (Cross-Origin Resource
Sharing) in requests.
+ * Implementations should be contributed to {@link
CorsHttpServletRequestFilter} and
+ * can use the useful methods from {@link CorsHandlerHelper}.
+ * @see CorsHandlerHelper
+ * @see CorsHandlerResult
+ * @see CorsHttpServletRequestFilter
+ * @since 5.8.2
+ */
+public interface CorsHandler
+{
+ /**
+ * Handles the CORS processing of a request, possibly doing nothing. This
method
+ * cannot return <code>null</code>.
+ * @return a {@linkplain CorsHandlerResult} object.
+ */
+ CorsHandlerResult handle(HttpServletRequest request, HttpServletResponse
response) throws IOException;
+}
diff --git
a/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHandlerHelper.java
b/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHandlerHelper.java
new file mode 100644
index 000000000..a0af7f18e
--- /dev/null
+++
b/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHandlerHelper.java
@@ -0,0 +1,186 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.http.services;
+
+import java.util.Optional;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
+
+/**
+ * Service that provides useful methods for {@link CorsHandler}
implementations.
+ *
+ * @see CorsHandler
+ * @since 5.8.2
+ */
+public interface CorsHandlerHelper {
+
+ /**
+ * Returns the requested URL path, using the same logic as {@link
Request#getPath()}.
+ * @param request an {@link HttpServletRequest}.
+ * @return a {@link String} with the path.
+ */
+ String getPath(HttpServletRequest request);
+
+ /**
+ * Returns the value of the Origin HTTP header.
+ * @param request an {@link HttpServletRequest}.
+ * @return an {@link Optional} wrapping the Origin HTTP header value.
+ */
+ Optional<String> getOrigin(HttpServletRequest request);
+
+ /**
+ * Tells whether this request is a CORS preflight one (i.e. HTTP method
OPTION and
+ * non-empty Origin HTTP header).
+ *
+ * @param request an {@link HttpServletRequest}.
+ * @return <code>true</code> if it's a preflight request,
<code>false</code> otherwise.
+ */
+ boolean isPreflight(HttpServletRequest request);
+
+ /**
+ * Returns the origin of this requests, if it's allowed.
+ * @param request an {@link HttpServletRequest}.
+ * @return an {@link Optional} wrapping the allowed origin.
+ * @see TapestryHttpSymbolConstants#CORS_ALLOWED_ORIGINS
+ */
+ Optional<String> getAllowedOrigin(HttpServletRequest request);
+
+ /**
+ * Sets the Access-Control-Allow-Origin HTTP header with a given value.
+ * @param value a {@linkplain String}.
+ * @param response an {@link HttpServletResponse} instance.
+ * @see #ALLOW_ORIGIN_HEADER
+ */
+ void configureOrigin(HttpServletResponse response, String value);
+
+ /**
+ * Conditionally sets the Access-Control-Allow-Credentials HTTP header.
+ * Out-of-the-box, this is done based on the
+ * {@link TapestryHttpSymbolConstants#CORS_ALLOW_CREDENTIALS} symbol.
+ * @param response an {@link HttpServletResponse}.
+ * @see TapestryHttpSymbolConstants#CORS_ALLOW_CREDENTIALS
+ */
+ void configureCredentials(HttpServletResponse response);
+
+ /**
+ * Conditionally sets the Access-Control-Allow-Methods HTTP header in
responses
+ * to preflight CORS requests.
+ * Out-of-the-box, the value comes from
+ * {@link TapestryHttpSymbolConstants#CORS_ALLOW_CREDENTIALS} symbol
+ * and the header is only set if the value isn't empty.
+ * @param response an {@link HttpServletResponse}.
+ * @see TapestryHttpSymbolConstants#CORS_ALLOW_CREDENTIALS
+ */
+ void configureMethods(HttpServletResponse response);
+
+ /**
+ * Conditionally the Access-Control-Request-Headers HTTP header.
+ * Out-of-the-box, the value comes from
+ * {@link TapestryHttpSymbolConstants#CORS_ALLOWED_HEADERS} symbol if not
empty.
+ * Otherwise, it comes from the value of the same HTTP header from the
request,
+ * also if not empty. Otherwise, the header isn't set.
+ * @param response an {@link HttpServletResponse}.
+ * @param request an {@link HttpServletRequest}.
+ * @see TapestryHttpSymbolConstants#CORS_ALLOWED_HEADERS
+ */
+ void configureAllowedHeaders(HttpServletResponse response,
HttpServletRequest request);
+
+ /**
+ * Conditionally sets the Access-Control-Expose-Headers HTTP header.
+ * Out-of-the-box, the value comes from
+ * {@link TapestryHttpSymbolConstants#CORS_EXPOSED_HEADERS} symbol, if not
empty.
+ * Otherwise, the header isn't set.
+ * @param response an {@link HttpServletResponse}.
+ * @see TapestryHttpSymbolConstants#CORS_EXPOSED_HEADERS
+ */
+ void configureExposeHeaders(HttpServletResponse response);
+
+ /**
+ * Conditionally sets the Access-Control-Max-Age HTTP header.
+ * Out-of-the-box, the value comes from
+ * {@link TapestryHttpSymbolConstants#CORS_MAX_AGE} symbol, if not empty.
+ * Otherwise, the header isn't set.
+ * @param response an {@link HttpServletResponse}.
+ * @see TapestryHttpSymbolConstants#CORS_MAX_AGE
+ */
+ void configureMaxAge(HttpServletResponse response);
+
+ /**
+ * Adds a value to the Vary HTTP header.
+ * @param response an {@link HttpServletResponse} instance.
+ * @param value the value to be added.
+ */
+ default void addValueToVaryHeader(HttpServletResponse response, String
value)
+ {
+ final String vary = response.getHeader(VARY_HEADER);
+ response.setHeader(VARY_HEADER, vary == null ? value : vary + ", " +
value);
+ }
+
+ /**
+ * Name of the Origin HTTP header.
+ */
+ String ORIGIN_HEADER = "Origin";
+
+ /**
+ * Name of the Access-Control-Allow-Origin HTTP header.
+ */
+ String ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
+
+ /**
+ * Name of the Access-Control-Allow-Credentials HTTP header.
+ */
+ String ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials";
+
+ /**
+ * Name of the Access-Control-Allow-Methods HTTP header.
+ */
+ String ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
+
+ /**
+ * Name of the Access-Control-Request-Headers HTTP header.
+ */
+ String REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers";
+
+ /**
+ * Name of the Access-Control-Allow-Headers HTTP header.
+ */
+ String ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
+
+ /**
+ * Name of the Access-Control-Expose-Headers HTTP header.
+ */
+ String EXPOSE_HEADERS_HEADER = "Access-Control-Expose-Headers";
+
+ /**
+ * Name of the Access-Control-Max-Age HTTP header.
+ */
+ String MAX_AGE_HEADER = "Access-Control-Max-Age";
+
+ /**
+ * Name of the Vary HTTP header.
+ */
+ String VARY_HEADER = "Vary";
+
+ /**
+ * OPTIONS HTTP method name.
+ */
+ String OPTIONS_METHOD = "OPTIONS";
+
+ /**
+ * The CORS Origin wildcard.
+ */
+ String ORIGIN_WILDCARD = "*";
+
+}
diff --git
a/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHttpServletRequestFilter.java
b/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHttpServletRequestFilter.java
new file mode 100644
index 000000000..07d3deb26
--- /dev/null
+++
b/tapestry-http/src/main/java/org/apache/tapestry5/http/services/CorsHttpServletRequestFilter.java
@@ -0,0 +1,71 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.http.services;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.CorsHandlerResult;
+import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration;
+
+/**
+ * {@linkplain HttpServletRequestFilter} that handles CORS. Uses an ordered
configuration of
+ * {@linkplain CorsHandler} instances.
+ * @see CorsHandler
+ * @since 5.8.2
+ */
+@UsesOrderedConfiguration(CorsHandler.class)
+public class CorsHttpServletRequestFilter implements HttpServletRequestFilter
+{
+
+ final List<CorsHandler> handlers;
+
+ public CorsHttpServletRequestFilter(List<CorsHandler> handlers)
+ {
+ this.handlers = new ArrayList<>(handlers);
+ }
+
+ @Override
+ public boolean service(HttpServletRequest request, HttpServletResponse
response, HttpServletRequestHandler handler) throws IOException
+ {
+
+ boolean serviced = false;
+ for (CorsHandler corsHandler : handlers)
+ {
+
+ final CorsHandlerResult result = corsHandler.handle(request,
response);
+ if (result.equals(CorsHandlerResult.CONTINUE_REQUEST_PROCESSING))
+ {
+ break;
+ }
+ else if (result.equals(CorsHandlerResult.STOP_REQUEST_PROCESSING))
+ {
+ serviced = true;
+ break;
+ }
+
+ }
+
+ if (!serviced)
+ {
+ serviced = handler.service(request, response);
+ }
+
+ return serviced;
+ }
+
+}
diff --git
a/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/CorsHandlerHelperImplTest.java
b/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/CorsHandlerHelperImplTest.java
new file mode 100644
index 000000000..10632f99b
--- /dev/null
+++
b/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/CorsHandlerHelperImplTest.java
@@ -0,0 +1,502 @@
+package org.apache.tapestry5.http.internal.services;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.services.CorsHandlerHelper;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Test suite for {@link CorsHandlerHelperImpl.
+ */
+public class CorsHandlerHelperImplTest {
+
+
+ @Test
+ public void get_origin()
+ {
+
+ final String[] allowedOrigins = new String[]
{"http://tapestry.apache.org", "https://apache.org"};
+
+ // Scenario 1: explicit origins
+
+ CorsHandlerHelper helper = create(String.join(" , ",
allowedOrigins));
+ HttpServletRequest request =
EasyMock.createMock(HttpServletRequest.class);
+
+ for (String origin : allowedOrigins)
+ {
+
+ // Allowed origins
+ EasyMock.reset(request);
+
EasyMock.expect(request.getHeader(CorsHandlerHelper.ORIGIN_HEADER)).andReturn(origin);
+ EasyMock.replay(request);
+ Assert.assertEquals("Allowed origin", origin,
helper.getAllowedOrigin(request).get());
+ EasyMock.verify(request);
+
+ // Non-allowed origins
+ EasyMock.reset(request);
+
EasyMock.expect(request.getHeader(CorsHandlerHelper.ORIGIN_HEADER)).andReturn(origin
+ "baaaa");
+ EasyMock.replay(request);
+ Assert.assertFalse("Non-allowed origins",
helper.getAllowedOrigin(request).isPresent());
+ EasyMock.verify(request);
+
+ }
+
+ // Scenario 2: allow all origins
+
+ helper = create(CorsHandlerHelper.ORIGIN_WILDCARD);
+ for (int i = 0; i < 10; i++)
+ {
+ final String origin = "https://web" + i + ".tapestry.apache.org";
+ EasyMock.reset(request);
+
EasyMock.expect(request.getHeader(CorsHandlerHelper.ORIGIN_HEADER)).andReturn(origin);
+ EasyMock.replay(request);
+ Assert.assertEquals("All origins should be accepted",
CorsHandlerHelper.ORIGIN_WILDCARD, helper.getAllowedOrigin(request).get());
+ EasyMock.verify(request);
+ }
+
+ }
+
+ @Test
+ public void is_preflight()
+ {
+
+ // Scenario 1: not OPTIONS
+ final String origin = "https://tapestry.apache.org";
+ CorsHandlerHelper helper = create(origin);
+ HttpServletRequest request =
EasyMock.createMock(HttpServletRequest.class);
+ EasyMock.expect(request.getMethod()).andReturn("PUT");
+ EasyMock.replay(request);
+ Assert.assertFalse("Not OPTIONS", helper.isPreflight(request));
+ EasyMock.verify(request);
+
+ // Scenario 2: no Origin
+ EasyMock.reset(request);
+
EasyMock.expect(request.getMethod()).andReturn(CorsHandlerHelper.OPTIONS_METHOD);
+
EasyMock.expect(request.getHeader(CorsHandlerHelper.ORIGIN_HEADER)).andReturn(null);
+ EasyMock.replay(request);
+ Assert.assertFalse("No Origin", helper.isPreflight(request));
+ EasyMock.verify(request);
+
+ // Scenario 3: accepted origin
+ EasyMock.reset(request);
+
EasyMock.expect(request.getMethod()).andReturn(CorsHandlerHelper.OPTIONS_METHOD);
+
EasyMock.expect(request.getHeader(CorsHandlerHelper.ORIGIN_HEADER)).andReturn(origin);
+ EasyMock.replay(request);
+ Assert.assertTrue("Preflight indeed", helper.isPreflight(request));
+ EasyMock.verify(request);
+
+ // Scenario 4: non-accepted origin
+ EasyMock.reset(request);
+
EasyMock.expect(request.getMethod()).andReturn(CorsHandlerHelper.OPTIONS_METHOD);
+
EasyMock.expect(request.getHeader(CorsHandlerHelper.ORIGIN_HEADER)).andReturn(origin
+ "baaah");
+ EasyMock.replay(request);
+ Assert.assertFalse("Non-accepted origin", helper.isPreflight(request));
+ EasyMock.verify(request);
+
+ }
+
+ @Test
+ public void get_path()
+ {
+
+ CorsHandlerHelper helper = create("*");
+ HttpServletRequest request =
EasyMock.createMock(HttpServletRequest.class);
+
+ final String pathInfo = "/blah/foo/";
+ EasyMock.expect(request.getPathInfo()).andReturn(pathInfo);
+ EasyMock.replay(request);
+ Assert.assertEquals(pathInfo, helper.getPath(request));
+ EasyMock.verify(request);
+
+ EasyMock.reset(request);
+ EasyMock.expect(request.getPathInfo()).andReturn("");
+ EasyMock.replay(request);
+ Assert.assertEquals("/", helper.getPath(request));
+ EasyMock.verify(request);
+
+ final String servletPath = "/servlet";
+ EasyMock.reset(request);
+ EasyMock.expect(request.getPathInfo()).andReturn(null);
+ EasyMock.expect(request.getServletPath()).andReturn(servletPath);
+ EasyMock.replay(request);
+ Assert.assertEquals(servletPath, helper.getPath(request));
+ EasyMock.verify(request);
+
+ }
+
+ @Test
+ public void configure_origin()
+ {
+ CorsHandlerHelper helper = create("*");
+ HttpServletResponse response = new TestableHttpServletResponse();;
+
+ final String origin = "https://apache.org";
+ helper.configureOrigin(response, origin);
+ Assert.assertEquals(origin,
response.getHeader(CorsHandlerHelper.ALLOW_ORIGIN_HEADER));
+ Assert.assertEquals(CorsHandlerHelper.ORIGIN_HEADER,
response.getHeader(CorsHandlerHelper.VARY_HEADER));
+ }
+
+ @Test
+ public void add_value_to_vary_header()
+ {
+ String[] values = new String[] {"Something", "else", "entirely"};
+ HttpServletResponse response = new TestableHttpServletResponse();
+ CorsHandlerHelper helper = create(CorsHandlerHelper.ORIGIN_WILDCARD);
+ for (String value : values) {
+ helper.addValueToVaryHeader(response, value);
+ }
+ Assert.assertEquals(String.join(", ", values),
response.getHeader(CorsHandlerHelper.VARY_HEADER));
+ }
+
+ @Test
+ public void configure_credentials()
+ {
+ HttpServletResponse response = new TestableHttpServletResponse();
+
+ CorsHandlerHelper helper = create(CorsHandlerHelper.ORIGIN_WILDCARD,
true);
+ helper.configureCredentials(response);
+ Assert.assertEquals("true",
response.getHeader(CorsHandlerHelper.ALLOW_CREDENTIALS_HEADER));
+
+ response = new TestableHttpServletResponse();
+
+ helper = create(CorsHandlerHelper.ORIGIN_WILDCARD, false);
+ helper.configureCredentials(response);
+ Assert.assertNull("Header shouldn't be set",
+
response.getHeader(CorsHandlerHelper.ALLOW_CREDENTIALS_HEADER));
+
+ }
+
+ @Test
+ public void configure_methods()
+ {
+ HttpServletResponse response = new TestableHttpServletResponse();
+
+ final String allowedMethods = "GET,PUT";
+ CorsHandlerHelper helper = new CorsHandlerHelperImpl("", false,
allowedMethods, "", "", "");
+ response = new TestableHttpServletResponse();
+ helper.configureMethods(response);
+ Assert.assertEquals(allowedMethods,
response.getHeader(CorsHandlerHelper.ALLOW_METHODS_HEADER));
+ }
+
+ @Test
+ public void configure_allowed_headers()
+ {
+ HttpServletResponse response = new TestableHttpServletResponse();
+ HttpServletRequest request =
EasyMock.createMock(HttpServletRequest.class);
+
+ // Scenario 1: configuration isn't empty
+ final String configuredHeaders = "Blah,Foo";
+ CorsHandlerHelper helper = new CorsHandlerHelperImpl("", false, "",
configuredHeaders, "", "");
+ EasyMock.replay(request);
+ helper.configureAllowedHeaders(response, request);
+ EasyMock.verify(request);
+ Assert.assertEquals(configuredHeaders,
response.getHeader(CorsHandlerHelper.ALLOW_HEADERS_HEADER));
+ Assert.assertNull(response.getHeader(CorsHandlerHelper.VARY_HEADER));
+
+ // Scenario 2: configuration empty, request's
Access-Control-Request-Headers not empty
+ response = new TestableHttpServletResponse();
+ final String requestHeaders = "Foo,Blah";
+ helper = new CorsHandlerHelperImpl("", false, "", "", "", "");
+ EasyMock.reset(request);
+
EasyMock.expect(request.getHeader(CorsHandlerHelper.REQUEST_HEADERS_HEADER)).andReturn(requestHeaders);
+ EasyMock.replay(request);
+ helper.configureAllowedHeaders(response, request);
+ EasyMock.verify(request);
+ Assert.assertEquals(requestHeaders,
response.getHeader(CorsHandlerHelper.ALLOW_HEADERS_HEADER));
+ Assert.assertEquals(CorsHandlerHelper.REQUEST_HEADERS_HEADER,
response.getHeader(CorsHandlerHelper.VARY_HEADER));
+
+ // Scenario 3: configuration empty, request's
Access-Control-Request-Headers also empty
+ response = new TestableHttpServletResponse();
+ EasyMock.reset(request);
+
EasyMock.expect(request.getHeader(CorsHandlerHelper.REQUEST_HEADERS_HEADER)).andReturn(null);
+ EasyMock.replay(request);
+ helper.configureAllowedHeaders(response, request);
+ EasyMock.verify(request);
+
Assert.assertNull(response.getHeader(CorsHandlerHelper.ALLOW_HEADERS_HEADER));
+ Assert.assertEquals(CorsHandlerHelper.REQUEST_HEADERS_HEADER,
response.getHeader(CorsHandlerHelper.VARY_HEADER));
+
+ }
+
+ @Test
+ public void configure_expose_headers()
+ {
+ HttpServletResponse response = new TestableHttpServletResponse();
+
+ final String exposeHeaders = "Blah,Foo,Bar";
+ CorsHandlerHelper helper = new CorsHandlerHelperImpl("", false, "",
"", exposeHeaders, "");
+ helper.configureExposeHeaders(response);
+ Assert.assertEquals(exposeHeaders,
response.getHeader(CorsHandlerHelper.EXPOSE_HEADERS_HEADER));
+
+ response = new TestableHttpServletResponse();
+
+ helper = new CorsHandlerHelperImpl("", false, "", "", "", "");
+ helper.configureExposeHeaders(response);
+ Assert.assertNull("Header shouldn't be set",
+ response.getHeader(CorsHandlerHelper.EXPOSE_HEADERS_HEADER));
+
+ }
+
+ @Test
+ public void configure_max_age()
+ {
+ HttpServletResponse response = new TestableHttpServletResponse();
+
+ final String maxAge = "1234567";
+ CorsHandlerHelper helper = new CorsHandlerHelperImpl("", false, "",
"", "", maxAge);
+ helper.configureMaxAge(response);
+ Assert.assertEquals(maxAge,
response.getHeader(CorsHandlerHelper.MAX_AGE_HEADER));
+
+ response = new TestableHttpServletResponse();
+
+ helper = new CorsHandlerHelperImpl("", false, "", "", "", "");
+ helper.configureExposeHeaders(response);
+ Assert.assertNull("Header shouldn't be set",
+ response.getHeader(CorsHandlerHelper.MAX_AGE_HEADER));
+
+ }
+
+ private CorsHandlerHelper create(String allowedOrigins)
+ {
+ return create(allowedOrigins, false);
+ }
+
+ private CorsHandlerHelper create(String allowedOrigins, boolean
allowCredentials)
+ {
+ return new CorsHandlerHelperImpl(allowedOrigins, allowCredentials, "",
"", "", "");
+ }
+
+ private final class TestableHttpServletResponse implements
HttpServletResponse
+ {
+
+ private final Map<String, String> headers = new HashMap<>();
+
+ @Override
+ public String getCharacterEncoding()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getContentType()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PrintWriter getWriter() throws IOException
+ {
+ return null;
+ }
+
+ @Override
+ public void setCharacterEncoding(String charset)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setContentLength(int len)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setContentType(String type)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setBufferSize(int size)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getBufferSize()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void flushBuffer() throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void resetBuffer()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isCommitted()
+ {
+ return false;
+ }
+
+ @Override
+ public void reset()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setLocale(Locale loc)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Locale getLocale()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addCookie(Cookie cookie)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean containsHeader(String name)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String encodeURL(String url)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String encodeRedirectURL(String url)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String encodeUrl(String url)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String encodeRedirectUrl(String url)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void sendRedirect(String location) throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setDateHeader(String name, long date)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addDateHeader(String name, long date)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setHeader(String name, String value)
+ {
+ headers.put(name, value);
+ }
+
+ @Override
+ public void addHeader(String name, String value)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setIntHeader(String name, int value)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addIntHeader(String name, int value)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setStatus(int sc)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setStatus(int sc, String sm)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getStatus()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return headers.get(name);
+ }
+
+ @Override
+ public Collection<String> getHeaders(String name)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Collection<String> getHeaderNames()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+}
diff --git
a/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/CorsHttpServletRequestFilterTest.java
b/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/CorsHttpServletRequestFilterTest.java
new file mode 100644
index 000000000..c39334425
--- /dev/null
+++
b/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/CorsHttpServletRequestFilterTest.java
@@ -0,0 +1,80 @@
+package org.apache.tapestry5.http.internal.services;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.CorsHandlerResult;
+import org.apache.tapestry5.http.services.CorsHandler;
+import org.apache.tapestry5.http.services.CorsHttpServletRequestFilter;
+import org.apache.tapestry5.http.services.HttpServletRequestHandler;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Test suite for {@link CorsHttpServletRequestFilter}.
+ */
+public class CorsHttpServletRequestFilterTest {
+
+ @Test
+ public void service() throws IOException
+ {
+
+ HttpServletRequest request =
EasyMock.createMock(HttpServletRequest.class);
+ HttpServletResponse response =
EasyMock.createMock(HttpServletResponse.class);
+ HttpServletRequestHandler httpServletRequestHandler =
EasyMock.createMock(HttpServletRequestHandler.class);
+
+ CorsHandler stopRequestProcessing = (req, res) ->
CorsHandlerResult.STOP_REQUEST_PROCESSING;
+ CorsHandler continueRequestProcessing = (req, res) ->
CorsHandlerResult.CONTINUE_REQUEST_PROCESSING;
+ CorsHandler throwException = (req, res) -> { throw new
RuntimeException("Shouldn't have been called"); };
+ TestCorsHandler testCorsHandler = new TestCorsHandler();
+ TestCorsHandler testCorsHandler2 = new TestCorsHandler();
+
+ // Scenario 1: all handlers return continue CORS processing.
+ CorsHttpServletRequestFilter filter =
+ new
CorsHttpServletRequestFilter(Arrays.asList(testCorsHandler, testCorsHandler2));
+ EasyMock.expect(httpServletRequestHandler.service(request,
response)).andReturn(true);
+ EasyMock.replay(httpServletRequestHandler);
+ filter.service(request, response, httpServletRequestHandler);
+ EasyMock.verify(httpServletRequestHandler);
+ Assert.assertTrue(testCorsHandler.isCalled());
+ Assert.assertTrue(testCorsHandler2.isCalled());
+
+ // Scenario 2: first handler stops CORS processing
+ EasyMock.reset(httpServletRequestHandler);
+ filter = new
CorsHttpServletRequestFilter(Arrays.asList(continueRequestProcessing,
throwException));
+ EasyMock.expect(httpServletRequestHandler.service(request,
response)).andReturn(true);
+ EasyMock.replay(httpServletRequestHandler);
+ filter.service(request, response, httpServletRequestHandler);
+ EasyMock.verify(httpServletRequestHandler);
+
+ // Scenario 3: first handler stops request processing
+ EasyMock.reset(httpServletRequestHandler);
+ filter = new
CorsHttpServletRequestFilter(Arrays.asList(stopRequestProcessing,
throwException));
+ EasyMock.replay(httpServletRequestHandler);
+ filter.service(request, response, httpServletRequestHandler);
+ EasyMock.verify(httpServletRequestHandler);
+
+
+ }
+
+ private static final class TestCorsHandler implements CorsHandler
+ {
+ private boolean called;
+
+ public boolean isCalled() {
+ return called;
+ }
+
+ @Override
+ public CorsHandlerResult handle(HttpServletRequest request,
HttpServletResponse response) throws IOException {
+ called = true;
+ return CorsHandlerResult.CONTINUE_CORS_PROCESSING;
+ }
+
+ }
+
+}
diff --git
a/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/DefaultCorsHandlerTest.java
b/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/DefaultCorsHandlerTest.java
new file mode 100644
index 000000000..e711e4591
--- /dev/null
+++
b/tapestry-http/src/test/java/org/apache/tapestry5/http/internal/services/DefaultCorsHandlerTest.java
@@ -0,0 +1,78 @@
+package org.apache.tapestry5.http.internal.services;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.CorsHandlerResult;
+import org.apache.tapestry5.http.services.CorsHandlerHelper;
+import org.apache.tapestry5.http.services.CorsHttpServletRequestFilter;
+import org.easymock.EasyMock;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Test suite for {@link CorsHttpServletRequestFilter}.
+ */
+public class DefaultCorsHandlerTest {
+
+ @Test
+ public void handle() throws IOException
+ {
+
+ HttpServletRequest request =
EasyMock.createMock(HttpServletRequest.class);
+ HttpServletResponse response =
EasyMock.createMock(HttpServletResponse.class);
+ CorsHandlerHelper helper =
EasyMock.createMock(CorsHandlerHelper.class);
+ DefaultCorsHandler handler = new DefaultCorsHandler(helper);
+
+ // Scenario 1: no preflight, no origin, not CORS
+ EasyMock.reset(helper, request, response);
+ EasyMock.expect(helper.isPreflight(request)).andReturn(false);
+
EasyMock.expect(helper.getAllowedOrigin(request)).andReturn(Optional.empty());
+ EasyMock.replay(helper, request, response);
+ Assert.assertEquals(CorsHandlerResult.CONTINUE_CORS_PROCESSING,
handler.handle(request, response));
+ EasyMock.verify(helper, request, response);
+
+ // Scenario 2: preflight, no allowed Origin, not CORS
+ EasyMock.reset(helper, request, response);
+ EasyMock.expect(helper.isPreflight(request)).andReturn(true);
+
EasyMock.expect(helper.getAllowedOrigin(request)).andReturn(Optional.empty());
+ EasyMock.replay(helper, request, response);
+ Assert.assertEquals(CorsHandlerResult.CONTINUE_CORS_PROCESSING,
handler.handle(request, response));
+ EasyMock.verify(helper, request, response);
+
+ // Scenario 3: preflight, allowed Origin, preflight CORS
+ final String origin = "https://tapestry.apache.org";
+ EasyMock.reset(helper, request, response);
+
+ EasyMock.expect(helper.isPreflight(request)).andReturn(true);
+
EasyMock.expect(helper.getAllowedOrigin(request)).andReturn(Optional.of(origin));
+ helper.configureOrigin(response, origin);
+ helper.configureCredentials(response);
+ helper.configureExposeHeaders(response);
+ helper.configureMethods(response);
+ helper.configureAllowedHeaders(response, request);
+ helper.configureMaxAge(response);
+ response.setContentLength(0);
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+
+ EasyMock.replay(helper, request, response);
+ Assert.assertEquals(CorsHandlerResult.STOP_REQUEST_PROCESSING,
handler.handle(request, response));
+ EasyMock.verify(helper, request, response);
+
+ // Scenario 4: no preflight, allowed Origin, non-preflight CORS
+ EasyMock.reset(helper, request, response);
+ EasyMock.expect(helper.isPreflight(request)).andReturn(false);
+
EasyMock.expect(helper.getAllowedOrigin(request)).andReturn(Optional.of(origin));
+ helper.configureOrigin(response, origin);
+ helper.configureCredentials(response);
+ helper.configureExposeHeaders(response);
+ EasyMock.replay(helper, request, response);
+ Assert.assertEquals(CorsHandlerResult.CONTINUE_CORS_PROCESSING,
handler.handle(request, response));
+ EasyMock.verify(helper, request, response);
+
+ }
+
+}