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