Commit a114f8b9 by Johannes Stelzer

Fixed 405-Response on application-endpoints for preflight OPTIONS-Request.

parent c17a5d53
......@@ -19,6 +19,7 @@ import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.mvc.JolokiaMvcEndpoint;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
......@@ -26,12 +27,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerInterceptor;
......@@ -41,7 +44,8 @@ import org.springframework.web.servlet.mvc.ServletWrappingController;
import de.codecentric.boot.admin.actuate.LogfileMvcEndpoint;
import de.codecentric.boot.admin.services.SpringBootAdminRegistrator;
import de.codecentric.boot.admin.web.BasicAuthHttpRequestInterceptor;
import de.codecentric.boot.admin.web.SimpleCORSHandlerInterceptor;
import de.codecentric.boot.admin.web.EndpointCorsFilter;
import de.codecentric.boot.admin.web.EndpointCorsInterceptor;
/**
* This configuration adds a registrator bean to the spring context. This bean checks periodicaly, if the using
......@@ -91,80 +95,88 @@ public class SpringBootAdminClientAutoConfiguration {
return registrar;
}
@Configuration
@ConditionalOnExpression("${endpoints.logfile.enabled:true}")
@ConditionalOnProperty("logging.file")
public static class LogfileEndpointAutoConfiguration {
/**
* Exposes the logfile as acutator endpoint
*/
@Bean
public LogfileMvcEndpoint logfileEndpoint() {
return new LogfileMvcEndpoint();
}
}
/**
* HTTP filter to enable Cross-Origin Resource Sharing.
*/
@Bean
@ConditionalOnMissingBean
public SimpleCORSHandlerInterceptor endpointCorsFilter() {
SimpleCORSHandlerInterceptor endpointCorsFilter = new SimpleCORSHandlerInterceptor();
return endpointCorsFilter;
public EndpointCorsFilter endpointCorsFilter(EndpointHandlerMapping endpointHandlerMapping) {
return new EndpointCorsFilter(endpointHandlerMapping);
}
/**
* This method applies several workarounds to apply CORS-Headers corretcly to all Endpoints
*/
@Autowired
private ApplicationContext applicationContext;
@Bean
public ApplicationListener<EmbeddedServletContainerInitializedEvent> listener() {
public ApplicationListener<EmbeddedServletContainerInitializedEvent> appListener() {
/*
* In case a second servletContainer is fired up (because server.port !=
* managament port), there is no viable way to register the endpointCorsFilter.
*
* Instead we register an HandlerInterceptor for the Endpoint handler mapping and
* Set jolokias AgentServlet to support Options request and the Dispatcher servlet
* to forward such.
* Also @see https://github.com/spring-projects/spring-boot/issues/1987
*/
return new ApplicationListener<EmbeddedServletContainerInitializedEvent>() {
@Override
public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) {
// We have to register the CORSHandler in this nasty way.
// With Spring Boot < 1.2.0.RC2 there is no elegant way to register
// HandlerInterceptors for the EndpointMapping
for (EndpointHandlerMapping endpointMappingHandler : event.getApplicationContext()
.getBeansOfType(EndpointHandlerMapping.class).values()) {
try {
Field interceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
interceptorsField.setAccessible(true);
@SuppressWarnings("unchecked")
List<HandlerInterceptor> adaptedInterceptors = (List<HandlerInterceptor>) interceptorsField
.get(endpointMappingHandler);
adaptedInterceptors.add(endpointCorsFilter());
if ("management".equals(event.getApplicationContext().getNamespace())) {
// register HandlerIntercepor
for (EndpointHandlerMapping handlerMapping : event.getApplicationContext()
.getBeansOfType(EndpointHandlerMapping.class).values()) {
try {
Field interceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
interceptorsField.setAccessible(true);
@SuppressWarnings("unchecked")
List<HandlerInterceptor> adaptedInterceptors = (List<HandlerInterceptor>) interceptorsField
.get(handlerMapping);
EndpointCorsInterceptor interceptor = new EndpointCorsInterceptor();
event.getApplicationContext().getBeanFactory().autowireBean(interceptor);
adaptedInterceptors.add(interceptor);
}
catch (Exception ex) {
throw new RuntimeException("Couldn't add handlerInterceptor for cors", ex);
}
}
catch (Exception ex) {
throw new RuntimeException("Couldn't register CORS-Filter for endpoints", ex);
}
}
// Options request must be forwarded to jolokias servlet
// @see https://github.com/spring-projects/spring-boot/issues/1987
for (DispatcherServlet dispatcher : event.getApplicationContext()
.getBeansOfType(DispatcherServlet.class).values()) {
dispatcher.setDispatchOptionsRequest(true);
}
// set DispatcherServlet to forward OptionsRequest
for (DispatcherServlet servlet : event.getApplicationContext()
.getBeansOfType(DispatcherServlet.class).values()) {
servlet.setDispatchOptionsRequest(true);
}
// Options request must be forwarded to jolokias servlet
// @see https://github.com/spring-projects/spring-boot/issues/1987
// Since JolokiasMvcEndpoint can't be substituted easily this workaround is needed
for (JolokiaMvcEndpoint jolokiaMvcEndpoint : event.getApplicationContext()
.getBeansOfType(JolokiaMvcEndpoint.class).values()) {
try {
Field controllerField = JolokiaMvcEndpoint.class.getDeclaredField("controller");
controllerField.setAccessible(true);
ServletWrappingController controller = (ServletWrappingController) controllerField
.get(jolokiaMvcEndpoint);
controller.setSupportedMethods("GET", "HEAD", "POST", "OPTIONS");
} catch (Exception ex) {
throw new RuntimeException("Couldn't reconfigure servletWrappingController for Jolokia", ex);
// set Jolokias ServletWrappingController to support OPTIONS
for (JolokiaMvcEndpoint jolokiaMvcEndpoint : SpringBootAdminClientAutoConfiguration.this.applicationContext
.getBeansOfType(JolokiaMvcEndpoint.class).values()) {
try {
Field controllerField = JolokiaMvcEndpoint.class.getDeclaredField("controller");
ReflectionUtils.makeAccessible(controllerField);
ServletWrappingController controller = (ServletWrappingController) controllerField
.get(jolokiaMvcEndpoint);
controller.setSupportedMethods("GET", "HEAD", "POST", "OPTIONS");
}
catch (Exception ex) {
throw new RuntimeException("Couldn't reconfigure servletWrappingController for Jolokia", ex);
}
}
}
}
};
}
@Configuration
@ConditionalOnExpression("${endpoints.logfile.enabled:true}")
@ConditionalOnProperty("logging.file")
public static class LogfileEndpointAutoConfiguration {
/**
* Exposes the logfile as acutator endpoint
*/
@Bean
public LogfileMvcEndpoint logfileEndpoint() {
return new LogfileMvcEndpoint();
}
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.web;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Request filter to allow Cross-Origin Resource Sharing.
*/
public class EndpointCorsFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(EndpointCorsFilter.class);
// Configurable origin for CORS - default: * (all)
@Value("${http.filter.cors.origin:*}")
private String origin;
@Value("${http.filter.cors.headers:Origin, X-Requested-With, Content-Type, Accept}")
private String headers;
private final EndpointHandlerMapping endpointHandlerMapping;
public EndpointCorsFilter(EndpointHandlerMapping endpointHandlerMapping) {
this.endpointHandlerMapping = endpointHandlerMapping;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
if (!endpointHandlerMapping.isDisabled() && endpointHandlerMapping.getHandler(request) != null) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Headers", headers);
}
}
catch (Exception ex) {
LOGGER.warn("Error occured while adding CORS-Headers", ex);
}
filterChain.doFilter(request, response);
}
public void setOrigin(String origin) {
this.origin = origin;
}
public String getOrigin() {
return origin;
}
public String getHeaders() {
return headers;
}
public void setHeaders(String headers) {
this.headers = headers;
}
}
......@@ -21,10 +21,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* Request filter to allow Cross-Origin Resource Sharing.
*/
public class SimpleCORSHandlerInterceptor extends HandlerInterceptorAdapter {
public class EndpointCorsInterceptor extends HandlerInterceptorAdapter {
// Configurable origin for CORS - default: * (all)
@Value("${http.filter.cors.origin:*}")
......@@ -33,12 +30,27 @@ public class SimpleCORSHandlerInterceptor extends HandlerInterceptorAdapter {
@Value("${http.filter.cors.headers:Origin, X-Requested-With, Content-Type, Accept}")
private String headers;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Headers", headers);
return true;
return super.preHandle(request, response, handler);
}
public void setOrigin(String origin) {
this.origin = origin;
}
public String getOrigin() {
return origin;
}
public String getHeaders() {
return headers;
}
public void setHeaders(String headers) {
this.headers = headers;
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.web;
import static org.junit.Assert.assertEquals;
......@@ -13,6 +28,9 @@ import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
......@@ -20,13 +38,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.web.CorsFilterTest.TestAdminApplication;
import de.codecentric.boot.admin.web.CorsFilterOnDifferentPortsTest.TestAdminApplication;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestAdminApplication.class)
@WebAppConfiguration
@IntegrationTest({ "server.port=0", "management.port=0", "spring.boot.admin.url=http://localhost:65000" })
public class CorsFilterTest {
public class CorsFilterOnDifferentPortsTest {
RestTemplate restTemplate = new TestRestTemplate();
......@@ -38,29 +56,53 @@ public class CorsFilterTest {
@Test
@SuppressWarnings("rawtypes")
public void testCORS_endpoint() {
public void testCORS_GET_info_endpoint() {
// DO serve CORS-Headers on management-endpoints
ResponseEntity<Map> info = new TestRestTemplate().getForEntity("http://localhost:" + managementPort + "/info",
Map.class);
assertEquals(HttpStatus.OK, info.getStatusCode());
assertEquals(Arrays.asList("*"), info.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(Arrays.asList("Origin, X-Requested-With, Content-Type, Accept"),
info.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_application() {
public void testCORS_OPTIONS_jolokia_endpoint() {
// DO serve CORS-Headers on management-endpoints
ResponseEntity<Void> options = new TestRestTemplate().exchange("http://localhost:" + managementPort
+ "/jolokia",
HttpMethod.OPTIONS, HttpEntity.EMPTY, Void.class);
assertEquals(HttpStatus.OK, options.getStatusCode());
assertEquals(Arrays.asList("*"), options.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(Arrays.asList("Origin, X-Requested-With, Content-Type, Accept"),
options.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_GET_application() {
// DO NOT serve CORS-Headers on application-endpoints
ResponseEntity<String> hello = new TestRestTemplate().getForEntity("http://localhost:" + serverPort + "/hello",
String.class);
assertEquals(HttpStatus.OK, hello.getStatusCode());
assertEquals(null, hello.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(null, hello.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_OPTIONS_application() {
// DO NOT serve CORS-Headers on application-endpoints
ResponseEntity<Void> options = new TestRestTemplate().exchange("http://localhost:" + serverPort + "/hello",
HttpMethod.OPTIONS, HttpEntity.EMPTY, Void.class);
assertEquals(HttpStatus.OK, options.getStatusCode());
assertEquals(null, options.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(null, options.getHeaders().get("Access-Control-Allow-Headers"));
}
@Configuration
@EnableAutoConfiguration
@RestController
public static class TestAdminApplication {
@RequestMapping("/hello")
public String hello() {
return "hello world!";
......
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.web;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.web.CorsFilterOnSamePortsTest.TestAdminApplication;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestAdminApplication.class)
@WebAppConfiguration
@IntegrationTest({ "server.port=0", "spring.boot.admin.url=http://localhost:65000" })
public class CorsFilterOnSamePortsTest {
RestTemplate restTemplate = new TestRestTemplate();
@Value("${local.server.port}")
private int serverPort = 0;
@Test
@SuppressWarnings("rawtypes")
public void testCORS_GET_info_endpoint() {
// DO serve CORS-Headers on management-endpoints
ResponseEntity<Map> info = new TestRestTemplate().getForEntity("http://localhost:" + serverPort + "/info",
Map.class);
assertEquals(HttpStatus.OK, info.getStatusCode());
assertEquals(Arrays.asList("*"), info.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(Arrays.asList("Origin, X-Requested-With, Content-Type, Accept"),
info.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_OPTIONS_jolokia_endpoint() {
// DO serve CORS-Headers on management-endpoints
ResponseEntity<Void> options = new TestRestTemplate().exchange("http://localhost:" + serverPort
+ "/jolokia",
HttpMethod.OPTIONS, HttpEntity.EMPTY, Void.class);
assertEquals(HttpStatus.OK, options.getStatusCode());
assertEquals(Arrays.asList("*"), options.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(Arrays.asList("Origin, X-Requested-With, Content-Type, Accept"),
options.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_GET_application() {
// DO NOT serve CORS-Headers on application-endpoints
ResponseEntity<String> hello = new TestRestTemplate().getForEntity("http://localhost:" + serverPort + "/hello",
String.class);
assertEquals(HttpStatus.OK, hello.getStatusCode());
assertEquals(null, hello.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(null, hello.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_OPTIONS_application() {
// DO NOT serve CORS-Headers on application-endpoints
ResponseEntity<Void> options = new TestRestTemplate().exchange("http://localhost:" + serverPort + "/hello",
HttpMethod.OPTIONS, HttpEntity.EMPTY, Void.class);
assertEquals(HttpStatus.OK, options.getStatusCode());
assertEquals(null, options.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(null, options.getHeaders().get("Access-Control-Allow-Headers"));
}
@Configuration
@EnableAutoConfiguration
@RestController
public static class TestAdminApplication {
@RequestMapping("/hello")
public String hello() {
return "hello world!";
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment