Fix HAL browser endpoint redirect and entry point with custom servlet path

Previously, the HAL browser endpoint did not consider the dispatcher
servlet’s path (server.servlet-path) when redirecting to browser.html
or when updating the API entry point in the served HTML.

This commit moves to using ServletUriComponentsBuilder to build the URI
for the redirect and the path for the entry point. In the interests of
simplicity the logic that sometimes redirected and sometimes forwarded
the request has been changed so that it will always perform a redirect.

Closes gh-6586
pull/6759/merge
Andy Wilkinson 8 years ago
parent ff48a88b91
commit 9874c22e23

@ -18,6 +18,8 @@ package org.springframework.boot.actuate.endpoint.mvc;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -26,11 +28,13 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.resource.ResourceTransformer; import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.ResourceTransformerChain; import org.springframework.web.servlet.resource.ResourceTransformerChain;
import org.springframework.web.servlet.resource.TransformedResource; import org.springframework.web.servlet.resource.TransformedResource;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
/** /**
* {@link MvcEndpoint} to expose a HAL browser. * {@link MvcEndpoint} to expose a HAL browser.
@ -61,12 +65,12 @@ public class HalBrowserMvcEndpoint extends HalJsonMvcEndpoint
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public String browse(HttpServletRequest request) { public String browse(HttpServletRequest request) {
String contextPath = getManagementServletContext().getContextPath() ServletUriComponentsBuilder builder = ServletUriComponentsBuilder
+ (getPath().endsWith("/") ? getPath() : getPath() + "/"); .fromRequest(request);
if (request.getRequestURI().endsWith("/")) { String uriString = builder.build().toUriString();
return "forward:" + contextPath + this.location.getHtmlFile();
} return "redirect:" + uriString + (uriString.endsWith("/") ? "" : "/")
return "redirect:" + contextPath; + this.location.getHtmlFile();
} }
@Override @Override
@ -143,17 +147,20 @@ public class HalBrowserMvcEndpoint extends HalJsonMvcEndpoint
resource = transformerChain.transform(request, resource); resource = transformerChain.transform(request, resource);
if (resource.getFilename().equalsIgnoreCase( if (resource.getFilename().equalsIgnoreCase(
HalBrowserMvcEndpoint.this.location.getHtmlFile())) { HalBrowserMvcEndpoint.this.location.getHtmlFile())) {
return replaceInitialLink(request.getContextPath(), resource); return replaceInitialLink(request, resource);
} }
return resource; return resource;
} }
private Resource replaceInitialLink(String contextPath, Resource resource) private Resource replaceInitialLink(HttpServletRequest request, Resource resource)
throws IOException { throws IOException {
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream()); byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
String content = new String(bytes, DEFAULT_CHARSET); String content = new String(bytes, DEFAULT_CHARSET);
String initial = contextPath + getManagementServletContext().getContextPath() List<String> pathSegments = new ArrayList<String>(ServletUriComponentsBuilder
+ getPath(); .fromRequest(request).build().getPathSegments());
pathSegments.remove(pathSegments.size() - 1);
String initial = "/"
+ StringUtils.collectionToDelimitedString(pathSegments, "/");
content = content.replace("entryPoint: '/'", "entryPoint: '" + initial + "'"); content = content.replace("entryPoint: '/'", "entryPoint: '" + initial + "'");
return new TransformedResource(resource, content.getBytes(DEFAULT_CHARSET)); return new TransformedResource(resource, content.getBytes(DEFAULT_CHARSET));
} }

@ -24,16 +24,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.MinimalActuatorHypermediaApplication; import org.springframework.boot.actuate.autoconfigure.MinimalActuatorHypermediaApplication;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -61,19 +60,17 @@ public class HalBrowserMvcEndpointBrowserPathIntegrationTests {
} }
@Test @Test
public void browser() throws Exception { public void requestWithTrailingSlashIsRedirectedToBrowserHtml() throws Exception {
MvcResult response = this.mockMvc this.mockMvc.perform(get("/actuator/").accept(MediaType.TEXT_HTML))
.perform(get("/actuator/").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()).andExpect(header().string(
.andExpect(status().isOk()).andReturn(); HttpHeaders.LOCATION, "http://localhost/actuator/browser.html"));
assertThat(response.getResponse().getForwardedUrl())
.isEqualTo("/actuator/browser.html");
} }
@Test @Test
public void redirect() throws Exception { public void requestWithoutTrailingSlashIsRedirectedToBrowserHtml() throws Exception {
this.mockMvc.perform(get("/actuator").accept(MediaType.TEXT_HTML)) this.mockMvc.perform(get("/actuator").accept(MediaType.TEXT_HTML))
.andExpect(status().isFound()) .andExpect(status().isFound()).andExpect(header().string("location",
.andExpect(header().string("location", "/actuator/")); "http://localhost/actuator/browser.html"));
} }
@MinimalActuatorHypermediaApplication @MinimalActuatorHypermediaApplication

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint.mvc; package org.springframework.boot.actuate.endpoint.mvc;
import com.google.common.net.HttpHeaders;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -29,11 +30,9 @@ import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@ -71,12 +70,10 @@ public class HalBrowserMvcEndpointDisabledIntegrationTests {
} }
@Test @Test
public void browser() throws Exception { public void browserRedirect() throws Exception {
MvcResult response = this.mockMvc this.mockMvc.perform(get("/actuator/").accept(MediaType.TEXT_HTML))
.perform(get("/actuator/").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()).andExpect(header().string(
.andExpect(status().isOk()).andReturn(); HttpHeaders.LOCATION, "http://localhost/actuator/browser.html"));
assertThat(response.getResponse().getForwardedUrl())
.isEqualTo("/actuator/browser.html");
} }
@Test @Test

@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.MinimalActuatorHypermediaApplication; import org.springframework.boot.actuate.autoconfigure.MinimalActuatorHypermediaApplication;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.ResourceSupport;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
@ -38,7 +39,7 @@ import static org.hamcrest.CoreMatchers.containsString;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -83,8 +84,8 @@ public class HalBrowserMvcEndpointManagementContextPathIntegrationTests {
@Test @Test
public void actuatorHomeHtml() throws Exception { public void actuatorHomeHtml() throws Exception {
this.mockMvc.perform(get("/admin/").accept(MediaType.TEXT_HTML)) this.mockMvc.perform(get("/admin/").accept(MediaType.TEXT_HTML))
.andExpect(status().isOk()) .andExpect(status().isFound()).andExpect(header().string(
.andExpect(forwardedUrl("/admin/browser.html")); HttpHeaders.LOCATION, "http://localhost/admin/browser.html"));
} }
@Test @Test

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint.mvc; package org.springframework.boot.actuate.endpoint.mvc;
import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import org.junit.Test; import org.junit.Test;
@ -69,14 +70,15 @@ public class HalBrowserMvcEndpointServerContextPathIntegrationTests {
} }
@Test @Test
public void actuatorBrowser() throws Exception { public void actuatorBrowserRedirect() throws Exception {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
ResponseEntity<String> entity = new TestRestTemplate().exchange( ResponseEntity<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/spring/actuator/", HttpMethod.GET, "http://localhost:" + this.port + "/spring/actuator/", HttpMethod.GET,
new HttpEntity<Void>(null, headers), String.class); new HttpEntity<Void>(null, headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND);
assertThat(entity.getBody()).contains("<title"); assertThat(entity.getHeaders().getLocation()).isEqualTo(URI.create(
"http://localhost:" + this.port + "/spring/actuator/browser.html"));
} }
@Test @Test

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint.mvc; package org.springframework.boot.actuate.endpoint.mvc;
import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import org.junit.Test; import org.junit.Test;
@ -82,14 +83,15 @@ public class HalBrowserMvcEndpointServerPortIntegrationTests {
} }
@Test @Test
public void browser() throws Exception { public void browserRedirect() throws Exception {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
ResponseEntity<String> entity = new TestRestTemplate().exchange( ResponseEntity<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/actuator/", HttpMethod.GET, "http://localhost:" + this.port + "/actuator/", HttpMethod.GET,
new HttpEntity<Void>(null, headers), String.class); new HttpEntity<Void>(null, headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND);
assertThat(entity.getBody()).contains("<title"); assertThat(entity.getHeaders().getLocation()).isEqualTo(
URI.create("http://localhost:" + this.port + "/actuator/browser.html"));
} }
@MinimalActuatorHypermediaApplication @MinimalActuatorHypermediaApplication

@ -0,0 +1,131 @@
/*
* Copyright 2012-2016 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 org.springframework.boot.actuate.endpoint.mvc;
import java.net.URI;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.actuate.autoconfigure.MinimalActuatorHypermediaApplication;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
/**
* Integration tests for {@link HalBrowserMvcEndpoint} when a custom server context path
* has been configured.
*
* @author Dave Syer
* @author Andy Wilkinson
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
"server.servlet-path=/spring" })
@DirtiesContext
public class HalBrowserMvcEndpointServerServletPathIntegrationTests {
@LocalServerPort
private int port;
@Test
public void linksAddedToHomePage() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
ResponseEntity<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/spring/", HttpMethod.GET,
new HttpEntity<Void>(null, headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("\"_links\":");
}
@Test
public void actuatorBrowserRedirect() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
ResponseEntity<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/spring/actuator/", HttpMethod.GET,
new HttpEntity<Void>(null, headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND);
assertThat(entity.getHeaders().getLocation()).isEqualTo(URI.create(
"http://localhost:" + this.port + "/spring/actuator/browser.html"));
}
@Test
public void actuatorBrowserEntryPoint() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
ResponseEntity<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/spring/actuator/browser.html",
HttpMethod.GET, new HttpEntity<Void>(null, headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("entryPoint: '/spring/actuator'");
}
@Test
public void actuatorLinks() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
ResponseEntity<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/spring/actuator", HttpMethod.GET,
new HttpEntity<Void>(null, headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("\"_links\":");
}
@Test
public void actuatorLinksWithTrailingSlash() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
ResponseEntity<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/spring/actuator/", HttpMethod.GET,
new HttpEntity<Void>(null, headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("\"_links\":");
}
@MinimalActuatorHypermediaApplication
@RestController
public static class SpringBootHypermediaApplication {
@RequestMapping("")
public ResourceSupport home() {
ResourceSupport resource = new ResourceSupport();
resource.add(linkTo(SpringBootHypermediaApplication.class).slash("/")
.withSelfRel());
return resource;
}
}
}

@ -28,16 +28,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.MinimalActuatorHypermediaApplication; import org.springframework.boot.actuate.autoconfigure.MinimalActuatorHypermediaApplication;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@ -84,11 +83,9 @@ public class HalBrowserMvcEndpointVanillaIntegrationTests {
@Test @Test
public void browser() throws Exception { public void browser() throws Exception {
MvcResult response = this.mockMvc this.mockMvc.perform(get("/actuator/").accept(MediaType.TEXT_HTML))
.perform(get("/actuator/").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()).andExpect(header().string(
.andExpect(status().isOk()).andReturn(); HttpHeaders.LOCATION, "http://localhost/actuator/browser.html"));
assertThat(response.getResponse().getForwardedUrl())
.isEqualTo("/actuator/browser.html");
} }
@Test @Test

Loading…
Cancel
Save