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}