001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.server.adapter; 018 019import java.net.URI; 020import java.util.Collections; 021import java.util.Locale; 022import java.util.Set; 023import java.util.function.Function; 024 025import org.springframework.context.ApplicationContext; 026import org.springframework.http.HttpHeaders; 027import org.springframework.http.server.reactive.ServerHttpRequest; 028import org.springframework.lang.Nullable; 029import org.springframework.util.LinkedCaseInsensitiveMap; 030import org.springframework.web.util.UriComponentsBuilder; 031 032/** 033 * Extract values from "Forwarded" and "X-Forwarded-*" headers to override 034 * the request URI (i.e. {@link ServerHttpRequest#getURI()}) so it reflects 035 * the client-originated protocol and address. 036 * 037 * <p>Alternatively if {@link #setRemoveOnly removeOnly} is set to "true", 038 * then "Forwarded" and "X-Forwarded-*" headers are only removed, and not used. 039 * 040 * <p>An instance of this class is typically declared as a bean with the name 041 * "forwardedHeaderTransformer" and detected by 042 * {@link WebHttpHandlerBuilder#applicationContext(ApplicationContext)}, or it 043 * can also be registered directly via 044 * {@link WebHttpHandlerBuilder#forwardedHeaderTransformer(ForwardedHeaderTransformer)}. 045 * 046 * @author Rossen Stoyanchev 047 * @since 5.1 048 * @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a> 049 */ 050public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, ServerHttpRequest> { 051 052 static final Set<String> FORWARDED_HEADER_NAMES = 053 Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)); 054 055 static { 056 FORWARDED_HEADER_NAMES.add("Forwarded"); 057 FORWARDED_HEADER_NAMES.add("X-Forwarded-Host"); 058 FORWARDED_HEADER_NAMES.add("X-Forwarded-Port"); 059 FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto"); 060 FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix"); 061 FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl"); 062 } 063 064 065 private boolean removeOnly; 066 067 068 /** 069 * Enable mode in which any "Forwarded" or "X-Forwarded-*" headers are 070 * removed only and the information in them ignored. 071 * @param removeOnly whether to discard and ignore forwarded headers 072 */ 073 public void setRemoveOnly(boolean removeOnly) { 074 this.removeOnly = removeOnly; 075 } 076 077 /** 078 * Whether the "remove only" mode is on. 079 * @see #setRemoveOnly 080 */ 081 public boolean isRemoveOnly() { 082 return this.removeOnly; 083 } 084 085 086 /** 087 * Apply and remove, or remove Forwarded type headers. 088 * @param request the request 089 */ 090 @Override 091 public ServerHttpRequest apply(ServerHttpRequest request) { 092 if (hasForwardedHeaders(request)) { 093 ServerHttpRequest.Builder builder = request.mutate(); 094 if (!this.removeOnly) { 095 URI uri = UriComponentsBuilder.fromHttpRequest(request).build(true).toUri(); 096 builder.uri(uri); 097 String prefix = getForwardedPrefix(request); 098 if (prefix != null) { 099 builder.path(prefix + uri.getRawPath()); 100 builder.contextPath(prefix); 101 } 102 } 103 removeForwardedHeaders(builder); 104 request = builder.build(); 105 } 106 return request; 107 } 108 109 /** 110 * Whether the request has any Forwarded headers. 111 * @param request the request 112 */ 113 protected boolean hasForwardedHeaders(ServerHttpRequest request) { 114 HttpHeaders headers = request.getHeaders(); 115 for (String headerName : FORWARDED_HEADER_NAMES) { 116 if (headers.containsKey(headerName)) { 117 return true; 118 } 119 } 120 return false; 121 } 122 123 private void removeForwardedHeaders(ServerHttpRequest.Builder builder) { 124 builder.headers(map -> FORWARDED_HEADER_NAMES.forEach(map::remove)); 125 } 126 127 128 @Nullable 129 private static String getForwardedPrefix(ServerHttpRequest request) { 130 HttpHeaders headers = request.getHeaders(); 131 String prefix = headers.getFirst("X-Forwarded-Prefix"); 132 if (prefix != null) { 133 int endIndex = prefix.length(); 134 while (endIndex > 1 && prefix.charAt(endIndex - 1) == '/') { 135 endIndex--; 136 } 137 prefix = (endIndex != prefix.length() ? prefix.substring(0, endIndex) : prefix); 138 } 139 return prefix; 140 } 141 142}