
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 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 ca.uhn.fhir.rest.api.Constants; 023import jakarta.servlet.ServletContext; 024import jakarta.servlet.http.HttpServletRequest; 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 java.util.Optional; 034 035import static java.util.Optional.ofNullable; 036 037/** 038 * Works like the normal 039 * {@link ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy} unless there's 040 * an x-forwarded-host present, in which case that's used in place of the 041 * server's address. 042 * <p> 043 * If the Apache Http Server <code>mod_proxy</code> isn't configured to supply 044 * <code>x-forwarded-proto</code>, the factory method that you use to create the 045 * address strategy will determine the default. Note that <code>mod_proxy</code> 046 * doesn't set this by default, but it can be configured via 047 * <code>RequestHeader set X-Forwarded-Proto http</code> (or https) 048 * </p> 049 * <p> 050 * List of supported forward headers: 051 * <ul> 052 * <li>x-forwarded-host - original host requested by the client throw proxy 053 * server 054 * <li>x-forwarded-proto - original protocol (http, https) requested by the 055 * client 056 * <li>x-forwarded-port - original port request by the client, assume default 057 * port if not defined 058 * <li>x-forwarded-prefix - original server prefix / context path requested by 059 * the client 060 * </ul> 061 * </p> 062 * <p> 063 * If you want to set the protocol based on something other than the constructor 064 * argument, you should be able to do so by overriding <code>protocol</code>. 065 * </p> 066 * <p> 067 * Note that while this strategy was designed to work with Apache Http Server, 068 * and has been tested against it, it should work with any proxy server that 069 * sets <code>x-forwarded-host</code> 070 * </p> 071 * 072 */ 073public class ApacheProxyAddressStrategy extends IncomingRequestAddressStrategy { 074 private static final Logger LOG = LoggerFactory.getLogger(ApacheProxyAddressStrategy.class); 075 076 private final boolean useHttps; 077 078 /** 079 * @param useHttps 080 * Is used when the {@code x-forwarded-proto} is not set in the 081 * request. 082 */ 083 public ApacheProxyAddressStrategy(boolean useHttps) { 084 this.useHttps = useHttps; 085 } 086 087 @Override 088 public String determineServerBase(ServletContext servletContext, HttpServletRequest request) { 089 String serverBase = super.determineServerBase(servletContext, request); 090 ServletServerHttpRequest requestWrapper = new ServletServerHttpRequest(request); 091 UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpRequest(requestWrapper); 092 uriBuilder.replaceQuery(null); 093 HttpHeaders headers = requestWrapper.getHeaders(); 094 adjustSchemeWithDefault(uriBuilder, headers); 095 return forwardedServerBase(serverBase, headers, uriBuilder); 096 } 097 098 /** 099 * If forward host ist defined, but no forward protocol, use the configured default. 100 * 101 * @param uriBuilder 102 * @param headers 103 */ 104 private void adjustSchemeWithDefault(UriComponentsBuilder uriBuilder, HttpHeaders headers) { 105 if (headers.getFirst(Constants.HEADER_X_FORWARDED_HOST) != null 106 && headers.getFirst(Constants.HEADER_X_FORWARDED_PROTO) == null) { 107 uriBuilder.scheme(useHttps ? "https" : "http"); 108 } 109 } 110 111 private String forwardedServerBase( 112 String originalServerBase, HttpHeaders headers, UriComponentsBuilder uriBuilder) { 113 Optional<String> forwardedPrefix = getForwardedPrefix(headers); 114 LOG.debug("serverBase: {}, forwardedPrefix: {}", originalServerBase, forwardedPrefix); 115 LOG.debug("request header: {}", headers); 116 117 String path = forwardedPrefix.orElseGet(() -> pathFrom(originalServerBase)); 118 uriBuilder.replacePath(path); 119 return uriBuilder.build().toUriString(); 120 } 121 122 private String pathFrom(String serverBase) { 123 UriComponents build = UriComponentsBuilder.fromHttpUrl(serverBase).build(); 124 return StringUtils.defaultIfBlank(build.getPath(), ""); 125 } 126 127 private Optional<String> getForwardedPrefix(HttpHeaders headers) { 128 return ofNullable(headers.getFirst(Constants.HEADER_X_FORWARDED_PREFIX)); 129 } 130 131 /** 132 * Static factory for instance using <code>http://</code> 133 */ 134 public static ApacheProxyAddressStrategy forHttp() { 135 return new ApacheProxyAddressStrategy(false); 136 } 137 138 /** 139 * Static factory for instance using <code>https://</code> 140 */ 141 public static ApacheProxyAddressStrategy forHttps() { 142 return new ApacheProxyAddressStrategy(true); 143 } 144}