package cn.myapps.runtime.security;

import cn.myapps.common.util.PropertyUtil;
import cn.myapps.common.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.owasp.validator.html.AntiSamy;
import org.owasp.validator.html.CleanResults;
import org.owasp.validator.html.Policy;
import org.owasp.validator.html.PolicyException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 防火墙
 * <p>
 * 用于拦截非法渗透攻击，包含： SQL注入、 XSS脚本攻击、 CSRF跨站请求伪造攻击、
 * </p>
 *
 * @author Happy
 *
 */
public class Firewall {

	/**
	 * 是否开启防火墙
	 */
	private boolean startFirewall = false;

	/**
	 * 是否阻止跨站请求伪造
	 */
	private boolean stopCSRF = false;

	/**
	 * 是否开启关键词拦截
	 */
	private boolean validKeyword = false;

	/**
	 * 强力拦截模式，此模式下可能存在低概率误杀的情况
	 */
	private boolean strongMode = false;

	/**
	 * 拦截关键词
	 */
	private String[] excludeChars = new String[] {};// 非法字符

	/**
	 * 拦截关键词的正则表达式
	 */
	private Set<Pattern> excludePatterns = new HashSet<Pattern>();

	/**
	 * 跨站请求伪造 例外地址（格式：http://xxx.xx.com:8001）服务器由前置机反向代理的情况下需要配置此属性
	 */
	private String[] excludeHostAddress = null;

	/**
	 * 参数拦截例外URL
	 */
	private String[] ignoreURL = new String[] {};

	private static Firewall instance = null;

	private Pattern cp = Pattern.compile("\\s*");

	private static Policy policy = null;

	private Firewall(boolean startFirewall, boolean validKeyword, String[] excludeChars, Set<Pattern> excludePatterns,
					 boolean stopCSRF, String[] excludeHostAddress, String[] ignoreURL, boolean strongMode) {
		super();
		this.startFirewall = startFirewall;
		this.validKeyword = validKeyword;
		this.excludeChars = excludeChars;
		this.excludePatterns = excludePatterns;
		this.stopCSRF = stopCSRF;
		this.excludeHostAddress = excludeHostAddress;
		this.ignoreURL = ignoreURL;
		this.strongMode = strongMode;
	}

	public static Firewall getInstance() {

		if (instance == null) {
			init();
		}

		return instance;
	}

	private static synchronized void init() {

		boolean startFirewall = false;
		boolean validKeyword = false;
		boolean stopCSRF = false;
		boolean strongMode = false;

		String[] excludeChars = new String[] {};
		Set<Pattern> excludePatterns = new HashSet<Pattern>();
		String[] excludeHostAddress = null;
		String[] ignoreURL = new String[] {};

		String _startFirewall = PropertyUtil.get("SecurityFilter.firewall.startFirewall");
		String _validKeyword = PropertyUtil.get("SecurityFilter.firewall.interceptor.keyword");
		String _stopCSRF = PropertyUtil.get("SecurityFilter.firewall.interceptor.CSRF");
		String _excludeChars = PropertyUtil.get("SecurityFilter.firewall.interceptor.keyword.excludeChars");
		String _excludePatterns = PropertyUtil.get("SecurityFilter.firewall.interceptor.keyword.excludePatterns");
		String _excludeHostAddress = PropertyUtil.get("SecurityFilter.firewall.interceptor.CSRF.excludeHostAddress");
		String _ignoreURL = PropertyUtil.get("SecurityFilter.firewall.interceptor.keyword.ignoreURL");
		String _strongMode = PropertyUtil.get("SecurityFilter.firewall.strongMode");

		if (!StringUtil.isBlank(_startFirewall)) {
			startFirewall = Boolean.parseBoolean(_startFirewall);
		}
		if (!StringUtil.isBlank(_strongMode)) {
			strongMode = Boolean.parseBoolean(_strongMode);
			if (strongMode) {
				_excludeChars += "|alert|'| and ";
				// _excludePatterns+="|\\d[+-]\\d";
			}
		}

		if (!StringUtil.isBlank(_validKeyword)) {
			validKeyword = Boolean.parseBoolean(_validKeyword);
		}
		if (!StringUtil.isBlank(_stopCSRF)) {
			stopCSRF = Boolean.parseBoolean(_stopCSRF);
		}

		if (!StringUtil.isBlank(_excludeChars)) {
			excludeChars = _excludeChars.split("\\|");
		}
		if (!StringUtil.isBlank(_ignoreURL)) {
			ignoreURL = _ignoreURL.split("\\|");
		}
		if (!StringUtil.isBlank(_excludePatterns)) {
			String[] regexs = _excludePatterns.split("\\|");
			for (String regex : regexs) {
				excludePatterns.add(Pattern.compile(StringEscapeUtils.unescapeJava(regex)));
			}
		}
		if (!StringUtil.isBlank(_excludeHostAddress)) {
			excludeHostAddress = _excludeHostAddress.split("\\|");
		}

		if (policy == null) {

//			String path = Firewall.class.getClassLoader().getResource("antisamy.xml").getFile(); // antisamy.xml此文件可能大装载会报错，直接抛出异常即可不用理会也行。
//			InputStream is = Firewall.class.getClassLoader().getResourceAsStream("antisamy.xml"); // antisamy.xml此文件可能大装载会报错，直接抛出异常即可不用理会也行。
			try {
				InputStream is = Firewall.class.getClassLoader().getResourceAsStream("antisamy.xml");
				policy = Policy.getInstance(is);
			} catch (PolicyException e) {
				e.printStackTrace();
			}
		}

		instance = new Firewall(startFirewall, validKeyword, excludeChars, excludePatterns, stopCSRF,
				excludeHostAddress, ignoreURL, strongMode);
	}

	/**
	 * 执行防火墙拦截
	 *
	 * @param hreq
	 * @param resp
	 * @throws Exception
	 */
	public boolean excute(HttpServletRequest hreq, HttpServletResponse resp) throws Exception {

		if (!startFirewall)
			return true;

		String uri = hreq.getRequestURI().toLowerCase();
		//AppScan Allaire JRun 2.3.X 样本源代码泄露
		if(uri.contains("viewsource.jsp")) {
			resp.setStatus(403);
			resp.setCharacterEncoding("UTF-8");
			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().write("非法操作，viewsource.jsp错误！");
			resp.getWriter().flush();
			resp.getWriter().close();
			return false;
		}
		String queryStr = hreq.getQueryString() != null ? hreq.getQueryString().toLowerCase() : "";
		if(queryStr.indexOf("password=") == 0  || queryStr.indexOf("&password=") > 0) {
			resp.setStatus(403);
			resp.setCharacterEncoding("UTF-8");
			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().write("非法操作，密码不能通过get请求传输！");
			resp.getWriter().flush();
			resp.getWriter().close();
			return false;
		}

		String password = hreq.getParameter("password");
		if(password != null && password.trim().length() <= 6) {
			resp.setStatus(403);
			resp.setCharacterEncoding("UTF-8");
			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().write("非法操作，密码必须通过加密方式传输！");
			resp.getWriter().flush();
			resp.getWriter().close();
			return false;
		}

		String requestMethod=hreq.getMethod().toUpperCase();
		if( !(requestMethod.equals("GET") || requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals("DELETE") || requestMethod.equals("PATCH") )) {
			resp.setStatus(403);
			resp.setCharacterEncoding("UTF-8");
			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().write("非法操作，不合法的请求方式！requestMethod");
			resp.getWriter().flush();
			resp.getWriter().close();
			return false;
		}

		if (stopCSRF) {
			String referer = hreq.getHeader("REFERER");
			boolean flag = true;
			if (referer != null) {
				String hostAddress=hreq.getScheme()+"://" + hreq.getServerName()+ ":" + hreq.getServerPort();
				if ( referer.startsWith(hostAddress.trim())) {
					flag = false;
				}else{
					if (excludeHostAddress != null) {
						for (String address : excludeHostAddress) {
							if (address == null || referer.startsWith(address.trim())) {
								flag = false;
								break;
							}
						}
					}
				}
				// int port = hreq.getServerPort();
				// String basePath = hreq.getScheme() + "://" +
				// hreq.getServerName()
				// + (port==80 || port==443? "":":" + hreq.getServerPort());

			} else {
//				String uri = hreq.getRequestURI().toLowerCase();
				if (uri.contains("login.jsp") || uri.contains("/phone/main.jsp") || uri.contains("/pm/wap/index.jsp")
						|| uri.contains("/qm/wap/center.jsp") || uri.contains("/wap/pendlist.jsp")
						|| uri.contains("/attendance/sign.jsp") || uri.contains("/attendance/wap/record.jsp")
						|| uri.contains("/contacts/index.jsp") || uri.contains("/km/wap/index.jsp")
						|| uri.contains("/runtime/app/") || (!uri.contains(".jsp") && !uri.contains(".action"))) {
					flag = false;
				}
			}
			if (flag) {
				resp.setStatus(403);
				resp.setCharacterEncoding("UTF-8");
				resp.setContentType("text/html; charset=UTF-8");
				resp.getWriter().write("非法操作，出于安全考虑系统不允许跨域请求！(security.properties中的防火墙配置文件)");
				resp.getWriter().flush();
				resp.getWriter().close();
				return false;
			}
		}
		// 请求参数关键字拦截
		if (validKeyword) {
//			String uri = hreq.getRequestURI().toLowerCase();
			if (isIgnoreUri(uri)) {
				return true;
			}

			Map<String, String[]> params = hreq.getParameterMap();
			StringBuffer queryString = new StringBuffer();
			for (String key : params.keySet()) {
				String[] values = params.get(key);
				for (int i = 0; i < values.length; i++) {
					String value = values[i];
					queryString.append(key + "=" + value + "&");
				}
			}
			// 去掉最后一个空格
			if (queryString.length() > 1) {
				queryString.setLength(queryString.length() - 1);
			}

			if (!checkParametersLegal(uri + "?" + queryString.toString())) {
				resp.setStatus(403);
				resp.setCharacterEncoding("UTF-8");
				resp.setContentType("text/html; charset=UTF-8");
				resp.getWriter().write("你提交或打开的链接里可能存在非法字符,请不要在参数中包含非法字符尝试注入攻击！");
				resp.getWriter().flush();
				resp.getWriter().close();
				return false;
			}
		}

		return true;
	}

	public boolean isIgnoreUri(String uri) {
		if(StringUtils.isBlank(uri)) {
			return false;
		}
//		if(uri.indexOf(".") == -1) {
//			return false;
//		}
//		if(  uri.indexOf(".jsp") == -1 &&  uri.indexOf(".action") == -1 ) {
//			return true;
//		}
		for (int i = 0; i < ignoreURL.length; i++) {
			if (StringUtil.isBlank(ignoreURL[i]))
				continue;

			if (uri.contains(ignoreURL[i].toLowerCase().trim()) || uri.contains("import")) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 检查请求参数的合法性
	 * <p>
	 * 如果请求参数包含非法字符，返回false，合法则返回true
	 * </p>
	 *
	 * @param queryString
	 * @return
	 */
	private boolean checkParametersLegal(String queryString) {
		if (StringUtil.isBlank(queryString))
			return true;
		queryString = queryString.toLowerCase().trim();

		Matcher m = cp.matcher(queryString);
		String _queryString = m.replaceAll("");
		try {
			_queryString = URLDecoder.decode(_queryString, "utf-8");
		} catch (Exception e1) {
		}

		AntiSamy as = new AntiSamy();
		try {
			CleanResults cleanResults = as.scan(_queryString, policy);
			List<String> errorMessages = cleanResults.getErrorMessages();
			if (!errorMessages.isEmpty()) {
				return false;
			}
		} catch (Exception e) {
		}
		for (Pattern pattern : excludePatterns) {
			if (pattern.matcher(_queryString).find()) {
				return false;
			}
		}

		try {
			queryString = URLDecoder.decode(queryString, "utf-8");
		} catch (UnsupportedEncodingException e1) {
		}
		for (int i = 0; i < excludeChars.length; i++) {
			String excludeChar = excludeChars[i];
			if (StringUtil.isBlank(excludeChar))
				continue;
			if (queryString.contains(excludeChar)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * 获取服务器地址
	 *
	 * @param request
	 * @return
	 */
	private String getServerHost(HttpServletRequest request) {
		StringBuilder host = new StringBuilder();
		host.append(request.getScheme()).append("://").append(request.getServerName());

		if (request.getServerPort() != 80) {
			host.append(":").append(request.getServerPort());
		}
		return host.toString();
	}

	/**
	 * @return the startFirewall
	 */
	public boolean isStartFirewall() {
		return startFirewall;
	}

	/**
	 * @return the stopCSRF
	 */
	public boolean isStopCSRF() {
		return stopCSRF;
	}

	/**
	 * @return the strongMode
	 */
	public boolean isStrongMode() {
		return strongMode;
	}

}
