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