diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/ssl/AnyHostnameVerifier.java b/cas-client-core/src/main/java/org/jasig/cas/client/ssl/AnyHostnameVerifier.java new file mode 100644 index 0000000..a7b4e51 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/ssl/AnyHostnameVerifier.java @@ -0,0 +1,34 @@ +/* + $Id$ + + Copyright (C) 2008-2009 Virginia Tech. + All rights reserved. + + SEE LICENSE FOR MORE INFORMATION + + Author: Middleware + Email: middleware@vt.edu + Version: $Revision$ + Updated: $Date$ +*/ +package org.jasig.cas.client.ssl; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * Hostname verifier that performs no host name verification for an SSL peer + * such that all hosts are allowed. + * + * @author Middleware + * @version $Revision$ + * + */ +public class AnyHostnameVerifier implements HostnameVerifier { + + /** {@inheritDoc} */ + public boolean verify(final String hostname, final SSLSession session) { + return true; + } + +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/ssl/RegexHostnameVerifier.java b/cas-client-core/src/main/java/org/jasig/cas/client/ssl/RegexHostnameVerifier.java new file mode 100644 index 0000000..43b41e3 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/ssl/RegexHostnameVerifier.java @@ -0,0 +1,49 @@ +/* + $Id$ + + Copyright (C) 2008-2009 Virginia Tech. + All rights reserved. + + SEE LICENSE FOR MORE INFORMATION + + Author: Middleware + Email: middleware@vt.edu + Version: $Revision$ + Updated: $Date$ +*/ +package org.jasig.cas.client.ssl; + +import java.util.regex.Pattern; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * Validates an SSL peer's hostname using a regular expression that a candidate + * host must match in order to be verified. + * + * @author Middleware + * @version $Revision$ + * + */ +public class RegexHostnameVerifier implements HostnameVerifier { + /** Allowed hostname pattern */ + private Pattern pattern; + + + /** + * Creates a new instance using the given regular expression. + * + * @param regex Regular expression describing allowed hosts. + */ + public RegexHostnameVerifier(final String regex) { + this.pattern = Pattern.compile(regex); + } + + + /** {@inheritDoc} */ + public boolean verify(final String hostname, final SSLSession session) { + return pattern.matcher(hostname).matches(); + } + +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifier.java b/cas-client-core/src/main/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifier.java new file mode 100644 index 0000000..07595bc --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifier.java @@ -0,0 +1,61 @@ +/* + $Id$ + + Copyright (C) 2008-2009 Virginia Tech. + All rights reserved. + + SEE LICENSE FOR MORE INFORMATION + + Author: Middleware + Email: middleware@vt.edu + Version: $Revision$ + Updated: $Date$ +*/ +package org.jasig.cas.client.ssl; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * Verifies a SSL peer host name based on an explicit whitelist of allowed hosts. + * + * @author Middleware + * @version $Revision$ + * + */ +public class WhitelistHostnameVerifier implements HostnameVerifier { + /** Allowed hosts */ + private String[] allowedHosts; + + + /** + * Creates a new instance using the given array of allowed hosts. + * + * @param allowed Array of allowed hosts. + */ + public WhitelistHostnameVerifier(final String[] allowed) { + this.allowedHosts = allowed; + } + + + /** + * Creates a new instance using the given list of allowed hosts. + * + * @param allowedList Comma-separated list of allowed hosts. + */ + public WhitelistHostnameVerifier(final String allowedList) { + this.allowedHosts = allowedList.split(",\\s*"); + } + + + /** {@inheritDoc} */ + public boolean verify(final String hostname, final SSLSession session) { + for (int i = 0; i < this.allowedHosts.length; i++) { + if (hostname.equalsIgnoreCase(this.allowedHosts[i])) { + return true; + } + } + return false; + } + +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java index 200ab59..133bd1d 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java @@ -9,14 +9,16 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.ServletRequest; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.net.URLConnection; import java.net.URLEncoder; import java.net.URL; import java.net.HttpURLConnection; @@ -274,10 +276,23 @@ public final class CommonUtils { * @return the response. */ public static String getResponseFromServer(final URL constructedUrl) { - HttpURLConnection conn = null; - try { - conn = (HttpURLConnection) constructedUrl.openConnection(); + return getResponseFromServer(constructedUrl, HttpsURLConnection.getDefaultHostnameVerifier()); + } + /** + * Contacts the remote URL and returns the response. + * + * @param constructedUrl the url to contact. + * @param hostnameVerifier Host name verifier to use for HTTPS connections. + * @return the response. + */ + public static String getResponseFromServer(final URL constructedUrl, final HostnameVerifier hostnameVerifier) { + URLConnection conn = null; + try { + conn = constructedUrl.openConnection(); + if (conn instanceof HttpsURLConnection) { + ((HttpsURLConnection)conn).setHostnameVerifier(hostnameVerifier); + } final BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line; @@ -294,13 +309,12 @@ public final class CommonUtils { LOG.error(e.getMessage(), e); throw new RuntimeException(e); } finally { - if (conn != null) { - conn.disconnect(); + if (conn != null && conn instanceof HttpURLConnection) { + ((HttpURLConnection)conn).disconnect(); } } } - /** * Contacts the remote URL and returns the response. * diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java index 2046fdf..fd732e8 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java @@ -8,10 +8,6 @@ package org.jasig.cas.client.validation; import org.jasig.cas.client.util.CommonUtils; import java.net.URL; -import java.net.HttpURLConnection; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.IOException; /** * Abstract class that knows the protocol for validating a CAS ticket. @@ -30,6 +26,10 @@ public abstract class AbstractCasProtocolUrlBasedTicketValidator extends Abstrac * Retrieves the response from the server by opening a connection and merely reading the response. */ protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) { - return CommonUtils.getResponseFromServer(validationUrl); + if (this.hostnameVerifier != null) { + return CommonUtils.getResponseFromServer(validationUrl, this.hostnameVerifier); + } else { + return CommonUtils.getResponseFromServer(validationUrl); + } } } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java index 26e284b..27a8d8f 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java @@ -8,6 +8,7 @@ package org.jasig.cas.client.validation; import org.jasig.cas.client.util.AbstractCasFilter; import org.jasig.cas.client.util.CommonUtils; +import javax.net.ssl.HostnameVerifier; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; @@ -16,6 +17,7 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.lang.reflect.Constructor; /** * The filter that handles all the work of validating ticket requests. @@ -55,9 +57,39 @@ public abstract class AbstractTicketValidationFilter extends AbstractCasFilter { * @param filterConfig the FilterConfiguration that may be needed to construct a validator. * @return the ticket validator. */ - protected TicketValidator getTicketValidator(FilterConfig filterConfig) { + protected TicketValidator getTicketValidator(final FilterConfig filterConfig) { return this.ticketValidator; } + + /** + * Gets the configured {@link HostnameVerifier} to use for HTTPS connections + * if one is configured for this filter. + * @param filterConfig Servlet filter configuration. + * @return Instance of specified host name verifier or null if none specified. + */ + protected HostnameVerifier getHostnameVerifier(final FilterConfig filterConfig) { + final String className = getPropertyFromInitParams(filterConfig, "hostnameVerifier", null); + log.trace("Using hostnameVerifier parameter: " + className); + final String config = getPropertyFromInitParams(filterConfig, "hostnameVerifierConfig", null); + log.trace("Using hostnameVerifierConfig parameter: " + config); + HostnameVerifier verifier = null; + if (className != null) { + try { + final Class verifierClass = Class.forName(className); + if (config != null) { + final Constructor cons = verifierClass.getConstructor(new Class[] {String.class}); + verifier = (HostnameVerifier) cons.newInstance(new Object[] {config}); + } else { + verifier = (HostnameVerifier) verifierClass.newInstance(); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Invalid HostnameVerifier class " + className); + } catch (Exception e) { + throw new IllegalArgumentException("Error creating instance of " + className, e); + } + } + return verifier; + } protected void initInternal(final FilterConfig filterConfig) throws ServletException { setExceptionOnValidationFailure(parseBoolean(getPropertyFromInitParams(filterConfig, "exceptionOnValidationFailure", "true"))); diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java index fcfcfc8..d1fde56 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java @@ -17,6 +17,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import javax.net.ssl.HostnameVerifier; + /** * Abstract validator implementation for tickets that must be validated against a server. * @@ -30,6 +32,11 @@ public abstract class AbstractUrlBasedTicketValidator implements TicketValidator * Commons Logging instance. */ protected final Log log = LogFactory.getLog(getClass()); + + /** + * Hostname verifier used when making an SSL request to the CAS server. + */ + protected HostnameVerifier hostnameVerifier; /** * Prefix for the CAS server. Should be everything up to the url endpoint, including the /. @@ -198,4 +205,8 @@ public abstract class AbstractUrlBasedTicketValidator implements TicketValidator public void setCustomParameters(final Map customParameters) { this.customParameters = customParameters; } + + public void setHostnameVerifier(final HostnameVerifier verifier) { + this.hostnameVerifier = verifier; + } } \ No newline at end of file diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidationFilter.java index 4ccb03f..6475706 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidationFilter.java @@ -22,6 +22,7 @@ public class Cas10TicketValidationFilter extends AbstractTicketValidationFilter final String casServerUrlPrefix = getPropertyFromInitParams(filterConfig, "casServerUrlPrefix", null); final Cas10TicketValidator validator = new Cas10TicketValidator(casServerUrlPrefix); validator.setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false"))); + validator.setHostnameVerifier(getHostnameVerifier(filterConfig)); return validator; } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java index c064b14..9a8e118 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java @@ -124,6 +124,7 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal } validator.setCustomParameters(additionalParameters); + validator.setHostnameVerifier(getHostnameVerifier(filterConfig)); return validator; } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java index b6ed011..4dc4c6f 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java @@ -29,6 +29,7 @@ public class Saml11TicketValidationFilter extends AbstractTicketValidationFilter final String tolerance = getPropertyFromInitParams(filterConfig, "tolerance", "1000"); validator.setTolerance(Long.parseLong(tolerance)); validator.setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false"))); + validator.setHostnameVerifier(getHostnameVerifier(filterConfig)); return validator; } } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java index 6c40ba3..f339fff 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java @@ -16,6 +16,8 @@ import java.util.*; import java.text.DateFormat; import java.text.SimpleDateFormat; +import javax.net.ssl.HttpsURLConnection; + /** * TicketValidator that can understand validating a SAML artifact. This includes the SOAP request/response. * @@ -175,6 +177,9 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator try { conn = (HttpURLConnection) validationUrl.openConnection(); + if (this.hostnameVerifier != null && conn instanceof HttpsURLConnection) { + ((HttpsURLConnection)conn).setHostnameVerifier(this.hostnameVerifier); + } conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "text/xml"); conn.setRequestProperty("Content-Length", Integer.toString(MESSAGE_TO_SEND.length())); diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/ssl/RegexHostnameVerifierTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/ssl/RegexHostnameVerifierTests.java new file mode 100644 index 0000000..55ab8e3 --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/ssl/RegexHostnameVerifierTests.java @@ -0,0 +1,39 @@ +/* + $Id$ + + Copyright (C) 2008-2009 Virginia Tech. + All rights reserved. + + SEE LICENSE FOR MORE INFORMATION + + Author: Middleware + Email: middleware@vt.edu + Version: $Revision$ + Updated: $Date$ +*/ +package org.jasig.cas.client.ssl; + +import junit.framework.Assert; +import junit.framework.TestCase; + +/** + * Unit test for {@link RegexHostnameVerifier} class. + * + * @author Middleware + * @version $Revision$ + * + */ +public class RegexHostnameVerifierTests extends TestCase { + + /** + * Test method for {@link RegexHostnameVerifier#verify(String, SSLSession)}. + */ + public void testVerify() { + final RegexHostnameVerifier verifier = new RegexHostnameVerifier("\\w+\\.vt\\.edu"); + Assert.assertTrue(verifier.verify("a.vt.edu", null)); + Assert.assertTrue(verifier.verify("host.vt.edu", null)); + Assert.assertFalse(verifier.verify("1-host.vt.edu", null)); + Assert.assertFalse(verifier.verify("mallory.example.com", null)); + } + +} diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifierTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifierTests.java new file mode 100644 index 0000000..fffd9bf --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifierTests.java @@ -0,0 +1,39 @@ +/* + $Id$ + + Copyright (C) 2008-2009 Virginia Tech. + All rights reserved. + + SEE LICENSE FOR MORE INFORMATION + + Author: Middleware + Email: middleware@vt.edu + Version: $Revision$ + Updated: $Date$ +*/ +package org.jasig.cas.client.ssl; + +import junit.framework.Assert; +import junit.framework.TestCase; + +/** + * Unit test for {@link WhitelistHostnameVerifier} class. + * + * @author Middleware + * @version $Revision$ + * + */ +public class WhitelistHostnameVerifierTests extends TestCase { + /** + * Test method for {@link WhitelistHostnameVerifier#verify(String, SSLSession)}. + */ + public void testVerify() { + final WhitelistHostnameVerifier verifier = new WhitelistHostnameVerifier( + "red.vt.edu, green.vt.edu,blue.vt.edu"); + Assert.assertTrue(verifier.verify("red.vt.edu", null)); + Assert.assertTrue(verifier.verify("green.vt.edu", null)); + Assert.assertTrue(verifier.verify("blue.vt.edu", null)); + Assert.assertFalse(verifier.verify("purple.vt.edu", null)); + } + +}