diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategy.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategy.java index eb574295f62..a05a2a78526 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategy.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategy.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.server; +import java.net.URI; + /* * #%L * HAPI FHIR - Server Framework @@ -20,68 +22,137 @@ package ca.uhn.fhir.rest.server; * #L% */ +import java.util.Optional; + import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.ServletServerHttpRequest; + +import static java.util.Optional.ofNullable; + +import ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy; + /** - * Works like the normal {@link ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy} unless there's an x-forwarded-host present, in which case that's used in place of the server's address. - *

- * If the Apache Http Server mod_proxy isn't configured to supply x-forwarded-proto, the factory method that you use to create the address strategy will determine the default. Note that - * mod_proxy doesn't set this by default, but it can be configured via RequestHeader set X-Forwarded-Proto http (or https) - *

- *

- * If you want to set the protocol based on something other than the constructor argument, you should be able to do so by overriding protocol. - *

- *

- * Note that while this strategy was designed to work with Apache Http Server, and has been tested against it, it should work with any proxy server that sets x-forwarded-host + * Works like the normal + * {@link ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy} unless there's + * an x-forwarded-host present, in which case that's used in place of the + * server's address. + *

+ * If the Apache Http Server mod_proxy isn't configured to supply + * x-forwarded-proto, the factory method that you use to create the + * address strategy will determine the default. Note that mod_proxy + * doesn't set this by default, but it can be configured via + * RequestHeader set X-Forwarded-Proto http (or https) + *

+ *

+ * List of supported forward headers: + *

+ *

+ *

+ * If you want to set the protocol based on something other than the constructor + * argument, you should be able to do so by overriding protocol. + *

+ *

+ * Note that while this strategy was designed to work with Apache Http Server, + * and has been tested against it, it should work with any proxy server that + * sets x-forwarded-host *

* - * @author Created by Bill de Beaubien on 3/30/2015. */ public class ApacheProxyAddressStrategy extends IncomingRequestAddressStrategy { - private boolean myUseHttps = false; + private static final String X_FORWARDED_PREFIX = "x-forwarded-prefix"; + private static final String X_FORWARDED_PROTO = "x-forwarded-proto"; + private static final String X_FORWARDED_HOST = "x-forwarded-host"; + private static final String X_FORWARDED_PORT = "x-forwarded-port"; - protected ApacheProxyAddressStrategy(boolean theUseHttps) { - myUseHttps = theUseHttps; + private static final Logger LOG = LoggerFactory + .getLogger(ApacheProxyAddressStrategy.class); + + private final boolean useHttps; + + /** + * @param useHttps + * Is used when the {@code x-forwarded-proto} is not set in the + * request. + */ + public ApacheProxyAddressStrategy(boolean useHttps) { + this.useHttps = useHttps; } @Override - public String determineServerBase(ServletContext theServletContext, HttpServletRequest theRequest) { - String forwardedHost = getForwardedHost(theRequest); - if (forwardedHost != null) { - return forwardedServerBase(theServletContext, theRequest, forwardedHost); - } - return super.determineServerBase(theServletContext, theRequest); + public String determineServerBase(ServletContext servletContext, + HttpServletRequest request) { + String serverBase = super.determineServerBase(servletContext, request); + ServletServerHttpRequest requestWrapper = new ServletServerHttpRequest( + request); + HttpHeaders headers = requestWrapper.getHeaders(); + Optional forwardedHost = headers + .getValuesAsList(X_FORWARDED_HOST).stream().findFirst(); + return forwardedHost + .map(s -> forwardedServerBase(serverBase, headers, s)) + .orElse(serverBase); } - public String forwardedServerBase(ServletContext theServletContext, HttpServletRequest theRequest, String theForwardedHost) { - String serverBase = super.determineServerBase(theServletContext, theRequest); - String host = theRequest.getHeader("host"); - if (host != null) { - serverBase = serverBase.replace(host, theForwardedHost); - serverBase = serverBase.substring(serverBase.indexOf("://")); - return protocol(theRequest) + serverBase; - } - return serverBase; + private String forwardedServerBase(String originalServerBase, + HttpHeaders headers, String forwardedHost) { + Optional forwardedPrefix = getForwardedPrefix(headers); + LOG.debug("serverBase: {}, forwardedHost: {}, forwardedPrefix: {}", + originalServerBase, forwardedHost, forwardedPrefix); + LOG.debug("request header: {}", headers); + + String host = protocol(headers) + "://" + forwardedHost; + String hostWithOptionalPort = port(headers).map(p -> (host + ":" + p)) + .orElse(host); + + String path = forwardedPrefix + .orElseGet(() -> pathFrom(originalServerBase)); + return joinStringsWith(hostWithOptionalPort, path, "/"); } - private String getForwardedHost(HttpServletRequest theRequest) { - String forwardedHost = theRequest.getHeader("x-forwarded-host"); - if (forwardedHost != null) { - int commaPos = forwardedHost.indexOf(','); - if (commaPos >= 0) { - forwardedHost = forwardedHost.substring(0, commaPos - 1); - } - } - return forwardedHost; + private Optional port(HttpHeaders headers) { + return ofNullable(headers.getFirst(X_FORWARDED_PORT)); } - protected String protocol(HttpServletRequest theRequest) { - String protocol = theRequest.getHeader("x-forwarded-proto"); + private String pathFrom(String serverBase) { + String serverBasePath = URI.create(serverBase).getPath(); + return StringUtils.defaultIfBlank(serverBasePath, ""); + } + + private static String joinStringsWith(String left, String right, + String joiner) { + if (left.endsWith(joiner) && right.startsWith(joiner)) { + return left + right.substring(1); + } else if (left.endsWith(joiner) || right.startsWith(joiner)) { + return left + right; + } else { + return left + joiner + right; + } + } + + private Optional getForwardedPrefix(HttpHeaders headers) { + return ofNullable(headers.getFirst(X_FORWARDED_PREFIX)); + } + + private String protocol(HttpHeaders headers) { + String protocol = headers.getFirst(X_FORWARDED_PROTO); if (protocol != null) { return protocol; } - return myUseHttps ? "https" : "http"; + return useHttps ? "https" : "http"; } /** @@ -97,4 +168,4 @@ public class ApacheProxyAddressStrategy extends IncomingRequestAddressStrategy { public static ApacheProxyAddressStrategy forHttps() { return new ApacheProxyAddressStrategy(true); } -} +} \ No newline at end of file diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategyTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategyTest.java new file mode 100644 index 00000000000..8f5c2f3fc47 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategyTest.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +public class ApacheProxyAddressStrategyTest { + + @Test + public void testWithoutForwarded() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://localhost/imagingstudy/fhir", serverBase); + } + + @Test + public void testWithForwardedHostWithoutForwardedProtoHttps() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + request.addHeader("X-Forwarded-Host", "my.example.host"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host/imagingstudy/fhir", serverBase); + } + + @Test + public void testWithForwardedHostWithoutForwardedProtoHttp() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + false); + MockHttpServletRequest request = prepareRequest(); + request.addHeader("X-Forwarded-Host", "my.example.host"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("http://my.example.host/imagingstudy/fhir", serverBase); + } + + @Test + public void testWithForwarded() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + request.addHeader("X-Forwarded-Host", "my.example.host"); + request.addHeader("X-Forwarded-Proto", "https"); + request.addHeader("X-Forwarded-Prefix", "server-prefix/fhir"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host/server-prefix/fhir", serverBase); + } + + @Test + public void testWithForwardedWithHostPrefixWithSlash() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + request.addHeader("host", "localhost"); + + request.addHeader("X-Forwarded-Host", "my.example.host"); + request.addHeader("X-Forwarded-Proto", "https"); + request.addHeader("X-Forwarded-Prefix", "/server-prefix/fhir"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host/server-prefix/fhir", serverBase); + } + + @Test + public void testWithForwardedWithoutPrefix() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + + request.addHeader("X-Forwarded-Host", "my.example.host"); + request.addHeader("X-Forwarded-Proto", "https"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host/imagingstudy/fhir", serverBase); + } + + @Test + public void testWithForwardedHostAndPort() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + + request.addHeader("X-Forwarded-Host", "my.example.host"); + request.addHeader("X-Forwarded-Port", "345"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host:345/imagingstudy/fhir", + serverBase); + } + + private MockHttpServletRequest prepareRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setScheme("https"); + request.setServerPort(443); + request.setServletPath("/fhir"); + request.setServerName("localhost"); + request.setRequestURI("/imagingstudy/fhir/imagingstudy?_format=json"); + request.setContextPath("/imagingstudy"); + return request; + } +} \ No newline at end of file