diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index b15c989c946..be914510a09 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -60,7 +60,6 @@ true - org.thymeleaf @@ -69,6 +68,22 @@ true + + + org.ebaysf.web + cors-filter + ${ebay_cors_filter_version} + true + + + org.apache.commons diff --git a/hapi-fhir-base/src/changes/changes.xml b/hapi-fhir-base/src/changes/changes.xml index 8419f2251b5..190f6feedf1 100644 --- a/hapi-fhir-base/src/changes/changes.xml +++ b/hapi-fhir-base/src/changes/changes.xml @@ -32,6 +32,12 @@ Contained/included resource instances received by a client are now automatically added to any ResourceReferenceDt instancea in other resources which reference them. + + Add documentation on how to use eBay CORS Filter to support Cross Origin Resource + Sharing (CORS) to server. CORS support that was built in to the server itself has + been removed, as it did not work correctly (and was reinventing a wheel that others + have done a great job inventing). + diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index be47c36be2b..98b881768c6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.rest.server; * #L% */ -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.*; import java.io.IOException; import java.io.OutputStreamWriter; @@ -102,20 +102,6 @@ public class RestfulServer extends HttpServlet { private String myServerVersion = VersionUtil.getVersion(); private boolean myStarted; private boolean myUseBrowserFriendlyContentTypes; - private String myCorsAllowDomain; - - public String getCorsAllowDomain() { - return myCorsAllowDomain; - } - - /** - * If set to anything other than null (which is the default), the server will return CORS (Cross Origin Resource Sharing) headers with the given domain string. - *

- * A value of "*" indicates that the server allows access to all domains (which may be appropriate in development situations but is generally not appropriate in production) - */ - public void setCorsAllowDomain(String theCorsAllowDomain) { - myCorsAllowDomain = theCorsAllowDomain; - } /** * Constructor @@ -137,13 +123,6 @@ public class RestfulServer extends HttpServlet { */ public void addHeadersToResponse(HttpServletResponse theHttpResponse) { theHttpResponse.addHeader("X-Powered-By", "HAPI FHIR " + VersionUtil.getVersion() + " RESTful Server"); - - if (isNotBlank(myCorsAllowDomain)) { - theHttpResponse.addHeader(Constants.HEADER_CORS_ALLOW_ORIGIN, myCorsAllowDomain); - theHttpResponse.addHeader(Constants.HEADER_CORS_ALLOW_METHODS, Constants.HEADERVALUE_CORS_ALLOW_METHODS_ALL); - theHttpResponse.addHeader(Constants.HEADER_CORS_EXPOSE_HEADERS, Constants.HEADER_CONTENT_LOCATION); - } - } private void assertProviderIsValid(Object theNext) throws ConfigurationException { diff --git a/hapi-fhir-base/src/site/site.xml b/hapi-fhir-base/src/site/site.xml index 1d36991a1cc..cd162df0422 100644 --- a/hapi-fhir-base/src/site/site.xml +++ b/hapi-fhir-base/src/site/site.xml @@ -67,6 +67,7 @@ + diff --git a/hapi-fhir-base/src/site/xdoc/doc_cors.xml b/hapi-fhir-base/src/site/xdoc/doc_cors.xml new file mode 100644 index 00000000000..126065c3ab0 --- /dev/null +++ b/hapi-fhir-base/src/site/xdoc/doc_cors.xml @@ -0,0 +1,108 @@ + + + + + CORS - HAPI FHIR + James Agnew + + + + +

+ +

+ 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. +

+ +

+ The recommended filter for this purpose is the + eBay Open Sourced + CORS Filter (Licensed under + the Apache Software License 2.0). +

+ +

+ To add CORS support using this library, there are two simple steps: +

+ + + +

+ In your server WAR file, you must include the cors-filter-X.X.X.JAR + dependency. This dependency is included in the HAPI distribution. +

+ +

+ If you are using Maven, this JAR is marked as optional so you will need to + explicitly include it in your project pom.xml using the following dependency: +

+ + org.ebaysf.web + cors-filter + 1.0.1 + true +]]> + +
+ + + +

+ In your web.xml file (within the WEB-INF directory in your WAR file), + the following filter definition adds the CORS filter, including support + for the X-FHIR-Starter header defined by SMART Platforms. +

+ + + CORS Filter + org.ebaysf.web.cors.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 + + + + 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-base/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java new file mode 100644 index 00000000000..c146dd277ee --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java @@ -0,0 +1,120 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.*; + +import java.util.EnumSet; +import java.util.concurrent.TimeUnit; + +import javax.servlet.DispatcherType; + +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpOptions; +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.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.rest.server.ResfulServerSelfReferenceTest.DummyPatientResourceProvider; +import ca.uhn.fhir.testutil.RandomServerPortProvider; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class CorsTest { + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CorsTest.class); + + @Test + public void testContextWithSpace() throws Exception { + int port = RandomServerPortProvider.findFreePort(); + Server server = new Server(port); + + RestfulServer restServer = new RestfulServer(); + restServer.setFhirContext(ourCtx); + restServer.setResourceProviders(new DummyPatientResourceProvider()); + + // 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.allowed.methods", "GET,POST,PUT,DELETE,OPTIONS"); + + 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(); + server.setHandler(contexts); + + server.setHandler(ch); + server.start(); + try { + String baseUri = "http://localhost:" + port + "/rootctx/rcp2/fhirctx/fcp2"; + + { + HttpOptions httpOpt = new HttpOptions(baseUri + "/Organization/b27ed191-f62d-4128-d99d-40b5e84f2bf2"); + httpOpt.addHeader("Access-Control-Request-Method", "POST"); + httpOpt.addHeader("Origin", "http://www.fhir-starter.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()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + assertEquals("POST", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_METHODS).getValue()); + assertEquals("http://www.fhir-starter.com", status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN).getValue()); + } + { + String uri = baseUri + "/Patient?identifier=urn:hapitest:mrns%7C00001"; + HttpGet httpGet = new HttpGet(uri); + httpGet.addHeader("X-FHIR-Starter", "urn:fhir.starter"); + httpGet.addHeader("Origin", "http://www.fhir-starter.com"); + HttpResponse status = ourClient.execute(httpGet); + + Header origin = status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN); + assertEquals("http://www.fhir-starter.com", origin.getValue()); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent); + + assertEquals(1, bundle.getEntries().size()); + } + } finally { + server.stop(); + } + + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = new FhirContext(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesTest.java index 7fa8fcb8dc6..711228a66e6 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesTest.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.rest.server; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; import java.util.Collections; import java.util.HashMap; @@ -10,11 +9,9 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; -import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpOptions; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -117,36 +114,7 @@ public class ServerFeaturesTest { } - @Test - public void testCors() throws Exception { - servlet.setCorsAllowDomain("http://foo.com"); - - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); - httpGet.addHeader("Accept", Constants.CT_FHIR_XML); - HttpResponse status = ourClient.execute(httpGet); - IOUtils.closeQuietly(status.getEntity().getContent()); - Header origin = status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN); - assertEquals("http://foo.com", origin.getValue()); - } - - - @Test - public void testOptions() throws Exception { - servlet.setCorsAllowDomain("http://foo.com"); - - HttpOptions httpGet = new HttpOptions("http://localhost:" + ourPort + "/"); - httpGet.addHeader("Accept", Constants.CT_FHIR_XML); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - - Header origin = status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN); - assertEquals("http://foo.com", origin.getValue()); - - assertThat(responseContent,StringContains.containsString(" uses - + consumes diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 072c05fba33..21d33b05e43 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -134,12 +134,26 @@ test
- commons-dbcp commons-dbcp 1.4 + + + + org.ebaysf.web + cors-filter + ${ebay_cors_filter_version} + true + + + servlet-api + javax.servlet + + + + diff --git a/pom.xml b/pom.xml index f6609443f9b..60769b184db 100644 --- a/pom.xml +++ b/pom.xml @@ -79,7 +79,7 @@ 4.11 1.1.1 2.9.1 - 3.3 + 3.4 1.1.8 1.9.5 UTF-8 @@ -87,6 +87,7 @@ 4.0.1.RELEASE 3.2.4.RELEASE 2.1.3.RELEASE + 1.0.1 diff --git a/restful-server-example/.settings/org.eclipse.wst.common.component b/restful-server-example/.settings/org.eclipse.wst.common.component index 263e87f0f0d..704fec3041b 100644 --- a/restful-server-example/.settings/org.eclipse.wst.common.component +++ b/restful-server-example/.settings/org.eclipse.wst.common.component @@ -6,7 +6,7 @@ uses - + consumes diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml index 328582cdf4c..dc4d58dfbb7 100644 --- a/restful-server-example/pom.xml +++ b/restful-server-example/pom.xml @@ -62,6 +62,21 @@ true + + + org.ebaysf.web + cors-filter + 1.0.1 + true + + + servlet-api + javax.servlet + + + + + diff --git a/restful-server-example/src/main/webapp/WEB-INF/web.xml b/restful-server-example/src/main/webapp/WEB-INF/web.xml index 6ae4ccafe07..3f0b32336e7 100644 --- a/restful-server-example/src/main/webapp/WEB-INF/web.xml +++ b/restful-server-example/src/main/webapp/WEB-INF/web.xml @@ -1,10 +1,7 @@ - + org.springframework.web.context.ContextLoaderListener @@ -30,11 +27,55 @@ spring /tester/* - + - + + + CORS Filter + org.ebaysf.web.cors.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 + + + + 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 + /* + + + + fhir ca.uhn.example.servlet.ExampleRestfulServlet @@ -42,7 +83,7 @@ fhir /fhir/* - + + - \ No newline at end of file