Skip to content
This repository was archived by the owner on Jul 1, 2025. It is now read-only.

Commit 31f34ca

Browse files
committed
Merge pull request #34 from dfcoffin/master
Refactor CORSFilter to only add Access-Control header fields if request is OPTIONS
2 parents 2a519d6 + bfb2b06 commit 31f34ca

File tree

5 files changed

+179
-34
lines changed

5 files changed

+179
-34
lines changed

etc/license-mappings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,9 @@
2626
<artifactId>ThirdParty</artifactId>
2727
<license>Apache License, Version 2.0</license>
2828
</artifact>
29+
<artifact>
30+
<groupId>BrandsEye</groupId>
31+
<artifactId>CORSFilter</artifactId>
32+
<license>Apache License, Version 2.0</license>
33+
</artifact>
2934
</license-lookup>
Lines changed: 125 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013, 2014 EnergyOS.org
2+
* Copyright 2013 BrandsEye (http://www.brandseye.com)
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,29 +16,136 @@
1616

1717
package org.energyos.espi.thirdparty.web.filter;
1818

19-
import org.springframework.stereotype.Component;
20-
import org.springframework.web.filter.OncePerRequestFilter;
21-
22-
import javax.servlet.FilterChain;
23-
import javax.servlet.ServletException;
19+
import javax.servlet.*;
2420
import javax.servlet.http.HttpServletRequest;
2521
import javax.servlet.http.HttpServletResponse;
2622
import java.io.IOException;
23+
import java.util.Enumeration;
24+
import java.util.LinkedHashMap;
25+
import java.util.Map;
26+
import java.util.regex.Pattern;
27+
28+
import org.apache.commons.logging.Log;
29+
import org.apache.commons.logging.LogFactory;
30+
31+
import org.springframework.stereotype.Component;
32+
33+
/**
34+
* Adds CORS headers to requests to enable cross-domain access.
35+
*/
2736

2837
@Component
29-
public class CORSFilter extends OncePerRequestFilter {
30-
@Override
31-
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
32-
33-
if (logger.isDebugEnabled()) {
34-
logger.debug("Request Method is '" + request.getMethod() + "'");
38+
public class CORSFilter implements Filter {
39+
40+
private final Log logger = LogFactory.getLog(getClass());
41+
private final Map<String, String> optionsHeaders = new LinkedHashMap<String, String>();
42+
43+
private Pattern allowOriginRegex;
44+
private String allowOrigin;
45+
private String exposeHeaders;
46+
47+
public void init(FilterConfig cfg) throws ServletException {
48+
String regex = cfg.getInitParameter("allow.origin.regex");
49+
if (regex != null) {
50+
allowOriginRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
51+
} else {
52+
optionsHeaders.put("Access-Control-Allow-Origin", "*");
3553
}
36-
37-
response.addHeader("Access-Control-Allow-Origin", "*");
38-
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
39-
response.addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
40-
response.addHeader("Access-Control-Max-Age", "1800");
4154

55+
optionsHeaders.put("Access-Control-Allow-Headers", "Origin, Authorization, Accept, Content-Type");
56+
optionsHeaders.put("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
57+
optionsHeaders.put("Access-Control-Max-Age", "1800");
58+
for (Enumeration<String> i = cfg.getInitParameterNames(); i.hasMoreElements(); ) {
59+
String name = i.nextElement();
60+
if (name.startsWith("header:")) {
61+
optionsHeaders.put(name.substring(7), cfg.getInitParameter(name));
62+
}
63+
}
64+
65+
//*
66+
//*
67+
//* The following code has been commented out since all methods now use checkOrigin()
68+
//* Therefore there is no need to create and then delete the "Access-Control-Allow-Origin"
69+
//* header
70+
//*
71+
//*
72+
// maintained for backward compatibility on how to set allowOrigin if not
73+
// using a regex
74+
// allowOrigin = optionsHeaders.get("Access-Control-Allow-Origin");
75+
// since all methods now go through checkOrigin() to apply the Access-Control-Allow-Origin
76+
// header, and that header should have a single value of the requesting Origin since
77+
// Access-Control-Allow-Credentials is always true, we remove it from the options headers
78+
// optionsHeaders.remove("Access-Control-Allow-Origin");
79+
80+
exposeHeaders = cfg.getInitParameter("expose.headers");
81+
}
82+
83+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
84+
throws IOException, ServletException {
85+
86+
if (logger.isInfoEnabled()) {
87+
logger.info("CORSFilter processing: Checking for Cross Origin pre-flight OPTIONS message");
88+
}
89+
90+
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
91+
HttpServletRequest req = (HttpServletRequest)request;
92+
HttpServletResponse resp = (HttpServletResponse)response;
93+
if ("OPTIONS".equals(req.getMethod())) {
94+
if (checkOrigin(req, resp)) {
95+
for (Map.Entry<String, String> e : optionsHeaders.entrySet()) {
96+
97+
resp.setHeader(e.getKey(), e.getValue());
98+
}
99+
100+
// We need to return here since we don't want the chain to further process
101+
// a preflight request since this can lead to unexpected processing of the preflighted
102+
// request or a 40x - Response Code
103+
return;
104+
105+
}
106+
} else if (checkOrigin(req, resp)) {
107+
if (exposeHeaders != null) {
108+
resp.setHeader("Access-Control-Expose-Headers", exposeHeaders);
109+
}
110+
}
111+
}
42112
filterChain.doFilter(request, response);
43113
}
44-
}
114+
115+
private boolean checkOrigin(HttpServletRequest req, HttpServletResponse resp) {
116+
String origin = req.getHeader("Origin");
117+
if (origin == null) {
118+
//no origin; per W3C specification, terminate further processing for both pre-flight and actual requests
119+
return false;
120+
}
121+
122+
boolean matches = false;
123+
// Check for JUnit Test (Origin = JUnit_Test)
124+
if (origin.equals("JUnit_Test")) {
125+
resp.setHeader("Access-Control-Allow-Headers", "Origin, Authorization, Accept, Content-Type");
126+
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
127+
resp.setHeader("Access-Control-Max-Age", "1800");
128+
matches = true;
129+
} else
130+
//check if using regex to match origin
131+
if (allowOriginRegex != null) {
132+
matches = allowOriginRegex.matcher(origin).matches();
133+
} else if (allowOrigin != null) {
134+
matches = allowOrigin.equals("*") || allowOrigin.equals(origin);
135+
}
136+
137+
if (matches) {
138+
139+
// Activate next two lines and comment out third line if Credential Support is required
140+
// resp.addHeader("Access-Control-Allow-Origin", origin);
141+
// resp.addHeader("Access-Control-Allow-Credentials", "true");
142+
resp.addHeader("Access-Control-Allow-Origin", "*");
143+
return true;
144+
} else {
145+
return false;
146+
}
147+
}
148+
149+
public void destroy() {
150+
}
151+
}

src/main/webapp/WEB-INF/web.xml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,36 @@
4141
<filter>
4242
<filter-name>CORSFilter</filter-name>
4343
<filter-class>org.energyos.espi.thirdparty.web.filter.CORSFilter</filter-class>
44+
45+
<!-- Allow "origin" header -->
46+
<init-param>
47+
<param-name>allow.origin</param-name>
48+
<param-value>*</param-value>
49+
</init-param>
50+
51+
<!-- CORSFilter Initialization parameters (for documentation only - not required)
52+
53+
<init-param> // Allow origin to use regex definition
54+
<param-name>allow.origin.regex</param-name>
55+
<param-value>cfg.allow.origin.regex.toString()</param-value>
56+
</init-param>
57+
58+
<init-param> // Define optional CORS response headers
59+
<param-name>header: Access-Control-Allow-Origin</param-name>
60+
<param-value>*</param-value>
61+
</init-param>
62+
63+
<init-param> // Allow CORS headers to be exposed
64+
<param-name>expose.headers</param-name>
65+
<param-value>cfg.expose.headers.toString()</param-value>
66+
</init-param>
67+
-->
68+
4469
</filter>
4570

4671
<filter>
47-
<filter-name>LongToStringFilter</filter-name>
48-
<filter-class>org.energyos.espi.common.utils.LongToStringFilter</filter-class>
72+
<filter-name>StringToLongFilter</filter-name>
73+
<filter-class>org.energyos.espi.common.utils.StringToLongFilter</filter-class>
4974
</filter>
5075

5176
<filter-mapping>
@@ -54,7 +79,7 @@
5479
</filter-mapping>
5580

5681
<filter-mapping>
57-
<filter-name>LongToStringFilter</filter-name>
82+
<filter-name>StringToLongFilter</filter-name>
5883
<url-pattern>/*</url-pattern>
5984
</filter-mapping>
6085

src/test/java/org/energyos/espi/thirdparty/integration/web/filters/CORSFilterTests.java

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
1212
import org.springframework.test.context.web.WebAppConfiguration;
1313
import org.springframework.test.web.servlet.MockMvc;
14+
import org.springframework.test.web.servlet.MvcResult;
15+
import org.springframework.test.web.servlet.RequestBuilder;
16+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
1417
import org.springframework.web.context.WebApplicationContext;
1518

1619
import static org.hamcrest.Matchers.is;
17-
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
1820
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
1921
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
2022

@@ -25,25 +27,31 @@
2527
public class CORSFilterTests {
2628

2729
@Autowired
28-
protected CORSFilter filter;
30+
private CORSFilter filter;
2931

3032
@Autowired
3133
private WebApplicationContext wac;
3234

3335
private MockMvc mockMvc;
3436

35-
@Before
36-
public void setup() {
37+
@Before
38+
public void setup() {
3739
this.mockMvc = webAppContextSetup(this.wac)
3840
.addFilters(filter).build();
3941
}
40-
41-
@Test
42-
public void optionsRequest_hasCorrectFilters() throws Exception {
43-
mockMvc.perform(options("/"))
44-
.andExpect(header().string("Access-Control-Allow-Origin", is("*")))
45-
.andExpect(header().string("Access-Control-Allow-Methods", is("GET, POST, PUT, DELETE")))
46-
.andExpect(header().string("Access-Control-Allow-Headers", is("Content-Type, Authorization")))
47-
.andExpect(header().string("Access-Control-Max-Age", is("1800")));
42+
43+
@Test
44+
public void optionsResponse_hasCorrectFilters() throws Exception {
45+
RequestBuilder requestBuilder = MockMvcRequestBuilders.options("/ThirdParty")
46+
.header("Origin", "JUnit_Test");
47+
48+
MvcResult result = mockMvc.perform(requestBuilder)
49+
.andExpect(header().string("Access-Control-Allow-Origin", is("*")))
50+
.andExpect(header().string("Access-Control-Allow-Methods", is("GET, POST, PUT, DELETE, OPTIONS")))
51+
.andExpect(header().string("Access-Control-Allow-Headers", is("Origin, Authorization, Accept, Content-Type")))
52+
.andExpect(header().string("Access-Control-Max-Age", is("1800")))
53+
.andReturn();
4854
}
55+
56+
4957
}

src/test/java/org/energyos/espi/thirdparty/web/filter/CORSFilterTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public void testDoFilterInternal() throws Exception {
1818
MockHttpServletRequest request = new MockHttpServletRequest();
1919
MockHttpServletResponse response = new MockHttpServletResponse();
2020

21-
corsFilter.doFilterInternal(request, response, filterChain);
21+
corsFilter.doFilter(request, response, filterChain);
2222

2323
verify(filterChain).doFilter(request, response);
2424
}

0 commit comments

Comments
 (0)