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