From bba9109d05415d5ba7d1b351509310a8441ceaa5 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 9 Jul 2015 21:18:23 +0200 Subject: [PATCH 1/2] getCompleteUrl of HttpUtil should now respect forwarding headers, see issue #748 --- .../main/java/sonia/scm/util/HttpUtil.java | 141 +++++++++++++++++- .../java/sonia/scm/util/HttpUtilTest.java | 130 ++++++++++++++++ 2 files changed, 267 insertions(+), 4 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java index 68d669d914..aca27eb917 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -35,7 +35,9 @@ package sonia.scm.util; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; +import com.google.common.base.Objects; import com.google.common.base.Strings; import org.slf4j.Logger; @@ -97,6 +99,24 @@ public final class HttpUtil /** authentication header */ public static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + /** + * The original host requested by the client in the Host HTTP request header. + * @since 1.47 + */ + public static final String HEADER_X_FORWARDED_HOST = "X-Forwarded-Host"; + + /** + * The original port requested by the client. + * @since 1.47 + */ + public static final String HEADER_X_FORWARDED_PORT = "X-Forwarded-Port"; + + /** + * The original protocol (http or https) requested by the client. + * @since 1.47 + */ + public static final String HEADER_X_FORWARDED_PROTO = "X-Forwarded-Proto"; + /** * Default http port * @since 1.5 @@ -579,21 +599,31 @@ public final class HttpUtil //~--- get methods ---------------------------------------------------------- /** - * Returns an absolute url with context path. + * Returns an absolute url with context path. The method creates the url from + * forwarding request headers, if they are available. * * * @param request http client request * @param pathSegments * * @return absolute url with context path + * + * @see Issue 748 * @since 1.16 */ public static String getCompleteUrl(HttpServletRequest request, String... pathSegments) { - String baseUrl = - request.getRequestURL().toString().replace(request.getRequestURI(), - Util.EMPTY_STRING).concat(request.getContextPath()); + String baseUrl; + + if (isForwarded(request)) + { + baseUrl = createForwardedBaseUrl(request); + } + else + { + baseUrl = createBaseUrl(request); + } if (Util.isNotEmpty(pathSegments)) { @@ -623,6 +653,24 @@ public final class HttpUtil return append(configuration.getBaseUrl(), path); } + /** + * Method description + * + * + * @param request + * @param header + * @param defaultValue + * + * @return + */ + public static String getHeader(HttpServletRequest request, String header, + String defaultValue) + { + String value = request.getHeader(header); + + return Objects.firstNonNull(value, defaultValue); + } + /** * Returns the port of the url parameter. * @@ -777,6 +825,21 @@ public final class HttpUtil return "chunked".equals(request.getHeader("Transfer-Encoding")); } + /** + * Returns {@code true} if the request is forwarded by a reverse proxy. The + * method uses the X-Forwarded-Host header to identify a forwarded request. + * + * @param request servlet request + * + * @return {@code true} if the request is forwarded + * + * @since 1.47 + */ + public static boolean isForwarded(HttpServletRequest request) + { + return !Strings.isNullOrEmpty(request.getHeader(HEADER_X_FORWARDED_HOST)); + } + /** * Returns true if the http request is send by the scm-manager web interface. * @@ -791,4 +854,74 @@ public final class HttpUtil return SCM_CLIENT_WUI.equalsIgnoreCase( request.getHeader(HEADER_SCM_CLIENT)); } + + //~--- methods -------------------------------------------------------------- + + /** + * Creates base url for request url. + * + * @param request http servlet request + * + * @return base url from request + * + * @since 1.47 + */ + @VisibleForTesting + static String createBaseUrl(HttpServletRequest request) + { + return request.getRequestURL().toString().replace(request.getRequestURI(), + Util.EMPTY_STRING).concat(request.getContextPath()); + } + + /** + * Creates base url from forwarding request headers. + * + * @param request http servlet request + * + * @return base url from forward headers + * + * @since 1.47 + */ + @VisibleForTesting + static String createForwardedBaseUrl(HttpServletRequest request) + { + String proto = getHeader(request, HEADER_X_FORWARDED_PROTO, + request.getScheme()); + String host; + String fhost = getHeader(request, HEADER_X_FORWARDED_HOST, + request.getScheme()); + String port = request.getHeader(HEADER_X_FORWARDED_PORT); + int s = fhost.indexOf(SEPARATOR_PORT); + + if (s > 0) + { + host = fhost.substring(0, s); + + if (Strings.isNullOrEmpty(port)) + { + port = fhost.substring(s + 1); + } + } + else + { + host = fhost; + } + + StringBuilder buffer = new StringBuilder(proto); + + buffer.append(SEPARATOR_SCHEME).append(host).append(SEPARATOR_PORT); + + if (Strings.isNullOrEmpty(port)) + { + buffer.append(String.valueOf(request.getServerPort())); + } + else + { + buffer.append(port); + } + + buffer.append(request.getContextPath()); + + return buffer.toString(); + } } diff --git a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java index 585bc0e005..b85221f8c8 100644 --- a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java @@ -168,6 +168,136 @@ public class HttpUtilTest HttpUtil.checkForCRLFInjection("abcka"); } + /** + * Method description + * + */ + @Test + public void testCreateBaseUrl() + { + HttpServletRequest request = mock(HttpServletRequest.class); + String url = "https://www.scm-manager.org/test/as/db"; + + when(request.getRequestURL()).thenReturn(new StringBuffer(url)); + when(request.getRequestURI()).thenReturn("/test/as/db"); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org/scm", + HttpUtil.createBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testCreateForwardedUrl() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "www.scm-manager.org"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PROTO)).thenReturn( + "https"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PORT)).thenReturn("443"); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org:443/scm", + HttpUtil.createForwardedBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testCreateForwardedUrlWithPortAndProtoFromRequest() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "www.scm-manager.org"); + when(request.getScheme()).thenReturn("https"); + when(request.getServerPort()).thenReturn(443); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org:443/scm", + HttpUtil.createForwardedBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testCreateForwardedUrlWithPortInHost() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "www.scm-manager.org:443"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PROTO)).thenReturn( + "https"); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org:443/scm", + HttpUtil.createForwardedBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testCreateForwardedUrlWithPortInHostAndPortHeader() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "www.scm-manager.org:80"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PROTO)).thenReturn( + "https"); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_PORT)).thenReturn("443"); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org:443/scm", + HttpUtil.createForwardedBaseUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testGetCompleteUrl() + { + HttpServletRequest request = mock(HttpServletRequest.class); + String url = "https://www.scm-manager.org/test/as/db"; + + when(request.getRequestURL()).thenReturn(new StringBuffer(url)); + when(request.getRequestURI()).thenReturn("/test/as/db"); + when(request.getScheme()).thenReturn("https"); + when(request.getServerPort()).thenReturn(443); + when(request.getContextPath()).thenReturn("/scm"); + assertEquals("https://www.scm-manager.org/scm", + HttpUtil.getCompleteUrl(request)); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn( + "scm.scm-manager.org"); + assertEquals("https://scm.scm-manager.org:443/scm", + HttpUtil.getCompleteUrl(request)); + } + + /** + * Method description + * + */ + @Test + public void testIsForwarded() + { + HttpServletRequest request = mock(HttpServletRequest.class); + + assertFalse(HttpUtil.isForwarded(request)); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn(""); + assertFalse(HttpUtil.isForwarded(request)); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn("ser"); + assertTrue(HttpUtil.isForwarded(request)); + } + /** * Method description * From cfa9663f8f44612071d3c05f8d78f837add244d4 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 9 Jul 2015 21:29:29 +0200 Subject: [PATCH 2/2] close branch issue-748