Skip to content

Commit b9be0e3

Browse files
wilkinsonaphilwebb
authored andcommitted
Skip actuator path extension content negotiation
Allow `PathExtensionContentNegotiationStrategy` to be bypassed by actuator endpoints. Prior to this commit calling `/loggers/com.aaa.cab` would return a HTTP 406 response due to `.cab` being a known extension. Fixes gh-8765
1 parent f3c4507 commit b9be0e3

File tree

6 files changed

+116
-13
lines changed

6 files changed

+116
-13
lines changed

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2016 the original author or authors.
2+
* Copyright 2012-2017 the original author or authors.
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.
@@ -26,17 +26,21 @@
2626
import java.util.Set;
2727

2828
import javax.servlet.http.HttpServletRequest;
29+
import javax.servlet.http.HttpServletResponse;
2930

3031
import org.springframework.boot.actuate.endpoint.Endpoint;
32+
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
3133
import org.springframework.context.ApplicationContext;
3234
import org.springframework.util.Assert;
3335
import org.springframework.util.ObjectUtils;
3436
import org.springframework.util.StringUtils;
37+
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
3538
import org.springframework.web.cors.CorsConfiguration;
3639
import org.springframework.web.cors.CorsUtils;
3740
import org.springframework.web.servlet.HandlerExecutionChain;
3841
import org.springframework.web.servlet.HandlerInterceptor;
3942
import org.springframework.web.servlet.HandlerMapping;
43+
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
4044
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
4145
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
4246
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@@ -204,6 +208,11 @@ protected HandlerExecutionChain getCorsHandlerExecutionChain(
204208
return addSecurityInterceptor(chain);
205209
}
206210

211+
@Override
212+
protected void extendInterceptors(List<Object> interceptors) {
213+
interceptors.add(new SkipPathExtensionContentNegotiation());
214+
}
215+
207216
private HandlerExecutionChain addSecurityInterceptor(HandlerExecutionChain chain) {
208217
List<HandlerInterceptor> interceptors = new ArrayList<HandlerInterceptor>();
209218
if (chain.getInterceptors() != null) {
@@ -279,4 +288,22 @@ protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
279288
return this.corsConfiguration;
280289
}
281290

291+
/**
292+
* {@link HandlerInterceptorAdapter} to ensure that
293+
* {@link PathExtensionContentNegotiationStrategy} is skipped for actuator endpoints.
294+
*/
295+
private static final class SkipPathExtensionContentNegotiation
296+
extends HandlerInterceptorAdapter {
297+
298+
@Override
299+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
300+
Object handler) throws Exception {
301+
request.setAttribute(
302+
WebMvcAutoConfiguration.SKIP_PATH_EXTENSION_CONTENT_NEGOTIATION_ATTRIBUTE,
303+
Boolean.TRUE);
304+
return true;
305+
}
306+
307+
}
308+
282309
}

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public void onDifferentPort() throws Exception {
193193
.getBean(ManagementContextResolver.class).getApplicationContext();
194194
List<?> interceptors = (List<?>) ReflectionTestUtils.getField(
195195
managementContext.getBean(EndpointHandlerMapping.class), "interceptors");
196-
assertThat(interceptors).hasSize(1);
196+
assertThat(interceptors).hasSize(2);
197197
}
198198

199199
@Test
@@ -211,7 +211,7 @@ public void onDifferentPortWithSpecificContainer() throws Exception {
211211
.getBean(ManagementContextResolver.class).getApplicationContext();
212212
List<?> interceptors = (List<?>) ReflectionTestUtils.getField(
213213
managementContext.getBean(EndpointHandlerMapping.class), "interceptors");
214-
assertThat(interceptors).hasSize(1);
214+
assertThat(interceptors).hasSize(2);
215215
EmbeddedServletContainerFactory parentContainerFactory = this.applicationContext
216216
.getBean(EmbeddedServletContainerFactory.class);
217217
EmbeddedServletContainerFactory managementContainerFactory = managementContext
@@ -536,7 +536,7 @@ public void managementSpecificSslUsingDifferentPort() throws Exception {
536536
.getBean(ManagementContextResolver.class).getApplicationContext();
537537
List<?> interceptors = (List<?>) ReflectionTestUtils.getField(
538538
managementContext.getBean(EndpointHandlerMapping.class), "interceptors");
539-
assertThat(interceptors).hasSize(1);
539+
assertThat(interceptors).hasSize(2);
540540
ManagementServerProperties managementServerProperties = this.applicationContext
541541
.getBean(ManagementServerProperties.class);
542542
assertThat(managementServerProperties.getSsl()).isNotNull();
@@ -577,7 +577,7 @@ public void managementServerCanDisableSslWhenUsingADifferentPort() throws Except
577577
.getBean(ManagementContextResolver.class).getApplicationContext();
578578
List<?> interceptors = (List<?>) ReflectionTestUtils.getField(
579579
managementContext.getBean(EndpointHandlerMapping.class), "interceptors");
580-
assertThat(interceptors).hasSize(1);
580+
assertThat(interceptors).hasSize(2);
581581
ManagementServerProperties managementServerProperties = this.applicationContext
582582
.getBean(ManagementServerProperties.class);
583583
assertThat(managementServerProperties.getSsl()).isNotNull();

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public void securityInterceptorIfNullShouldNotBeAdded() throws Exception {
6060
Collections.singletonList(endpoint));
6161
mapping.setApplicationContext(this.context);
6262
mapping.afterPropertiesSet();
63-
assertThat(mapping.getHandler(request("POST", "/a")).getInterceptors()).isNull();
63+
assertThat(mapping.getHandler(request("POST", "/a")).getInterceptors())
64+
.hasSize(1);
6465
}
6566

6667
@Test
@@ -75,8 +76,8 @@ public void securityInterceptorShouldBePresentAfterCorsInterceptorForCorsRequest
7576
mapping.afterPropertiesSet();
7677
MockHttpServletRequest request = request("POST", "/a");
7778
request.addHeader("Origin", "http://example.com");
78-
assertThat(mapping.getHandler(request).getInterceptors().length).isEqualTo(2);
79-
assertThat(mapping.getHandler(request).getInterceptors()[1])
79+
assertThat(mapping.getHandler(request).getInterceptors().length).isEqualTo(3);
80+
assertThat(mapping.getHandler(request).getInterceptors()[2])
8081
.isEqualTo(securityInterceptor);
8182
}
8283

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,16 @@ public void setLoggerWithWrongLogLevel() throws Exception {
178178
verifyZeroInteractions(this.loggingSystem);
179179
}
180180

181+
@Test
182+
public void logLevelForLoggerWithNameThatCouldBeMistakenForAPathExtension()
183+
throws Exception {
184+
given(this.loggingSystem.getLoggerConfiguration("com.png"))
185+
.willReturn(new LoggerConfiguration("com.png", null, LogLevel.DEBUG));
186+
this.mvc.perform(get("/loggers/com.png")).andExpect(status().isOk())
187+
.andExpect(content().string(equalTo(
188+
"{\"configuredLevel\":null," + "\"effectiveLevel\":\"DEBUG\"}")));
189+
}
190+
181191
@Configuration
182192
@Import({ JacksonAutoConfiguration.class,
183193
HttpMessageConvertersAutoConfiguration.class,

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ public void specificMetric() throws Exception {
125125
.andExpect(content().string(equalTo("{\"foo\":1}")));
126126
}
127127

128+
@Test
129+
public void specificMetricWithNameThatCouldBeMistakenForAPathExtension()
130+
throws Exception {
131+
this.mvc.perform(get("/metrics/bar.png")).andExpect(status().isOk())
132+
.andExpect(content().string(equalTo("{\"bar.png\":1}")));
133+
}
134+
128135
@Test
129136
public void specificMetricWhenDisabled() throws Exception {
130137
this.context.getBean(MetricsEndpoint.class).setEnabled(false);
@@ -138,7 +145,8 @@ public void specificMetricThatDoesNotExist() throws Exception {
138145

139146
@Test
140147
public void regexAll() throws Exception {
141-
String expected = "\"foo\":1,\"group1.a\":1,\"group1.b\":1,\"group2.a\":1,\"group2_a\":1";
148+
String expected = "\"foo\":1,\"bar.png\":1,\"group1.a\":1,\"group1.b\":1"
149+
+ ",\"group2.a\":1,\"group2_a\":1";
142150
this.mvc.perform(get("/metrics/.*")).andExpect(status().isOk())
143151
.andExpect(content().string(containsString(expected)));
144152
}
@@ -178,6 +186,7 @@ public MetricsEndpoint endpoint() {
178186
public Collection<Metric<?>> metrics() {
179187
ArrayList<Metric<?>> metrics = new ArrayList<Metric<?>>();
180188
metrics.add(new Metric<Integer>("foo", 1));
189+
metrics.add(new Metric<Integer>("bar.png", 1));
181190
metrics.add(new Metric<Integer>("group1.a", 1));
182191
metrics.add(new Metric<Integer>("group1.b", 1));
183192
metrics.add(new Metric<Integer>("group2.a", 1));

spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Collections;
2121
import java.util.Date;
2222
import java.util.List;
23+
import java.util.ListIterator;
2324
import java.util.Map;
2425
import java.util.Map.Entry;
2526

@@ -67,8 +68,13 @@
6768
import org.springframework.validation.DefaultMessageCodesResolver;
6869
import org.springframework.validation.MessageCodesResolver;
6970
import org.springframework.validation.Validator;
71+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
7072
import org.springframework.web.accept.ContentNegotiationManager;
73+
import org.springframework.web.accept.ContentNegotiationStrategy;
74+
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
7175
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
76+
import org.springframework.web.context.request.NativeWebRequest;
77+
import org.springframework.web.context.request.RequestAttributes;
7278
import org.springframework.web.context.request.RequestContextListener;
7379
import org.springframework.web.filter.HiddenHttpMethodFilter;
7480
import org.springframework.web.filter.HttpPutFormContentFilter;
@@ -125,9 +131,16 @@
125131
ValidationAutoConfiguration.class })
126132
public class WebMvcAutoConfiguration {
127133

128-
public static String DEFAULT_PREFIX = "";
134+
public static final String DEFAULT_PREFIX = "";
129135

130-
public static String DEFAULT_SUFFIX = "";
136+
public static final String DEFAULT_SUFFIX = "";
137+
138+
/**
139+
* Attribute that can be added to the web request when the
140+
* {@link PathExtensionContentNegotiationStrategy} should be be skipped.
141+
*/
142+
public static final String SKIP_PATH_EXTENSION_CONTENT_NEGOTIATION_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
143+
.getName() + ".SKIP";
131144

132145
@Bean
133146
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@@ -404,8 +417,7 @@ public Validator mvcValidator() {
404417
getClass().getClassLoader())) {
405418
return super.mvcValidator();
406419
}
407-
return WebMvcValidator.get(getApplicationContext(),
408-
getValidator());
420+
return WebMvcValidator.get(getApplicationContext(), getValidator());
409421
}
410422

411423
@Override
@@ -453,6 +465,22 @@ protected void configureHandlerExceptionResolvers(
453465
}
454466
}
455467

468+
@Bean
469+
@Override
470+
public ContentNegotiationManager mvcContentNegotiationManager() {
471+
ContentNegotiationManager manager = super.mvcContentNegotiationManager();
472+
List<ContentNegotiationStrategy> strategies = manager.getStrategies();
473+
ListIterator<ContentNegotiationStrategy> iterator = strategies.listIterator();
474+
while (iterator.hasNext()) {
475+
ContentNegotiationStrategy strategy = iterator.next();
476+
if (strategy instanceof PathExtensionContentNegotiationStrategy) {
477+
iterator.set(new OptionalPathExtensionContentNegotiationStrategy(
478+
strategy));
479+
}
480+
}
481+
return manager;
482+
}
483+
456484
}
457485

458486
@Configuration
@@ -550,4 +578,32 @@ private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) {
550578

551579
}
552580

581+
/**
582+
* Decorator to make {@link PathExtensionContentNegotiationStrategy} optional
583+
* depending on a request attribute.
584+
*/
585+
static class OptionalPathExtensionContentNegotiationStrategy
586+
implements ContentNegotiationStrategy {
587+
588+
private final ContentNegotiationStrategy delegate;
589+
590+
OptionalPathExtensionContentNegotiationStrategy(
591+
ContentNegotiationStrategy delegate) {
592+
this.delegate = delegate;
593+
}
594+
595+
@Override
596+
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
597+
throws HttpMediaTypeNotAcceptableException {
598+
Object skip = webRequest.getAttribute(
599+
SKIP_PATH_EXTENSION_CONTENT_NEGOTIATION_ATTRIBUTE,
600+
RequestAttributes.SCOPE_REQUEST);
601+
if (skip != null && Boolean.parseBoolean(skip.toString())) {
602+
return Collections.emptyList();
603+
}
604+
return this.delegate.resolveMediaTypes(webRequest);
605+
}
606+
607+
}
608+
553609
}

0 commit comments

Comments
 (0)