Captcha.java

package sk.iway.iwcm.system.captcha;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Locale;

import javax.imageio.ImageIO;
import javax.net.ssl.HttpsURLConnection;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.octo.captcha.service.CaptchaServiceException;

import sk.iway.iwcm.Constants;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.i18n.Prop;

/**
 *  Captcha.java
 *
 *@Title        webjet4
 *@Company      Interway s.r.o. (www.interway.sk)
 *@Copyright    Interway s.r.o. (c) 2001-2010
 *@author       $Author: jeeff $
 *@version      $Revision: 1.1 $
 *@created      Date: 2.2.2010 10:57:33
 *@modified     $Date: 2010/02/09 08:56:19 $
 */
public class Captcha
{
	private static String captchaDiv = "<div class=\"g-recaptcha\"id=\"wjReCaptcha\"></div><script src=\"https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit\"async defer></script><script type=\"text/javascript\">var reCaptchaWidgetId=-1;function isReCaptchaValid(){return serverRequest(false)}function serverRequest(setId){var isValid=false;var captchaId=$('#g-recaptcha-response').val();var url='/components/form/re_captcha_ajax.jsp';if(setId)url='/components/form/set_re_catpcha_ajax.jsp';$.ajax({type:'POST',url:url,data:{capchaId:captchaId},success:function(data){if(data.trim()=='OK')isValid=true;else grecaptcha.reset(reCaptchaWidgetId)},async:false});return isValid}var verifyCallback=function(response){serverRequest(true)};var onloadCallback=function(){reCaptchaWidgetId=grecaptcha.render('wjReCaptcha',{'sitekey':'"+Constants.getString("reCaptchaSiteKey")+"','callback':verifyCallback,'theme':'light'})};</script>";
	/**
	 * Vrati true ak je pre zadanu komponentu vyzadovana captcha (nastavuje sa konfiguracnou premennou captchaComponents ako zoznam oddeleny medzerami)
	 * @param component
	 * @return
	 */
	public static boolean isRequired(String component)
	{
		if (Tools.isEmpty(component)) return true;

		if ((" "+Constants.getString("captchaComponents")+" ").indexOf(" "+component+" ")!=-1 || (","+Constants.getString("captchaComponents")+",").indexOf(","+component+",")!=-1) return true;
		return false;
	}

	/**
	 * Zvaliduje a VYMAZE odpoved (malo by sa pouzit vo finalnej validacii na strane servera)
	 * @param httpServletRequest
	 * @param response
	 * @param component - meno komponenty, alebo null ak kontrolu na povolenie komponenty nie je potrebne vykonat
	 * @return
	 */
	public static boolean validateResponse(HttpServletRequest httpServletRequest, String response, String component)
	{
		try
		{
			if (component != null && isRequired(component)==false)
				return true;
			if("invisible".equals(Constants.getString("captchaType"))) {
				if (response == null) response = httpServletRequest.getParameter("g-recaptcha-response");
				return reCaptchaValidate(response);
			}
			else if ("reCaptcha".equals(Constants.getString("captchaType")))
			{
				return reCaptchaValidate(httpServletRequest.getSession().getAttribute("sessionId"));
			}
			else if ("reCaptchaV3".equals(Constants.getString("captchaType")))
			{
				return reCaptchaV3Validate(httpServletRequest);
			}
			//preber si priamo wjcaptcha parameter
            if (Tools.isEmpty(response)) response = httpServletRequest.getParameter("wjcaptcha");
			if (Tools.isEmpty(response)) return false;

			String captchaId = httpServletRequest.getSession().getId();
			return CaptchaServiceSingleton.getInstance().validateResponseForID(captchaId, response.toUpperCase());
		}
		catch (Exception e)
		{

		}
		return false;
	}

	/** Zisti, ci je reCaptcha spravna
	 *
	 * @return
	 */
	private static boolean reCaptchaValidate(Object capchaId)
	{
		if(capchaId == null || Tools.isEmpty(capchaId+""))
		{
			Logger.debug(Captcha.class, "ReCaptcha BAD id: "+capchaId);
			return false;
		}

		try
		{
			String url = "https://www.google.com/recaptcha/api/siteverify";
			URL obj = new URL(url);
			HttpsURLConnection con = (HttpsURLConnection) obj.openConnection();

			con.setRequestMethod("POST");
			con.setRequestProperty("User-Agent", null);
			con.setRequestProperty("Accept-Language", "en-US,en;q=0.5");

			String urlParameters = "secret="+Constants.getString("reCaptchaSecret")+"&response="+capchaId;

			con.setDoOutput(true);
			DataOutputStream wr = new DataOutputStream(con.getOutputStream());
			wr.writeBytes(urlParameters);
			wr.flush();
			wr.close();

			int responseCode = con.getResponseCode();
			if(responseCode != 200)
			{
				Logger.debug(null, "Sending 'POST' request to URL : " + url);
				Logger.debug(null, "Post parameters : " + urlParameters);
				Logger.debug(null, "Response Code : " + responseCode);
			}

			BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
			String inputLine;
			StringBuffer postResponse = new StringBuffer();

			while ((inputLine = in.readLine()) != null) {
				postResponse.append(inputLine);
			}
			in.close();
			if(postResponse.toString().indexOf("\"success\": true") != -1)
			{
				Logger.debug(Captcha.class, "ReCaptcha OK id: "+capchaId);
				return true;
			}
			else if(postResponse.toString().indexOf("\"success\": false") != -1)
			{
				Logger.debug(Captcha.class, "ReCaptcha BAD id: "+capchaId);
				return false;
			}
			else
			{
				Logger.debug(Captcha.class, "ReCaptcha Response Error id: "+capchaId);
				Logger.debug(Captcha.class, postResponse.toString());
			}
		}
		catch(Exception ex)
		{
			Logger.debug(Captcha.class,"ReCaptcha Verify Error id: "+capchaId);
			sk.iway.iwcm.Logger.error(ex);
		}
		return false;
	}

	private static boolean reCaptchaV3Validate(HttpServletRequest request) {
		String token = Tools.getParameter(request, "g-recaptcha-response");
		if (Tools.isEmpty(token)) {
			Logger.debug(Captcha.class, "reCaptchaV3Validate - Google token empty");
			return false;
		}

		String ip = Tools.getRemoteIP(request);
		if (Tools.isEmpty(ip)) {
			Logger.debug("reCaptchaV3Validate - ip empty");
			return false;
		}

		RestTemplate restTemplate = new RestTemplate();

		ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
		restTemplate.setRequestFactory(factory);

		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

		MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
		map.add("secret", Constants.getString("reCaptchaSecret"));
		map.add("response", token);
		map.add("remoteip", ip);

		HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);

		ResponseEntity<GoogleRecaptchaResponse> response =
				restTemplate.exchange("https://www.google.com/recaptcha/api/siteverify",
						HttpMethod.POST,
						entity,
						GoogleRecaptchaResponse.class);

		double minimalScore = Tools.getDoubleValue(Constants.getString("recaptchaMinScore"), 0.5);

		if (response.getStatusCode() != HttpStatus.OK) {
			Logger.debug("reCaptchaV3Validate - Google response status not ok: {}", response.getStatusCode().name());
			return false;
		}

		GoogleRecaptchaResponse body = response.getBody();
		if (body == null) {
			Logger.debug("reCaptchaV3Validate - Google response body null");
			return false;
		}

		if (!body.isSuccess()) {
			Logger.debug("reCaptchaV3Validate - Google response success false, error codes: {}", Tools.join(body.getErrorCodes(), ", "));
			return false;
		}

		if (body.getScore() < minimalScore) {
			Logger.debug("reCaptchaV3Validate - Google response score: "+body.getScore()+", minimal score: "+minimalScore);
			return false;
		}

		return true;
	}

	/**
	 * Overi spravnost odpovede voci obrazku, pouziva sa opakovane cez AJAX volanie (viz check_form_impl.jsp)
	 * NEPOUZIVAT V JAVA KODE NA OVERENIE SPRAVNOSTI CAPTCHA, na to treba pouzit validateResponse!!!!
	 * @param httpServletRequest
	 * @param response
	 * @return
	 */
	public static boolean isReponseCorrect(HttpServletRequest httpServletRequest, String response)
	{
		if (Tools.isEmpty(response)) return false;

		Boolean isResponseCorrect = Boolean.FALSE;
		// remenber that we need an id to validate!
		String captchaId = httpServletRequest.getSession().getId();
		// retrieve the response
		// Call the Service method
		if("invisible".equals(Constants.getString("captchaType"))) {
			return reCaptchaValidate(response);
		}
		else if ("reCaptcha".equals(Constants.getString("captchaType")))
		{
			return reCaptchaValidate(httpServletRequest.getSession().getAttribute("sessionId"));
		}
		try
		{
			isResponseCorrect = CaptchaServiceSingleton.getInstance().testResponseForID(captchaId, response);
			//Logger.debug(Captcha.class, "ID="+captchaId+", response="+response+", correct="+isResponseCorrect);
		}
		catch (CaptchaServiceException e)
		{
			// should not happen, may be thrown if the id is not valid
			sk.iway.iwcm.Logger.error(e);
		}
		if (isResponseCorrect == null) isResponseCorrect = Boolean.FALSE;
		return isResponseCorrect.booleanValue();
	}

	/**
	 * Vygeneruje JPG obrazok captchy
	 * @param httpServletRequest
	 * @param httpServletResponse
	 * @throws ServletException
	 * @throws IOException
	 */
	public static void getImage(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
	{
		if("invisible".equals(Constants.getString("captchaType")) || "reCaptcha".equals(Constants.getString("captchaType")))// pre reCaptcha nepotrebujeme
			return;

		// the output stream to render the captcha image as jpeg into
		try
		{
			// get the session id that will identify the generated captcha.
			// the same id must be used to validate the response, the session id is
			// a good candidate!
			String captchaId = httpServletRequest.getSession().getId();
			//Logger.debug(Captcha.class, "Generating CAPTCHA for "+captchaId);
			// call the ImageCaptchaService getChallenge method

			ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
			BufferedImage challenge = CaptchaServiceSingleton.getInstance().getImageChallengeForID(captchaId, Locale.ENGLISH);
			ImageIO.write(challenge, "jpg", jpegOutputStream);
			byte[] captchaChallengeAsJpeg = null;
			captchaChallengeAsJpeg = jpegOutputStream.toByteArray();


			// flush it in the response
			httpServletResponse.setHeader("Cache-Control", "no-store");
			httpServletResponse.setHeader("Pragma", "no-cache");
			httpServletResponse.setDateHeader("Expires", 0);
			httpServletResponse.setContentType("image/jpeg");
			ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();


			responseOutputStream.write(captchaChallengeAsJpeg);

			//use imageio to write image
			//ImageIO.write(challenge, "jpeg", responseOutputStream);
			responseOutputStream.flush();
			responseOutputStream.close();
		}
		catch (CaptchaServiceException e)
		{
			httpServletResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			return;
		}
	}

	/**
	 * Vrati HTML kod captcha obrazku
	 * @param request
	 * @return
	 */
	public static String getImage(HttpServletRequest request)
	{
		Prop prop = Prop.getInstance(request);
		if("invisible".equals(Constants.getString("captchaType")) || "reCaptcha".equals(Constants.getString("captchaType")))
			return captchaDiv;
		return ("<img id=\"wjcaptchaImg1\" class=\"captchaImage\" src=\"/captcha.jpg\" onclick=\"this.src='/captcha.jpg?rnd='+(new Date().getTime());\" style=\"cursor: pointer;\" alt=\""+prop.getText("captcha.imageAlt")+"\" title=\""+prop.getText("captcha.imageAlt")+"\"/>");
	}
}