package com.eactive.eai.rms.common.filter;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.lang3.StringUtils;

class RequestWrapper extends HttpServletRequestWrapper {

	// GS인증 크로스사이트스크립트 보안성 문제로 XSS 공격을 방지하기 위한 악성 스크립트 패턴 정의.
	// 2025.05.27 패턴 추가
	// 2025.12.30 src 패턴 수정 - 화이트리스트 방식 (http, https, data:image/ 만 허용)
	private static final Pattern[] PATTERNS = new Pattern[] {
			Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
			Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
			Pattern.compile("<script(.*?)>",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("eval\\((.*?)\\)",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("expression\\((.*?)\\)",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
			Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
			Pattern.compile("onload(.*?)=",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("onmouseover(.*?)=",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("onmouseout(.*?)=",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("onclick(.*?)=",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("onfocus(.*?)=",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("onblur(.*?)=",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
			Pattern.compile("onerror(.*?)=",
									Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
	};

	// src 속성 화이트리스트 패턴 - data:image/만 허용
	private static final Pattern SRC_ATTR_PATTERN = Pattern.compile(
			"(src\\s*=\\s*)([\"'])([^\"']*?)\\2",
			Pattern.CASE_INSENSITIVE);

	// 허용되는 src 값 패턴 (화이트리스트) - data:image/만 허용
	private static final Pattern ALLOWED_SRC_PATTERN = Pattern.compile(
			"^data:image/.*",
			Pattern.CASE_INSENSITIVE);

	// XSS 문자 변환을 건너뛸 파라미터 목록 (리치 텍스트 에디터 콘텐츠, 메시지 템플릿 본문 등)
	// XSS 패턴 필터링은 적용하지만 &lt;, &gt;, &#38; 등으로 변환하지 않음
	// (메시지 템플릿은 SMS/메신저 평문으로 전송되므로 '&'가 '&#38;'로 깨지면 안 됨)
	private static final String[] SKIP_CHAR_CONVERT_PARAMS = {"contents", "content", "description",
			"subjectTemplate", "smsTemplate", "emailTemplate", "messengerTemplate"};

	// 현재 처리 중인 파라미터명 (cleanXSS에서 문자 변환 여부 결정에 사용)
	private ThreadLocal<String> currentParameter = new ThreadLocal<>();

	public RequestWrapper(HttpServletRequest servletRequest) {
		super(servletRequest);
	}

	@Override
	public String[] getParameterValues(String parameter) {
		String[] values = super.getParameterValues(parameter);

		if (values == null) { return values; }

		int count = values.length;
		String[] encodedValues = new String[count];

		currentParameter.set(parameter);
		try {
			for (int i = 0; i < count; i++) {
				encodedValues[i] = cleanXSS(values[i]);
			}
		} finally {
			currentParameter.remove();
		}

		return encodedValues;
	}

	@Override
	public Map<String, String[]> getParameterMap() {
		Map<String, String[]> map = super.getParameterMap();
		Map<String, String[]> clonedMap = new HashMap<>();

		for (Entry<String, String[]> entry : map.entrySet()) {
			String[] values = entry.getValue();
			String paramName = entry.getKey();

			if (values != null && values.length > 0) {
				String[] cloneValues = new String[values.length];
				currentParameter.set(paramName);
				try {
					for (int idx = 0; idx < values.length; idx++) {
						cloneValues[idx] = cleanXSS(values[idx]);
					}
				} finally {
					currentParameter.remove();
				}
				clonedMap.put(paramName, cloneValues);
			}
		}

		return clonedMap;
	}

	@Override
	public String getParameter(String parameter) {
		String value = super.getParameter(parameter);

		if (value == null || StringUtils.equals(parameter, "gridData")) {
			return value;
		}

		currentParameter.set(parameter);
		try {
			return cleanXSS(value);
		} finally {
			currentParameter.remove();
		}
	}

	@Override
	public String getHeader(String name) {
		String value = super.getHeader(name);

		if (value == null || StringUtils.equals(name, "gridData")) {
			return value;
		}

		return cleanXSS(value);
	}

	private String cleanXSS(String value) {
		if (value == null) { return null; }

		// 추가적인 정규 표현식을 이용한 필터링
		for (Pattern scriptPattern : PATTERNS) {
			Matcher matcher = scriptPattern.matcher(value);
			value = matcher.replaceAll("");
		}

		// <script> 태그를 제거
		value = value.replaceAll("(?i)<script.*?>.*?</script>", "");

		// script 문자열을 완전히 제거
		value = value.replaceAll("(?i)\\bscript\\b", "");

		// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
		value = filterSrcAttribute(value);

		// 리치 텍스트 파라미터는 문자 변환을 건너뛰고 XSS 패턴 필터링만 적용
		if (!shouldSkipCharConvert()) {
			StringBuilder sb = new StringBuilder();
			value = convertChars(value, sb);
		}

		// eval, javascript 등 스크립트 관련 문자열 제거
		value = value.replaceAll("(?i)eval\\((.*?)\\)", "");
		value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*?)\\)", "\"\"");
		return value;
	}

	/**
	 * 현재 파라미터가 문자 변환을 건너뛸지 여부 확인
	 * 리치 텍스트 에디터 콘텐츠 등은 HTML 태그가 필요하므로 &lt; &gt; 변환을 하지 않음
	 */
	private boolean shouldSkipCharConvert() {
		String param = currentParameter.get();
		if (param == null) {
			return false;
		}
		for (String skipParam : SKIP_CHAR_CONVERT_PARAMS) {
			if (StringUtils.equalsIgnoreCase(param, skipParam)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * src 속성을 화이트리스트 방식으로 필터링
	 * 허용: data:image/ (Base64 이미지)
	 * 비허용: 그 외 모든 프로토콜
	 */
	private String filterSrcAttribute(String value) {
		if (value == null || !value.toLowerCase().contains("src")) {
			return value;
		}

		Matcher matcher = SRC_ATTR_PATTERN.matcher(value);
		StringBuffer result = new StringBuffer();

		while (matcher.find()) {
			String srcPrefix = matcher.group(1);  // src=
			String quote = matcher.group(2);      // " 또는 '
			String srcValue = matcher.group(3);   // src 값

			// 빈 값이거나 화이트리스트에 매칭되면 유지
			if (srcValue.isEmpty() || ALLOWED_SRC_PATTERN.matcher(srcValue).matches()) {
				matcher.appendReplacement(result, Matcher.quoteReplacement(srcPrefix + quote + srcValue + quote));
			} else {
				// 허용되지 않는 프로토콜은 src="" 로 대체
				matcher.appendReplacement(result, Matcher.quoteReplacement(srcPrefix + quote + quote));
			}
		}
		matcher.appendTail(result);

		return result.toString();
	}

	// '(', ')', '"' 문자는 getMethod()가 "get"일 때만 변환됨.
	// 이는 gridData에서 사용되는 큰따옴표(")와 변환 함수에서 사용하는 괄호('(', ')')의 처리를 위함.
	private String convertChars(String value, StringBuilder sb) {

		for (char c : value.toCharArray()) {

			switch (c) {
			case '<':
				sb.append("&lt;");
				break;
			case '>':
				sb.append("&gt;");
				break;
			case '(':
				convertGetMethod(c, "&#40;", sb);
				break;
			case ')':
				convertGetMethod(c, "&#41;", sb);
				break;
			case '"':
				convertGetMethod(c, "&quot;", sb);
				break;
			case '#':
				sb.append("&#35;");
				break;
			case '&':
				sb.append("&#38;");
				break;
			case '\'':
				sb.append("&#39;");
				break;
			default:
				sb.append(c);
			}

		}

		return sb.toString();
	}

	private void convertGetMethod(char value, String convertValue,
							StringBuilder sb) {

		if (StringUtils.equalsIgnoreCase(getMethod(), "get")) {
			sb.append(convertValue);
		} else {
			sb.append(value);
		}

	}
}
