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));
}
-
+
+
}