/*
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.netflix.zuul.filters;
import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import static com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE;
/**
* @author Spencer Gibb
* @author Dave Syer
* @author Mathias Düsterhöft
*/
@Data
@ConfigurationProperties("zuul")
public class ZuulProperties {
/**
* Headers that are generally expected to be added by Spring Security, and hence often
* duplicated if the proxy and the backend are secured with Spring. By default they
* are added to the ignored headers if Spring Security is present and ignoreSecurityHeaders = true.
*/
public static final List<String> SECURITY_HEADERS = Arrays.asList("Pragma",
"Cache-Control", "X-Frame-Options", "X-Content-Type-Options",
"X-XSS-Protection", "Expires");
/**
* A common prefix for all routes.
*/
private String prefix = "";
/**
* Flag saying whether to strip the prefix from the path before forwarding.
*/
private boolean stripPrefix = true;
/**
* Flag for whether retry is supported by default (assuming the routes themselves
* support it).
*/
private Boolean retryable = false;
/**
* Map of route names to properties.
*/
private Map<String, ZuulRoute> routes = new LinkedHashMap<>();
/**
* Flag to determine whether the proxy adds X-Forwarded-* headers.
*/
private boolean addProxyHeaders = true;
/**
* Flag to determine whether the proxy forwards the Host header.
*/
private boolean addHostHeader = false;
/**
* Set of service names not to consider for proxying automatically. By default all
* services in the discovery client will be proxied.
*/
private Set<String> ignoredServices = new LinkedHashSet<>();
private Set<String> ignoredPatterns = new LinkedHashSet<>();
/**
* Names of HTTP headers to ignore completely (i.e. leave them out of downstream
* requests and drop them from downstream responses).
*/
private Set<String> ignoredHeaders = new LinkedHashSet<>();
/**
* Flag to say that SECURITY_HEADERS are added to ignored headers if spring security is on the classpath.
* By setting ignoreSecurityHeaders to false we can switch off this default behaviour. This should be used together with
* disabling the default spring security headers
* see https://docs.spring.io/spring-security/site/docs/current/reference/html/headers.html#default-security-headers
*/
private boolean ignoreSecurityHeaders = true;
/**
* Flag to force the original query string encoding when building the backend URI in
* SimpleHostRoutingFilter. When activated, query string will be built using
* HttpServletRequest getQueryString() method instead of UriTemplate. Note that this
* flag is not used in RibbonRoutingFilter with services found via DiscoveryClient
* (like Eureka).
*/
private boolean forceOriginalQueryStringEncoding = false;
/**
* Path to install Zuul as a servlet (not part of Spring MVC). The servlet is more
* memory efficient for requests with large bodies, e.g. file uploads.
*/
private String servletPath = "/zuul";
private boolean ignoreLocalService = true;
/**
* Host properties controlling default connection pool properties.
*/
private Host host = new Host();
/**
* Flag to say that request bodies can be traced.
*/
private boolean traceRequestBody = true;
/**
* Flag to say that path elements past the first semicolon can be dropped.
*/
private boolean removeSemicolonContent = true;
/**
* List of sensitive headers that are not passed to downstream requests. Defaults to a
* "safe" set of headers that commonly contain user credentials. It's OK to remove
* those from the list if the downstream service is part of the same system as the
* proxy, so they are sharing authentication data. If using a physical URL outside
* your own domain, then generally it would be a bad idea to leak user credentials.
*/
private Set<String> sensitiveHeaders = new LinkedHashSet<>(
Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
/**
* Flag to say whether the hostname for ssl connections should be verified or not. Default is true.
* This should only be used in test setups!
*/
private boolean sslHostnameValidationEnabled =true;
private ExecutionIsolationStrategy ribbonIsolationStrategy = SEMAPHORE;
private HystrixSemaphore semaphore = new HystrixSemaphore();
public Set<String> getIgnoredHeaders() {
Set<String> ignoredHeaders = new LinkedHashSet<>(this.ignoredHeaders);
if (ClassUtils.isPresent(
"org.springframework.security.config.annotation.web.WebSecurityConfigurer",
null) && Collections.disjoint(ignoredHeaders, SECURITY_HEADERS) && ignoreSecurityHeaders) {
// Allow Spring Security in the gateway to control these headers
ignoredHeaders.addAll(SECURITY_HEADERS);
}
return ignoredHeaders;
}
public void setIgnoredHeaders(Set<String> ignoredHeaders) {
this.ignoredHeaders.addAll(ignoredHeaders);
}
@PostConstruct
public void init() {
for (Entry<String, ZuulRoute> entry : this.routes.entrySet()) {
ZuulRoute value = entry.getValue();
if (!StringUtils.hasText(value.getLocation())) {
value.serviceId = entry.getKey();
}
if (!StringUtils.hasText(value.getId())) {
value.id = entry.getKey();
}
if (!StringUtils.hasText(value.getPath())) {
value.path = "/" + entry.getKey() + "/**";
}
}
}
@Data
@NoArgsConstructor
public static class ZuulRoute {
/**
* The ID of the route (the same as its map key by default).
*/
private String id;
/**
* The path (pattern) for the route, e.g. /foo/**.
*/
private String path;
/**
* The service ID (if any) to map to this route. You can specify a physical URL or
* a service, but not both.
*/
private String serviceId;
/**
* A full physical URL to map to the route. An alternative is to use a service ID
* and service discovery to find the physical address.
*/
private String url;
/**
* Flag to determine whether the prefix for this route (the path, minus pattern
* patcher) should be stripped before forwarding.
*/
private boolean stripPrefix = true;
/**
* Flag to indicate that this route should be retryable (if supported). Generally
* retry requires a service ID and ribbon.
*/
private Boolean retryable;
/**
* List of sensitive headers that are not passed to downstream requests. Defaults
* to a "safe" set of headers that commonly contain user credentials. It's OK to
* remove those from the list if the downstream service is part of the same system
* as the proxy, so they are sharing authentication data. If using a physical URL
* outside your own domain, then generally it would be a bad idea to leak user
* credentials.
*/
private Set<String> sensitiveHeaders = new LinkedHashSet<>();
private boolean customSensitiveHeaders = false;
public ZuulRoute(String id, String path, String serviceId, String url,
boolean stripPrefix, Boolean retryable, Set<String> sensitiveHeaders) {
this.id = id;
this.path = path;
this.serviceId = serviceId;
this.url = url;
this.stripPrefix = stripPrefix;
this.retryable = retryable;
this.sensitiveHeaders = sensitiveHeaders;
this.customSensitiveHeaders = sensitiveHeaders != null;
}
public ZuulRoute(String text) {
String location = null;
String path = text;
if (text.contains("=")) {
String[] values = StringUtils
.trimArrayElements(StringUtils.split(text, "="));
location = values[1];
path = values[0];
}
this.id = extractId(path);
if (!path.startsWith("/")) {
path = "/" + path;
}
setLocation(location);
this.path = path;
}
public ZuulRoute(String path, String location) {
this.id = extractId(path);
this.path = path;
setLocation(location);
}
public String getLocation() {
if (StringUtils.hasText(this.url)) {
return this.url;
}
return this.serviceId;
}
public void setLocation(String location) {
if (location != null
&& (location.startsWith("http:") || location.startsWith("https:"))) {
this.url = location;
}
else {
this.serviceId = location;
}
}
private String extractId(String path) {
path = path.startsWith("/") ? path.substring(1) : path;
path = path.replace("/*", "").replace("*", "");
return path;
}
public Route getRoute(String prefix) {
return new Route(this.id, this.path, getLocation(), prefix, this.retryable,
isCustomSensitiveHeaders() ? this.sensitiveHeaders : null);
}
public void setSensitiveHeaders(Set<String> headers) {
this.customSensitiveHeaders = true;
this.sensitiveHeaders = new LinkedHashSet<>(headers);
}
public boolean isCustomSensitiveHeaders() {
return this.customSensitiveHeaders;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Host {
/**
* The maximum number of total connections the proxy can hold open to backends.
*/
private int maxTotalConnections = 200;
/**
* The maximum number of connections that can be used by a single route.
*/
private int maxPerRouteConnections = 20;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class HystrixSemaphore {
/**
* The maximum number of total semaphores for Hystrix.
*/
private int maxSemaphores = 100;
}
public String getServletPattern() {
String path = this.servletPath;
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!path.contains("*")) {
path = path.endsWith("/") ? (path + "*") : (path + "/*");
}
return path;
}
}