From a503bb48c136ff40244ebdd868573077d4e12f2b Mon Sep 17 00:00:00 2001 From: Ivan Mrsulja Date: Mon, 20 Nov 2023 18:29:07 +0100 Subject: [PATCH 01/29] Added captcha generation using nano captcha library. Added initial validation logic. --- api/pom.xml | 5 + .../vitro/webapp/beans/CaptchaBundle.java | 48 ++ .../webapp/beans/CaptchaServiceBean.java | 79 +++ .../freemarker/ContactFormController.java | 11 +- .../freemarker/ContactMailController.java | 474 ++++++++++-------- .../body/contactForm/contactForm-form.ftl | 16 +- 6 files changed, 400 insertions(+), 233 deletions(-) create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java diff --git a/api/pom.xml b/api/pom.xml index deebfb9e14..6b40d67307 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -65,6 +65,11 @@ argon2-jvm 2.11 + + net.logicsquad + nanocaptcha + 1.5 + org.apache.httpcomponents fluent-hc diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java new file mode 100644 index 0000000000..a8c58c14ec --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java @@ -0,0 +1,48 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.util.Objects; + +public class CaptchaBundle { + + private final String b64Image; + + private final String code; + + private final String challengeId; + + + public CaptchaBundle(String b64Image, String code, String challengeId) { + this.b64Image = b64Image; + this.code = code; + this.challengeId = challengeId; + } + + public String getB64Image() { + return b64Image; + } + + public String getCode() { + return code; + } + + public String getCaptchaId() { + return challengeId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CaptchaBundle that = (CaptchaBundle) o; + return Objects.equals(code, that.code) && Objects.equals(challengeId, that.challengeId); + } + + @Override + public int hashCode() { + return Objects.hash(code, challengeId); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java new file mode 100644 index 0000000000..6285692342 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java @@ -0,0 +1,79 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.awt.Color; +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Base64; +import java.util.UUID; + +import javax.imageio.ImageIO; + +import net.logicsquad.nanocaptcha.image.ImageCaptcha; +import net.logicsquad.nanocaptcha.image.backgrounds.GradiatedBackgroundProducer; +import net.logicsquad.nanocaptcha.image.filter.FishEyeImageFilter; +import net.logicsquad.nanocaptcha.image.filter.StretchImageFilter; +import net.logicsquad.nanocaptcha.image.noise.CurvedLineNoiseProducer; +import net.logicsquad.nanocaptcha.image.noise.StraightLineNoiseProducer; + +public class CaptchaServiceBean { + + public static CaptchaBundle generateChallenge() throws IOException { + ImageCaptcha imageCaptcha = + new ImageCaptcha.Builder(200, 75) + .addContent(5) + .addBackground(new GradiatedBackgroundProducer()) + .addNoise(new CurvedLineNoiseProducer(getRandomColor(), 2f)) + .addNoise(new StraightLineNoiseProducer(getRandomColor(), 2)) + .addFilter(new StretchImageFilter()) + .addFilter(new FishEyeImageFilter()) + .addBorder() + .build(); + return new CaptchaBundle(convertToBase64(imageCaptcha.getImage()), imageCaptcha.getContent(), + UUID.randomUUID().toString()); + } + + private static String convertToBase64(BufferedImage image) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, "png", baos); + byte[] imageBytes = baos.toByteArray(); + + return Base64.getEncoder().encodeToString(imageBytes); + } + + private static Color getRandomColor() { + SecureRandom random = new SecureRandom(); + int r = random.nextInt(256); + int g = random.nextInt(256); + int b = random.nextInt(256); + return new Color(r, g, b); + } + + private static ArrayList getRandomColors(int count) { + ArrayList randomFontList = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + randomFontList.add(getRandomColor()); + } + + return randomFontList; + } + + private static ArrayList getRandomFonts(int count) { + Font[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); + + ArrayList randomFontList = new ArrayList<>(); + SecureRandom random = new SecureRandom(); + + for (int i = 0; i < count; i++) { + int randomIndex = random.nextInt(fonts.length); + randomFontList.add(fonts[randomIndex]); + } + + return randomFontList; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java index 9c33959562..361e62fdaa 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java @@ -2,9 +2,13 @@ package edu.cornell.mannlib.vitro.webapp.controller.freemarker; +import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.UUID; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -36,7 +40,7 @@ protected String getTitle(String siteName, VitroRequest vreq) { } @Override - protected ResponseValues processRequest(VitroRequest vreq) { + protected ResponseValues processRequest(VitroRequest vreq) throws IOException { ApplicationBean appBean = vreq.getAppBean(); @@ -59,6 +63,11 @@ else if (StringUtils.isBlank(appBean.getContactMail())) { else { + CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); + ContactMailController.getCaptchaChallenges().add(captchaChallenge); + + body.put("challenge", captchaChallenge.getB64Image()); + body.put("challengeId", captchaChallenge.getCaptchaId()); body.put("formAction", "submitFeedback"); if (vreq.getHeader("Referer") == null) { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java index 1444e9115b..c08f0f1041 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java @@ -7,10 +7,14 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import javax.mail.Address; import javax.mail.Message; @@ -24,28 +28,29 @@ import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; import edu.cornell.mannlib.vitro.webapp.beans.ApplicationBean; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.TemplateProcessingHelper.TemplateProcessingException; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; import edu.cornell.mannlib.vitro.webapp.email.FreemarkerEmailFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; @WebServlet(name = "sendMail", urlPatterns = {"/submitFeedback"}, loadOnStartup = 5) public class ContactMailController extends FreemarkerHttpServlet { - private static final Log log = LogFactory - .getLog(ContactMailController.class); + private static final Log log = LogFactory + .getLog(ContactMailController.class); private static final long serialVersionUID = 1L; - private final static String SPAM_MESSAGE = "Your message was flagged as spam."; + private final static String SPAM_MESSAGE = "Your message was flagged as spam."; - private final static String WEB_USERNAME_PARAM = "webusername"; + private final static String WEB_USERNAME_PARAM = "webusername"; private final static String WEB_USEREMAIL_PARAM = "webuseremail"; - private final static String COMMENTS_PARAM = "s34gfd88p9x1"; + private final static String COMMENTS_PARAM = "s34gfd88p9x1"; private final static String TEMPLATE_CONFIRMATION = "contactForm-confirmation.ftl"; private final static String TEMPLATE_EMAIL = "contactForm-email.ftl"; @@ -53,135 +58,136 @@ public class ContactMailController extends FreemarkerHttpServlet { private final static String TEMPLATE_ERROR = "contactForm-error.ftl"; private final static String TEMPLATE_FORM = "contactForm-form.ftl"; - private static final String EMAIL_JOURNAL_FILE_DIR = "emailJournal"; - private static final String EMAIL_JOURNAL_FILE_NAME = "contactFormEmails.html"; + private static final String EMAIL_JOURNAL_FILE_DIR = "emailJournal"; + private static final String EMAIL_JOURNAL_FILE_NAME = "contactFormEmails.html"; - @Override + private static List captchaChallenges = Collections.synchronizedList(new ArrayList<>()); + + @Override protected String getTitle(String siteName, VitroRequest vreq) { return siteName + " Feedback Form"; } @Override - protected ResponseValues processRequest(VitroRequest vreq) { - if (!FreemarkerEmailFactory.isConfigured(vreq)) { - return errorNoSmtpServer(); - } - - String[] recipients = figureRecipients(vreq); - if (recipients.length == 0) { - return errorNoRecipients(); - } - - String webusername = nonNullAndTrim(vreq, WEB_USERNAME_PARAM); - String webuseremail = nonNullAndTrim(vreq, WEB_USEREMAIL_PARAM); - String comments = nonNullAndTrim(vreq, COMMENTS_PARAM); - String formType = nonNullAndTrim(vreq, "DeliveryType"); - String captchaInput = nonNullAndTrim(vreq, "defaultReal"); - String captchaDisplay = nonNullAndTrim(vreq, "defaultRealHash"); - - String errorMsg = validateInput(webusername, webuseremail, comments, captchaInput, captchaDisplay); - - if ( errorMsg != null) { - return errorParametersNotValid(errorMsg, webusername, webuseremail, comments); - } - - String spamReason = checkForSpam(comments, formType); - if (spamReason != null) { - return errorSpam(); - } - - return processValidRequest(vreq, webusername, webuseremail, recipients, comments); - } - - private String[] figureRecipients(VitroRequest vreq) { - String contactMailAddresses = vreq.getAppBean().getContactMail().trim(); - if ((contactMailAddresses == null) || contactMailAddresses.isEmpty()) { - return new String[0]; - } - - return contactMailAddresses.split(","); - } - - private ResponseValues processValidRequest(VitroRequest vreq, - String webusername, String webuseremail, String[] recipients, - String comments) throws Error { - String statusMsg = null; // holds the error status - - ApplicationBean appBean = vreq.getAppBean(); - String deliveryfrom = "Message from the " + appBean.getApplicationName() + " Contact Form"; - - String originalReferer = getOriginalRefererFromSession(vreq); - - String msgText = composeEmail(webusername, webuseremail, comments, - deliveryfrom, originalReferer, vreq.getRemoteAddr(), vreq); - - try { - // Write the message to the journal file - FileWriter fw = new FileWriter(locateTheJournalFile(), true); - PrintWriter outFile = new PrintWriter(fw); - writeBackupCopy(outFile, msgText, vreq); - - try { - // Send the message - Session s = FreemarkerEmailFactory.getEmailSession(vreq); - sendMessage(s, webuseremail, webusername, recipients, deliveryfrom, msgText); - } catch (AddressException e) { - statusMsg = "Please supply a valid email address."; - outFile.println( statusMsg ); - outFile.println( e.getMessage() ); - } catch (SendFailedException e) { - statusMsg = "The system was unable to deliver your mail. Please try again later. [SEND FAILED]"; - outFile.println( statusMsg ); - outFile.println( e.getMessage() ); - } catch (MessagingException e) { - statusMsg = "The system was unable to deliver your mail. Please try again later. [MESSAGING]"; - outFile.println( statusMsg ); - outFile.println( e.getMessage() ); - e.printStackTrace(); - } - - outFile.close(); - } - catch (IOException e){ - log.error("Can't open file to write email backup"); - } - - if (statusMsg == null) { - // Message was sent successfully - return new TemplateResponseValues(TEMPLATE_CONFIRMATION); - } else { - Map body = new HashMap(); - body.put("errorMessage", statusMsg); - return new TemplateResponseValues(TEMPLATE_ERROR, body); - } - } - - /** - * The journal file belongs in a sub-directory of the Vitro home directory. - * If the sub-directory doesn't exist, create it. - */ - private File locateTheJournalFile() { - File homeDir = ApplicationUtils.instance().getHomeDirectory().getPath().toFile(); - File journalDir = new File(homeDir, EMAIL_JOURNAL_FILE_DIR); - if (!journalDir.exists()) { - boolean created = journalDir.mkdir(); - if (!created) { - throw new IllegalStateException( - "Unable to create email journal directory at '" - + journalDir + "'"); - } - } - - File journalFile = new File(journalDir, EMAIL_JOURNAL_FILE_NAME); - return journalFile; - } - - - private String getOriginalRefererFromSession(VitroRequest vreq) { - String originalReferer = (String) vreq.getSession().getAttribute("contactFormReferer"); - if (originalReferer != null) { - vreq.getSession().removeAttribute("contactFormReferer"); + protected ResponseValues processRequest(VitroRequest vreq) throws IOException { + if (!FreemarkerEmailFactory.isConfigured(vreq)) { + return errorNoSmtpServer(); + } + + String[] recipients = figureRecipients(vreq); + if (recipients.length == 0) { + return errorNoRecipients(); + } + + String webusername = nonNullAndTrim(vreq, WEB_USERNAME_PARAM); + String webuseremail = nonNullAndTrim(vreq, WEB_USEREMAIL_PARAM); + String comments = nonNullAndTrim(vreq, COMMENTS_PARAM); + String formType = nonNullAndTrim(vreq, "DeliveryType"); + String captchaInput = nonNullAndTrim(vreq, "userSolution"); + String captchaId = nonNullAndTrim(vreq, "challengeId"); + + String errorMsg = validateInput(webusername, webuseremail, comments, captchaInput, captchaId); + + if (errorMsg != null) { + return errorParametersNotValid(errorMsg, webusername, webuseremail, comments); + } + + String spamReason = checkForSpam(comments, formType); + if (spamReason != null) { + return errorSpam(); + } + + return processValidRequest(vreq, webusername, webuseremail, recipients, comments); + } + + private String[] figureRecipients(VitroRequest vreq) { + String contactMailAddresses = vreq.getAppBean().getContactMail().trim(); + if ((contactMailAddresses == null) || contactMailAddresses.isEmpty()) { + return new String[0]; + } + + return contactMailAddresses.split(","); + } + + private ResponseValues processValidRequest(VitroRequest vreq, + String webusername, String webuseremail, String[] recipients, + String comments) throws Error { + String statusMsg = null; // holds the error status + + ApplicationBean appBean = vreq.getAppBean(); + String deliveryfrom = "Message from the " + appBean.getApplicationName() + " Contact Form"; + + String originalReferer = getOriginalRefererFromSession(vreq); + + String msgText = composeEmail(webusername, webuseremail, comments, + deliveryfrom, originalReferer, vreq.getRemoteAddr(), vreq); + + try { + // Write the message to the journal file + FileWriter fw = new FileWriter(locateTheJournalFile(), true); + PrintWriter outFile = new PrintWriter(fw); + writeBackupCopy(outFile, msgText, vreq); + + try { + // Send the message + Session s = FreemarkerEmailFactory.getEmailSession(vreq); + sendMessage(s, webuseremail, webusername, recipients, deliveryfrom, msgText); + } catch (AddressException e) { + statusMsg = "Please supply a valid email address."; + outFile.println(statusMsg); + outFile.println(e.getMessage()); + } catch (SendFailedException e) { + statusMsg = "The system was unable to deliver your mail. Please try again later. [SEND FAILED]"; + outFile.println(statusMsg); + outFile.println(e.getMessage()); + } catch (MessagingException e) { + statusMsg = "The system was unable to deliver your mail. Please try again later. [MESSAGING]"; + outFile.println(statusMsg); + outFile.println(e.getMessage()); + e.printStackTrace(); + } + + outFile.close(); + } catch (IOException e) { + log.error("Can't open file to write email backup"); + } + + if (statusMsg == null) { + // Message was sent successfully + return new TemplateResponseValues(TEMPLATE_CONFIRMATION); + } else { + Map body = new HashMap(); + body.put("errorMessage", statusMsg); + return new TemplateResponseValues(TEMPLATE_ERROR, body); + } + } + + /** + * The journal file belongs in a sub-directory of the Vitro home directory. + * If the sub-directory doesn't exist, create it. + */ + private File locateTheJournalFile() { + File homeDir = ApplicationUtils.instance().getHomeDirectory().getPath().toFile(); + File journalDir = new File(homeDir, EMAIL_JOURNAL_FILE_DIR); + if (!journalDir.exists()) { + boolean created = journalDir.mkdir(); + if (!created) { + throw new IllegalStateException( + "Unable to create email journal directory at '" + + journalDir + "'"); + } + } + + File journalFile = new File(journalDir, EMAIL_JOURNAL_FILE_NAME); + return journalFile; + } + + + private String getOriginalRefererFromSession(VitroRequest vreq) { + String originalReferer = (String) vreq.getSession().getAttribute("contactFormReferer"); + if (originalReferer != null) { + vreq.getSession().removeAttribute("contactFormReferer"); /* does not support legitimate clients that don't send the Referer header String referer = request.getHeader("Referer"); if (referer == null || @@ -192,25 +198,28 @@ private String getOriginalRefererFromSession(VitroRequest vreq) { statusMsg = SPAM_MESSAGE; } */ - } else { - originalReferer = "none"; - } - return originalReferer; - } - - /** Intended to mangle url so it can get through spam filtering - * http://host/dir/servlet?param=value -> host: dir/servlet?param=value */ - public String stripProtocol( String in ){ - if( in == null ) + } else { + originalReferer = "none"; + } + return originalReferer; + } + + /** + * Intended to mangle url so it can get through spam filtering + * http://host/dir/servlet?param=value -> host: dir/servlet?param=value + */ + public String stripProtocol(String in) { + if (in == null) { return ""; - else - return in.replaceAll("http://", "host: " ); + } else { + return in.replaceAll("http://", "host: "); + } } private String composeEmail(String webusername, String webuseremail, - String comments, String deliveryfrom, - String originalReferer, String ipAddr, - HttpServletRequest request) { + String comments, String deliveryfrom, + String originalReferer, String ipAddr, + HttpServletRequest request) { Map email = new HashMap(); String template = TEMPLATE_EMAIL; @@ -220,7 +229,7 @@ private String composeEmail(String webusername, String webuseremail, email.put("emailAddress", webuseremail); email.put("comments", comments); email.put("ip", ipAddr); - if ( !(originalReferer == null || originalReferer.equals("none")) ) { + if (!(originalReferer == null || originalReferer.equals("none"))) { email.put("referrer", UrlBuilder.urlDecode(originalReferer)); } @@ -233,13 +242,13 @@ private String composeEmail(String webusername, String webuseremail, } private void writeBackupCopy(PrintWriter outFile, String msgText, - HttpServletRequest request) { + HttpServletRequest request) { Map backup = new HashMap(); String template = TEMPLATE_BACKUP; - Calendar cal = Calendar.getInstance(); - backup.put("datetime", cal.getTime().toString()); + Calendar cal = Calendar.getInstance(); + backup.put("datetime", cal.getTime().toString()); backup.put("msgText", msgText); try { @@ -253,61 +262,61 @@ private void writeBackupCopy(PrintWriter outFile, String msgText, } private void sendMessage(Session s, String webuseremail, String webusername, - String[] recipients, String deliveryfrom, String msgText) - throws AddressException, SendFailedException, MessagingException { + String[] recipients, String deliveryfrom, String msgText) + throws MessagingException { // Construct the message - MimeMessage msg = new MimeMessage( s ); + MimeMessage msg = new MimeMessage(s); //System.out.println("trying to send message from servlet"); // Set the reply address try { - msg.setReplyTo( new Address[] { new InternetAddress( webuseremail, webusername ) } ); + msg.setReplyTo(new Address[] {new InternetAddress(webuseremail, webusername)}); } catch (UnsupportedEncodingException e) { - log.error("Can't set message reply with personal name " + webusername + - " due to UnsupportedEncodingException"); + log.error("Can't set message reply with personal name " + webusername + + " due to UnsupportedEncodingException"); // msg.setFrom( new InternetAddress( webuseremail ) ); } // Set the recipient address - InternetAddress[] address=new InternetAddress[recipients.length]; - for (int i=0; i 0) { - msg.setFrom(address[0]); - } else { - msg.setFrom( new InternetAddress( webuseremail ) ); - } + // Set the from address + if (address != null && address.length > 0) { + msg.setFrom(address[0]); + } else { + msg.setFrom(new InternetAddress(webuseremail)); + } // Set the subject and text - msg.setSubject( deliveryfrom ); + msg.setSubject(deliveryfrom); // add the multipart to the message - msg.setContent(msgText,"text/html; charset=UTF-8"); + msg.setContent(msgText, "text/html; charset=UTF-8"); // set the Date: header - msg.setSentDate( new Date() ); + msg.setSentDate(new Date()); - Transport.send( msg ); // try to send the message via smtp - catch error exceptions + Transport.send(msg); // try to send the message via smtp - catch error exceptions } - private String nonNullAndTrim(HttpServletRequest req, String key) { - String value = req.getParameter(key); - return (value == null) ? "" : value.trim(); - } + private String nonNullAndTrim(HttpServletRequest req, String key) { + String value = req.getParameter(key); + return (value == null) ? "" : value.trim(); + } private String validateInput(String webusername, String webuseremail, - String comments, String captchaInput, String captchaDisplay) { + String comments, String captchaInput, String challengeId) { - if( webusername.isEmpty() ){ + if (webusername.isEmpty()) { return "Please enter a value in the Full name field."; } - if( webuseremail.isEmpty() ){ + if (webuseremail.isEmpty()) { return "Please enter a valid email address."; } @@ -319,11 +328,12 @@ private String validateInput(String webusername, String webuseremail, return "Please enter the contents of the gray box in the security field provided."; } - if ( !captchaHash(captchaInput).equals(captchaDisplay) ) { - return "The value you entered in the security field did not match the letters displayed in the gray box."; - } + Optional optionalChallenge = getChallenge(challengeId); + if (optionalChallenge.isPresent() && optionalChallenge.get().getCode().equals(captchaInput)) { + return null; + } - return null; + return "The value you entered in the security field did not match the letters displayed in the gray box."; } /** @@ -331,22 +341,22 @@ private String validateInput(String webusername, String webuseremail, * containing the reason the message was flagged as spam. */ private String checkForSpam(String comments, String formType) { - /* If the form doesn't specify a delivery type, treat as spam. */ - if (!"contact".equals(formType)) { - return "The form specifies no delivery type."; - } + /* If the form doesn't specify a delivery type, treat as spam. */ + if (!"contact".equals(formType)) { + return "The form specifies no delivery type."; + } /* if this blog markup is found, treat comment as blog spam */ if ( (comments.indexOf("[/url]") > -1 - || comments.indexOf("[/URL]") > -1 - || comments.indexOf("[url=") > -1 - || comments.indexOf("[URL=") > -1)) { + || comments.indexOf("[/URL]") > -1 + || comments.indexOf("[url=") > -1 + || comments.indexOf("[URL=") > -1)) { return "The message contained blog link markup."; } /* if message is absurdly short, treat as blog spam */ - if (comments.length()<15) { + if (comments.length() < 15) { return "The message was too short."; } @@ -354,45 +364,65 @@ private String checkForSpam(String comments, String formType) { } - private String captchaHash(String value) { - int hash = 5381; - value = value.toUpperCase(); - for(int i = 0; i < value.length(); i++) { - hash = ((hash << 5) + hash) + value.charAt(i); - } - return String.valueOf(hash); - } + private String captchaHash(String value) { + int hash = 5381; + value = value.toUpperCase(); + for (int i = 0; i < value.length(); i++) { + hash = ((hash << 5) + hash) + value.charAt(i); + } + return String.valueOf(hash); + } - private ResponseValues errorNoSmtpServer() { + private ResponseValues errorNoSmtpServer() { Map body = new HashMap(); body.put("errorMessage", - "This application has not yet been configured to send mail. " + + "This application has not yet been configured to send mail. " + "Email properties must be specified in the configuration properties file."); - return new TemplateResponseValues(TEMPLATE_ERROR, body); - } - - private ResponseValues errorNoRecipients() { - Map body = new HashMap(); - body.put("errorMessage", "To establish the Contact Us mail capability " - + "the system administrators must specify " - + "at least one email address."); - return new TemplateResponseValues(TEMPLATE_ERROR, body); - } - - private ResponseValues errorParametersNotValid(String errorMsg, String webusername, String webuseremail, String comments) { + return new TemplateResponseValues(TEMPLATE_ERROR, body); + } + + private ResponseValues errorNoRecipients() { + Map body = new HashMap(); + body.put("errorMessage", "To establish the Contact Us mail capability " + + "the system administrators must specify " + + "at least one email address."); + return new TemplateResponseValues(TEMPLATE_ERROR, body); + } + + private ResponseValues errorParametersNotValid(String errorMsg, String webusername, String webuseremail, + String comments) throws IOException { Map body = new HashMap(); - body.put("errorMessage", errorMsg); - body.put("formAction", "submitFeedback"); - body.put("webusername", webusername); - body.put("webuseremail", webuseremail); - body.put("comments", comments); - return new TemplateResponseValues(TEMPLATE_FORM, body); - } - - private ResponseValues errorSpam() { - Map body = new HashMap(); - body.put("errorMessage", SPAM_MESSAGE); - return new TemplateResponseValues(TEMPLATE_ERROR, body); - } + body.put("errorMessage", errorMsg); + body.put("formAction", "submitFeedback"); + body.put("webusername", webusername); + body.put("webuseremail", webuseremail); + body.put("comments", comments); + + CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); + ContactMailController.getCaptchaChallenges().add(captchaChallenge); + body.put("challenge", captchaChallenge.getB64Image()); + body.put("challengeId", captchaChallenge.getCaptchaId()); + + return new TemplateResponseValues(TEMPLATE_FORM, body); + } + + private ResponseValues errorSpam() { + Map body = new HashMap(); + body.put("errorMessage", SPAM_MESSAGE); + return new TemplateResponseValues(TEMPLATE_ERROR, body); + } + private Optional getChallenge(String captchaId) { + for (CaptchaBundle challenge : captchaChallenges) { + if (challenge.getCaptchaId().equals(captchaId)) { + captchaChallenges.remove(challenge); + return Optional.of(challenge); + } + } + return Optional.empty(); + } + + public static List getCaptchaChallenges() { + return captchaChallenges; + } } diff --git a/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl b/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl index 55c0fb688e..34ac683807 100644 --- a/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl +++ b/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl @@ -29,10 +29,12 @@ - -

- -

+

+ + Refresh page if not displayed... + + +


@@ -51,10 +53,4 @@ ${stylesheets.add('', '')} ${scripts.add('', - '', '')} - From 461213d50b5e784d65a85c2cbb2882b6eecfbe93 Mon Sep 17 00:00:00 2001 From: Ivan Mrsulja Date: Mon, 20 Nov 2023 19:08:36 +0100 Subject: [PATCH 02/29] Added variable challenge length. --- .../mannlib/vitro/webapp/beans/CaptchaServiceBean.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java index 6285692342..e498f91f70 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java @@ -22,10 +22,13 @@ public class CaptchaServiceBean { + private static final SecureRandom random = new SecureRandom(); + + public static CaptchaBundle generateChallenge() throws IOException { ImageCaptcha imageCaptcha = new ImageCaptcha.Builder(200, 75) - .addContent(5) + .addContent(random.nextInt(2) + 5) .addBackground(new GradiatedBackgroundProducer()) .addNoise(new CurvedLineNoiseProducer(getRandomColor(), 2f)) .addNoise(new StraightLineNoiseProducer(getRandomColor(), 2)) @@ -46,7 +49,6 @@ private static String convertToBase64(BufferedImage image) throws IOException { } private static Color getRandomColor() { - SecureRandom random = new SecureRandom(); int r = random.nextInt(256); int g = random.nextInt(256); int b = random.nextInt(256); @@ -67,8 +69,6 @@ private static ArrayList getRandomFonts(int count) { Font[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); ArrayList randomFontList = new ArrayList<>(); - SecureRandom random = new SecureRandom(); - for (int i = 0; i < count; i++) { int randomIndex = random.nextInt(fonts.length); randomFontList.add(fonts[randomIndex]); From d06b4001d02a7eb9bd29122f0ef2ff28cc97cca0 Mon Sep 17 00:00:00 2001 From: Ivan Mrsulja Date: Tue, 21 Nov 2023 15:32:16 +0100 Subject: [PATCH 03/29] Added Google reCaptcha. Added simple configuration feature toggle. --- .../webapp/beans/CaptchaServiceBean.java | 78 +++++++++++++------ .../vitro/webapp/beans/ReCaptchaResponse.java | 54 +++++++++++++ .../freemarker/ContactFormController.java | 39 ++++++---- .../freemarker/ContactMailController.java | 78 +++++++++++-------- .../config/example.runtime.properties | 8 ++ .../body/contactForm/contactForm-form.ftl | 30 +++++-- 6 files changed, 215 insertions(+), 72 deletions(-) create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java index e498f91f70..168262358a 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java @@ -9,21 +9,62 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Base64; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import javax.imageio.ImageIO; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import net.logicsquad.nanocaptcha.image.ImageCaptcha; import net.logicsquad.nanocaptcha.image.backgrounds.GradiatedBackgroundProducer; import net.logicsquad.nanocaptcha.image.filter.FishEyeImageFilter; import net.logicsquad.nanocaptcha.image.filter.StretchImageFilter; import net.logicsquad.nanocaptcha.image.noise.CurvedLineNoiseProducer; import net.logicsquad.nanocaptcha.image.noise.StraightLineNoiseProducer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; public class CaptchaServiceBean { private static final SecureRandom random = new SecureRandom(); + private static final Log log = LogFactory.getLog(CaptchaServiceBean.class.getName()); + + private static final ConcurrentHashMap captchaChallenges = new ConcurrentHashMap<>(); + + + public static boolean validateReCaptcha(String recaptchaResponse, VitroRequest vreq) { + String secretKey = + Objects.requireNonNull(ConfigurationProperties.getBean(vreq).getProperty("recaptcha.secretKey"), + "You have to provide a secret key through configuration file."); + String verificationUrl = + "https://www.google.com/recaptcha/api/siteverify?secret=" + secretKey + "&response=" + recaptchaResponse; + + try { + HttpClient httpClient = HttpClients.createDefault(); + HttpGet verificationRequest = new HttpGet(verificationUrl); + + HttpResponse verificationResponse = httpClient.execute(verificationRequest); + String responseBody = EntityUtils.toString(verificationResponse.getEntity()); + ObjectMapper objectMapper = new ObjectMapper(); + ReCaptchaResponse response = objectMapper.readValue(responseBody, ReCaptchaResponse.class); + + return response.isSuccess(); + } catch (IOException e) { + log.warn("ReCaptcha validation failed."); + } + + return false; + } public static CaptchaBundle generateChallenge() throws IOException { ImageCaptcha imageCaptcha = @@ -40,6 +81,21 @@ public static CaptchaBundle generateChallenge() throws IOException { UUID.randomUUID().toString()); } + public static Optional getChallenge(String captchaId, String remoteAddress) { + CaptchaBundle challengeForHost = captchaChallenges.getOrDefault(remoteAddress, new CaptchaBundle("", "", "")); + captchaChallenges.remove(remoteAddress); + + if (!challengeForHost.getCaptchaId().equals(captchaId)) { + return Optional.empty(); + } + + return Optional.of(challengeForHost); + } + + public static ConcurrentHashMap getCaptchaChallenges() { + return captchaChallenges; + } + private static String convertToBase64(BufferedImage image) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(image, "png", baos); @@ -54,26 +110,4 @@ private static Color getRandomColor() { int b = random.nextInt(256); return new Color(r, g, b); } - - private static ArrayList getRandomColors(int count) { - ArrayList randomFontList = new ArrayList<>(); - - for (int i = 0; i < count; i++) { - randomFontList.add(getRandomColor()); - } - - return randomFontList; - } - - private static ArrayList getRandomFonts(int count) { - Font[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); - - ArrayList randomFontList = new ArrayList<>(); - for (int i = 0; i < count; i++) { - int randomIndex = random.nextInt(fonts.length); - randomFontList.add(fonts[randomIndex]); - } - - return randomFontList; - } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java new file mode 100644 index 0000000000..2d8d4d86e5 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java @@ -0,0 +1,54 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.util.Date; + +public class ReCaptchaResponse { + + private boolean success; + + private Date challenge_ts; + + private String hostname; + + public ReCaptchaResponse() { + } + + public ReCaptchaResponse(boolean success, Date challenge_ts, String hostname) { + this.success = success; + this.challenge_ts = challenge_ts; + this.hostname = hostname; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public Date getChallenge_ts() { + return challenge_ts; + } + + public void setChallenge_ts(Date challenge_ts) { + this.challenge_ts = challenge_ts; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + @Override + public String toString() { + return "ReCaptchaResponse{" + + "success=" + success + + ", challenge_ts=" + challenge_ts + + ", hostname='" + hostname + '\'' + + '}'; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java index 361e62fdaa..dce5a23bbc 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java @@ -5,21 +5,21 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import java.util.UUID; +import java.util.Objects; -import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; -import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import javax.servlet.annotation.WebServlet; import edu.cornell.mannlib.vitro.webapp.beans.ApplicationBean; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; import edu.cornell.mannlib.vitro.webapp.email.FreemarkerEmailFactory; - -import javax.servlet.annotation.WebServlet; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; /** * Controller for comments ("contact us") page @@ -34,6 +34,7 @@ public class ContactFormController extends FreemarkerHttpServlet { private static final String TEMPLATE_DEFAULT = "contactForm-form.ftl"; private static final String TEMPLATE_ERROR = "contactForm-error.ftl"; + @Override protected String getTitle(String siteName, VitroRequest vreq) { return siteName + " Feedback Form"; @@ -41,7 +42,6 @@ protected String getTitle(String siteName, VitroRequest vreq) { @Override protected ResponseValues processRequest(VitroRequest vreq) throws IOException { - ApplicationBean appBean = vreq.getAppBean(); String templateName; @@ -62,13 +62,26 @@ else if (StringUtils.isBlank(appBean.getContactMail())) { } else { + String captchaImpl = + ConfigurationProperties.getBean(vreq).getProperty("captcha.implementation"); + if (captchaImpl == null) { + captchaImpl = ""; + } + + if (captchaImpl.equals("RECAPTCHA")) { + body.put("siteKey", + Objects.requireNonNull(ConfigurationProperties.getBean(vreq).getProperty("recaptcha.siteKey"), + "You have to provide a site key through configuration file.")); + } else { + CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); + CaptchaServiceBean.getCaptchaChallenges().put(vreq.getRemoteAddr(), captchaChallenge); - CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); - ContactMailController.getCaptchaChallenges().add(captchaChallenge); + body.put("challenge", captchaChallenge.getB64Image()); + body.put("challengeId", captchaChallenge.getCaptchaId()); + } - body.put("challenge", captchaChallenge.getB64Image()); - body.put("challengeId", captchaChallenge.getCaptchaId()); body.put("formAction", "submitFeedback"); + body.put("captchaToUse", captchaImpl); if (vreq.getHeader("Referer") == null) { vreq.getSession().setAttribute("contactFormReferer","none"); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java index c08f0f1041..f6c1a38d01 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java @@ -7,13 +7,11 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; -import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import javax.mail.Address; @@ -32,6 +30,7 @@ import edu.cornell.mannlib.vitro.webapp.beans.ApplicationBean; import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.TemplateProcessingHelper.TemplateProcessingException; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; @@ -61,7 +60,7 @@ public class ContactMailController extends FreemarkerHttpServlet { private static final String EMAIL_JOURNAL_FILE_DIR = "emailJournal"; private static final String EMAIL_JOURNAL_FILE_NAME = "contactFormEmails.html"; - private static List captchaChallenges = Collections.synchronizedList(new ArrayList<>()); + private String captchaImpl; @Override protected String getTitle(String siteName, VitroRequest vreq) { @@ -80,17 +79,32 @@ protected ResponseValues processRequest(VitroRequest vreq) throws IOException { return errorNoRecipients(); } + captchaImpl = ConfigurationProperties.getBean(vreq).getProperty("captcha.implementation"); + if (captchaImpl == null) { + captchaImpl = ""; + } + String webusername = nonNullAndTrim(vreq, WEB_USERNAME_PARAM); String webuseremail = nonNullAndTrim(vreq, WEB_USEREMAIL_PARAM); String comments = nonNullAndTrim(vreq, COMMENTS_PARAM); String formType = nonNullAndTrim(vreq, "DeliveryType"); - String captchaInput = nonNullAndTrim(vreq, "userSolution"); - String captchaId = nonNullAndTrim(vreq, "challengeId"); - String errorMsg = validateInput(webusername, webuseremail, comments, captchaInput, captchaId); + String captchaInput; + String captchaId = ""; + switch(captchaImpl) { + case "RECAPTCHA": + captchaInput = nonNullAndTrim(vreq, "g-recaptcha-response"); + break; + case "DEFAULT": + default: + captchaInput = nonNullAndTrim(vreq, "userSolution"); + captchaId = nonNullAndTrim(vreq, "challengeId"); + } + + String errorMsg = validateInput(webusername, webuseremail, comments, captchaInput, captchaId, vreq); if (errorMsg != null) { - return errorParametersNotValid(errorMsg, webusername, webuseremail, comments); + return errorParametersNotValid(errorMsg, webusername, webuseremail, comments, vreq); } String spamReason = checkForSpam(comments, formType); @@ -310,7 +324,7 @@ private String nonNullAndTrim(HttpServletRequest req, String key) { } private String validateInput(String webusername, String webuseremail, - String comments, String captchaInput, String challengeId) { + String comments, String captchaInput, String challengeId, VitroRequest vreq) { if (webusername.isEmpty()) { return "Please enter a value in the Full name field."; @@ -328,11 +342,20 @@ private String validateInput(String webusername, String webuseremail, return "Please enter the contents of the gray box in the security field provided."; } - Optional optionalChallenge = getChallenge(challengeId); - if (optionalChallenge.isPresent() && optionalChallenge.get().getCode().equals(captchaInput)) { - return null; + switch(captchaImpl) { + case "RECAPTCHA": + if (CaptchaServiceBean.validateReCaptcha(captchaInput, vreq)){ + return null; + } + case "DEFAULT": + default: + Optional optionalChallenge = CaptchaServiceBean.getChallenge(challengeId, vreq.getRemoteAddr()); + if (optionalChallenge.isPresent() && optionalChallenge.get().getCode().equals(captchaInput)) { + return null; + } } + return "The value you entered in the security field did not match the letters displayed in the gray box."; } @@ -390,18 +413,25 @@ private ResponseValues errorNoRecipients() { } private ResponseValues errorParametersNotValid(String errorMsg, String webusername, String webuseremail, - String comments) throws IOException { + String comments, VitroRequest vreq) throws IOException { Map body = new HashMap(); body.put("errorMessage", errorMsg); body.put("formAction", "submitFeedback"); body.put("webusername", webusername); body.put("webuseremail", webuseremail); body.put("comments", comments); + body.put("captchaToUse", captchaImpl); - CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); - ContactMailController.getCaptchaChallenges().add(captchaChallenge); - body.put("challenge", captchaChallenge.getB64Image()); - body.put("challengeId", captchaChallenge.getCaptchaId()); + if (!captchaImpl.equals("RECAPTCHA")) { + CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); + CaptchaServiceBean.getCaptchaChallenges().put(vreq.getRemoteAddr(), captchaChallenge); + body.put("challenge", captchaChallenge.getB64Image()); + body.put("challengeId", captchaChallenge.getCaptchaId()); + } else { + body.put("siteKey", + Objects.requireNonNull(ConfigurationProperties.getBean(vreq).getProperty("recaptcha.siteKey"), + "You have to provide a site key through configuration file.")); + } return new TemplateResponseValues(TEMPLATE_FORM, body); } @@ -411,18 +441,4 @@ private ResponseValues errorSpam() { body.put("errorMessage", SPAM_MESSAGE); return new TemplateResponseValues(TEMPLATE_ERROR, body); } - - private Optional getChallenge(String captchaId) { - for (CaptchaBundle challenge : captchaChallenges) { - if (challenge.getCaptchaId().equals(captchaId)) { - captchaChallenges.remove(challenge); - return Optional.of(challenge); - } - } - return Optional.empty(); - } - - public static List getCaptchaChallenges() { - return captchaChallenges; - } } diff --git a/home/src/main/resources/config/example.runtime.properties b/home/src/main/resources/config/example.runtime.properties index 989cf19e3f..71ac75a901 100644 --- a/home/src/main/resources/config/example.runtime.properties +++ b/home/src/main/resources/config/example.runtime.properties @@ -194,3 +194,11 @@ proxy.eligibleTypeList = http://www.w3.org/2002/07/owl#Thing #fileUpload.maxFileSize = 10485760 #comma separated list of mime types allowed for upload #fileUpload.allowedMIMETypes = image/png, application/pdf + +# Captcha configuration. Available implementations are: DEFAULT (text-based) and RECAPTCHA +# If captcha.implementation property is not provided, system will fall back to DEFAULT implementation +# For RECAPTCHA method, you have to provide siteKey and secretKey. +# More information on siteKey and secretKey is available on: https://www.google.com/recaptcha +captcha.implementation = DEFAULT +#recaptcha.siteKey = +#recaptcha.secretKey = diff --git a/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl b/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl index 34ac683807..29a4f9d8f1 100644 --- a/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl +++ b/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl @@ -29,12 +29,19 @@ -

- - Refresh page if not displayed... - - -

+ + <#if captchaToUse == "RECAPTCHA"> +
+ + <#else> + +

+ + Refresh page if not displayed... + + +

+

@@ -54,3 +61,14 @@ ${stylesheets.add('')} ${scripts.add('', '')} + +<#if captchaToUse == "RECAPTCHA"> + + + From 58bdd92c92e368ad434038bb67dd85b37b746b08 Mon Sep 17 00:00:00 2001 From: Ivan Mrsulja Date: Wed, 22 Nov 2023 10:05:07 +0100 Subject: [PATCH 04/29] Added java docs. --- .../vitro/webapp/beans/CaptchaBundle.java | 7 +++ .../webapp/beans/CaptchaServiceBean.java | 52 +++++++++++++++++-- .../vitro/webapp/beans/ReCaptchaResponse.java | 8 +++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java index a8c58c14ec..d61c4f3e29 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java @@ -2,6 +2,13 @@ import java.util.Objects; +/** + * Represents a bundle containing a CAPTCHA image in Base64 format, the associated code, + * and a unique challenge identifier. + * + * @author Ivan Mrsulja + * @version 1.0 + */ public class CaptchaBundle { private final String b64Image; diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java index 168262358a..4cd8fefbaf 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java @@ -1,13 +1,10 @@ package edu.cornell.mannlib.vitro.webapp.beans; import java.awt.Color; -import java.awt.Font; -import java.awt.GraphicsEnvironment; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.SecureRandom; -import java.util.ArrayList; import java.util.Base64; import java.util.Objects; import java.util.Optional; @@ -33,6 +30,15 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; + +/** + * This class provides services related to CAPTCHA challenges and reCAPTCHA validation. + * It includes methods for generating CAPTCHA challenges, validating reCAPTCHA responses, + * and managing CAPTCHA challenges for specific hosts. + * + * @author Ivan Mrsulja + * @version 1.0 + */ public class CaptchaServiceBean { private static final SecureRandom random = new SecureRandom(); @@ -42,6 +48,13 @@ public class CaptchaServiceBean { private static final ConcurrentHashMap captchaChallenges = new ConcurrentHashMap<>(); + /** + * Validates a reCAPTCHA response using Google's reCAPTCHA API. + * + * @param recaptchaResponse The reCAPTCHA response to validate. + * @param vreq The VitroRequest associated with the validation. + * @return True if the reCAPTCHA response is valid, false otherwise. + */ public static boolean validateReCaptcha(String recaptchaResponse, VitroRequest vreq) { String secretKey = Objects.requireNonNull(ConfigurationProperties.getBean(vreq).getProperty("recaptcha.secretKey"), @@ -66,6 +79,13 @@ public static boolean validateReCaptcha(String recaptchaResponse, VitroRequest v return false; } + /** + * Generates a new CAPTCHA challenge using the nanocaptcha library. + * + * @return A CaptchaBundle containing the CAPTCHA image in Base64 format, the content, + * and a unique identifier. + * @throws IOException If an error occurs during image conversion. + */ public static CaptchaBundle generateChallenge() throws IOException { ImageCaptcha imageCaptcha = new ImageCaptcha.Builder(200, 75) @@ -81,6 +101,15 @@ public static CaptchaBundle generateChallenge() throws IOException { UUID.randomUUID().toString()); } + /** + * Retrieves a CAPTCHA challenge for a specific host based on the provided CAPTCHA ID + * and remote address. Removes the challenge from the storage after retrieval. + * + * @param captchaId The CAPTCHA ID to match. + * @param remoteAddress The remote address associated with the host. + * @return An Optional containing the CaptchaBundle if a matching challenge is found, + * or an empty Optional otherwise. + */ public static Optional getChallenge(String captchaId, String remoteAddress) { CaptchaBundle challengeForHost = captchaChallenges.getOrDefault(remoteAddress, new CaptchaBundle("", "", "")); captchaChallenges.remove(remoteAddress); @@ -92,10 +121,22 @@ public static Optional getChallenge(String captchaId, String remo return Optional.of(challengeForHost); } + /** + * Gets the map containing CAPTCHA challenges for different hosts. + * + * @return A ConcurrentHashMap with host addresses as keys and CaptchaBundle objects as values. + */ public static ConcurrentHashMap getCaptchaChallenges() { return captchaChallenges; } + /** + * Converts a BufferedImage object to Base64 format. + * + * @param image The BufferedImage to convert. + * @return The Base64-encoded string representation of the image. + * @throws IOException If an error occurs during image conversion. + */ private static String convertToBase64(BufferedImage image) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(image, "png", baos); @@ -104,6 +145,11 @@ private static String convertToBase64(BufferedImage image) throws IOException { return Base64.getEncoder().encodeToString(imageBytes); } + /** + * Generates a random Color object. + * + * @return A randomly generated Color object. + */ private static Color getRandomColor() { int r = random.nextInt(256); int g = random.nextInt(256); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java index 2d8d4d86e5..a45582f283 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java @@ -2,6 +2,14 @@ import java.util.Date; +/** + * Represents the response from Google's reCAPTCHA API. + * It includes information about the success of the reCAPTCHA verification, + * the timestamp of the challenge, and the hostname associated with the verification. + * + * @author Ivan Mrsulja + * @version 1.0 + */ public class ReCaptchaResponse { private boolean success; From ca187592b7d43a0760e47abf00581348ddbbd95c Mon Sep 17 00:00:00 2001 From: Ivan Mrsulja Date: Wed, 22 Nov 2023 14:13:05 +0100 Subject: [PATCH 05/29] Fixed log4J version bug. Added unit tests for captcha functionality. --- api/pom.xml | 5 + .../service/CaptchaServiceBeanTest.java | 95 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java diff --git a/api/pom.xml b/api/pom.xml index 6b40d67307..955af10557 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -70,6 +70,11 @@ nanocaptcha 1.5 + + org.slf4j + slf4j-api + 1.7.25 + org.apache.httpcomponents fluent-hc diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java new file mode 100644 index 0000000000..81385d08cc --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java @@ -0,0 +1,95 @@ +package edu.cornell.mannlib.vitro.webapp.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import org.junit.Before; +import org.junit.Test; +import stubs.edu.cornell.mannlib.vitro.webapp.config.ConfigurationPropertiesStub; +import stubs.javax.servlet.ServletContextStub; +import stubs.javax.servlet.http.HttpServletRequestStub; +import stubs.javax.servlet.http.HttpSessionStub; + +public class CaptchaServiceBeanTest extends AbstractTestClass { + + HttpServletRequestStub httpServletRequest; + + + @Before + public void createConfigurationProperties() throws Exception { + ConfigurationPropertiesStub props = new ConfigurationPropertiesStub(); + props.setProperty("recaptcha.secretKey", "secretKey"); + + ServletContextStub ctx = new ServletContextStub(); + props.setBean(ctx); + + HttpSessionStub session = new HttpSessionStub(); + session.setServletContext(ctx); + + httpServletRequest = new HttpServletRequestStub(); + httpServletRequest.setSession(session); + } + + @Test + public void validateReCaptcha_InvalidResponse_ReturnsFalse() throws IOException { + // Given + VitroRequest vitroRequest = new VitroRequest(httpServletRequest); + + // When + boolean result = CaptchaServiceBean.validateReCaptcha("invalidResponse", vitroRequest); + + // Then + assertFalse(result); + } + + @Test + public void generateChallenge_ValidChallengeGenerated() throws IOException { + // When + CaptchaBundle captchaBundle = CaptchaServiceBean.generateChallenge(); + + // Then + assertNotNull(captchaBundle); + assertNotNull(captchaBundle.getB64Image()); + assertNotNull(captchaBundle.getCode()); + assertNotNull(captchaBundle.getCaptchaId()); + } + + @Test + public void getChallenge_MatchingCaptchaIdAndRemoteAddress_ReturnsCaptchaBundle() { + // Given + String captchaId = "sampleCaptchaId"; + String remoteAddress = "sampleRemoteAddress"; + CaptchaBundle sampleChallenge = new CaptchaBundle("sampleB64Image", "sampleCode", captchaId); + CaptchaServiceBean.getCaptchaChallenges().put(remoteAddress, sampleChallenge); + + // When + Map captchaChallenges = CaptchaServiceBean.getCaptchaChallenges(); + Optional result = CaptchaServiceBean.getChallenge(captchaId, remoteAddress); + + // Then + assertTrue(result.isPresent()); + assertEquals(sampleChallenge, result.get()); + assertTrue(captchaChallenges.isEmpty()); // Ensure the challenge is removed from the map + } + + @Test + public void getChallenge_NonMatchingCaptchaIdAndRemoteAddress_ReturnsEmptyOptional() { + // When + Map captchaChallenges = CaptchaServiceBean.getCaptchaChallenges(); + Optional result = CaptchaServiceBean.getChallenge("nonMatchingId", "nonMatchingAddress"); + + // Then + assertFalse(result.isPresent()); + assertTrue(captchaChallenges.isEmpty()); // Ensure the map remains empty + } +} From fe87586d7db369d9deed52cb9e2b315b4850dcc7 Mon Sep 17 00:00:00 2001 From: Ivan Mrsulja Date: Wed, 22 Nov 2023 16:14:45 +0100 Subject: [PATCH 06/29] Added guava cache for mitigation of DoS by generating challenges. --- api/pom.xml | 5 ++++ .../webapp/beans/CaptchaServiceBean.java | 27 +++++++++++-------- .../freemarker/ContactFormController.java | 2 +- .../freemarker/ContactMailController.java | 4 +-- .../service/CaptchaServiceBeanTest.java | 17 ++++++------ 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index 955af10557..2dc9198ef8 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -75,6 +75,11 @@ slf4j-api 1.7.25 + + com.google.guava + guava + 30.1-jre + org.apache.httpcomponents fluent-hc diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java index 4cd8fefbaf..e15204ac45 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java @@ -9,11 +9,13 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import net.logicsquad.nanocaptcha.image.ImageCaptcha; @@ -45,7 +47,11 @@ public class CaptchaServiceBean { private static final Log log = LogFactory.getLog(CaptchaServiceBean.class.getName()); - private static final ConcurrentHashMap captchaChallenges = new ConcurrentHashMap<>(); + private static final Cache captchaChallenges = + CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(); /** @@ -103,21 +109,20 @@ public static CaptchaBundle generateChallenge() throws IOException { /** * Retrieves a CAPTCHA challenge for a specific host based on the provided CAPTCHA ID - * and remote address. Removes the challenge from the storage after retrieval. + * Removes the challenge from the storage after retrieval. * - * @param captchaId The CAPTCHA ID to match. - * @param remoteAddress The remote address associated with the host. + * @param captchaId The CAPTCHA ID to match. * @return An Optional containing the CaptchaBundle if a matching challenge is found, * or an empty Optional otherwise. */ - public static Optional getChallenge(String captchaId, String remoteAddress) { - CaptchaBundle challengeForHost = captchaChallenges.getOrDefault(remoteAddress, new CaptchaBundle("", "", "")); - captchaChallenges.remove(remoteAddress); - - if (!challengeForHost.getCaptchaId().equals(captchaId)) { + public static Optional getChallenge(String captchaId) { + CaptchaBundle challengeForHost = captchaChallenges.getIfPresent(captchaId); + if (challengeForHost == null) { return Optional.empty(); } + captchaChallenges.invalidate(captchaId); + return Optional.of(challengeForHost); } @@ -126,7 +131,7 @@ public static Optional getChallenge(String captchaId, String remo * * @return A ConcurrentHashMap with host addresses as keys and CaptchaBundle objects as values. */ - public static ConcurrentHashMap getCaptchaChallenges() { + public static Cache getCaptchaChallenges() { return captchaChallenges; } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java index dce5a23bbc..afebb46fe0 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java @@ -74,7 +74,7 @@ else if (StringUtils.isBlank(appBean.getContactMail())) { "You have to provide a site key through configuration file.")); } else { CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); - CaptchaServiceBean.getCaptchaChallenges().put(vreq.getRemoteAddr(), captchaChallenge); + CaptchaServiceBean.getCaptchaChallenges().put(captchaChallenge.getCaptchaId(), captchaChallenge); body.put("challenge", captchaChallenge.getB64Image()); body.put("challengeId", captchaChallenge.getCaptchaId()); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java index f6c1a38d01..20a49ba4aa 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java @@ -349,7 +349,7 @@ private String validateInput(String webusername, String webuseremail, } case "DEFAULT": default: - Optional optionalChallenge = CaptchaServiceBean.getChallenge(challengeId, vreq.getRemoteAddr()); + Optional optionalChallenge = CaptchaServiceBean.getChallenge(challengeId); if (optionalChallenge.isPresent() && optionalChallenge.get().getCode().equals(captchaInput)) { return null; } @@ -424,7 +424,7 @@ private ResponseValues errorParametersNotValid(String errorMsg, String webuserna if (!captchaImpl.equals("RECAPTCHA")) { CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); - CaptchaServiceBean.getCaptchaChallenges().put(vreq.getRemoteAddr(), captchaChallenge); + CaptchaServiceBean.getCaptchaChallenges().put(captchaChallenge.getCaptchaId(), captchaChallenge); body.put("challenge", captchaChallenge.getB64Image()); body.put("challengeId", captchaChallenge.getCaptchaId()); } else { diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java index 81385d08cc..1967494c02 100644 --- a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java @@ -6,9 +6,9 @@ import static org.junit.Assert.assertTrue; import java.io.IOException; -import java.util.Map; import java.util.Optional; +import com.google.common.cache.Cache; import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; @@ -68,28 +68,27 @@ public void generateChallenge_ValidChallengeGenerated() throws IOException { public void getChallenge_MatchingCaptchaIdAndRemoteAddress_ReturnsCaptchaBundle() { // Given String captchaId = "sampleCaptchaId"; - String remoteAddress = "sampleRemoteAddress"; CaptchaBundle sampleChallenge = new CaptchaBundle("sampleB64Image", "sampleCode", captchaId); - CaptchaServiceBean.getCaptchaChallenges().put(remoteAddress, sampleChallenge); + CaptchaServiceBean.getCaptchaChallenges().put(captchaId, sampleChallenge); // When - Map captchaChallenges = CaptchaServiceBean.getCaptchaChallenges(); - Optional result = CaptchaServiceBean.getChallenge(captchaId, remoteAddress); + Cache captchaChallenges = CaptchaServiceBean.getCaptchaChallenges(); + Optional result = CaptchaServiceBean.getChallenge(captchaId); // Then assertTrue(result.isPresent()); assertEquals(sampleChallenge, result.get()); - assertTrue(captchaChallenges.isEmpty()); // Ensure the challenge is removed from the map + assertEquals(0, captchaChallenges.size()); } @Test public void getChallenge_NonMatchingCaptchaIdAndRemoteAddress_ReturnsEmptyOptional() { // When - Map captchaChallenges = CaptchaServiceBean.getCaptchaChallenges(); - Optional result = CaptchaServiceBean.getChallenge("nonMatchingId", "nonMatchingAddress"); + Cache captchaChallenges = CaptchaServiceBean.getCaptchaChallenges(); + Optional result = CaptchaServiceBean.getChallenge("nonMatchingId"); // Then assertFalse(result.isPresent()); - assertTrue(captchaChallenges.isEmpty()); // Ensure the map remains empty + assertEquals(0, captchaChallenges.size()); } } From 262b99abf6952227c4b70e571f9049e8d6d4a528 Mon Sep 17 00:00:00 2001 From: Ivan Mrsulja Date: Thu, 23 Nov 2023 09:38:56 +0100 Subject: [PATCH 07/29] Aligned sl4j-api dependency version. --- api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/pom.xml b/api/pom.xml index 2dc9198ef8..0a9d9e2b9b 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -73,7 +73,7 @@ org.slf4j slf4j-api - 1.7.25 + 1.7.36 com.google.guava From 83c553b29bdbab725b3da14d30aba2577ca7a3b1 Mon Sep 17 00:00:00 2001 From: Ivan Mrsulja Date: Tue, 28 Nov 2023 18:51:55 +0100 Subject: [PATCH 08/29] Added refresh button for default challenge. --- .../beans/RefreshCaptchaController.java | 31 +++++++++++++++++++ .../freemarker/ContactFormController.java | 1 + .../freemarker/ContactMailController.java | 1 + .../body/contactForm/contactForm-form.ftl | 29 ++++++++++++++++- .../freemarker/edit/forms/css/customForm.css | 22 +++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/RefreshCaptchaController.java diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/RefreshCaptchaController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/RefreshCaptchaController.java new file mode 100644 index 0000000000..958183e00a --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/RefreshCaptchaController.java @@ -0,0 +1,31 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.twelvemonkeys.servlet.HttpServlet; + +@WebServlet(name = "refreshCaptcha", urlPatterns = {"/refreshCaptcha"}, loadOnStartup = 5) +public class RefreshCaptchaController extends HttpServlet { + + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String oldChallengeId = request.getParameter("oldChallengeId"); + + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + + CaptchaBundle newChallenge = CaptchaServiceBean.generateChallenge(); + CaptchaServiceBean.getCaptchaChallenges().invalidate(oldChallengeId); + CaptchaServiceBean.getCaptchaChallenges().put(newChallenge.getCaptchaId(), newChallenge); + + out.println("{\"challenge\": \"" + newChallenge.getB64Image() + "\", \"challengeId\": \"" + + newChallenge.getCaptchaId() + "\"}"); + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java index afebb46fe0..1d0ad6e1f0 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java @@ -80,6 +80,7 @@ else if (StringUtils.isBlank(appBean.getContactMail())) { body.put("challengeId", captchaChallenge.getCaptchaId()); } + body.put("contextPath", vreq.getContextPath()); body.put("formAction", "submitFeedback"); body.put("captchaToUse", captchaImpl); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java index 20a49ba4aa..3910ddf0f0 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java @@ -421,6 +421,7 @@ private ResponseValues errorParametersNotValid(String errorMsg, String webuserna body.put("webuseremail", webuseremail); body.put("comments", comments); body.put("captchaToUse", captchaImpl); + body.put("contextPath", vreq.getContextPath()); if (!captchaImpl.equals("RECAPTCHA")) { CaptchaBundle captchaChallenge = CaptchaServiceBean.generateChallenge(); diff --git a/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl b/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl index 29a4f9d8f1..80012d69b8 100644 --- a/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl +++ b/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl @@ -37,7 +37,11 @@

- Refresh page if not displayed... +

+ + Refresh page if not displayed... +
+

@@ -71,4 +75,27 @@ ${scripts.add(' -<#else> +<#elseif captchaToUse == "NANOCAPTCHA"> ', '')} -<#if captchaToUse == "RECAPTCHA"> +<#if captchaToUse == "RECAPTCHAv2"> ', '')} -<#if captchaToUse == "RECAPTCHAv2"> +<#if captchaToUse == "RECAPTCHAV2">