Extend support for additional forwared headers x-forwarded-port and (#1788)

x-forwarded-prefix
This commit is contained in:
Thomas Papke 2020-04-29 15:01:39 +02:00 committed by GitHub
parent e219c1774b
commit ea817de68a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 214 additions and 42 deletions

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import java.net.URI;
/* /*
* #%L * #%L
* HAPI FHIR - Server Framework * HAPI FHIR - Server Framework
@ -20,68 +22,137 @@ package ca.uhn.fhir.rest.server;
* #L% * #L%
*/ */
import java.util.Optional;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest; 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. * 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.
* <p> * <p>
* If the Apache Http Server <code>mod_proxy</code> isn't configured to supply <code>x-forwarded-proto</code>, the factory method that you use to create the address strategy will determine the default. Note that * If the Apache Http Server <code>mod_proxy</code> isn't configured to supply
* <code>mod_proxy</code> doesn't set this by default, but it can be configured via <code>RequestHeader set X-Forwarded-Proto http</code> (or https) * <code>x-forwarded-proto</code>, the factory method that you use to create the
* address strategy will determine the default. Note that <code>mod_proxy</code>
* doesn't set this by default, but it can be configured via
* <code>RequestHeader set X-Forwarded-Proto http</code> (or https)
* </p> * </p>
* <p> * <p>
* If you want to set the protocol based on something other than the constructor argument, you should be able to do so by overriding <code>protocol</code>. * List of supported forward headers:
* <ul>
* <li>x-forwarded-host - original host requested by the client throw proxy
* server
* <li>x-forwarded-proto - original protocol (http, https) requested by the
* client
* <li>x-forwarded-port - original port request by the client, assume default
* port if not defined
* <li>x-forwarded-prefix - original server prefix / context path requested by
* the client
* </ul>
* </p> * </p>
* <p> * <p>
* 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 <code>x-forwarded-host</code> * If you want to set the protocol based on something other than the constructor
* argument, you should be able to do so by overriding <code>protocol</code>.
* </p>
* <p>
* 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 <code>x-forwarded-host</code>
* </p> * </p>
* *
* @author Created by Bill de Beaubien on 3/30/2015.
*/ */
public class ApacheProxyAddressStrategy extends IncomingRequestAddressStrategy { 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) { private static final Logger LOG = LoggerFactory
myUseHttps = theUseHttps; .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 @Override
public String determineServerBase(ServletContext theServletContext, HttpServletRequest theRequest) { public String determineServerBase(ServletContext servletContext,
String forwardedHost = getForwardedHost(theRequest); HttpServletRequest request) {
if (forwardedHost != null) { String serverBase = super.determineServerBase(servletContext, request);
return forwardedServerBase(theServletContext, theRequest, forwardedHost); ServletServerHttpRequest requestWrapper = new ServletServerHttpRequest(
} request);
return super.determineServerBase(theServletContext, theRequest); HttpHeaders headers = requestWrapper.getHeaders();
Optional<String> 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) { private String forwardedServerBase(String originalServerBase,
String serverBase = super.determineServerBase(theServletContext, theRequest); HttpHeaders headers, String forwardedHost) {
String host = theRequest.getHeader("host"); Optional<String> forwardedPrefix = getForwardedPrefix(headers);
if (host != null) { LOG.debug("serverBase: {}, forwardedHost: {}, forwardedPrefix: {}",
serverBase = serverBase.replace(host, theForwardedHost); originalServerBase, forwardedHost, forwardedPrefix);
serverBase = serverBase.substring(serverBase.indexOf("://")); LOG.debug("request header: {}", headers);
return protocol(theRequest) + serverBase;
} String host = protocol(headers) + "://" + forwardedHost;
return serverBase; 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) { private Optional<String> port(HttpHeaders headers) {
String forwardedHost = theRequest.getHeader("x-forwarded-host"); return ofNullable(headers.getFirst(X_FORWARDED_PORT));
if (forwardedHost != null) {
int commaPos = forwardedHost.indexOf(',');
if (commaPos >= 0) {
forwardedHost = forwardedHost.substring(0, commaPos - 1);
}
}
return forwardedHost;
} }
protected String protocol(HttpServletRequest theRequest) { private String pathFrom(String serverBase) {
String protocol = theRequest.getHeader("x-forwarded-proto"); 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<String> 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) { if (protocol != null) {
return protocol; return protocol;
} }
return myUseHttps ? "https" : "http"; return useHttps ? "https" : "http";
} }
/** /**

View File

@ -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;
}
}