package org.apereo.cas.web.flow.client;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apereo.cas.support.spnego.util.ReverseDNSRunnable;
import org.apereo.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Abstract class for defining a simple binary filter to determine whether a
* given client system should be prompted for SPNEGO / KRB / NTLM credentials.
*
* Envisioned implementations would include LDAP and DNS based determinations,
* but of course others may have value as well for local architectures.
*
* @author Sean Baker sean.baker@usuhs.edu
* @author Misagh Moayyed
* @since 4.1
*/
public class BaseSpnegoKnownClientSystemsFilterAction extends AbstractAction {
private static final Logger LOGGER = LoggerFactory.getLogger(BaseSpnegoKnownClientSystemsFilterAction.class);
/** Pattern of ip addresses to check. **/
private Pattern ipsToCheckPattern;
/** Alternative remote host attribute. **/
private String alternativeRemoteHostAttribute;
/**
* Set timeout (ms) for DNS requests; valuable for heterogeneous environments employing
* fall-through authentication mechanisms.
*/
private long timeout;
/**
* Instantiates a new Base.
*
* @param ipsToCheckPattern the ips to check pattern
*/
public BaseSpnegoKnownClientSystemsFilterAction(final String ipsToCheckPattern) {
setIpsToCheckPattern(ipsToCheckPattern);
}
/**
* Instantiates a new Base.
*
* @param ipsToCheckPattern the ips to check pattern
* @param alternativeRemoteHostAttribute the alternative remote host attribute
* @param dnsTimeout # of milliseconds to wait for a DNS request to return
*/
public BaseSpnegoKnownClientSystemsFilterAction(final String ipsToCheckPattern, final String alternativeRemoteHostAttribute, final long dnsTimeout) {
setIpsToCheckPattern(ipsToCheckPattern);
this.alternativeRemoteHostAttribute = alternativeRemoteHostAttribute;
this.timeout = dnsTimeout;
}
/**
* {@inheritDoc}
* Gets the remote ip from the request, and invokes spnego if it isn't filtered.
*
* @param context the request context
* @return {@link #yes()} if spnego should be invoked and ip isn't filtered,
* {@link #no()} otherwise.
*/
@Override
protected Event doExecute(final RequestContext context) {
final String remoteIp = getRemoteIp(context);
LOGGER.debug("Current user IP [{}]", remoteIp);
if (shouldDoSpnego(remoteIp)) {
LOGGER.info("Spnego should be activated for [{}]", remoteIp);
return yes();
}
LOGGER.info("Spnego should is skipped for [{}]", remoteIp);
return no();
}
/**
* Default implementation -- simply check the IP filter.
* @param remoteIp the remote ip
* @return true boolean
*/
protected boolean shouldDoSpnego(final String remoteIp) {
return ipPatternCanBeChecked(remoteIp) && ipPatternMatches(remoteIp);
}
/**
* Base class definition for whether the IP should be checked or not; overridable.
* @param remoteIp the remote ip
* @return whether or not the IP can / should be matched against the pattern
*/
protected boolean ipPatternCanBeChecked(final String remoteIp) {
return this.ipsToCheckPattern != null && StringUtils.isNotBlank(remoteIp);
}
/**
* Simple pattern match to determine whether an IP should be checked.
* Could stand to be extended to support "real" IP addresses and patterns, but
* for the local / first implementation regex made more sense.
* @param remoteIp the remote ip
* @return whether the remote ip received should be queried
*/
protected boolean ipPatternMatches(final String remoteIp) {
final Matcher matcher = this.ipsToCheckPattern.matcher(remoteIp);
if (matcher.find()) {
LOGGER.debug("Remote IP address [{}] should be checked based on the defined pattern [{}]",
remoteIp, this.ipsToCheckPattern.pattern());
return true;
}
LOGGER.debug("No pattern or remote IP defined, or pattern does not match remote IP [{}]",
remoteIp);
return false;
}
/**
* Pulls the remote IP from the current HttpServletRequest, or grabs the value
* for the specified alternative attribute (say, for proxied requests). Falls
* back to providing the "normal" remote address if no value can be retrieved
* from the specified alternative header value.
* @param context the context
* @return the remote ip
*/
private String getRemoteIp(final RequestContext context) {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
String userAddress = request.getRemoteAddr();
LOGGER.debug("Remote Address = [{}]", userAddress);
if (StringUtils.isNotBlank(this.alternativeRemoteHostAttribute)) {
userAddress = request.getHeader(this.alternativeRemoteHostAttribute);
LOGGER.debug("Header Attribute [{}] = [{}]", this.alternativeRemoteHostAttribute, userAddress);
if (StringUtils.isBlank(userAddress)) {
userAddress = request.getRemoteAddr();
LOGGER.warn("No value could be retrieved from the header [{}]. Falling back to [{}].",
this.alternativeRemoteHostAttribute, userAddress);
}
}
return userAddress;
}
/**
* Alternative header to be used for retrieving the remote system IP address.
* @param alternativeRemoteHostAttribute the alternative remote host attribute
*/
public void setAlternativeRemoteHostAttribute(final String alternativeRemoteHostAttribute) {
this.alternativeRemoteHostAttribute = alternativeRemoteHostAttribute;
}
/**
* Regular expression string to define IPs which should be considered.
* @param ipsToCheckPattern the ips to check as a regex pattern
*/
public void setIpsToCheckPattern(final String ipsToCheckPattern) {
this.ipsToCheckPattern = Pattern.compile(ipsToCheckPattern);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("ipsToCheckPattern", this.ipsToCheckPattern)
.append("alternativeRemoteHostAttribute", this.alternativeRemoteHostAttribute)
.append("timeout", this.timeout)
.toString();
}
/**
* Convenience method to perform a reverse DNS lookup. Threads the request
* through a custom Runnable class in order to prevent inordinately long
* user waits while performing reverse lookup.
* @param remoteIp the remote ip
* @return the remote host name
*/
protected String getRemoteHostName(final String remoteIp) {
final ReverseDNSRunnable revDNS = new ReverseDNSRunnable(remoteIp);
final Thread t = new Thread(revDNS);
t.start();
try {
t.join(this.timeout);
} catch (final InterruptedException e) {
LOGGER.debug("Threaded lookup failed. Defaulting to IP [{}].", remoteIp, e);
}
final String remoteHostName = revDNS.get();
LOGGER.debug("Found remote host name [{}].", remoteHostName);
return StringUtils.isNotBlank(remoteHostName) ? remoteHostName : remoteIp;
}
}