001package ca.uhn.fhir.rest.server; 002 003import java.net.URI; 004 005/* 006 * #%L 007 * HAPI FHIR - Server Framework 008 * %% 009 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 010 * %% 011 * Licensed under the Apache License, Version 2.0 (the "License"); 012 * you may not use this file except in compliance with the License. 013 * You may obtain a copy of the License at 014 * 015 * http://www.apache.org/licenses/LICENSE-2.0 016 * 017 * Unless required by applicable law or agreed to in writing, software 018 * distributed under the License is distributed on an "AS IS" BASIS, 019 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 020 * See the License for the specific language governing permissions and 021 * limitations under the License. 022 * #L% 023 */ 024 025import java.util.Optional; 026 027import javax.servlet.ServletContext; 028import javax.servlet.http.HttpServletRequest; 029 030import org.apache.commons.lang3.StringUtils; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033import org.springframework.http.HttpHeaders; 034import org.springframework.http.server.ServletServerHttpRequest; 035 036import static java.util.Optional.ofNullable; 037 038import ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy; 039 040/** 041 * Works like the normal 042 * {@link ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy} unless there's 043 * an x-forwarded-host present, in which case that's used in place of the 044 * server's address. 045 * <p> 046 * If the Apache Http Server <code>mod_proxy</code> isn't configured to supply 047 * <code>x-forwarded-proto</code>, the factory method that you use to create the 048 * address strategy will determine the default. Note that <code>mod_proxy</code> 049 * doesn't set this by default, but it can be configured via 050 * <code>RequestHeader set X-Forwarded-Proto http</code> (or https) 051 * </p> 052 * <p> 053 * List of supported forward headers: 054 * <ul> 055 * <li>x-forwarded-host - original host requested by the client throw proxy 056 * server 057 * <li>x-forwarded-proto - original protocol (http, https) requested by the 058 * client 059 * <li>x-forwarded-port - original port request by the client, assume default 060 * port if not defined 061 * <li>x-forwarded-prefix - original server prefix / context path requested by 062 * the client 063 * </ul> 064 * </p> 065 * <p> 066 * If you want to set the protocol based on something other than the constructor 067 * argument, you should be able to do so by overriding <code>protocol</code>. 068 * </p> 069 * <p> 070 * Note that while this strategy was designed to work with Apache Http Server, 071 * and has been tested against it, it should work with any proxy server that 072 * sets <code>x-forwarded-host</code> 073 * </p> 074 * 075 */ 076public class ApacheProxyAddressStrategy extends IncomingRequestAddressStrategy { 077 private static final String X_FORWARDED_PREFIX = "x-forwarded-prefix"; 078 private static final String X_FORWARDED_PROTO = "x-forwarded-proto"; 079 private static final String X_FORWARDED_HOST = "x-forwarded-host"; 080 private static final String X_FORWARDED_PORT = "x-forwarded-port"; 081 082 private static final Logger LOG = LoggerFactory 083 .getLogger(ApacheProxyAddressStrategy.class); 084 085 private final boolean useHttps; 086 087 /** 088 * @param useHttps 089 * Is used when the {@code x-forwarded-proto} is not set in the 090 * request. 091 */ 092 public ApacheProxyAddressStrategy(boolean useHttps) { 093 this.useHttps = useHttps; 094 } 095 096 @Override 097 public String determineServerBase(ServletContext servletContext, 098 HttpServletRequest request) { 099 String serverBase = super.determineServerBase(servletContext, request); 100 ServletServerHttpRequest requestWrapper = new ServletServerHttpRequest( 101 request); 102 HttpHeaders headers = requestWrapper.getHeaders(); 103 Optional<String> forwardedHost = headers 104 .getValuesAsList(X_FORWARDED_HOST).stream().findFirst(); 105 return forwardedHost 106 .map(s -> forwardedServerBase(serverBase, headers, s)) 107 .orElse(serverBase); 108 } 109 110 private String forwardedServerBase(String originalServerBase, 111 HttpHeaders headers, String forwardedHost) { 112 Optional<String> forwardedPrefix = getForwardedPrefix(headers); 113 LOG.debug("serverBase: {}, forwardedHost: {}, forwardedPrefix: {}", 114 originalServerBase, forwardedHost, forwardedPrefix); 115 LOG.debug("request header: {}", headers); 116 117 String host = protocol(headers) + "://" + forwardedHost; 118 String hostWithOptionalPort = port(headers).map(p -> (host + ":" + p)) 119 .orElse(host); 120 121 String path = forwardedPrefix 122 .orElseGet(() -> pathFrom(originalServerBase)); 123 return joinStringsWith(hostWithOptionalPort, path, "/"); 124 } 125 126 private Optional<String> port(HttpHeaders headers) { 127 return ofNullable(headers.getFirst(X_FORWARDED_PORT)); 128 } 129 130 private String pathFrom(String serverBase) { 131 String serverBasePath = URI.create(serverBase).getPath(); 132 return StringUtils.defaultIfBlank(serverBasePath, ""); 133 } 134 135 private static String joinStringsWith(String left, String right, 136 String joiner) { 137 if (left.endsWith(joiner) && right.startsWith(joiner)) { 138 return left + right.substring(1); 139 } else if (left.endsWith(joiner) || right.startsWith(joiner)) { 140 return left + right; 141 } else { 142 return left + joiner + right; 143 } 144 } 145 146 private Optional<String> getForwardedPrefix(HttpHeaders headers) { 147 return ofNullable(headers.getFirst(X_FORWARDED_PREFIX)); 148 } 149 150 private String protocol(HttpHeaders headers) { 151 String protocol = headers.getFirst(X_FORWARDED_PROTO); 152 if (protocol != null) { 153 return protocol; 154 } 155 return useHttps ? "https" : "http"; 156 } 157 158 /** 159 * Static factory for instance using <code>http://</code> 160 */ 161 public static ApacheProxyAddressStrategy forHttp() { 162 return new ApacheProxyAddressStrategy(false); 163 } 164 165 /** 166 * Static factory for instance using <code>https://</code> 167 */ 168 public static ApacheProxyAddressStrategy forHttps() { 169 return new ApacheProxyAddressStrategy(true); 170 } 171}