blob: f5dff51de228eb22bac9ab08abee2b36058753d2 [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import java.util.Date;
import java.util.Iterator;
import org.apache.hc.client5.http.cache.HeaderConstants;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Determines whether a given {@link HttpCacheEntry} is suitable to be
* used as a response for a given {@link HttpRequest}.
*
* @since 4.1
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE)
class CachedResponseSuitabilityChecker {
private final Logger log = LogManager.getLogger(getClass());
private final boolean sharedCache;
private final boolean useHeuristicCaching;
private final float heuristicCoefficient;
private final long heuristicDefaultLifetime;
private final CacheValidityPolicy validityStrategy;
CachedResponseSuitabilityChecker(final CacheValidityPolicy validityStrategy,
final CacheConfig config) {
super();
this.validityStrategy = validityStrategy;
this.sharedCache = config.isSharedCache();
this.useHeuristicCaching = config.isHeuristicCachingEnabled();
this.heuristicCoefficient = config.getHeuristicCoefficient();
this.heuristicDefaultLifetime = config.getHeuristicDefaultLifetime();
}
CachedResponseSuitabilityChecker(final CacheConfig config) {
this(new CacheValidityPolicy(), config);
}
private boolean isFreshEnough(final HttpCacheEntry entry, final HttpRequest request, final Date now) {
if (validityStrategy.isResponseFresh(entry, now)) {
return true;
}
if (useHeuristicCaching &&
validityStrategy.isResponseHeuristicallyFresh(entry, now, heuristicCoefficient, heuristicDefaultLifetime)) {
return true;
}
if (originInsistsOnFreshness(entry)) {
return false;
}
final long maxstale = getMaxStale(request);
if (maxstale == -1) {
return false;
}
return (maxstale > validityStrategy.getStalenessSecs(entry, now));
}
private boolean originInsistsOnFreshness(final HttpCacheEntry entry) {
if (validityStrategy.mustRevalidate(entry)) {
return true;
}
if (!sharedCache) {
return false;
}
return validityStrategy.proxyRevalidate(entry) ||
validityStrategy.hasCacheControlDirective(entry, "s-maxage");
}
private long getMaxStale(final HttpRequest request) {
long maxstale = -1;
final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.CACHE_CONTROL);
while (it.hasNext()) {
final HeaderElement elt = it.next();
if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
if ((elt.getValue() == null || "".equals(elt.getValue().trim())) && maxstale == -1) {
maxstale = Long.MAX_VALUE;
} else {
try {
long val = Long.parseLong(elt.getValue());
if (val < 0) {
val = 0;
}
if (maxstale == -1 || val < maxstale) {
maxstale = val;
}
} catch (final NumberFormatException nfe) {
// err on the side of preserving semantic transparency
maxstale = 0;
}
}
}
}
return maxstale;
}
/**
* Determine if I can utilize a {@link HttpCacheEntry} to respond to the given
* {@link HttpRequest}
*
* @param host
* {@link HttpHost}
* @param request
* {@link HttpRequest}
* @param entry
* {@link HttpCacheEntry}
* @param now
* Right now in time
* @return boolean yes/no answer
*/
public boolean canCachedResponseBeUsed(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry, final Date now) {
if (!isFreshEnough(entry, request, now)) {
log.trace("Cache entry was not fresh enough");
return false;
}
if (isGet(request) && !validityStrategy.contentLengthHeaderMatchesActualLength(entry)) {
log.debug("Cache entry Content-Length and header information do not match");
return false;
}
if (hasUnsupportedConditionalHeaders(request)) {
log.debug("Request contained conditional headers we don't handle");
return false;
}
if (!isConditional(request) && entry.getStatus() == HttpStatus.SC_NOT_MODIFIED) {
return false;
}
if (isConditional(request) && !allConditionalsMatch(request, entry, now)) {
return false;
}
if (hasUnsupportedCacheEntryForGet(request, entry)) {
log.debug("HEAD response caching enabled but the cache entry does not contain a " +
"request method, entity or a 204 response");
return false;
}
final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.CACHE_CONTROL);
while (it.hasNext()) {
final HeaderElement elt = it.next();
if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) {
log.trace("Response contained NO CACHE directive, cache was not suitable");
return false;
}
if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elt.getName())) {
log.trace("Response contained NO STORE directive, cache was not suitable");
return false;
}
if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
try {
final int maxage = Integer.parseInt(elt.getValue());
if (validityStrategy.getCurrentAgeSecs(entry, now) > maxage) {
log.trace("Response from cache was NOT suitable due to max age");
return false;
}
} catch (final NumberFormatException ex) {
// err conservatively
log.debug("Response from cache was malformed" + ex.getMessage());
return false;
}
}
if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
try {
final int maxstale = Integer.parseInt(elt.getValue());
if (validityStrategy.getFreshnessLifetimeSecs(entry) > maxstale) {
log.trace("Response from cache was not suitable due to Max stale freshness");
return false;
}
} catch (final NumberFormatException ex) {
// err conservatively
log.debug("Response from cache was malformed: " + ex.getMessage());
return false;
}
}
if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())) {
try {
final long minfresh = Long.parseLong(elt.getValue());
if (minfresh < 0L) {
return false;
}
final long age = validityStrategy.getCurrentAgeSecs(entry, now);
final long freshness = validityStrategy.getFreshnessLifetimeSecs(entry);
if (freshness - age < minfresh) {
log.trace("Response from cache was not suitable due to min fresh " +
"freshness requirement");
return false;
}
} catch (final NumberFormatException ex) {
// err conservatively
log.debug("Response from cache was malformed: " + ex.getMessage());
return false;
}
}
}
log.trace("Response from cache was suitable");
return true;
}
private boolean isGet(final HttpRequest request) {
return request.getMethod().equals(HeaderConstants.GET_METHOD);
}
private boolean entryIsNotA204Response(final HttpCacheEntry entry) {
return entry.getStatus() != HttpStatus.SC_NO_CONTENT;
}
private boolean cacheEntryDoesNotContainMethodAndEntity(final HttpCacheEntry entry) {
return entry.getRequestMethod() == null && entry.getResource() == null;
}
private boolean hasUnsupportedCacheEntryForGet(final HttpRequest request, final HttpCacheEntry entry) {
return isGet(request) && cacheEntryDoesNotContainMethodAndEntity(entry) && entryIsNotA204Response(entry);
}
/**
* Is this request the type of conditional request we support?
* @param request The current httpRequest being made
* @return {@code true} if the request is supported
*/
public boolean isConditional(final HttpRequest request) {
return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request);
}
/**
* Check that conditionals that are part of this request match
* @param request The current httpRequest being made
* @param entry the cache entry
* @param now right NOW in time
* @return {@code true} if the request matches all conditionals
*/
public boolean allConditionalsMatch(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
final boolean hasEtagValidator = hasSupportedEtagValidator(request);
final boolean hasLastModifiedValidator = hasSupportedLastModifiedValidator(request);
final boolean etagValidatorMatches = (hasEtagValidator) && etagValidatorMatches(request, entry);
final boolean lastModifiedValidatorMatches = (hasLastModifiedValidator) && lastModifiedValidatorMatches(request, entry, now);
if ((hasEtagValidator && hasLastModifiedValidator)
&& !(etagValidatorMatches && lastModifiedValidatorMatches)) {
return false;
} else if (hasEtagValidator && !etagValidatorMatches) {
return false;
}
if (hasLastModifiedValidator && !lastModifiedValidatorMatches) {
return false;
}
return true;
}
private boolean hasUnsupportedConditionalHeaders(final HttpRequest request) {
return (request.getFirstHeader(HeaderConstants.IF_RANGE) != null
|| request.getFirstHeader(HeaderConstants.IF_MATCH) != null
|| hasValidDateField(request, HeaderConstants.IF_UNMODIFIED_SINCE));
}
private boolean hasSupportedEtagValidator(final HttpRequest request) {
return request.containsHeader(HeaderConstants.IF_NONE_MATCH);
}
private boolean hasSupportedLastModifiedValidator(final HttpRequest request) {
return hasValidDateField(request, HeaderConstants.IF_MODIFIED_SINCE);
}
/**
* Check entry against If-None-Match
* @param request The current httpRequest being made
* @param entry the cache entry
* @return boolean does the etag validator match
*/
private boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
final String etag = (etagHeader != null) ? etagHeader.getValue() : null;
final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.IF_NONE_MATCH);
while (it.hasNext()) {
final HeaderElement elt = it.next();
final String reqEtag = elt.toString();
if (("*".equals(reqEtag) && etag != null) || reqEtag.equals(etag)) {
return true;
}
}
return false;
}
/**
* Check entry against If-Modified-Since, if If-Modified-Since is in the future it is invalid as per
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
* @param request The current httpRequest being made
* @param entry the cache entry
* @param now right NOW in time
* @return boolean Does the last modified header match
*/
private boolean lastModifiedValidatorMatches(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
final Header lastModifiedHeader = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
Date lastModified = null;
if (lastModifiedHeader != null) {
lastModified = DateUtils.parseDate(lastModifiedHeader.getValue());
}
if (lastModified == null) {
return false;
}
for (final Header h : request.getHeaders(HeaderConstants.IF_MODIFIED_SINCE)) {
final Date ifModifiedSince = DateUtils.parseDate(h.getValue());
if (ifModifiedSince != null) {
if (ifModifiedSince.after(now) || lastModified.after(ifModifiedSince)) {
return false;
}
}
}
return true;
}
private boolean hasValidDateField(final HttpRequest request, final String headerName) {
for(final Header h : request.getHeaders(headerName)) {
final Date date = DateUtils.parseDate(h.getValue());
return date != null;
}
return false;
}
}