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}