package org.apereo.cas.ticket.support; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apereo.cas.ticket.TicketState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; /** * Implementation of an expiration policy that adds the concept of saying that a * ticket can only be used once every X milliseconds to prevent mis-configured * clients from consuming resources by doing constant redirects. * * @author Scott Battaglia * @since 3.0.0 */ @JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include= JsonTypeInfo.As.PROPERTY) public class ThrottledUseAndTimeoutExpirationPolicy extends AbstractCasExpirationPolicy { /** Serialization support. */ private static final long serialVersionUID = 205979491183779408L; /** * The Logger instance for this class. Using a transient instance field for the Logger doesn't work, on object * deserialization the field is null. */ private static final Logger LOGGER = LoggerFactory.getLogger(ThrottledUseAndTimeoutExpirationPolicy.class); /** The time to kill in seconds. */ private long timeToKillInSeconds; private long timeInBetweenUsesInSeconds; /** * Instantiates a new Throttled use and timeout expiration policy. */ public ThrottledUseAndTimeoutExpirationPolicy(){} @JsonCreator public ThrottledUseAndTimeoutExpirationPolicy(@JsonProperty("timeToLive") final long timeToKillInSeconds, @JsonProperty("timeToIdle") final long timeInBetweenUsesInSeconds) { this.timeToKillInSeconds = timeToKillInSeconds; this.timeInBetweenUsesInSeconds = timeInBetweenUsesInSeconds; } public void setTimeInBetweenUsesInSeconds(final long timeInBetweenUsesInSeconds) { this.timeInBetweenUsesInSeconds = timeInBetweenUsesInSeconds; } public void setTimeToKillInSeconds(final long timeToKillInSeconds) { this.timeToKillInSeconds = timeToKillInSeconds; } @Override public boolean isExpired(final TicketState ticketState) { final ZonedDateTime currentTime = ZonedDateTime.now(ZoneOffset.UTC); final ZonedDateTime lastTimeUsed = ticketState.getLastTimeUsed(); final ZonedDateTime killTime = lastTimeUsed.plus(this.timeToKillInSeconds, ChronoUnit.SECONDS); if (ticketState.getCountOfUses() == 0 && currentTime.isBefore(killTime)) { LOGGER.debug("Ticket is not expired due to a count of zero and the time being less " + "than the timeToKillInSeconds"); return false; } if (currentTime.isAfter(killTime)) { LOGGER.debug("Ticket is expired due to the time being greater than the timeToKillInSeconds"); return true; } final ZonedDateTime dontUseUntil = lastTimeUsed.plus(this.timeInBetweenUsesInSeconds, ChronoUnit.SECONDS); if (currentTime.isBefore(dontUseUntil)) { LOGGER.warn("Ticket is expired due to the time being less than the waiting period."); return true; } return false; } @Override public Long getTimeToLive() { return this.timeToKillInSeconds; } @Override public Long getTimeToIdle() { return this.timeInBetweenUsesInSeconds; } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (obj.getClass() != getClass()) { return false; } final ThrottledUseAndTimeoutExpirationPolicy rhs = (ThrottledUseAndTimeoutExpirationPolicy) obj; return new EqualsBuilder() .append(this.timeToKillInSeconds, rhs.timeToKillInSeconds) .append(this.timeInBetweenUsesInSeconds, rhs.timeInBetweenUsesInSeconds) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder() .append(timeToKillInSeconds) .append(timeInBetweenUsesInSeconds) .toHashCode(); } }