diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 789672194..f00a67b91 100755 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -753,6 +753,10 @@ Then visit `/hystrix` and point the dashboard to an individual instance's `/hyst NOTE: When connecting to a `/hystrix.stream` endpoint that uses HTTPS, the certificate used by the server must be trusted by the JVM. If the certificate is not trusted, you must import the certificate into the JVM in order for the Hystrix Dashboard to make a successful connection to the stream endpoint. +NOTE: In order to use the `/proxy.stream` endpoint you must configure a list of hosts to allow connections to. To set the +list of allowed hosts use `hystrix.dashboard.proxyStreamAllowList`. You can use an Ant-style pattern in the host name to +match against a wider range of host names. + === Turbine Looking at an individual instance's Hystrix data is not very useful in terms of the overall health of the system. https://github.com/Netflix/Turbine[Turbine] is an application that aggregates all of the relevant `/hystrix.stream` endpoints into a combined `/turbine.stream` for use in the Hystrix Dashboard. diff --git a/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java b/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java index 9e0fa7431..4775881d9 100644 --- a/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java +++ b/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java @@ -19,7 +19,10 @@ package org.springframework.cloud.netflix.hystrix.dashboard; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; import java.util.Map; +import java.util.Optional; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -47,6 +50,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.ui.freemarker.SpringTemplateLoader; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; /** @@ -88,8 +93,9 @@ public class HystrixDashboardConfiguration { } @Bean - public ServletRegistrationBean proxyStreamServlet() { - final ProxyStreamServlet proxyStreamServlet = new ProxyStreamServlet(); + public ServletRegistrationBean proxyStreamServlet( + HystrixDashboardProperties properties) { + final ProxyStreamServlet proxyStreamServlet = new ProxyStreamServlet(properties); proxyStreamServlet.setEnableIgnoreConnectionCloseHeader( this.dashboardProperties.isEnableIgnoreConnectionCloseHeader()); final ServletRegistrationBean registration = new ServletRegistrationBean( @@ -118,6 +124,8 @@ public class HystrixDashboardConfiguration { private boolean enableIgnoreConnectionCloseHeader = false; + private HystrixDashboardProperties properties; + public void setEnableIgnoreConnectionCloseHeader( boolean enableIgnoreConnectionCloseHeader) { this.enableIgnoreConnectionCloseHeader = enableIgnoreConnectionCloseHeader; @@ -125,6 +133,11 @@ public class HystrixDashboardConfiguration { public ProxyStreamServlet() { super(); + this.properties = new HystrixDashboardProperties(); + } + + public ProxyStreamServlet(HystrixDashboardProperties properties) { + this.properties = properties; } /** @@ -169,10 +182,18 @@ public class HystrixDashboardConfiguration { url.append(key).append("=").append(value); } } - String proxyUrl = url.toString(); - log.info("\n\nProxy opening connection to: " + proxyUrl + "\n\n"); + String proxyUrlString = url.toString(); + + if (!isAllowedToProxy(proxyUrlString)) { + log.warn("Origin parameter: " + origin + + " is not in the allowed list of proxy host names. If it " + + "should be allowed add it to hystrix.dashboard.proxyStreamAllowList."); + return; + } + + log.info("\n\nProxy opening connection to: " + proxyUrlString + "\n\n"); try { - httpget = new HttpGet(proxyUrl); + httpget = new HttpGet(proxyUrlString); HttpClient client = ProxyConnectionManager.httpClient; HttpResponse httpResponse = client.execute(httpget); int statusCode = httpResponse.getStatusLine().getStatusCode(); @@ -218,7 +239,7 @@ public class HystrixDashboardConfiguration { } } else { - log.warn("Failed opening connection to " + proxyUrl + " : " + log.warn("Failed opening connection to " + proxyUrlString + " : " + statusCode + " : " + httpResponse.getStatusLine()); } } @@ -250,6 +271,22 @@ public class HystrixDashboardConfiguration { } + private boolean isAllowedToProxy(String proxyUrlString) + throws MalformedURLException { + + URL proxyUrl = new URL(proxyUrlString); + String host = proxyUrl.getHost(); + PathMatcher pathMatcher = new AntPathMatcher("."); + Optional optionalPattern = properties.getProxyStreamAllowList() + .stream().filter(pattern -> pathMatcher.match(pattern, host)) + .findFirst(); + + if (optionalPattern.isPresent()) { + return true; + } + return false; + } + private void copyHeadersToServletResponse(Header[] headers, HttpServletResponse response) { for (Header header : headers) { diff --git a/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardProperties.java b/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardProperties.java index 932f7aa8c..9145135f0 100644 --- a/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardProperties.java +++ b/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardProperties.java @@ -16,7 +16,10 @@ package org.springframework.cloud.netflix.hystrix.dashboard; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -41,6 +44,20 @@ public class HystrixDashboardProperties { */ private Map initParameters = new HashMap<>(); + private List proxyStreamAllowList = new ArrayList<>(); + + public List getProxyStreamAllowList() { + return proxyStreamAllowList; + } + + public void setProxyStreamAllowList(List proxyStreamAllowList) { + this.proxyStreamAllowList = proxyStreamAllowList; + } + + public void setProxyStreamAllowList(String... regex) { + this.proxyStreamAllowList = Arrays.asList(regex); + } + public boolean isEnableIgnoreConnectionCloseHeader() { return enableIgnoreConnectionCloseHeader; } diff --git a/spring-cloud-netflix-hystrix-dashboard/src/test/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfigurationTests.java b/spring-cloud-netflix-hystrix-dashboard/src/test/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfigurationTests.java index 8245745ef..928ae2b7b 100644 --- a/spring-cloud-netflix-hystrix-dashboard/src/test/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfigurationTests.java +++ b/spring-cloud-netflix-hystrix-dashboard/src/test/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfigurationTests.java @@ -41,7 +41,8 @@ public class HystrixDashboardConfigurationTests { MockHttpServletResponse response = new MockHttpServletResponse(); Header[] headers = new Header[1]; headers[0] = new BasicHeader("Content-Type", "text/proxy.stream"); - HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet(); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet( + new HystrixDashboardProperties()); ReflectionTestUtils.invokeMethod(proxyStreamServlet, "copyHeadersToServletResponse", headers, response); assertThat(response.getHeaderNames().size()).isEqualTo(1); @@ -54,7 +55,8 @@ public class HystrixDashboardConfigurationTests { Header[] headers = new Header[2]; headers[0] = new BasicHeader("Content-Type", "text/proxy.stream"); headers[1] = new BasicHeader("Connection", "close"); - HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet(); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet( + new HystrixDashboardProperties()); ReflectionTestUtils.invokeMethod(proxyStreamServlet, "copyHeadersToServletResponse", headers, response); assertThat(response.getHeaderNames().size()).isEqualTo(2); @@ -68,7 +70,8 @@ public class HystrixDashboardConfigurationTests { Header[] headers = new Header[2]; headers[0] = new BasicHeader("Content-Type", "text/proxy.stream"); headers[1] = new BasicHeader("Connection", "close"); - HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet(); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet( + new HystrixDashboardProperties()); proxyStreamServlet.setEnableIgnoreConnectionCloseHeader(true); ReflectionTestUtils.invokeMethod(proxyStreamServlet, "copyHeadersToServletResponse", headers, response); @@ -83,7 +86,8 @@ public class HystrixDashboardConfigurationTests { Header[] headers = new Header[2]; headers[0] = new BasicHeader("Content-Type", "text/proxy.stream"); headers[1] = new BasicHeader("Connection", "close"); - HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet(); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet( + new HystrixDashboardProperties()); proxyStreamServlet.setEnableIgnoreConnectionCloseHeader(false); ReflectionTestUtils.invokeMethod(proxyStreamServlet, "copyHeadersToServletResponse", headers, response); @@ -111,4 +115,39 @@ public class HystrixDashboardConfigurationTests { }); } + @Test + public void allowedHostsTest() { + HystrixDashboardProperties properties = new HystrixDashboardProperties(); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet( + properties); + boolean allowed = ReflectionTestUtils.invokeMethod(proxyStreamServlet, + "isAllowedToProxy", "http://foo.com"); + assertThat(allowed).isFalse(); + + properties.setProxyStreamAllowList("foo.com", "bar.*", "*world.com"); + allowed = ReflectionTestUtils.invokeMethod(proxyStreamServlet, "isAllowedToProxy", + "http://user:password@foo.com"); + assertThat(allowed).isTrue(); + + allowed = ReflectionTestUtils.invokeMethod(proxyStreamServlet, "isAllowedToProxy", + "http://bar.com"); + assertThat(allowed).isTrue(); + + allowed = ReflectionTestUtils.invokeMethod(proxyStreamServlet, "isAllowedToProxy", + "http://bar.org"); + assertThat(allowed).isTrue(); + + allowed = ReflectionTestUtils.invokeMethod(proxyStreamServlet, "isAllowedToProxy", + "http://helloworld.com"); + assertThat(allowed).isTrue(); + + allowed = ReflectionTestUtils.invokeMethod(proxyStreamServlet, "isAllowedToProxy", + "http://world.com"); + assertThat(allowed).isTrue(); + + allowed = ReflectionTestUtils.invokeMethod(proxyStreamServlet, "isAllowedToProxy", + "http://world.org"); + assertThat(allowed).isFalse(); + } + }