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}