package org.apereo.cas.web.support; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Iterator; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * Implementation of a HandlerInterceptorAdapter that keeps track of a mapping * of IP Addresses to number of failures to authenticate. * * @author Scott Battaglia * @since 3.0.0 */ public abstract class AbstractInMemoryThrottledSubmissionHandlerInterceptorAdapter extends AbstractThrottledSubmissionHandlerInterceptorAdapter implements InMemoryThrottledSubmissionHandlerInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractInMemoryThrottledSubmissionHandlerInterceptorAdapter.class); private static final double SUBMISSION_RATE_DIVIDEND = 1000.0; private ConcurrentMap<String, ZonedDateTime> ipMap = new ConcurrentHashMap<>(); public AbstractInMemoryThrottledSubmissionHandlerInterceptorAdapter(final int failureThreshold, final int failureRangeInSeconds, final String usernameParameter) { super(failureThreshold, failureRangeInSeconds, usernameParameter); } @Override public boolean exceedsThreshold(final HttpServletRequest request) { final ZonedDateTime last = this.ipMap.get(constructKey(request)); return last != null && submissionRate(ZonedDateTime.now(ZoneOffset.UTC), last) > getThresholdRate(); } @Override public void recordSubmissionFailure(final HttpServletRequest request) { this.ipMap.put(constructKey(request), ZonedDateTime.now(ZoneOffset.UTC)); } /** * This class relies on an external configuration to clean it up. * It ignores the threshold data in the parent class. */ @Override public void decrement() { LOGGER.info("Beginning audit cleanup..."); final Set<Entry<String, ZonedDateTime>> keys = this.ipMap.entrySet(); LOGGER.debug("Decrementing counts for throttler. Starting key count: [{}]", keys.size()); final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); for (final Iterator<Entry<String, ZonedDateTime>> iter = keys.iterator(); iter.hasNext();) { final Entry<String, ZonedDateTime> entry = iter.next(); if (submissionRate(now, entry.getValue()) < getThresholdRate()) { LOGGER.trace("Removing entry for key [{}]", entry.getKey()); iter.remove(); } } LOGGER.debug("Done decrementing count for throttler."); } /** * Computes the instantaneous rate in between two given dates corresponding to two submissions. * * @param a First date. * @param b Second date. * @return Instantaneous submission rate in submissions/sec, e.g. {@code a - b}. */ private static double submissionRate(final ZonedDateTime a, final ZonedDateTime b) { return SUBMISSION_RATE_DIVIDEND / (a.toInstant().toEpochMilli() - b.toInstant().toEpochMilli()); } }