Fix CORS support

This commit is contained in:
jamesagnew 2014-08-08 07:33:12 -04:00
parent 95f5031251
commit ba3bc70eae
13 changed files with 338 additions and 71 deletions

View File

@ -60,7 +60,6 @@
<optional>true</optional>
</dependency>
<!-- Only required for narrative generator support -->
<dependency>
<groupId>org.thymeleaf</groupId>
@ -69,6 +68,22 @@
<optional>true</optional>
</dependency>
<!-- Only required for CORS support -->
<dependency>
<groupId>org.ebaysf.web</groupId>
<artifactId>cors-filter</artifactId>
<version>${ebay_cors_filter_version}</version>
<optional>true</optional>
<!--
<exclusions>
<exclusion>
<artifactId>servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
-->
</dependency>
<!-- General -->
<dependency>
<groupId>org.apache.commons</groupId>

View File

@ -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.
</action>
<action type="add">
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).
</action>
</release>
<release version="0.5" date="2014-Jul-30">
<action type="add">

View File

@ -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 <code>null</code> (which is the default), the server will return CORS (Cross Origin Resource Sharing) headers with the given domain string.
* <p>
* 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 {

View File

@ -67,6 +67,7 @@
<item name="RESTful Operations" href="./doc_rest_operations.html" />
<item name="FHIR Tester App" href="./doc_server_tester.html" />
<item name="Narrative Generator" href="./doc_narrative.html" />
<item name="CORS Support" href="./doc_cors.html" />
</item>
<item name="Logging" href="./doc_logging.html" />
<item name="Tinder Plugin" href="./doc_tinder.html" />

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties>
<title>CORS - HAPI FHIR</title>
<author email="jamesagnew@users.sourceforge.net">James Agnew</author>
</properties>
<body>
<section name="CORS">
<p>
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.
</p>
<p>
The recommended filter for this purpose is the
eBay Open Sourced
<a href="https://github.com/ebay/cors-filter">CORS Filter</a> (Licensed under
the Apache Software License 2.0).
</p>
<p>
To add CORS support using this library, there are two simple steps:
</p>
<subsection name="Include cors-filter JAR">
<p>
In your server WAR file, you must include the <b>cors-filter-X.X.X.JAR</b>
dependency. This dependency is included in the HAPI distribution.
</p>
<p>
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:
</p>
<source><![CDATA[<dependency>
<groupId>org.ebaysf.web</groupId>
<artifactId>cors-filter</artifactId>
<version>1.0.1</version>
<optional>true</optional>
</dependency>]]></source>
</subsection>
<subsection name="Add the filter to your web.xml">
<p>
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.
</p>
<source><![CDATA[<filter>
<filter-name>CORS Filter</filter-name>
<filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
<init-param>
<description>A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials.</description>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<description>A comma separated list of HTTP verbs, using which a CORS request can be made.</description>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<description>A comma separated list of allowed headers when making a non simple CORS request.</description>
<param-name>cors.allowed.headers</param-name>
<param-value>X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value></param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>
<param-name>cors.support.credentials</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>A flag to control logging</description>
<param-name>cors.logging.enabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache.</description>
<param-name>cors.preflight.maxage</param-name>
<param-value>300</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>]]></source>
</subsection>
</section>
</body>
</document>

View File

@ -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();
}
}

View File

@ -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("<Conformance"));
}
@Test
@ -278,7 +246,6 @@ public class ServerFeaturesTest {
@Before
public void before() {
servlet.setServerAddressStrategy(new IncomingRequestAddressStrategy());
servlet.setCorsAllowDomain(null);
}

View File

@ -12,7 +12,7 @@
<dependent-module archiveName="hapi-fhir-base-0.6-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-testpage-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-tester-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependency-type>consumes</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">

View File

@ -134,12 +134,26 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<!-- Only required for CORS support -->
<dependency>
<groupId>org.ebaysf.web</groupId>
<artifactId>cors-filter</artifactId>
<version>${ebay_cors_filter_version}</version>
<optional>true</optional>
<exclusions>
<exclusion>
<artifactId>servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>

View File

@ -79,7 +79,7 @@
<junit_version>4.11</junit_version>
<logback_version>1.1.1</logback_version>
<maven_javadoc_plugin_version>2.9.1</maven_javadoc_plugin_version>
<maven_site_plugin_version>3.3</maven_site_plugin_version>
<maven_site_plugin_version>3.4</maven_site_plugin_version>
<mitreid-connect-version>1.1.8</mitreid-connect-version>
<mockito_version>1.9.5</mockito_version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -87,6 +87,7 @@
<spring_version>4.0.1.RELEASE</spring_version>
<spring_security_version>3.2.4.RELEASE</spring_security_version>
<thymeleaf-version>2.1.3.RELEASE</thymeleaf-version>
<ebay_cors_filter_version>1.0.1</ebay_cors_filter_version>
</properties>
<build>

View File

@ -6,7 +6,7 @@
<dependent-module archiveName="hapi-fhir-base-0.6-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-testpage-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-tester-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependency-type>consumes</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">

View File

@ -62,6 +62,21 @@
<optional>true</optional>
</dependency>
<!-- Used for CORS support -->
<dependency>
<groupId>org.ebaysf.web</groupId>
<artifactId>cors-filter</artifactId>
<version>1.0.1</version>
<optional>true</optional>
<exclusions>
<exclusion>
<artifactId>servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>

View File

@ -1,10 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5">
<!--
The following entries are all here to enable the tester web application, and
are not actually a part of the underlying FHIR server.
-->
<!-- The following entries are all here to enable the tester web application, and are not actually a part of the underlying FHIR server. -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
@ -30,11 +27,55 @@
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/tester/*</url-pattern>
</servlet-mapping>
</servlet-mapping>
<!--
The following entries are for the actual underlying FHIR server.
-->
<!-- This filters provide support for Cross Origin Resource Sharing (CORS) -->
<filter>
<filter-name>CORS Filter</filter-name>
<filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
<init-param>
<description>A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials.</description>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<description>A comma separated list of HTTP verbs, using which a CORS request can be made.</description>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<description>A comma separated list of allowed headers when making a non simple CORS request.</description>
<param-name>cors.allowed.headers</param-name>
<param-value>X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>
<param-name>cors.exposed.headers</param-name>
<param-value></param-value>
</init-param>
<init-param>
<description>A flag that suggests if CORS is supported with cookies</description>
<param-name>cors.support.credentials</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>A flag to control logging</description>
<param-name>cors.logging.enabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<description>Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache.</description>
<param-name>cors.preflight.maxage</param-name>
<param-value>300</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- The following entries are for the actual underlying FHIR server. -->
<servlet>
<servlet-name>fhir</servlet-name>
<servlet-class>ca.uhn.example.servlet.ExampleRestfulServlet</servlet-class>
@ -42,7 +83,7 @@
<servlet-mapping>
<servlet-name>fhir</servlet-name>
<url-pattern>/fhir/*</url-pattern>
</servlet-mapping>
</servlet-mapping>
</web-app>