package it.mice.voila.runtime.springmvc.filters;

import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.crypto.codec.Hex;

public class CSPPoliciesApplierFilter implements Filter {

	/** Configuration member to specify if web app use web fonts */
	private boolean APP_USE_WEBFONTS = false;

	/** Configuration member to specify if web app use videos or audios */
	private boolean APP_USE_AUDIOS_OR_VIDEOS = false;

	/**
	 * Configuration member to specify if filter must add CSP directive used by
	 * Mozilla (Firefox)
	 */
	private boolean INCLUDE_MOZILLA_CSP_DIRECTIVES = true;

	private boolean APPLY_XSS_PROTECTION = true;
	private boolean APPLY_X_CONTENT_TYPE_OPTIONS_PROTECTION = true;
	
	private String ORIGIN_LOCATION_REF = "'self' https://*.google.com https://*.googleapis.com http://*.googleapis.com https://*.gstatic.com http://*.gstatic.com";

	/** Filter configuration */
	private FilterConfig filterConfig = null;

	/** List CSP HTTP Headers */
	private List<String> cspHeaders = new ArrayList<String>();

	/** Collection of CSP polcies that will be applied */
	private String policies = null;

	/** Used for Script Nonce */
	private SecureRandom prng = null;

	/**
	 * Used to prepare (one time for all) set of CSP policies that will be
	 * applied on each HTTP response.
	 * 
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	public void init(FilterConfig fConfig) {
		// Get filter configuration
		this.filterConfig = fConfig;

		APP_USE_WEBFONTS = Boolean.parseBoolean(filterConfig.getInitParameter("APP_USE_WEBFONTS"));
		APP_USE_AUDIOS_OR_VIDEOS = Boolean.parseBoolean(filterConfig.getInitParameter("APP_USE_AUDIOS_OR_VIDEOS"));
		INCLUDE_MOZILLA_CSP_DIRECTIVES = Boolean.parseBoolean(filterConfig.getInitParameter("INCLUDE_MOZILLA_CSP_DIRECTIVES"));
		APPLY_XSS_PROTECTION = Boolean.parseBoolean(filterConfig.getInitParameter("APPLY_XSS_PROTECTION"));
		APPLY_X_CONTENT_TYPE_OPTIONS_PROTECTION = Boolean.parseBoolean(filterConfig.getInitParameter("APPLY_X_CONTENT_TYPE_OPTIONS_PROTECTION"));
		ORIGIN_LOCATION_REF = filterConfig.getInitParameter("ORIGIN_LOCATION_REF") != null ? filterConfig.getInitParameter("ORIGIN_LOCATION_REF") : ORIGIN_LOCATION_REF;

		// Init secure random
		try {
			this.prng = SecureRandom.getInstance("SHA1PRNG");
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}

		// Define list of CSP HTTP Headers
		this.cspHeaders.add("Content-Security-Policy");
		this.cspHeaders.add("X-Content-Security-Policy");
		this.cspHeaders.add("X-WebKit-CSP");

		// Define CSP policies
		// Loading policies for Frame and Sandboxing will be dynamically defined
		// : We need to know if context use Frame
		List<String> cspPolicies = new ArrayList<String>();
		// --Disable default source in order to avoid browser fallback loading
		// using 'default-src' locations
		cspPolicies.add("default-src 'none'");
		// --Define loading policies for Scripts
		cspPolicies.add("script-src " + ORIGIN_LOCATION_REF + " 'unsafe-inline' 'unsafe-eval'");
		if (INCLUDE_MOZILLA_CSP_DIRECTIVES) {
			cspPolicies.add("options inline-script eval-script");
			cspPolicies.add("xhr-src 'self'");
		}
		// --Define loading policies for Plugins
		cspPolicies.add("object-src " + ORIGIN_LOCATION_REF);
		// --Define loading policies for Styles (CSS)
		cspPolicies.add("style-src " + ORIGIN_LOCATION_REF + " 'unsafe-inline'");
		cspPolicies.add("frame-src " + ORIGIN_LOCATION_REF + " 'unsafe-inline'");
		// --Define loading policies for Images
		cspPolicies.add("img-src " + ORIGIN_LOCATION_REF + " data:");
		// --Define loading policies for Form
		cspPolicies.add("form-action " + ORIGIN_LOCATION_REF);
		// --Define loading policies for Audios/Videos
		if (APP_USE_AUDIOS_OR_VIDEOS) {
			cspPolicies.add("media-src " + ORIGIN_LOCATION_REF);
		}
		// --Define loading policies for Fonts
		if (APP_USE_WEBFONTS) {
			cspPolicies.add("font-src " + ORIGIN_LOCATION_REF + " data:");
		}
		// --Define loading policies for Connection
		cspPolicies.add("connect-src " + ORIGIN_LOCATION_REF);
		// --Define loading policies for Plugins Types
		cspPolicies
				.add("plugin-types application/pdf application/x-shockwave-flash");
		// --Define browser XSS filtering feature running mode
		cspPolicies.add("reflected-xss block");

		// Target formating
		this.policies = cspPolicies.toString().replaceAll("(\\[|\\])", "")
				.replaceAll(",", ";").trim();
	}

	/**
	 * Add CSP policies on each HTTP response.
	 * 
	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
	 *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
	 */
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain fchain) throws IOException, ServletException {
		HttpServletRequest httpRequest = ((HttpServletRequest) request);
		HttpServletResponse httpResponse = ((HttpServletResponse) response);

		/* Step 1 : Detect if target resource is a Frame */
		// Customize here according to your context...
		boolean isFrame = false;

		/* Step 2 : Add CSP policies to HTTP response */
		StringBuilder policiesBuffer = new StringBuilder(this.policies);

		// If resource is a frame add Frame/Sandbox CSP policy
		if (isFrame) {
			// Frame + Sandbox : Here sandbox allow nothing, customize sandbox
			// options depending on your app....
			policiesBuffer.append(";").append("frame-src 'self';sandbox");
			if (INCLUDE_MOZILLA_CSP_DIRECTIVES) {
				policiesBuffer.append(";").append("frame-ancestors 'self'");
			}
		}

		// Add Script Nonce CSP Policy
		// --Generate a random number
		String randomNum = new Integer(this.prng.nextInt()).toString();
		// --Get its digest
		MessageDigest sha;
		try {
			sha = MessageDigest.getInstance("SHA-1");
		} catch (NoSuchAlgorithmException e) {
			throw new ServletException(e);
		}
		byte[] digest = sha.digest(randomNum.getBytes());
		// --Encode it into HEXA
		String scriptNonce = new String(Hex.encode(digest));
		//policiesBuffer.append(";").append("script-nonce ").append(scriptNonce);
		// --Made available script nonce in view app layer
		httpRequest.setAttribute("CSP_SCRIPT_NONCE", scriptNonce);

		// Add policies to all HTTP headers
		for (String header : this.cspHeaders) {
			httpResponse.setHeader(header, policiesBuffer.toString());
		}

		if (APPLY_XSS_PROTECTION) {
			httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
		}
		if (APPLY_X_CONTENT_TYPE_OPTIONS_PROTECTION) {
			httpResponse.setHeader("X-Content-Type-Options", "nosniff");
		}
		/* Step 3 : Let request continue chain filter */
		fchain.doFilter(request, response);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see javax.servlet.Filter#destroy()
	 */
	public void destroy() {
		// Not used
	}
}
