/*
 * Copyright 2013-2017 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 java.util.concurrent.TimeUnit;

import static com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE;

/**
 * @author Spencer Gibb
 * @author Dave Syer
 * @author Mathias Düsterhöft
 * @author Bilal Alp
 */
@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;
		/**
		 * The lifetime for the connection pool.
		 */
		private long timeToLive = -1;
		/**
		 * The time unit for timeToLive.
		 */
		private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
	}
	
	@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;
	}

}
