diff --git a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java index 1e4468c6a27..a9e73a97646 100644 --- a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java +++ b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java @@ -18,11 +18,19 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -30,12 +38,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.webflux.autoconfigure.WebFluxProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver; @@ -280,6 +292,78 @@ public de.codecentric.boot.admin.server.ui.web.servlet.HomepageForwardingFilter homepageForwardingFilterConfig); } + @Bean + public FilterRegistrationBean sbaSettingsAcceptHeaderFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>( + new SbaSettingsAcceptHeaderFilter()); + registration.addUrlPatterns(this.adminServer.path("/sba-settings.js")); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registration; + } + + private static final class SbaSettingsAcceptHeaderFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + filterChain.doFilter( + shouldRewriteAcceptHeader(request) ? new AcceptHeaderRequestWrapper(request) : request, + response); + } + + private static boolean shouldRewriteAcceptHeader(HttpServletRequest request) { + String accept = request.getHeader(HttpHeaders.ACCEPT); + if (accept == null || accept.isBlank()) { + return true; + } + try { + return MediaType.parseMediaTypes(accept) + .stream() + .allMatch((mediaType) -> "*".equals(mediaType.getType()) + && "*".equals(mediaType.getSubtype())); + } + catch (IllegalArgumentException ex) { + return false; + } + } + + } + + private static final class AcceptHeaderRequestWrapper extends HttpServletRequestWrapper { + + private static final String ACCEPT = "application/javascript"; + + private AcceptHeaderRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public String getHeader(String name) { + if (HttpHeaders.ACCEPT.equalsIgnoreCase(name)) { + return ACCEPT; + } + return super.getHeader(name); + } + + @Override + public Enumeration getHeaders(String name) { + if (HttpHeaders.ACCEPT.equalsIgnoreCase(name)) { + return Collections.enumeration(List.of(ACCEPT)); + } + return super.getHeaders(name); + } + + @Override + public Enumeration getHeaderNames() { + List names = Collections.list(super.getHeaderNames()); + if (names.stream().noneMatch(HttpHeaders.ACCEPT::equalsIgnoreCase)) { + names.add(HttpHeaders.ACCEPT); + } + return Collections.enumeration(names); + } + + } + } } diff --git a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AbstractAdminUiApplicationTest.java b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AbstractAdminUiApplicationTest.java index f5c15632e10..ce1648b2651 100644 --- a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AbstractAdminUiApplicationTest.java +++ b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AbstractAdminUiApplicationTest.java @@ -135,6 +135,17 @@ public void should_contain_only_one_language() { //@formatter:on } + @Test + public void should_not_return_sba_settings_for_json_accept() { + //@formatter:off + this.webClient.get() + .uri("/sba-settings.js") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isEqualTo(HttpStatus.NOT_ACCEPTABLE); + //@formatter:on + } + @Test public void should_return_defaults_for_pollTimers() { //@formatter:off diff --git a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AdminUiServletApplicationTest.java b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AdminUiServletApplicationTest.java index 9ebcc2dbfe6..a72290b0749 100644 --- a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AdminUiServletApplicationTest.java +++ b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AdminUiServletApplicationTest.java @@ -29,6 +29,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import de.codecentric.boot.admin.server.config.EnableAdminServer; @@ -73,6 +75,16 @@ protected SecurityFilterChain filterChain(HttpSecurity http) { .build(); } + @Bean + WebMvcConfigurer defaultJsonContentNegotiation() { + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.defaultContentType(MediaType.APPLICATION_JSON); + } + }; + } + } }