diff --git a/examples/pom.xml b/examples/pom.xml index 8be183a5e85..a99a2f77ec8 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -73,6 +73,10 @@ 3.0 provided + + org.springframework + spring-web + diff --git a/examples/src/main/java/example/ServletExamples.java b/examples/src/main/java/example/ServletExamples.java index 9a244b938f4..95b52bda27e 100644 --- a/examples/src/main/java/example/ServletExamples.java +++ b/examples/src/main/java/example/ServletExamples.java @@ -1,17 +1,16 @@ package example; +import java.util.Arrays; + import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import org.hl7.fhir.instance.hapi.validation.FhirInstanceValidator; +import org.springframework.web.cors.CorsConfiguration; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor; +import ca.uhn.fhir.rest.server.interceptor.*; import ca.uhn.fhir.validation.ResultSeverityEnum; @SuppressWarnings("serial") @@ -119,4 +118,38 @@ public class ServletExamples { } // END SNIPPET: responseHighlighterInterceptor + // START SNIPPET: corsInterceptor + @WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server") + public class RestfulServerWithCors extends RestfulServer { + + @Override + protected void initialize() throws ServletException { + + // ... define your resource providers here ... + + // Define your CORS configuration. This is an example + // showing a typical setup. You should customize this + // to your specific needs + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedHeader("x-fhir-starter"); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); + + config.addAllowedOrigin("*"); + + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); + + // Create the interceptor and register it + CorsInterceptor interceptor = new CorsInterceptor(config); + registerInterceptor(interceptor); + + } + + } + // END SNIPPET: corsInterceptor + } diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 0712657c7ab..149ff98acdb 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -43,15 +43,6 @@ true - - - org.ebaysf.web - cors-filter - true - - - com.phloc @@ -123,6 +114,17 @@ + + org.springframework + spring-web + true + + + commons-logging + commons-logging + + + diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/CorsInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/CorsInterceptor.java new file mode 100644 index 00000000000..538d7ea3694 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/CorsInterceptor.java @@ -0,0 +1,71 @@ +package ca.uhn.fhir.rest.server.interceptor; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.Validate; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsProcessor; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.DefaultCorsProcessor; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; + +public class CorsInterceptor extends InterceptorAdapter { + + private CorsProcessor myCorsProcessor; + private CorsConfiguration myConfig; + + /** + * Constructor + */ + public CorsInterceptor() { + this(new CorsConfiguration()); + } + + /** + * Constructor + * + * @param theConfiguration + * The CORS configuration + */ + public CorsInterceptor(CorsConfiguration theConfiguration) { + Validate.notNull(theConfiguration, "theConfiguration must not be null"); + myCorsProcessor = new DefaultCorsProcessor(); + setConfig(theConfiguration); + } + + /** + * Sets the CORS configuration + */ + public void setConfig(CorsConfiguration theConfiguration) { + myConfig = theConfiguration; + } + + /** + * Gets the CORS configuration + */ + public CorsConfiguration getConfig() { + return myConfig; + } + + @Override + public boolean incomingRequestPreProcessed(HttpServletRequest theRequest, HttpServletResponse theResponse) { + if (CorsUtils.isCorsRequest(theRequest)) { + boolean isValid; + try { + isValid = myCorsProcessor.processRequest(myConfig, theRequest, theResponse); + } catch (IOException e) { + throw new InternalErrorException(e); + } + if (!isValid || CorsUtils.isPreFlightRequest(theRequest)) { + return false; + } + } + + return super.incomingRequestPreProcessed(theRequest, theResponse); + } + +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index 0f451b0ce71..25a9af6e789 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -171,10 +171,6 @@ thymeleaf-spring4 - - org.ebaysf.web - cors-filter - com.phloc phloc-schematron diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index d37c8a1d817..c5ef57fb2ab 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -147,11 +147,6 @@ com.phloc phloc-schematron - - - org.apache.tomcat - tomcat-catalina - diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index e32751b40e3..031d2de4ade 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.demo; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -9,6 +10,7 @@ import javax.servlet.ServletException; import org.hl7.fhir.dstu3.model.Meta; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.cors.CorsConfiguration; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; @@ -30,6 +32,7 @@ import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; public class JpaServerDemo extends RestfulServer { @@ -47,7 +50,7 @@ public class JpaServerDemo extends RestfulServer { // Get the spring context from the web container (it's declared in web.xml) myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); - + /* * The hapi-fhir-server-resourceproviders-dev.xml file is a spring configuration * file which is automatically generated as a part of hapi-fhir-jpaserver-base and @@ -139,6 +142,22 @@ public class JpaServerDemo extends RestfulServer { */ setPagingProvider(new FifoMemoryPagingProvider(10)); + // Register a CORS filter + CorsConfiguration config = new CorsConfiguration(); + CorsInterceptor corsInterceptor = new CorsInterceptor(config); + config.addAllowedHeader("x-fhir-starter"); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); + config.addAllowedHeader("Access-Control-Request-Method"); + config.addAllowedHeader("Access-Control-Request-Headers"); + config.addAllowedOrigin("*"); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); + registerInterceptor(corsInterceptor); + /* * Load interceptors for the server from Spring (these are defined in FhirServerConfig.java) */ diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/webapp/WEB-INF/web.xml index dd56fbedd0f..fe694c4a12b 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/webapp/WEB-INF/web.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/webapp/WEB-INF/web.xml @@ -61,52 +61,4 @@ / - - - - - CORS Filter - org.apache.catalina.filters.CorsFilter - - A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. - cors.allowed.origins - * - - - A comma separated list of HTTP verbs, using which a CORS request can be made. - cors.allowed.methods - GET,POST,PUT,DELETE,OPTIONS - - - A comma separated list of allowed headers when making a non simple CORS request. - cors.allowed.headers - X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers - - - A comma separated list non-standard response headers that will be exposed to XHR2 object. - cors.exposed.headers - Location,Content-Location - - - A flag that suggests if CORS is supported with cookies - cors.support.credentials - true - - - A flag to control logging - cors.logging.enabled - true - - - Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. - cors.preflight.maxage - 300 - - - - CORS Filter - /* - - - diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 3c66a1a57b4..d06d09142dd 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -130,12 +130,14 @@ commons-dbcp2 test + + javax.servlet javax.servlet-api diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/DispatcherServletConfig.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/DispatcherServletConfig.java index ab3e2abc8da..8fb525d7509 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/DispatcherServletConfig.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/DispatcherServletConfig.java @@ -1,8 +1,12 @@ package ca.uhn.fhir.jpa.config; +import javax.el.ExpressionFactory; + import org.springframework.context.annotation.Configuration; @Configuration public class DispatcherServletConfig { //nothing + + ExpressionFactory myFacg; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java index b8eba4d4988..c857ca21f28 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java @@ -3,18 +3,14 @@ package ca.uhn.fhir.jpa.provider.dstu3; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.ArrayList; -import java.util.EnumSet; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; -import javax.servlet.DispatcherType; - -import org.apache.catalina.filters.CorsFilter; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.dstu3.model.Bundle; @@ -28,6 +24,7 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.DispatcherServlet; import ca.uhn.fhir.jpa.config.dstu3.WebsocketDstu3Config; @@ -40,6 +37,7 @@ import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.client.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.util.TestUtil; public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { @@ -116,16 +114,22 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test { subsServletHolder.setInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, WebsocketDstu3Config.class.getName()); proxyHandler.addServlet(subsServletHolder, "/*"); - FilterHolder corsFilterHolder = new FilterHolder(); - corsFilterHolder.setHeldClass(CorsFilter.class); - corsFilterHolder.setInitParameter("cors.allowed.origins", "*"); - corsFilterHolder.setInitParameter("cors.allowed.methods", "GET,POST,PUT,DELETE,OPTIONS"); - corsFilterHolder.setInitParameter("cors.allowed.headers", "X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers"); - corsFilterHolder.setInitParameter("cors.exposed.headers", "Location,Content-Location"); - corsFilterHolder.setInitParameter("cors.support.credentials", "true"); - corsFilterHolder.setInitParameter("cors.logging.enabled", "true"); - proxyHandler.addFilter(corsFilterHolder, "/*", EnumSet.of(DispatcherType.REQUEST)); - + // Register a CORS filter + CorsConfiguration config = new CorsConfiguration(); + CorsInterceptor corsInterceptor = new CorsInterceptor(config); + config.addAllowedHeader("x-fhir-starter"); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); + config.addAllowedHeader("Access-Control-Request-Method"); + config.addAllowedHeader("Access-Control-Request-Headers"); + config.addAllowedOrigin("*"); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); + ourRestServer.registerInterceptor(corsInterceptor); + server.setHandler(proxyHandler); server.start(); diff --git a/hapi-fhir-jpaserver-example/pom.xml b/hapi-fhir-jpaserver-example/pom.xml index 39d509f53a1..e165465674e 100644 --- a/hapi-fhir-jpaserver-example/pom.xml +++ b/hapi-fhir-jpaserver-example/pom.xml @@ -151,17 +151,6 @@ phloc-schematron - - - org.apache.tomcat - tomcat-catalina - - diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index 92147e542af..a5d5dda1101 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.demo; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -8,6 +9,7 @@ import javax.servlet.ServletException; import org.hl7.fhir.dstu3.model.Meta; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.cors.CorsConfiguration; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; @@ -29,6 +31,7 @@ import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; public class JpaServerDemo extends RestfulServer { @@ -140,6 +143,23 @@ public class JpaServerDemo extends RestfulServer { */ setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); + /* + * Enable CORS + */ + CorsConfiguration config = new CorsConfiguration(); + CorsInterceptor corsInterceptor = new CorsInterceptor(config); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); + config.addAllowedHeader("Access-Control-Request-Method"); + config.addAllowedHeader("Access-Control-Request-Headers"); + config.addAllowedOrigin("*"); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); + registerInterceptor(corsInterceptor); + /* * Load interceptors for the server from Spring (these are defined in FhirServerConfig.java) */ diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index a4fe817a222..8fafdf518ef 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -159,17 +159,6 @@ commons-dbcp2 - - - org.apache.tomcat - tomcat-catalina - provided - - diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/CORSFilter_.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/CORSFilter_.java deleted file mode 100755 index 418ca86c90b..00000000000 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/CORSFilter_.java +++ /dev/null @@ -1,1157 +0,0 @@ -/** - * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. - * - * 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 ca.uhn.fhirtest; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * This is a clone of the - * https://github.com/eBay/cors-filter - * CORSFilter, but with a single change applied to allow null origins to work. - * - * This change has been submitted as a pull request against the origin - * project: - * https://github.com/eBay/cors-filter/pull/7 - */ -public final class CORSFilter_ implements Filter { - // ----------------------------------------------------- Instance variables - /** - * Holds filter configuration. - */ - private FilterConfig filterConfig; - - /** - * A {@link Collection} of origins consisting of zero or more origins that - * are allowed access to the resource. - */ - private final Collection allowedOrigins; - - /** - * Determines if any origin is allowed to make request. - */ - private boolean anyOriginAllowed; - - /** - * A {@link Collection} of methods consisting of zero or more methods that - * are supported by the resource. - */ - private final Collection allowedHttpMethods; - - /** - * A {@link Collection} of headers consisting of zero or more header field - * names that are supported by the resource. - */ - private final Collection allowedHttpHeaders; - - /** - * A {@link Collection} of exposed headers consisting of zero or more header - * field names of headers other than the simple response headers that the - * resource might use and can be exposed. - */ - private final Collection exposedHeaders; - - /** - * A supports credentials flag that indicates whether the resource supports - * user credentials in the request. It is true when the resource does and - * false otherwise. - */ - private boolean supportsCredentials; - - /** - * Indicates (in seconds) how long the results of a pre-flight request can - * be cached in a pre-flight result cache. - */ - private long preflightMaxAge; - - /** - * Controls access log logging. - */ - private boolean loggingEnabled; - - /** - * Determines if the request should be decorated or not. - */ - private boolean decorateRequest; - - // --------------------------------------------------------- Constructor(s) - public CORSFilter_() { - this.allowedOrigins = new HashSet(); - this.allowedHttpMethods = new HashSet(); - this.allowedHttpHeaders = new HashSet(); - this.exposedHeaders = new HashSet(); - } - - // --------------------------------------------------------- Public methods - @Override - public void doFilter(final ServletRequest servletRequest, - final ServletResponse servletResponse, - final FilterChain filterChain) throws IOException, - ServletException { - if (!(servletRequest instanceof HttpServletRequest) - || !(servletResponse instanceof HttpServletResponse)) { - String message = - "CORS doesn't support non-HTTP request or response."; - throw new ServletException(message); - } - - // Safe to downcast at this point. - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpServletResponse response = (HttpServletResponse) servletResponse; - - // Determines the CORS request type. - CORSFilter_.CORSRequestType requestType = checkRequestType(request); - - // Adds CORS specific attributes to request. - if (decorateRequest) { - CORSFilter_.decorateCORSProperties(request, requestType); - } - switch (requestType) { - case SIMPLE: - // Handles a Simple CORS request. - this.handleSimpleCORS(request, response, filterChain); - break; - case ACTUAL: - // Handles an Actual CORS request. - this.handleSimpleCORS(request, response, filterChain); - break; - case PRE_FLIGHT: - // Handles a Pre-flight CORS request. - this.handlePreflightCORS(request, response, filterChain); - break; - case NOT_CORS: - // Handles a Normal request that is not a cross-origin request. - this.handleNonCORS(request, response, filterChain); - break; - default: - // Handles a CORS request that violates specification. - this.handleInvalidCORS(request, response, filterChain); - break; - } - } - - @Override - public void init(final FilterConfig filterConfig) throws ServletException { - // Initialize defaults - parseAndStore(DEFAULT_ALLOWED_ORIGINS, DEFAULT_ALLOWED_HTTP_METHODS, - DEFAULT_ALLOWED_HTTP_HEADERS, DEFAULT_EXPOSED_HEADERS, - DEFAULT_SUPPORTS_CREDENTIALS, DEFAULT_PREFLIGHT_MAXAGE, - DEFAULT_LOGGING_ENABLED, DEFAULT_DECORATE_REQUEST); - - this.filterConfig = filterConfig; - this.loggingEnabled = false; - - if (filterConfig != null) { - String configAllowedOrigins = - filterConfig.getInitParameter(PARAM_CORS_ALLOWED_ORIGINS); - String configAllowedHttpMethods = - filterConfig.getInitParameter(PARAM_CORS_ALLOWED_METHODS); - String configAllowedHttpHeaders = - filterConfig.getInitParameter(PARAM_CORS_ALLOWED_HEADERS); - String configExposedHeaders = - filterConfig.getInitParameter(PARAM_CORS_EXPOSED_HEADERS); - String configSupportsCredentials = - filterConfig - .getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS); - String configPreflightMaxAge = - filterConfig.getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE); - String configLoggingEnabled = - filterConfig.getInitParameter(PARAM_CORS_LOGGING_ENABLED); - String configDecorateRequest = - filterConfig.getInitParameter(PARAM_CORS_REQUEST_DECORATE); - - parseAndStore(configAllowedOrigins, configAllowedHttpMethods, - configAllowedHttpHeaders, - configExposedHeaders, configSupportsCredentials, - configPreflightMaxAge, - configLoggingEnabled, configDecorateRequest); - } - } - - // --------------------------------------------------------------- Handlers - /** - * Handles a CORS request of type {@link CORSRequestType}.SIMPLE. - * - * @param request - * The {@link HttpServletRequest} object. - * @param response - * The {@link HttpServletResponse} object. - * @param filterChain - * The {@link FilterChain} object. - * @throws IOException - * @throws ServletException - * @see Simple - * Cross-Origin Request, Actual Request, and Redirects - */ - public void handleSimpleCORS(final HttpServletRequest request, - final HttpServletResponse response, final FilterChain filterChain) - throws IOException, ServletException { - CORSFilter_.CORSRequestType requestType = - checkRequestType(request); - if (!(requestType == CORSFilter_.CORSRequestType.SIMPLE - || requestType == CORSFilter_.CORSRequestType.ACTUAL)) { - String message = - "Expects a HttpServletRequest object of type " - + CORSFilter_.CORSRequestType.SIMPLE - + " or " - + CORSFilter_.CORSRequestType.ACTUAL; - throw new IllegalArgumentException(message); - } - - final String origin = - request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); - final String method = request.getMethod(); - - // Section 6.1.2 - if (!isOriginAllowed(origin)) { - handleInvalidCORS(request, response, filterChain); - return; - } - - if (!allowedHttpMethods.contains(method)) { - handleInvalidCORS(request, response, filterChain); - return; - } - - // Section 6.1.3 - // Add a single Access-Control-Allow-Origin header. - if (anyOriginAllowed && !supportsCredentials) { - // If resource doesn't support credentials and if any origin is - // allowed - // to make CORS request, return header with '*'. - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - } else { - // If the resource supports credentials add a single - // Access-Control-Allow-Origin header, with the value of the Origin - // header as value. - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, - origin); - } - // Section 6.1.3 - // If the resource supports credentials, add a single - // Access-Control-Allow-Credentials header with the case-sensitive - // string "true" as value. - if (supportsCredentials) { - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, - "true"); - } - - // Section 6.1.4 - // If the list of exposed headers is not empty add one or more - // Access-Control-Expose-Headers headers, with as values the header - // field names given in the list of exposed headers. - if ((exposedHeaders != null) && (exposedHeaders.size() > 0)) { - String exposedHeadersString = join(exposedHeaders, ","); - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, - exposedHeadersString); - } - - // Forward the request down the filter chain. - filterChain.doFilter(request, response); - } - - /** - * Handles CORS pre-flight request. - * - * @param request - * The {@link HttpServletRequest} object. - * @param response - * The {@link HttpServletResponse} object. - * @param filterChain - * The {@link FilterChain} object. - * @throws IOException - * @throws ServletException - */ - public void handlePreflightCORS(final HttpServletRequest request, - final HttpServletResponse response, final FilterChain filterChain) - throws IOException, ServletException { - CORSRequestType requestType = checkRequestType(request); - if (requestType != CORSRequestType.PRE_FLIGHT) { - throw new IllegalArgumentException( - "Expects a HttpServletRequest object of type " - + CORSRequestType.PRE_FLIGHT.name().toLowerCase()); - } - - final String origin = - request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); - - // Section 6.2.2 - if (!isOriginAllowed(origin)) { - handleInvalidCORS(request, response, filterChain); - return; - } - - // Section 6.2.3 - String accessControlRequestMethod = - request.getHeader(CORSFilter_.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); - if (accessControlRequestMethod == null - || (!HTTP_METHODS - .contains(accessControlRequestMethod.trim()))) { - handleInvalidCORS(request, response, filterChain); - return; - } else { - accessControlRequestMethod = accessControlRequestMethod.trim(); - } - - // Section 6.2.4 - String accessControlRequestHeadersHeader = - request.getHeader(CORSFilter_.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); - List accessControlRequestHeaders = new LinkedList(); - if (accessControlRequestHeadersHeader != null - && !accessControlRequestHeadersHeader.trim().isEmpty()) { - String[] headers = - accessControlRequestHeadersHeader.trim().split(","); - for (String header : headers) { - accessControlRequestHeaders.add(header.trim().toLowerCase()); - } - } - - // Section 6.2.5 - if (!allowedHttpMethods.contains(accessControlRequestMethod)) { - handleInvalidCORS(request, response, filterChain); - return; - } - - // Section 6.2.6 - if (!accessControlRequestHeaders.isEmpty()) { - for (String header : accessControlRequestHeaders) { - if (!allowedHttpHeaders.contains(header)) { - handleInvalidCORS(request, response, filterChain); - return; - } - } - } - - // Section 6.2.7 - if (supportsCredentials) { - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, - origin); - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, - "true"); - } else { - if (anyOriginAllowed) { - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, - "*"); - } else { - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, - origin); - } - } - - // Section 6.2.8 - if (preflightMaxAge > 0) { - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE, - String.valueOf(preflightMaxAge)); - } - - // Section 6.2.9 - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS, - accessControlRequestMethod); - - // Section 6.2.10 - if ((allowedHttpHeaders != null) && (!allowedHttpHeaders.isEmpty())) { - response.addHeader( - CORSFilter_.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, - join(allowedHttpHeaders, ",")); - } - - // Do not forward the request down the filter chain. - } - - /** - * Handles a request, that's not a CORS request, but is a valid request i.e. - * it is not a cross-origin request. This implementation, just forwards the - * request down the filter chain. - * - * @param request - * The {@link HttpServletRequest} object. - * @param response - * The {@link HttpServletResponse} object. - * @param filterChain - * The {@link FilterChain} object. - * @throws IOException - * @throws ServletException - */ - public void handleNonCORS(final HttpServletRequest request, - final HttpServletResponse response, final FilterChain filterChain) - throws IOException, ServletException { - // Let request pass. - filterChain.doFilter(request, response); - } - - /** - * Handles a CORS request that violates specification. - * - * @param request - * The {@link HttpServletRequest} object. - * @param response - * The {@link HttpServletResponse} object. - * @param filterChain - * The {@link FilterChain} object. - * @throws IOException - * @throws ServletException - */ - public void handleInvalidCORS(final HttpServletRequest request, - final HttpServletResponse response, final FilterChain filterChain) { - String origin = request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN); - String method = request.getMethod(); - String accessControlRequestHeaders = - request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); - - String message = - "Invalid CORS request; Origin=" + origin + ";Method=" + method; - if (accessControlRequestHeaders != null) { - message = - message + ";Access-Control-Request-Headers=" - + accessControlRequestHeaders; - } - response.setContentType("text/plain"); - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - response.resetBuffer(); - - log(message); - } - - @Override - public void destroy() { - // NOOP - } - - // -------------------------------------------------------- Utility methods - /** - * Decorates the {@link HttpServletRequest}, with CORS attributes. - *
    - *
  • cors.isCorsRequest: Flag to determine if request is a CORS - * request. Set to true if CORS request; false - * otherwise.
  • - *
  • cors.request.origin: The Origin URL.
  • - *
  • cors.request.type: Type of request. Values: - * simple or preflight or not_cors or - * invalid_cors
  • - *
  • cors.request.headers: Request headers sent as - * 'Access-Control-Request-Headers' header, for pre-flight request.
  • - *
- * - * @param request - * The {@link HttpServletRequest} object. - * @param corsRequestType - * The {@link CORSRequestType} object. - */ - public static void decorateCORSProperties(final HttpServletRequest request, - final CORSRequestType corsRequestType) { - if (request == null) { - throw new IllegalArgumentException( - "HttpServletRequest object is null"); - } - - if (corsRequestType == null) { - throw new IllegalArgumentException("CORSRequestType object is null"); - } - - switch (corsRequestType) { - case SIMPLE: - request.setAttribute( - CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); - request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, - request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); - request.setAttribute( - CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, - corsRequestType.name().toLowerCase()); - break; - case ACTUAL: - request.setAttribute( - CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); - request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, - request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); - request.setAttribute( - CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, - corsRequestType.name().toLowerCase()); - break; - case PRE_FLIGHT: - request.setAttribute( - CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, true); - request.setAttribute(CORSFilter_.HTTP_REQUEST_ATTRIBUTE_ORIGIN, - request.getHeader(CORSFilter_.REQUEST_HEADER_ORIGIN)); - request.setAttribute( - CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, - corsRequestType.name().toLowerCase()); - String headers = - request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); - if (headers == null) { - headers = ""; - } - request.setAttribute( - CORSFilter_.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS, - headers - ); - break; - case NOT_CORS: - request.setAttribute( - CORSFilter_.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, false); - break; - default: - // Don't set any attributes - break; - } - } - - /** - * Joins elements of {@link Set} into a string, where each element is - * separated by the provided separator. - * - * @param elements - * The {@link Set} containing elements to join together. - * @param joinSeparator - * The character to be used for separating elements. - * @return The joined {@link String}; null if elements - * {@link Set} is null. - */ - public static String join(final Collection elements, - final String joinSeparator) { - String separator = ","; - if (elements == null) { - return null; - } - if (joinSeparator != null) { - separator = joinSeparator; - } - StringBuilder buffer = new StringBuilder(); - boolean isFirst = true; - for (String element : elements) { - if (!isFirst) { - buffer.append(separator); - } else { - isFirst = false; - } - - if (element != null) { - buffer.append(element); - } - } - - return buffer.toString(); - } - - /** - * Determines the request type. - * - * @param request - * @return - */ - public CORSRequestType checkRequestType(final HttpServletRequest request) { - CORSRequestType requestType = CORSRequestType.INVALID_CORS; - if (request == null) { - throw new IllegalArgumentException( - "HttpServletRequest object is null"); - } - String originHeader = request.getHeader(REQUEST_HEADER_ORIGIN); - // Section 6.1.1 and Section 6.2.1 - if (originHeader != null) { - if (originHeader.isEmpty()) { - requestType = CORSRequestType.INVALID_CORS; - } else if ("null".equals(originHeader) == false && !isValidOrigin(originHeader)) { - requestType = CORSRequestType.INVALID_CORS; - } else { - String method = request.getMethod(); - if (method != null && HTTP_METHODS.contains(method)) { - if ("OPTIONS".equals(method)) { - String accessControlRequestMethodHeader = - request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); - if (accessControlRequestMethodHeader != null - && !accessControlRequestMethodHeader.isEmpty()) { - requestType = CORSRequestType.PRE_FLIGHT; - } else if (accessControlRequestMethodHeader != null - && accessControlRequestMethodHeader.isEmpty()) { - requestType = CORSRequestType.INVALID_CORS; - } else { - requestType = CORSRequestType.ACTUAL; - } - } else if ("GET".equals(method) || "HEAD".equals(method)) { - requestType = CORSRequestType.SIMPLE; - } else if ("POST".equals(method)) { - String contentType = request.getContentType(); - if (contentType != null) { - contentType = contentType.toLowerCase().trim(); - if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES - .contains(contentType)) { - requestType = CORSRequestType.SIMPLE; - } else { - requestType = CORSRequestType.ACTUAL; - } - } - } else if (COMPLEX_HTTP_METHODS.contains(method)) { - requestType = CORSRequestType.ACTUAL; - } - } - } - } else { - requestType = CORSRequestType.NOT_CORS; - } - - return requestType; - } - - /** - * Checks if the Origin is allowed to make a CORS request. - * - * @param origin - * The Origin. - * @return true if origin is allowed; false - * otherwise. - */ - private boolean isOriginAllowed(final String origin) { - if (anyOriginAllowed) { - return true; - } - - // If 'Origin' header is a case-sensitive match of any of allowed - // origins, then return true, else return false. - return allowedOrigins.contains(origin); - } - - private void log(String message) { - if (loggingEnabled) { - filterConfig.getServletContext().log(message); - } - } - - /** - * Parses each param-value and populates configuration variables. If a param - * is provided, it overrides the default. - * - * @param allowedOrigins - * A {@link String} of comma separated origins. - * @param allowedHttpMethods - * A {@link String} of comma separated HTTP methods. - * @param allowedHttpHeaders - * A {@link String} of comma separated HTTP headers. - * @param exposedHeaders - * A {@link String} of comma separated headers that needs to be - * exposed. - * @param supportsCredentials - * "true" if support credentials needs to be enabled. - * @param preflightMaxAge - * The amount of seconds the user agent is allowed to cache the - * result of the pre-flight request. - * @param loggingEnabled - * Flag to control logging to access log. - * @throws ServletException - */ - private void parseAndStore(final String allowedOrigins, - final String allowedHttpMethods, final String allowedHttpHeaders, - final String exposedHeaders, final String supportsCredentials, - final String preflightMaxAge, final String loggingEnabled, - final String decorateRequest) - throws ServletException { - if (allowedOrigins != null) { - if (allowedOrigins.trim().equals("*")) { - this.anyOriginAllowed = true; - } else { - this.anyOriginAllowed = false; - Set setAllowedOrigins = - parseStringToSet(allowedOrigins); - this.allowedOrigins.clear(); - this.allowedOrigins.addAll(setAllowedOrigins); - } - } - - if (allowedHttpMethods != null) { - Set setAllowedHttpMethods = - parseStringToSet(allowedHttpMethods); - this.allowedHttpMethods.clear(); - this.allowedHttpMethods.addAll(setAllowedHttpMethods); - } - - if (allowedHttpHeaders != null) { - Set setAllowedHttpHeaders = - parseStringToSet(allowedHttpHeaders); - Set lowerCaseHeaders = new HashSet(); - for (String header : setAllowedHttpHeaders) { - String lowerCase = header.toLowerCase(); - lowerCaseHeaders.add(lowerCase); - } - this.allowedHttpHeaders.clear(); - this.allowedHttpHeaders.addAll(lowerCaseHeaders); - } - - if (exposedHeaders != null) { - Set setExposedHeaders = parseStringToSet(exposedHeaders); - this.exposedHeaders.clear(); - this.exposedHeaders.addAll(setExposedHeaders); - } - - if (supportsCredentials != null) { - // For any value other then 'true' this will be false. - this.supportsCredentials = - Boolean.parseBoolean(supportsCredentials); - } - - if (preflightMaxAge != null) { - try { - if (!preflightMaxAge.isEmpty()) { - this.preflightMaxAge = Long.parseLong(preflightMaxAge); - } else { - this.preflightMaxAge = 0L; - } - } catch (NumberFormatException e) { - throw new ServletException("Unable to parse preflightMaxAge", e); - } - } - - if (loggingEnabled != null) { - // For any value other then 'true' this will be false. - this.loggingEnabled = Boolean.parseBoolean(loggingEnabled); - } - - if (decorateRequest != null) { - // For any value other then 'true' this will be false. - this.decorateRequest = Boolean.parseBoolean(decorateRequest); - } - } - - /** - * Takes a comma separated list and returns a Set. - * - * @param data - * A comma separated list of strings. - * @return Set - */ - private Set parseStringToSet(final String data) { - String[] splits; - - if (data != null && data.length() > 0) { - splits = data.split(","); - } else { - splits = new String[] {}; - } - - Set set = new HashSet(); - if (splits.length > 0) { - for (String split : splits) { - set.add(split.trim()); - } - } - - return set; - } - - /** - * Checks if a given origin is valid or not. Criteria: - *
    - *
  • If an encoded character is present in origin, it's not valid.
  • - *
  • Origin should be a valid {@link URI}
  • - *
- * - * @param origin - * @see RFC952 - * @return - */ - public static boolean isValidOrigin(String origin) { - // Checks for encoded characters. Helps prevent CRLF injection. - if (origin.contains("%")) { - return false; - } - - URI originURI; - - try { - originURI = new URI(origin); - } catch (URISyntaxException e) { - return false; - } - // If scheme for URI is null, return false. Return true otherwise. - return originURI.getScheme() != null; - - } - - // -------------------------------------------------------------- Accessors - /** - * Determines if logging is enabled or not. - * - * @return true if it's enabled; false otherwise. - */ - public boolean isLoggingEnabled() { - return loggingEnabled; - } - - /** - * Determines if any origin is allowed to make CORS request. - * - * @return true if it's enabled; false otherwise. - */ - public boolean isAnyOriginAllowed() { - return anyOriginAllowed; - } - - /** - * Returns a {@link Set} of headers that should be exposed by browser. - * - * @return - */ - public Collection getExposedHeaders() { - return exposedHeaders; - } - - /** - * Determines is supports credentials is enabled - * - * @return - */ - public boolean isSupportsCredentials() { - return supportsCredentials; - } - - /** - * Returns the preflight response cache time in seconds. - * - * @return Time to cache in seconds. - */ - public long getPreflightMaxAge() { - return preflightMaxAge; - } - - /** - * Returns the {@link Set} of allowed origins that are allowed to make - * requests. - * - * @return {@link Set} - */ - public Collection getAllowedOrigins() { - return allowedOrigins; - } - - /** - * Returns a {@link Set} of HTTP methods that are allowed to make requests. - * - * @return {@link Set} - */ - public Collection getAllowedHttpMethods() { - return allowedHttpMethods; - } - - /** - * Returns a {@link Set} of headers support by resource. - * - * @return {@link Set} - */ - public Collection getAllowedHttpHeaders() { - return allowedHttpHeaders; - } - - // -------------------------------------------------- CORS Response Headers - /** - * The Access-Control-Allow-Origin header indicates whether a resource can - * be shared based by returning the value of the Origin request header in - * the response. - */ - public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = - "Access-Control-Allow-Origin"; - - /** - * The Access-Control-Allow-Credentials header indicates whether the - * response to request can be exposed when the omit credentials flag is - * unset. When part of the response to a preflight request it indicates that - * the actual request can include user credentials. - */ - public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = - "Access-Control-Allow-Credentials"; - - /** - * The Access-Control-Expose-Headers header indicates which headers are safe - * to expose to the API of a CORS API specification - */ - public static final String RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = - "Access-Control-Expose-Headers"; - - /** - * The Access-Control-Max-Age header indicates how long the results of a - * preflight request can be cached in a preflight result cache. - */ - public static final String RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE = - "Access-Control-Max-Age"; - - /** - * The Access-Control-Allow-Methods header indicates, as part of the - * response to a preflight request, which methods can be used during the - * actual request. - */ - public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS = - "Access-Control-Allow-Methods"; - - /** - * The Access-Control-Allow-Headers header indicates, as part of the - * response to a preflight request, which header field names can be used - * during the actual request. - */ - public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS = - "Access-Control-Allow-Headers"; - - // -------------------------------------------------- CORS Request Headers - /** - * The Origin header indicates where the cross-origin request or preflight - * request originates from. - */ - public static final String REQUEST_HEADER_ORIGIN = "Origin"; - - /** - * The Access-Control-Request-Method header indicates which method will be - * used in the actual request as part of the preflight request. - */ - public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD = - "Access-Control-Request-Method"; - - /** - * The Access-Control-Request-Headers header indicates which headers will be - * used in the actual request as part of the preflight request. - */ - public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS = - "Access-Control-Request-Headers"; - - // ----------------------------------------------------- Request attributes - /** - * The prefix to a CORS request attribute. - */ - public static final String HTTP_REQUEST_ATTRIBUTE_PREFIX = "cors."; - - /** - * Attribute that contains the origin of the request. - */ - public static final String HTTP_REQUEST_ATTRIBUTE_ORIGIN = - HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.origin"; - - /** - * Boolean value, suggesting if the request is a CORS request or not. - */ - public static final String HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST = - HTTP_REQUEST_ATTRIBUTE_PREFIX + "isCorsRequest"; - - /** - * Type of CORS request, of type {@link CORSRequestType}. - */ - public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE = - HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.type"; - - /** - * Request headers sent as 'Access-Control-Request-Headers' header, for - * pre-flight request. - */ - public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS = - HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.headers"; - - // -------------------------------------------------------------- Constants - /** - * Enumerates varies types of CORS requests. Also, provides utility methods - * to determine the request type. - */ - public static enum CORSRequestType { - /** - * A simple HTTP request, i.e. it shouldn't be pre-flighted. - */ - SIMPLE, - /** - * A HTTP request that needs to be pre-flighted. - */ - ACTUAL, - /** - * A pre-flight CORS request, to get meta information, before a - * non-simple HTTP request is sent. - */ - PRE_FLIGHT, - /** - * Not a CORS request, but a normal request. - */ - NOT_CORS, - /** - * An invalid CORS request, i.e. it qualifies to be a CORS request, but - * fails to be a valid one. - */ - INVALID_CORS - } - - /** - * {@link Collection} of HTTP methods. Case sensitive. - * - * @see http://tools.ietf.org/html/rfc2616#section-5.1.1 - */ - public static final Collection HTTP_METHODS = new HashSet( - Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", - "TRACE", "CONNECT")); - /** - * {@link Collection} of non-simple HTTP methods. Case sensitive. - */ - public static final Collection COMPLEX_HTTP_METHODS = - new HashSet( - Arrays.asList("PUT", "DELETE", "TRACE", "CONNECT")); - /** - * {@link Collection} of Simple HTTP methods. Case sensitive. - * - * @see http://www.w3.org/TR/cors/#terminology - */ - public static final Collection SIMPLE_HTTP_METHODS = - new HashSet( - Arrays.asList("GET", "POST", "HEAD")); - - /** - * {@link Collection} of Simple HTTP request headers. Case in-sensitive. - * - * @see http://www.w3.org/TR/cors/#terminology - */ - public static final Collection SIMPLE_HTTP_REQUEST_HEADERS = - new HashSet(Arrays.asList("Accept", "Accept-Language", - "Content-Language")); - - /** - * {@link Collection} of Simple HTTP request headers. Case in-sensitive. - * - * @see http://www.w3.org/TR/cors/#terminology - */ - public static final Collection SIMPLE_HTTP_RESPONSE_HEADERS = - new HashSet(Arrays.asList("Cache-Control", - "Content-Language", "Content-Type", "Expires", - "Last-Modified", "Pragma")); - - /** - * {@link Collection} of Simple HTTP request headers. Case in-sensitive. - * - * @see http://www.w3.org/TR/cors/#terminology - */ - public static final Collection SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES = - new HashSet(Arrays.asList( - "application/x-www-form-urlencoded", "multipart/form-data", - "text/plain")); - - // ------------------------------------------------ Configuration Defaults - /** - * By default, all origins are allowed to make requests. - */ - public static final String DEFAULT_ALLOWED_ORIGINS = "*"; - - /** - * By default, following methods are supported: GET, POST, HEAD and OPTIONS. - */ - public static final String DEFAULT_ALLOWED_HTTP_METHODS = - "GET,POST,HEAD,OPTIONS"; - - /** - * By default, time duration to cache pre-flight response is 30 mins. - */ - public static final String DEFAULT_PREFLIGHT_MAXAGE = "1800"; - - /** - * By default, support credentials is turned on. - */ - public static final String DEFAULT_SUPPORTS_CREDENTIALS = "true"; - - /** - * By default, following headers are supported: - * Origin,Accept,X-Requested-With, Content-Type, - * Access-Control-Request-Method, and Access-Control-Request-Headers. - */ - public static final String DEFAULT_ALLOWED_HTTP_HEADERS = - "Origin,Accept,X-Requested-With,Content-Type," - + - "Access-Control-Request-Method,Access-Control-Request-Headers"; - - /** - * By default, none of the headers are exposed in response. - */ - public static final String DEFAULT_EXPOSED_HEADERS = ""; - - /** - * By default, access log logging is turned off - */ - public static final String DEFAULT_LOGGING_ENABLED = "false"; - - /** - * By default, request is decorated with CORS attributes. - */ - public static final String DEFAULT_DECORATE_REQUEST = "true"; - - // ----------------------------------------Filter Config Init param-name(s) - /** - * Key to retrieve allowed origins from {@link FilterConfig}. - */ - public static final String PARAM_CORS_ALLOWED_ORIGINS = - "cors.allowed.origins"; - - /** - * Key to retrieve support credentials from {@link FilterConfig}. - */ - public static final String PARAM_CORS_SUPPORT_CREDENTIALS = - "cors.support.credentials"; - - /** - * Key to retrieve exposed headers from {@link FilterConfig}. - */ - public static final String PARAM_CORS_EXPOSED_HEADERS = - "cors.exposed.headers"; - - /** - * Key to retrieve allowed headers from {@link FilterConfig}. - */ - public static final String PARAM_CORS_ALLOWED_HEADERS = - "cors.allowed.headers"; - - /** - * Key to retrieve allowed methods from {@link FilterConfig}. - */ - public static final String PARAM_CORS_ALLOWED_METHODS = - "cors.allowed.methods"; - - /** - * Key to retrieve preflight max age from {@link FilterConfig}. - */ - public static final String PARAM_CORS_PREFLIGHT_MAXAGE = - "cors.preflight.maxage"; - - /** - * Key to retrieve access log logging flag. - */ - public static final String PARAM_CORS_LOGGING_ENABLED = - "cors.logging.enabled"; - - /** - * Key to determine if request should be decorated. - */ - public static final String PARAM_CORS_REQUEST_DECORATE = - "cors.request.decorate"; -} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index 8b076a2d19b..3069eed77d8 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -1,6 +1,7 @@ package ca.uhn.fhirtest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -12,6 +13,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.cors.CorsConfiguration; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.config.WebsocketDstu2Config; @@ -27,19 +29,15 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; -import ca.uhn.fhir.rest.server.ETagSupportEnum; -import ca.uhn.fhir.rest.server.EncodingEnum; -import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; -import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.*; import ca.uhn.fhir.rest.server.interceptor.BanUnsupportedHttpMethodsInterceptor; +import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; -import ca.uhn.fhirtest.config.TestDstu3Config; import ca.uhn.fhirtest.config.TdlDstu2Config; import ca.uhn.fhirtest.config.TdlDstu3Config; import ca.uhn.fhirtest.config.TestDstu2Config; +import ca.uhn.fhirtest.config.TestDstu3Config; public class TestRestfulServer extends RestfulServer { @@ -172,6 +170,23 @@ public class TestRestfulServer extends RestfulServer { setPlainProviders(plainProviders); + /* + * Enable CORS + */ + CorsConfiguration config = new CorsConfiguration(); + CorsInterceptor corsInterceptor = new CorsInterceptor(config); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); + config.addAllowedHeader("Access-Control-Request-Method"); + config.addAllowedHeader("Access-Control-Request-Headers"); + config.addAllowedOrigin("*"); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); + registerInterceptor(corsInterceptor); + /* * We want to format the response using nice HTML if it's a browser, since this * makes things a little easier for testers. diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml index b7f03fb93f2..8f294ce04c8 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml @@ -76,6 +76,7 @@ 1 + fhirServletDstu1 @@ -132,11 +133,11 @@ /baseStu3/* + fhirServletDstu2 @@ -156,52 +158,4 @@ / - - - - - CORS Filter - org.apache.catalina.filters.CorsFilter - - A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. - cors.allowed.origins - * - - - A comma separated list of HTTP verbs, using which a CORS request can be made. - cors.allowed.methods - GET,POST,PUT,DELETE,OPTIONS - - - A comma separated list of allowed headers when making a non simple CORS request. - cors.allowed.headers - X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers - - - A comma separated list non-standard response headers that will be exposed to XHR2 object. - cors.exposed.headers - Location,Content-Location - - - A flag that suggests if CORS is supported with cookies - cors.support.credentials - true - - - A flag to control logging - cors.logging.enabled - true - - - Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. - cors.preflight.maxage - 300 - - - - CORS Filter - /* - - - diff --git a/hapi-fhir-narrativegenerator/pom.xml b/hapi-fhir-narrativegenerator/pom.xml index 62752b48420..e113c8a434a 100644 --- a/hapi-fhir-narrativegenerator/pom.xml +++ b/hapi-fhir-narrativegenerator/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 2.1-SNAPSHOT + 2.2-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -19,18 +19,15 @@ ca.uhn.hapi.fhir hapi-fhir-base - 2.1-SNAPSHOT - - - - ca.uhn.hapi.fhir - hapi-fhir-structures-dstu2 - 2.1-SNAPSHOT + 2.2-SNAPSHOT + + ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 2.1-SNAPSHOT + 2.2-SNAPSHOT + test diff --git a/hapi-fhir-structures-dstu/pom.xml b/hapi-fhir-structures-dstu/pom.xml index 91ebb524d52..0dbe5272026 100644 --- a/hapi-fhir-structures-dstu/pom.xml +++ b/hapi-fhir-structures-dstu/pom.xml @@ -73,11 +73,6 @@ true test - - org.ebaysf.web - cors-filter - test - org.thymeleaf thymeleaf @@ -94,13 +89,13 @@ test + - org.apache.tomcat - tomcat-catalina + + org.springframework + spring-web test - - net.sf.json-lib json-lib diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java index 872f3a6b32a..4e81f56f017 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java @@ -1,15 +1,15 @@ package ca.uhn.fhir.rest.server; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.EnumSet; import java.util.concurrent.TimeUnit; import javax.servlet.DispatcherType; -import org.apache.catalina.filters.CorsFilter; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpResponse; @@ -29,11 +29,14 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.filter.CorsFilter; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.rest.server.RestfulServerSelfReferenceTest.DummyPatientResourceProvider; +import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; @@ -55,11 +58,26 @@ public class CorsTest { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info("Response was:\n{}", responseContent); - assertEquals("GET", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue()); + assertEquals("GET,POST,PUT,DELETE,OPTIONS", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue()); assertEquals("null", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue()); } } + @Test + public void testRequestWithInvalidOrigin() throws ClientProtocolException, IOException { + { + HttpOptions httpOpt = new HttpOptions(ourBaseUri + "/Organization/b27ed191-f62d-4128-d99d-40b5e84f2bf2"); + httpOpt.addHeader("Access-Control-Request-Method", "GET"); + httpOpt.addHeader("Origin", "http://yahoo.com"); + httpOpt.addHeader("Access-Control-Request-Headers", "accept, x-fhir-starter, content-type"); + HttpResponse status = ourClient.execute(httpOpt); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + assertEquals(403, status.getStatusLine().getStatusCode()); + } + } + @Test public void testContextWithSpace() throws Exception { { @@ -71,7 +89,7 @@ public class CorsTest { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info("Response was:\n{}", responseContent); - assertEquals("POST", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue()); + assertEquals("GET,POST,PUT,DELETE,OPTIONS", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue()); assertEquals("http://www.fhir-starter.com", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue()); } { @@ -113,6 +131,13 @@ public class CorsTest { ourClient.close(); } + @Test + public void testCorsConfigMethods() { + CorsInterceptor corsInterceptor = new CorsInterceptor(); + assertNotNull(corsInterceptor.getConfig()); + corsInterceptor.setConfig(new CorsConfiguration()); + } + @BeforeClass public static void beforeClass() throws Exception { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); @@ -129,18 +154,26 @@ public class CorsTest { // ServletHandler proxyHandler = new ServletHandler(); ServletHolder servletHolder = new ServletHolder(restServer); - FilterHolder fh = new FilterHolder(); - fh.setHeldClass(CorsFilter.class); - fh.setInitParameter("cors.logging.enabled", "true"); - fh.setInitParameter("cors.allowed.origins", "*"); - fh.setInitParameter("cors.allowed.headers", "x-fhir-starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers"); - fh.setInitParameter("cors.exposed.headers", "Location,Content-Location"); - fh.setInitParameter("cors.allowed.methods", "GET,POST,PUT,DELETE,OPTIONS"); - + CorsConfiguration config = new CorsConfiguration(); + CorsInterceptor interceptor = new CorsInterceptor(config); + config.addAllowedHeader("x-fhir-starter"); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); + config.addAllowedHeader("Access-Control-Request-Method"); + config.addAllowedHeader("Access-Control-Request-Headers"); + config.addAllowedOrigin("http://www.fhir-starter.com"); + config.addAllowedOrigin("null"); + config.addAllowedOrigin("file://"); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); + restServer.registerInterceptor(interceptor); + ServletContextHandler ch = new ServletContextHandler(); ch.setContextPath("/rootctx/rcp2"); ch.addServlet(servletHolder, "/fhirctx/fcp2/*"); - ch.addFilter(fh, "/*", EnumSet.of(DispatcherType.INCLUDE, DispatcherType.REQUEST)); ContextHandlerCollection contexts = new ContextHandlerCollection(); ourServer.setHandler(contexts); diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 89c4e7705a3..6d29c3b9798 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -81,11 +81,6 @@ true test - - org.ebaysf.web - cors-filter - test - org.thymeleaf thymeleaf @@ -142,6 +137,11 @@ test + + org.springframework + spring-web + test + diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java index 62c7dcbae5d..0e1dfbfb51a 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java @@ -16,11 +16,7 @@ import static org.mockito.Mockito.when; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; @@ -33,7 +29,6 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.ebaysf.web.cors.CORSFilter; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -42,6 +37,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.springframework.web.cors.CorsConfiguration; import com.phloc.commons.collections.iterate.ArrayEnumeration; @@ -62,11 +58,7 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.api.RequestTypeEnum; -import ca.uhn.fhir.rest.server.BundleInclusionRule; -import ca.uhn.fhir.rest.server.Constants; -import ca.uhn.fhir.rest.server.EncodingEnum; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.*; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.PortUtil; @@ -708,14 +700,30 @@ public class ResponseHighlightingInterceptorTest { ServletHandler proxyHandler = new ServletHandler(); ourServlet = new RestfulServer(ourCtx); + + /* + * Enable CORS + */ + CorsConfiguration config = new CorsConfiguration(); + CorsInterceptor corsInterceptor = new CorsInterceptor(config); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader("Content-Type"); + config.addAllowedHeader("Access-Control-Request-Method"); + config.addAllowedHeader("Access-Control-Request-Headers"); + config.addAllowedOrigin("*"); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS")); + ourServlet.registerInterceptor(corsInterceptor); + ourServlet.registerInterceptor(new ResponseHighlighterInterceptor()); ourServlet.setResourceProviders(patientProvider, new DummyBinaryResourceProvider()); ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE); ServletHolder servletHolder = new ServletHolder(ourServlet); proxyHandler.addServletWithMapping(servletHolder, "/*"); - proxyHandler.addFilterWithMapping(CORSFilter.class, "/*", 1); - ourServer.setHandler(proxyHandler); ourServer.start(); diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index a6174903b0d..c283b4cff6e 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -95,11 +95,6 @@ true test - - org.ebaysf.web - cors-filter - test - org.thymeleaf thymeleaf @@ -153,6 +148,11 @@ test + + org.springframework + spring-web + test + diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 51ecc587e6d..6f50e0e67c9 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -94,11 +94,6 @@ true test - - org.ebaysf.web - cors-filter - test - org.thymeleaf thymeleaf @@ -155,6 +150,11 @@ test + + org.springframework + spring-web + test + diff --git a/pom.xml b/pom.xml index c1c05255b6b..52cf29a3c3f 100644 --- a/pom.xml +++ b/pom.xml @@ -309,8 +309,6 @@ 4.4.5 4.3.1.RELEASE 3.0.1.RELEASE - 8.0.39 - 1.0.1 1.6 @@ -526,12 +524,6 @@ wagon-scm 2.10 - - org.apache.tomcat - tomcat-catalina - - ${tomcat_version} - org.apache.velocity velocity @@ -547,11 +539,6 @@ woodstox-core-asl 4.4.1 - - org.ebaysf.web - cors-filter - ${ebay_cors_filter_version} - org.eclipse.jetty jetty-http @@ -1592,6 +1579,7 @@ hapi-fhir-base-test-mindeps-server hapi-tinder-plugin hapi-tinder-test + hapi-fhir-narrativegenerator hapi-fhir-structures-dstu hapi-fhir-validation-resources-dstu2 hapi-fhir-structures-dstu2 diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d29375d85fc..2675db22047 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -30,11 +30,13 @@ to GitHub user @vijayt27 for reporting! - All server examples as well as the CLI have been - switched from using eBay's CORS filter to using - the Apache Tomcat CORS filter instead. The former - has become unmaintained and has unfixed bugs so - it is no longer recommended for use. + As the eBay CORS interceptor has gone dormant, we have introduced a new + HAPI server interceptor which can be used to implement CORS support + instead of using the previously recommended Servlet Filter. All server + examples as well as the CLI have been switched to use this new interceptor. + See the + CORS Documentation]]> + for more information. Make the parser configurable so that when diff --git a/src/site/xdoc/doc_cors.xml.vm b/src/site/xdoc/doc_cors.xml.vm index 9a5599d861f..08b376ff00a 100644 --- a/src/site/xdoc/doc_cors.xml.vm +++ b/src/site/xdoc/doc_cors.xml.vm @@ -10,13 +10,7 @@
-

- If you are intending to support JavaScript clients in your server application, - you will need to enable Cross Origin Resource Sharing (CORS). There are - a number of ways of supporting this, but the easiest is to use a servlet filter. -

- -

+

Note that in previous revisions of this document we recommended using the eBay CORS Filter, but as of 2016 the eBay filter is no longer being maintained and contains known bugs. @@ -24,35 +18,80 @@

- The following examples show how to use the Apache Tomcat CorsFilter to enable - CORS support. The instructions below should work even on platforms other than - Tomcat (in other words, you can deploy the Tomcat CorsFilter to Jetty or JBoss if you like) - but if you run into conflicts it may be worth investigating if there is a dedicated - CORS filter for the platform you are using. + If you are intending to support JavaScript clients in your server application, + you will generally need to enable Cross Origin Resource Sharing (CORS). There are + a number of ways of supporting this, so two are shown here:

+
    +
  • An approach using a HAPI FHIR Server Interceptor (Requires SpringFramework)
  • +
  • An approach using a servlet Filter (Container Specific)
  • +
- - + +

- If you are deploying to a platform other than Tomcat, add the - following dependency to your Maven POM. If you are deploying - to Tomcat, the required classes are present on the classpath - so youdo not need to do this step. + The HAPI FHIR server framework includes an interceptor that can be + used to provide CORS functionality on your server. This mechanism is + nice because it relies purely on Java configuration (no messing around with + web.xml files). HAPI's interceptor is a thin wrapper around Spring Framework's + CorsProcessor class, so it requires Spring to be present on your classpath.

- +

- Add the following dependency to your POM: + Spring is generally unlikely to conflict with other libraries so it is usually + safe to add it to your classpath, but it is a fairly large library so if size is + a concern you might opt to use a filter instead. +

+ +

+ The following steps outline how to enable HAPI's CorsInterceptor: +

+ +

+ Add the following dependency to your POM. Note the exclusion of + commons-logging, as we are using SLF4j without commons-logging in + most of our examples. If your application uses commons-logging you don't need + to exclude that dependency.

- org.apache.tomcat - tomcat-catalina - ${tomcat_version} + org.springframework + spring-web + ${spring_version} + + + commons-logging + commons-logging + + ]]> - + +

+ In your server's initialization method, create and register + a CorsInterceptor: +

+ + + + +
- - + + +

+ The following examples show how to use the Apache Tomcat CorsFilter to enable + CORS support. The filter being used + (org.apache.catalina.filters.CorsFilter) is bundled with Apache + Tomcat so if you are deploying to that server you can use the filter. +

+ +

+ Other containers have similar filters you can use, so consult the documentation + for the given container you are using for more information. (If you have + an example for how to configure a different CORS filter, please send it + our way! Examples are always useful!) +

+

In your web.xml file (within the WEB-INF directory in your WAR file), the following filter definition adds the CORS filter, including support diff --git a/src/site/xdoc/doc_rest_server_interceptor.xml b/src/site/xdoc/doc_rest_server_interceptor.xml index 5f3e37a7627..24ed03d5402 100644 --- a/src/site/xdoc/doc_rest_server_interceptor.xml +++ b/src/site/xdoc/doc_rest_server_interceptor.xml @@ -290,6 +290,17 @@ + + +

+ HAPI FHIR includes an interceptor which can be used to + implement CORS support on your server. See HAPI's + CORS Documentation for information + on how to use this interceptor. +

+ +
+