diff --git a/.project b/.project index 08328ec..56f8001 100644 --- a/.project +++ b/.project @@ -5,11 +5,6 @@ - - org.eclipse.wst.common.project.facet.core.builder - - - org.eclipse.jdt.core.javabuilder @@ -25,6 +20,5 @@ org.springframework.ide.eclipse.core.springnature org.springsource.ide.eclipse.gradle.core.nature org.eclipse.jdt.core.javanature - org.eclipse.wst.common.project.facet.core.nature diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 38cfd99..e174e59 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,5 +1,5 @@ # -#Wed Sep 16 07:23:26 CEST 2015 +#Wed Oct 28 07:37:57 CET 2015 org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve diff --git a/build.gradle b/build.gradle index bc33bf7..223bf31 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,9 @@ dependencies { testCompile libraries.spring_boot_starter_test testCompile libraries.jsonassert testCompile libraries.jsonpath + + testCompile 'com.jayway.restassured:rest-assured:2.4.1' + testCompile 'com.jayway.restassured:spring-mock-mvc:2.4.1' } uploadArchives { diff --git a/src/main/java/ch/psi/daq/queryrest/config/QueryRestConfig.java b/src/main/java/ch/psi/daq/queryrest/config/QueryRestConfig.java index 523df3c..3dc83c4 100644 --- a/src/main/java/ch/psi/daq/queryrest/config/QueryRestConfig.java +++ b/src/main/java/ch/psi/daq/queryrest/config/QueryRestConfig.java @@ -24,12 +24,6 @@ import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; - import ch.psi.daq.cassandra.util.test.CassandraDataGen; import ch.psi.daq.common.json.deserialize.AttributeBasedDeserializer; import ch.psi.daq.common.statistic.StorelessStatistics; @@ -42,12 +36,18 @@ import ch.psi.daq.query.model.Query; import ch.psi.daq.query.model.QueryField; import ch.psi.daq.query.model.impl.AbstractQuery; import ch.psi.daq.queryrest.controller.validator.QueryValidator; -import ch.psi.daq.queryrest.filter.SimpleCORSFilter; +import ch.psi.daq.queryrest.filter.CorsFilter; import ch.psi.daq.queryrest.model.PropertyFilterMixin; import ch.psi.daq.queryrest.response.JsonByteArraySerializer; import ch.psi.daq.queryrest.response.JsonStreamSerializer; import ch.psi.daq.queryrest.response.ResponseStreamWriter; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + @Configuration @PropertySource(value = {"classpath:queryrest.properties"}) @PropertySource(value = {"file:${user.home}/.config/daq/queryrest.properties"}, ignoreResourceNotFound = true) @@ -180,10 +180,15 @@ public class QueryRestConfig extends WebMvcConfigurerAdapter { public Validator queryValidator() { return new QueryValidator(); } + +// @Bean +// public Filter regexCORSFilter() { +// return new RegexCORSFilter(); +// } @Bean - public Filter simpleCORSFilter() { - return new SimpleCORSFilter(); + public Filter corsFilter() { + return new CorsFilter(); } // ========================================================================================== diff --git a/src/main/java/ch/psi/daq/queryrest/filter/CorsFilter.java b/src/main/java/ch/psi/daq/queryrest/filter/CorsFilter.java new file mode 100644 index 0000000..31c24a5 --- /dev/null +++ b/src/main/java/ch/psi/daq/queryrest/filter/CorsFilter.java @@ -0,0 +1,80 @@ +package ch.psi.daq.queryrest.filter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.filter.OncePerRequestFilter; + +public class CorsFilter extends OncePerRequestFilter { + + private static final String ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin"; + + @Value("${queryrest.cors.allowedorigins}") + private String configuredOrigins; + + @Value("${queryrest.cors.forceallheaders}") + private boolean forceAllHeaders; + + + + /** + * @{inheritDoc + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + Set allowedOrigins = new HashSet(Arrays.asList(configuredOrigins.split(",")) + .stream() + .map(s -> { + return s.trim(); }) + .collect(Collectors.toList())); + + String originHeader = request.getHeader("Origin"); + if (forceAllHeaders) { + // include headers no matter what - good for development + if (allowedOrigins.contains(originHeader)) { + response.addHeader(ALLOW_ORIGIN_HEADER, originHeader); + } else { + response.addHeader(ALLOW_ORIGIN_HEADER, "*"); + } + setDefaultCorsHeaders(response); + + } else if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equals(request.getMethod())) { + // this is for 'real' Cross-site browser requests + if (allowedOrigins.contains(originHeader)) { + response.addHeader(ALLOW_ORIGIN_HEADER, originHeader); + } + setDefaultCorsHeaders(response); + } + + filterChain.doFilter(request, response); + } + + + private void setDefaultCorsHeaders(HttpServletResponse response) { + response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + response.addHeader("Access-Control-Allow-Headers", "Origin, Authorization, Accept, Content-Type"); + response.addHeader("Access-Control-Max-Age", "1800"); + } + + + public void setConfiguredOrigins(String configuredOrigins) { + this.configuredOrigins = configuredOrigins; + } + + + public void setForceAllHeaders(boolean forceAllHeaders) { + this.forceAllHeaders = forceAllHeaders; + } + +} diff --git a/src/main/java/ch/psi/daq/queryrest/filter/SimpleCORSFilter.java b/src/main/java/ch/psi/daq/queryrest/filter/SimpleCORSFilter.java deleted file mode 100644 index fae4adb..0000000 --- a/src/main/java/ch/psi/daq/queryrest/filter/SimpleCORSFilter.java +++ /dev/null @@ -1,41 +0,0 @@ -package ch.psi.daq.queryrest.filter; - -import java.io.IOException; - -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.HttpServletResponse; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -public class SimpleCORSFilter implements Filter { - - @Value("${queryrest.enableCORS}") - private boolean enableCORS; - - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, - ServletException { - - if (enableCORS) { - HttpServletResponse response = (HttpServletResponse) res; - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); - response.setHeader("Access-Control-Max-Age", "3600"); - response.setHeader("Access-Control-Allow-Headers", - "Content-Type, Cache-Control, Accept, Authorization, X-Requested-With"); - } - chain.doFilter(req, res); - } - - public void init(FilterConfig filterConfig) { - } - - public void destroy() { - } -} diff --git a/src/main/resources/queryrest.properties b/src/main/resources/queryrest.properties index 0915ca8..4acde24 100644 --- a/src/main/resources/queryrest.properties +++ b/src/main/resources/queryrest.properties @@ -6,4 +6,12 @@ queryrest.default.response.fields=channel,pulseId,globalMillis,globalNanos,iocMi queryrest.default.response.aggregations=min,max,sum # enables / disables the CORS servlet filter. Adds multiple CORS headers to the response -queryrest.enableCORS=true \ No newline at end of file +queryrest.enableCORS=true + +# includes the CORS headers no matter what request or preflight was sent. If an Origin header is set, this header will be used. +# If no Origin header is set, '*' will be used. +queryrest.cors.forceallheaders=true + +# defines a specific regex to be used in the Access-Control-Allow-Origin. This property needs to be specified +# but might be null or the empty string (""), i.e. don't comment it out. +queryrest.cors.allowedorigins=http://localhost:8080, * \ No newline at end of file diff --git a/src/test/java/ch/psi/daq/test/queryrest/AbstractDaqRestTest.java b/src/test/java/ch/psi/daq/test/queryrest/AbstractDaqRestTest.java index bf3ae66..fd725a4 100644 --- a/src/test/java/ch/psi/daq/test/queryrest/AbstractDaqRestTest.java +++ b/src/test/java/ch/psi/daq/test/queryrest/AbstractDaqRestTest.java @@ -27,9 +27,9 @@ import ch.psi.daq.test.cassandra.CassandraDaqUnitDependencyInjectionTestExecutio CassandraDaqUnitDependencyInjectionTestExecutionListener.class, DependencyInjectionTestExecutionListener.class}) @SpringApplicationConfiguration(classes = { - QueryRestApplication.class - ,QueryRestConfig.class - ,DaqWebMvcConfig.class + QueryRestApplication.class, + QueryRestConfig.class, + DaqWebMvcConfig.class }) @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) @WebAppConfiguration diff --git a/src/test/java/ch/psi/daq/test/queryrest/controller/DaqRestControllerTest.java b/src/test/java/ch/psi/daq/test/queryrest/controller/QueryRestControllerTest.java similarity index 66% rename from src/test/java/ch/psi/daq/test/queryrest/controller/DaqRestControllerTest.java rename to src/test/java/ch/psi/daq/test/queryrest/controller/QueryRestControllerTest.java index 6745580..03c91d5 100644 --- a/src/test/java/ch/psi/daq/test/queryrest/controller/DaqRestControllerTest.java +++ b/src/test/java/ch/psi/daq/test/queryrest/controller/QueryRestControllerTest.java @@ -8,6 +8,7 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; import ch.psi.daq.cassandra.util.test.CassandraDataGen; import ch.psi.daq.common.ordering.Ordering; @@ -16,30 +17,34 @@ import ch.psi.daq.query.model.impl.PulseRangeQuery; import ch.psi.daq.query.model.impl.TimeRangeQuery; import ch.psi.daq.query.model.impl.TimeRangeQueryDate; import ch.psi.daq.queryrest.controller.QueryRestController; +import ch.psi.daq.queryrest.filter.CorsFilter; import ch.psi.daq.test.cassandra.admin.CassandraTestAdmin; import ch.psi.daq.test.queryrest.AbstractDaqRestTest; /** * Tests the {@link DaqController} implementation. */ -public class DaqRestControllerTest extends AbstractDaqRestTest { - +public class QueryRestControllerTest extends AbstractDaqRestTest { + @Resource private CassandraTestAdmin cassandraTestAdmin; @Resource private CassandraDataGen dataGen; - - public static final String[] TEST_CHANNEL_NAMES = new String[]{"testChannel1", "testChannel2"}; - + + @Resource + private CorsFilter corsFilter; + + public static final String[] TEST_CHANNEL_NAMES = new String[] {"testChannel1", "testChannel2"}; + @After public void tearDown() throws Exception {} @Test public void testChannelNameQuery() throws Exception { - + this.mockMvc.perform( - MockMvcRequestBuilders + MockMvcRequestBuilders .get(QueryRestController.CHANNELS) .contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultHandlers.print()) @@ -48,15 +53,15 @@ public class DaqRestControllerTest extends AbstractDaqRestTest { .andExpect(MockMvcResultMatchers.jsonPath("$[0]").exists()) .andExpect(MockMvcResultMatchers.jsonPath("$[0]").value("BooleanScalar")) .andExpect(MockMvcResultMatchers.jsonPath("$[1]").value("BooleanWaveform")); - + } - + @Test public void testSpecificChannelSearch() throws Exception { this.mockMvc.perform( MockMvcRequestBuilders - .get(QueryRestController.CHANNELS + "/integer") - .contentType(MediaType.APPLICATION_JSON)) + .get(QueryRestController.CHANNELS + "/integer") + .contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$").isArray()) @@ -67,7 +72,93 @@ public class DaqRestControllerTest extends AbstractDaqRestTest { .andExpect(MockMvcResultMatchers.jsonPath("$[3]").value("UIntegerWaveform")) .andExpect(MockMvcResultMatchers.jsonPath("$[4]").doesNotExist()); } - + + @Test + public void testCorsFilterNoHeaders() throws Exception { + corsFilter.setForceAllHeaders(false); + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters(corsFilter).build(); + + this.mockMvc.perform( + MockMvcRequestBuilders + .options(QueryRestController.CHANNELS) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + // we didn't set the 'Origin' header so no access-control + .andExpect(MockMvcResultMatchers.header().doesNotExist("Access-Control-Allow-Origin")); + } + + @Test + public void testCorsFilterIncludesHeaders() throws Exception { + // all headers are set + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters(corsFilter).build(); + + this.mockMvc.perform( + MockMvcRequestBuilders + .options(QueryRestController.CHANNELS) + .header("Origin", "*") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + // we didn't set the 'Origin' header so no access-control + .andExpect(MockMvcResultMatchers.header().string("Access-Control-Allow-Origin", "*")); + + this.mockMvc.perform( + MockMvcRequestBuilders + .options(QueryRestController.CHANNELS) + .header("Origin", "http://localhost:8080") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + // we didn't set the 'Origin' header so no access-control + .andExpect(MockMvcResultMatchers.header().string("Access-Control-Allow-Origin", "http://localhost:8080")); + + this.mockMvc.perform( + MockMvcRequestBuilders + .options(QueryRestController.CHANNELS) + .header("Origin", "someBogusDomain.com") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.header().string("Access-Control-Allow-Origin", "*")); + + } + + @Test + public void testCorsFilterMismatchSpecificOrigin() throws Exception { + corsFilter.setForceAllHeaders(false); + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters(corsFilter).build(); + + this.mockMvc + .perform( + MockMvcRequestBuilders + .options(QueryRestController.CHANNELS) + .header("Origin", "*") + .header("Access-Control-Request-Method", "GET") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.header().string("Access-Control-Allow-Origin", "*")) + .andExpect( + MockMvcResultMatchers.header().string("Access-Control-Allow-Headers", + "Origin, Authorization, Accept, Content-Type")); + + this.mockMvc + .perform( + MockMvcRequestBuilders + .options(QueryRestController.CHANNELS) + .header("Origin", "someBogusDomain.com") + .header("Access-Control-Request-Method", "GET") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.header().doesNotExist("Access-Control-Allow-Origin")) + .andExpect( + MockMvcResultMatchers.header().string("Access-Control-Allow-Headers", + "Origin, Authorization, Accept, Content-Type")); + } + + @Test public void testPulseRangeQuery() throws Exception { PulseRangeQuery request = new PulseRangeQuery( @@ -77,9 +168,9 @@ public class DaqRestControllerTest extends AbstractDaqRestTest { String content = mapper.writeValueAsString(request); System.out.println(content); - + content = "{\"channels\":[\"testChannel1\",\"testChannel2\"],\"startPulseId\":100,\"endPulseId\":101}"; - + this.mockMvc .perform(MockMvcRequestBuilders .post(QueryRestController.QUERY) @@ -111,9 +202,9 @@ public class DaqRestControllerTest extends AbstractDaqRestTest { String content = mapper.writeValueAsString(request); this.mockMvc.perform(MockMvcRequestBuilders - .post(QueryRestController.QUERY) - .contentType(MediaType.APPLICATION_JSON) - .content(content)) + .post(QueryRestController.QUERY) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -145,9 +236,9 @@ public class DaqRestControllerTest extends AbstractDaqRestTest { this.mockMvc .perform( MockMvcRequestBuilders - .post(QueryRestController.QUERY) - .contentType(MediaType.APPLICATION_JSON) - .content(content) + .post(QueryRestController.QUERY) + .contentType(MediaType.APPLICATION_JSON) + .content(content) ) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -163,7 +254,7 @@ public class DaqRestControllerTest extends AbstractDaqRestTest { .andExpect(MockMvcResultMatchers.jsonPath("$[1].data[0].pulseId").value(100)) .andExpect(MockMvcResultMatchers.jsonPath("$[1].data[1].pulseId").value(101)); } - + @Test public void testExtremaAggregation() throws Exception { PulseRangeQuery request = new PulseRangeQuery( @@ -183,7 +274,7 @@ public class DaqRestControllerTest extends AbstractDaqRestTest { .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) - + .andExpect(MockMvcResultMatchers.jsonPath("$").exists()) .andExpect(MockMvcResultMatchers.jsonPath("$[0].channel").exists()) .andExpect(MockMvcResultMatchers.jsonPath("$[0].channel").value(TEST_CHANNEL_NAMES[0])) @@ -203,5 +294,6 @@ public class DaqRestControllerTest extends AbstractDaqRestTest { .andExpect(MockMvcResultMatchers.jsonPath("$[0].data.maxima.max.event.pulseId").exists()) .andExpect(MockMvcResultMatchers.jsonPath("$[0].data.maxima.max.event.pulseId").value(101)); } - + + }