HTTPCLIENT-2277: Removal of the HTTP protocol logic presently enforced in the caching layer directly related to the caching functionality. The enforcement of general protocol requirements should be implemented in the protocol layer.

This commit is contained in:
Oleg Kalnichevski 2023-07-23 17:23:19 +02:00
parent a19adcb0dd
commit 0f7de55d41
10 changed files with 11 additions and 735 deletions

View File

@ -112,12 +112,11 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final CacheableRequestPolicy cacheableRequestPolicy,
final CachedResponseSuitabilityChecker suitabilityChecker,
final ResponseProtocolCompliance responseCompliance,
final RequestProtocolCompliance requestCompliance,
final DefaultAsyncCacheRevalidator cacheRevalidator,
final ConditionalRequestBuilder<HttpRequest> conditionalRequestBuilder,
final CacheConfig config) {
super(validityPolicy, responseCachingPolicy, responseGenerator, cacheableRequestPolicy,
suitabilityChecker, responseCompliance, requestCompliance, config);
suitabilityChecker, responseCompliance, config);
this.responseCache = responseCache;
this.cacheRevalidator = cacheRevalidator;
this.conditionalRequestBuilder = conditionalRequestBuilder;
@ -231,7 +230,6 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
return;
}
requestCompliance.makeRequestCompliant(request);
request.addHeader(HttpHeaders.VIA,via);
final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);
@ -243,12 +241,6 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
public void completed(final CacheMatch result) {
final CacheHit hit = result != null ? result.hit : null;
final CacheHit root = result != null ? result.root : null;
final SimpleHttpResponse fatalErrorResponse = getFatallyNonCompliantResponse(request, context, hit != null);
if (fatalErrorResponse != null) {
triggerResponse(fatalErrorResponse, scope, asyncExecCallback);
return;
}
if (hit == null) {
LOG.debug("Cache miss");
handleCacheMiss(requestCacheControl, root, target, request, entityProducer, scope, chain, asyncExecCallback);

View File

@ -117,8 +117,10 @@ public class CacheConfig implements Cloneable {
*/
public final static boolean DEFAULT_303_CACHING_ENABLED = false;
/** Default setting to allow weak tags on PUT/DELETE methods
/**
* @deprecated No longer applicable. Do not use.
*/
@Deprecated
public final static boolean DEFAULT_WEAK_ETAG_ON_PUTDELETE_ALLOWED = false;
/** Default setting for heuristic caching
@ -146,7 +148,6 @@ public class CacheConfig implements Cloneable {
private final int maxCacheEntries;
private final int maxUpdateRetries;
private final boolean allow303Caching;
private final boolean weakETagOnPutDeleteAllowed;
private final boolean heuristicCachingEnabled;
private final float heuristicCoefficient;
private final TimeValue heuristicDefaultLifetime;
@ -168,7 +169,6 @@ public class CacheConfig implements Cloneable {
final int maxCacheEntries,
final int maxUpdateRetries,
final boolean allow303Caching,
final boolean weakETagOnPutDeleteAllowed,
final boolean heuristicCachingEnabled,
final float heuristicCoefficient,
final TimeValue heuristicDefaultLifetime,
@ -183,7 +183,6 @@ public class CacheConfig implements Cloneable {
this.maxCacheEntries = maxCacheEntries;
this.maxUpdateRetries = maxUpdateRetries;
this.allow303Caching = allow303Caching;
this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
this.heuristicCachingEnabled = heuristicCachingEnabled;
this.heuristicCoefficient = heuristicCoefficient;
this.heuristicDefaultLifetime = heuristicDefaultLifetime;
@ -268,9 +267,12 @@ public class CacheConfig implements Cloneable {
/**
* Returns whether weak etags is allowed with PUT/DELETE methods.
* @return {@code true} if it is allowed.
*
* @deprecated Do not use.
*/
@Deprecated
public boolean isWeakETagOnPutDeleteAllowed() {
return weakETagOnPutDeleteAllowed;
return true;
}
/**
@ -349,14 +351,12 @@ public class CacheConfig implements Cloneable {
.setStaleIfErrorEnabled(config.isStaleIfErrorEnabled());
}
public static class Builder {
private long maxObjectSize;
private int maxCacheEntries;
private int maxUpdateRetries;
private boolean allow303Caching;
private boolean weakETagOnPutDeleteAllowed;
private boolean heuristicCachingEnabled;
private float heuristicCoefficient;
private TimeValue heuristicDefaultLifetime;
@ -372,7 +372,6 @@ public class CacheConfig implements Cloneable {
this.maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES;
this.maxUpdateRetries = DEFAULT_MAX_UPDATE_RETRIES;
this.allow303Caching = DEFAULT_303_CACHING_ENABLED;
this.weakETagOnPutDeleteAllowed = DEFAULT_WEAK_ETAG_ON_PUTDELETE_ALLOWED;
this.heuristicCachingEnabled = DEFAULT_HEURISTIC_CACHING_ENABLED;
this.heuristicCoefficient = DEFAULT_HEURISTIC_COEFFICIENT;
this.heuristicDefaultLifetime = DEFAULT_HEURISTIC_LIFETIME;
@ -418,12 +417,10 @@ public class CacheConfig implements Cloneable {
}
/**
* Allows or disallows weak etags to be used with PUT/DELETE If-Match requests.
* @param weakETagOnPutDeleteAllowed should be {@code true} to
* permit weak etags, {@code false} to reject them.
* @deprecated No longer applicable. Do not use.
*/
@Deprecated
public Builder setWeakETagOnPutDeleteAllowed(final boolean weakETagOnPutDeleteAllowed) {
this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
return this;
}
@ -541,7 +538,6 @@ public class CacheConfig implements Cloneable {
maxCacheEntries,
maxUpdateRetries,
allow303Caching,
weakETagOnPutDeleteAllowed,
heuristicCachingEnabled,
heuristicCoefficient,
heuristicDefaultLifetime,
@ -562,7 +558,6 @@ public class CacheConfig implements Cloneable {
.append(", maxCacheEntries=").append(this.maxCacheEntries)
.append(", maxUpdateRetries=").append(this.maxUpdateRetries)
.append(", 303CachingEnabled=").append(this.allow303Caching)
.append(", weakETagOnPutDeleteAllowed=").append(this.weakETagOnPutDeleteAllowed)
.append(", heuristicCachingEnabled=").append(this.heuristicCachingEnabled)
.append(", heuristicCoefficient=").append(this.heuristicCoefficient)
.append(", heuristicDefaultLifetime=").append(this.heuristicDefaultLifetime)

View File

@ -163,31 +163,4 @@ class CachedHttpResponseGenerator {
return Method.GET.isSame(request.getMethod()) && cacheEntry.getResource() != null;
}
/**
* Extract error information about the {@link HttpRequest} telling the 'caller'
* that a problem occurred.
*
* @param errorCheck What type of error should I get
* @return The {@link HttpResponse} that is the error generated
*/
public SimpleHttpResponse getErrorForRequest(final RequestProtocolError errorCheck) {
switch (errorCheck) {
case BODY_BUT_NO_LENGTH_ERROR:
return SimpleHttpResponse.create(HttpStatus.SC_LENGTH_REQUIRED);
case WEAK_ETAG_AND_RANGE_ERROR:
return SimpleHttpResponse.create(HttpStatus.SC_BAD_REQUEST,
"Weak eTag not compatible with byte range", ContentType.DEFAULT_TEXT);
case WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR:
return SimpleHttpResponse.create(HttpStatus.SC_PRECONDITION_FAILED,
"Weak eTag not compatible with PUT or DELETE requests");
default:
throw new IllegalStateException(
"The request was compliant, therefore no error can be generated for it.");
}
}
}

View File

@ -120,12 +120,11 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
final CacheableRequestPolicy cacheableRequestPolicy,
final CachedResponseSuitabilityChecker suitabilityChecker,
final ResponseProtocolCompliance responseCompliance,
final RequestProtocolCompliance requestCompliance,
final DefaultCacheRevalidator cacheRevalidator,
final ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder,
final CacheConfig config) {
super(validityPolicy, responseCachingPolicy, responseGenerator, cacheableRequestPolicy,
suitabilityChecker, responseCompliance, requestCompliance, config);
suitabilityChecker, responseCompliance, config);
this.responseCache = responseCache;
this.cacheRevalidator = cacheRevalidator;
this.conditionalRequestBuilder = conditionalRequestBuilder;
@ -170,12 +169,6 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
final CacheHit hit = result != null ? result.hit : null;
final CacheHit root = result != null ? result.root : null;
final SimpleHttpResponse fatalErrorResponse = getFatallyNonCompliantResponse(request, context, hit != null);
if (fatalErrorResponse != null) {
return convert(fatalErrorResponse, scope);
}
requestCompliance.makeRequestCompliant(request);
request.addHeader(HttpHeaders.VIA, via);
final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);

View File

@ -28,7 +28,6 @@ package org.apache.hc.client5.http.impl.cache;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@ -70,13 +69,10 @@ public class CachingExecBase {
final CacheableRequestPolicy cacheableRequestPolicy;
final CachedResponseSuitabilityChecker suitabilityChecker;
final ResponseProtocolCompliance responseCompliance;
final RequestProtocolCompliance requestCompliance;
final CacheConfig cacheConfig;
private static final Logger LOG = LoggerFactory.getLogger(CachingExecBase.class);
private static final TimeValue ONE_DAY = TimeValue.ofHours(24);
CachingExecBase(
final CacheValidityPolicy validityPolicy,
final ResponseCachingPolicy responseCachingPolicy,
@ -84,14 +80,12 @@ public class CachingExecBase {
final CacheableRequestPolicy cacheableRequestPolicy,
final CachedResponseSuitabilityChecker suitabilityChecker,
final ResponseProtocolCompliance responseCompliance,
final RequestProtocolCompliance requestCompliance,
final CacheConfig config) {
this.responseCachingPolicy = responseCachingPolicy;
this.validityPolicy = validityPolicy;
this.responseGenerator = responseGenerator;
this.cacheableRequestPolicy = cacheableRequestPolicy;
this.suitabilityChecker = suitabilityChecker;
this.requestCompliance = requestCompliance;
this.responseCompliance = responseCompliance;
this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
}
@ -104,7 +98,6 @@ public class CachingExecBase {
this.cacheableRequestPolicy = new CacheableRequestPolicy();
this.suitabilityChecker = new CachedResponseSuitabilityChecker(this.validityPolicy, this.cacheConfig);
this.responseCompliance = new ResponseProtocolCompliance();
this.requestCompliance = new RequestProtocolCompliance(this.cacheConfig.isWeakETagOnPutDeleteAllowed());
this.responseCachingPolicy = new ResponseCachingPolicy(
this.cacheConfig.getMaxObjectSize(),
this.cacheConfig.isSharedCache(),
@ -141,21 +134,6 @@ public class CachingExecBase {
return cacheUpdates.get();
}
/**
* @since 5.2
*/
SimpleHttpResponse getFatallyNonCompliantResponse(
final HttpRequest request,
final HttpContext context,
final boolean resourceExists) {
final List<RequestProtocolError> fatalError = requestCompliance.requestIsFatallyNonCompliant(request, resourceExists);
if (fatalError != null && !fatalError.isEmpty()) {
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
return responseGenerator.getErrorForRequest(fatalError.get(0));
}
return null;
}
void recordCacheMiss(final HttpHost target, final HttpRequest request) {
cacheMisses.getAndIncrement();
if (LOG.isDebugEnabled()) {

View File

@ -1,199 +0,0 @@
/*
* ====================================================================
* 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 static org.apache.hc.client5.http.utils.DateUtils.parseStandardDate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.ProtocolVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class RequestProtocolCompliance {
private final boolean weakETagOnPutDeleteAllowed;
private static final Logger LOG = LoggerFactory.getLogger(RequestProtocolCompliance.class);
public RequestProtocolCompliance() {
super();
this.weakETagOnPutDeleteAllowed = false;
}
public RequestProtocolCompliance(final boolean weakETagOnPutDeleteAllowed) {
super();
this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
}
/**
* Test to see if the {@link HttpRequest} is HTTP1.1 compliant or not
* and if not, we can not continue.
*
* @param request the HttpRequest Object
* @return list of {@link RequestProtocolError}
*/
public List<RequestProtocolError> requestIsFatallyNonCompliant(final HttpRequest request, final boolean resourceExists) {
final List<RequestProtocolError> theErrors = new ArrayList<>();
RequestProtocolError anError = requestHasWeakETagAndRange(request);
if (anError != null) {
theErrors.add(anError);
}
if (!weakETagOnPutDeleteAllowed) {
anError = requestHasWeekETagForPUTOrDELETEIfMatch(request, resourceExists);
if (anError != null) {
theErrors.add(anError);
}
}
return theErrors;
}
/**
* If the {@link HttpRequest} is non-compliant but 'fixable' we go ahead and
* fix the request here.
*
* @param request the request to check for compliance
*/
public void makeRequestCompliant(final HttpRequest request) {
decrementOPTIONSMaxForwardsIfGreaterThen0(request);
if (requestVersionIsTooLow(request) || requestMinorVersionIsTooHighMajorVersionsMatch(request)) {
request.setVersion(HttpVersion.HTTP_1_1);
}
}
private void decrementOPTIONSMaxForwardsIfGreaterThen0(final HttpRequest request) {
if (!Method.OPTIONS.isSame(request.getMethod())) {
return;
}
final Header maxForwards = request.getFirstHeader(HttpHeaders.MAX_FORWARDS);
if (maxForwards == null) {
return;
}
request.removeHeaders(HttpHeaders.MAX_FORWARDS);
final int currentMaxForwards = Integer.parseInt(maxForwards.getValue());
request.setHeader(HttpHeaders.MAX_FORWARDS, Integer.toString(currentMaxForwards - 1));
}
protected boolean requestMinorVersionIsTooHighMajorVersionsMatch(final HttpRequest request) {
final ProtocolVersion requestProtocol = request.getVersion();
if (requestProtocol == null) {
return false;
}
if (requestProtocol.getMajor() != HttpVersion.HTTP_1_1.getMajor()) {
return false;
}
return requestProtocol.getMinor() > HttpVersion.HTTP_1_1.getMinor();
}
protected boolean requestVersionIsTooLow(final HttpRequest request) {
final ProtocolVersion requestProtocol = request.getVersion();
return requestProtocol != null && requestProtocol.compareToVersion(HttpVersion.HTTP_1_1) < 0;
}
private RequestProtocolError requestHasWeakETagAndRange(final HttpRequest request) {
final String method = request.getMethod();
if (!(Method.GET.isSame(method) || Method.HEAD.isSame(method))) {
return null;
}
if (!request.containsHeader(HttpHeaders.RANGE)) {
return null;
}
final Instant ifRangeInstant = parseStandardDate(request, HttpHeaders.IF_RANGE);
final Instant lastModifiedInstant = parseStandardDate(request, HttpHeaders.LAST_MODIFIED);
for (final Iterator<Header> it = request.headerIterator(HttpHeaders.IF_RANGE); it.hasNext(); ) {
final String val = it.next().getValue();
if (val.startsWith("W/")) {
if (LOG.isDebugEnabled()) {
LOG.debug("Weak ETag found in If-Range header");
}
return RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR;
} else {
// Not a strong validator or doesn't match Last-Modified
if (ifRangeInstant != null && lastModifiedInstant != null
&& !ifRangeInstant.equals(lastModifiedInstant)) {
if (LOG.isDebugEnabled()) {
LOG.debug("If-Range does not match Last-Modified");
}
return RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR;
}
}
}
return null;
}
private RequestProtocolError requestHasWeekETagForPUTOrDELETEIfMatch(final HttpRequest request, final boolean resourceExists) {
final String method = request.getMethod();
if (!(Method.PUT.isSame(method) || Method.DELETE.isSame(method) || Method.POST.isSame(method))
) {
return null;
}
for (final Iterator<Header> it = request.headerIterator(HttpHeaders.IF_MATCH); it.hasNext();) {
final String val = it.next().getValue();
if (val.equals("*") && !resourceExists) {
return null;
}
if (val.startsWith("W/")) {
if (LOG.isDebugEnabled()) {
LOG.debug("Weak ETag found in If-Match header");
}
return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR;
}
}
for (final Iterator<Header> it = request.headerIterator(HttpHeaders.IF_NONE_MATCH); it.hasNext();) {
final String val = it.next().getValue();
if (val.startsWith("W/") || (val.equals("*") && resourceExists)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Weak ETag found in If-None-Match header");
}
return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR;
}
}
return null;
}
}

View File

@ -1,36 +0,0 @@
/*
* ====================================================================
* 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;
enum RequestProtocolError {
UNKNOWN,
BODY_BUT_NO_LENGTH_ERROR,
WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR,
WEAK_ETAG_AND_RANGE_ERROR
}

View File

@ -49,7 +49,6 @@ import org.apache.hc.client5.http.cache.HttpCacheEntryFactory;
import org.apache.hc.client5.http.cache.HttpCacheStorage;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpOptions;
import org.apache.hc.client5.http.protocol.HttpClientContext;
@ -58,7 +57,6 @@ import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus;
@ -103,8 +101,6 @@ public class TestCachingExecChain {
@Mock
ResponseProtocolCompliance responseCompliance;
@Mock
RequestProtocolCompliance requestCompliance;
@Mock
ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder;
@Mock
HttpCache responseCache;
@ -249,16 +245,6 @@ public class TestCachingExecChain {
Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, context.getCacheResponseStatus());
}
@Test
public void testSetsModuleGeneratedResponseContextForFatallyNoncompliantRequest() throws Exception {
final ClassicHttpRequest req = new HttpGet("http://foo.example.com/");
req.setHeader("Range", "bytes=0-50");
req.setHeader("If-Range", "W/\"weak-etag\"");
execute(req);
Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, context.getCacheResponseStatus());
}
@Test
public void testRecordsClientProtocolInViaHeaderIfRequestNotServableFromCache() throws Exception {
final ClassicHttpRequest originalRequest = new BasicClassicHttpRequest("GET", "/");
@ -1500,39 +1486,4 @@ public class TestCachingExecChain {
Mockito.verify(mockExecChain, Mockito.times(5)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testRequestWithWeakETagAndRange() throws Exception {
final ClassicHttpRequest req1 = new HttpGet("http://foo1.example.com/");
req1.addHeader(HttpHeaders.IF_MATCH, "W/\"weak1\"");
req1.addHeader(HttpHeaders.RANGE, "bytes=0-50");
req1.addHeader(HttpHeaders.IF_RANGE, "W/\"weak2\""); // ETag doesn't match with If-Match ETag
final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
resp1.setEntity(HttpTestUtils.makeBody(128));
resp1.setHeader("Content-Length", "128");
resp1.setHeader("ETag", "\"etag\"");
resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
resp1.setHeader("Cache-Control", "public, max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
final ClassicHttpResponse result = execute(req1);
Assertions.assertEquals(HttpStatus.SC_BAD_REQUEST, result.getCode());
}
@Test
public void testRequestWithWeakETagForPUTOrDELETEIfMatch() throws Exception {
final ClassicHttpRequest req1 = new HttpDelete("http://foo1.example.com/");
req1.addHeader(HttpHeaders.IF_MATCH, "W/\"weak1\"");
final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
resp1.setEntity(HttpTestUtils.makeBody(128));
resp1.setHeader("Content-Length", "128");
resp1.setHeader("ETag", "\"etag\"");
resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
resp1.setHeader("Cache-Control", "public, max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
final ClassicHttpResponse result = execute(req1);
Assertions.assertEquals(HttpStatus.SC_PRECONDITION_FAILED, result.getCode());
}
}

View File

@ -700,29 +700,6 @@ public class TestProtocolRequirements {
execute(request);
}
/*
* "If the Max-Forwards field-value is an integer greater than zero, the
* proxy MUST decrement the field-value when it forwards the request."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
*/
@Test
public void testDecrementsMaxForwardsWhenForwardingOPTIONSRequest() throws Exception {
request = new BasicClassicHttpRequest("OPTIONS", "*");
request.setHeader("Max-Forwards", "7");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
final ClassicHttpRequest captured = reqCapture.getValue();
Assertions.assertEquals("6", captured.getFirstHeader("Max-Forwards").getValue());
}
/*
* "If no Max-Forwards field is present in the request, then the forwarded
* request MUST NOT include a Max-Forwards field."
@ -1110,51 +1087,6 @@ public class TestProtocolRequirements {
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
/*
* "If the [206] response is the result of an If-Range request that used a
* weak validator, the response MUST NOT include other entity-headers; this
* prevents inconsistencies between cached entity-bodies and updated
* headers."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
*/
@Test
public void test206ResponseToConditionalRangeRequestDoesNotIncludeOtherEntityHeaders() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final Instant now = Instant.now();
final Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
originResponse = HttpTestUtils.make200Response();
originResponse.addHeader("Allow", "GET,HEAD");
originResponse.addHeader("Cache-Control", "max-age=3600");
originResponse.addHeader("Content-Language", "en");
originResponse.addHeader("Content-Encoding", "x-coding");
originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
originResponse.addHeader("Content-Length", "128");
originResponse.addHeader("Content-Type", "application/octet-stream");
originResponse.addHeader("Last-Modified", DateUtils.formatStandardDate(oneHourAgo));
originResponse.addHeader("ETag", "W/\"weak-tag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.addHeader("If-Range", "W/\"weak-tag\"");
req2.addHeader("Range", "bytes=0-50");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
Assertions.assertNull(result.getFirstHeader("Allow"));
Assertions.assertNull(result.getFirstHeader("Content-Encoding"));
Assertions.assertNull(result.getFirstHeader("Content-Language"));
Assertions.assertNull(result.getFirstHeader("Content-MD5"));
Assertions.assertNull(result.getFirstHeader("Last-Modified"));
}
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
/*
* "Otherwise, the [206] response MUST include all of the entity-headers
* that would have been returned with a 200 (OK) response to the same
@ -1967,122 +1899,6 @@ public class TestProtocolRequirements {
Assertions.assertEquals("\"etag1\"", result.getFirstHeader("ETag").getValue());
}
/*
* "Clients MAY issue simple (non-subrange) GET requests with either weak
* validators or strong validators. Clients MUST NOT use weak validators in
* other forms of request."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
*
* Note that we can't determine a priori whether a given HTTP-date is a weak
* or strong validator, because that might depend on an upstream client
* having a cache with a Last-Modified and Date entry that allows the date
* to be a strong validator. We can tell when *we* are generating a request
* for validation, but we can't tell if we receive a conditional request
* from upstream.
*/
private ClassicHttpResponse testRequestWithWeakETagValidatorIsNotAllowed(final String header) throws Exception {
final ClassicHttpResponse response = execute(request);
// it's probably ok to return a 400 (Bad Request) to this client
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain, Mockito.atMostOnce()).proceed(reqCapture.capture(), Mockito.any());
final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
if (!allRequests.isEmpty()) {
final ClassicHttpRequest forwarded = reqCapture.getValue();
if (forwarded != null) {
final Header h = forwarded.getFirstHeader(header);
if (h != null) {
Assertions.assertFalse(h.getValue().startsWith("W/"));
}
}
}
return response;
}
@Test
public void testSubrangeGETWithWeakETagIsNotAllowed() throws Exception {
request = new BasicClassicHttpRequest("GET", "/");
request.setHeader("Range", "bytes=0-500");
request.setHeader("If-Range", "W/\"etag\"");
final ClassicHttpResponse response = testRequestWithWeakETagValidatorIsNotAllowed("If-Range");
Assertions.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getCode());
}
@Test
public void testPUTWithIfMatchWeakETagIsNotAllowed() throws Exception {
final ClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
put.setEntity(HttpTestUtils.makeBody(128));
put.setHeader("Content-Length", "128");
put.setHeader("If-Match", "W/\"etag\"");
request = put;
testRequestWithWeakETagValidatorIsNotAllowed("If-Match");
}
@Test
public void testPUTWithIfNoneMatchWeakETagIsNotAllowed() throws Exception {
final ClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
put.setEntity(HttpTestUtils.makeBody(128));
put.setHeader("Content-Length", "128");
put.setHeader("If-None-Match", "W/\"etag\"");
request = put;
testRequestWithWeakETagValidatorIsNotAllowed("If-None-Match");
}
@Test
public void testDELETEWithIfMatchWeakETagIsNotAllowed() throws Exception {
request = new BasicClassicHttpRequest("DELETE", "/");
request.setHeader("If-Match", "W/\"etag\"");
testRequestWithWeakETagValidatorIsNotAllowed("If-Match");
}
@Test
public void testDELETEWithIfNoneMatchWeakETagIsNotAllowed() throws Exception {
request = new BasicClassicHttpRequest("DELETE", "/");
request.setHeader("If-None-Match", "W/\"etag\"");
testRequestWithWeakETagValidatorIsNotAllowed("If-None-Match");
}
/*
* "A cache or origin server receiving a conditional request, other than a
* full-body GET request, MUST use the strong comparison function to
* evaluate the condition."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
*/
@Test
public void testSubrangeGETMustUseStrongComparisonForCachedResponse() throws Exception {
final Instant now = Instant.now();
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(now));
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("ETag", "\"etag\"");
// according to weak comparison, this would match. Strong
// comparison doesn't, because the cache entry's ETag is not
// marked weak. Therefore, the If-Range must fail and we must
// either get an error back or the full entity, but we better
// not get the conditionally-requested Partial Content (206).
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Range", "bytes=0-50");
req2.setHeader("If-Range", "W/\"etag\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertNotEquals(HttpStatus.SC_PARTIAL_CONTENT, result.getCode());
Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
}
/*
* "HTTP/1.1 clients: - If an entity tag has been provided by the origin
* server, MUST use that entity tag in any cache-conditional request (using

View File

@ -1,187 +0,0 @@
/*
* ====================================================================
* 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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Collections;
import java.util.List;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.ProtocolVersion;
import org.apache.hc.core5.http.message.BasicHttpRequest;
import org.apache.hc.core5.http.support.BasicRequestBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class TestRequestProtocolCompliance {
private RequestProtocolCompliance impl;
@BeforeEach
public void setUp() {
impl = new RequestProtocolCompliance(false);
}
@Test
public void testRequestWithWeakETagAndRange() throws Exception {
final HttpRequest req = new BasicHttpRequest("GET", "/");
req.setHeader("Range", "bytes=0-499");
req.setHeader("If-Range", "W/\"weak\"");
assertEquals(1, impl.requestIsFatallyNonCompliant(req, false).size());
}
@Test
public void testRequestWithWeekETagForPUTOrDELETEIfMatch() throws Exception {
final HttpRequest req = new BasicHttpRequest("PUT", "http://example.com/");
req.setHeader("If-Match", "W/\"weak\"");
assertEquals(1, impl.requestIsFatallyNonCompliant(req, false).size());
}
@Test
public void testRequestWithWeekETagForPUTOrDELETEIfMatchAllowed() throws Exception {
final HttpRequest req = new BasicHttpRequest("PUT", "http://example.com/");
req.setHeader("If-Match", "W/\"weak\"");
impl = new RequestProtocolCompliance(true);
assertEquals(Collections.emptyList(), impl.requestIsFatallyNonCompliant(req, false));
}
@Test
public void doesNotModifyACompliantRequest() throws Exception {
final HttpRequest req = new BasicHttpRequest("GET", "/");
final HttpRequest wrapper = BasicRequestBuilder.copy(req).build();
impl.makeRequestCompliant(wrapper);
assertTrue(HttpTestUtils.equivalent(req, wrapper));
}
@Test
public void upgrades1_0RequestTo1_1() throws Exception {
final HttpRequest req = new BasicHttpRequest("GET", "/");
req.setVersion(HttpVersion.HTTP_1_0);
final HttpRequest wrapper = BasicRequestBuilder.copy(req).build();
impl.makeRequestCompliant(wrapper);
assertEquals(HttpVersion.HTTP_1_1, wrapper.getVersion());
}
@Test
public void downgrades1_2RequestTo1_1() throws Exception {
final HttpRequest req = new BasicHttpRequest("GET", "/");
req.setVersion(new ProtocolVersion("HTTP", 1, 2));
final HttpRequest wrapper = BasicRequestBuilder.copy(req).build();
impl.makeRequestCompliant(wrapper);
assertEquals(HttpVersion.HTTP_1_1, wrapper.getVersion());
}
@Test
public void testRequestWithMultipleIfMatchHeaders() {
final HttpRequest req = new BasicHttpRequest("PUT", "http://example.com/");
req.addHeader(HttpHeaders.IF_MATCH, "W/\"weak1\"");
req.addHeader(HttpHeaders.IF_MATCH, "W/\"weak2\"");
assertEquals(1, impl.requestIsFatallyNonCompliant(req, false).size());
}
@Test
public void testRequestWithMultipleIfNoneMatchHeaders() {
final HttpRequest req = new BasicHttpRequest("PUT", "http://example.com/");
req.addHeader(HttpHeaders.IF_NONE_MATCH, "W/\"weak1\"");
req.addHeader(HttpHeaders.IF_NONE_MATCH, "W/\"weak2\"");
assertEquals(1, impl.requestIsFatallyNonCompliant(req, false).size());
}
@Test
public void testRequestWithPreconditionFailed() {
final HttpRequest req = new BasicHttpRequest("GET", "http://example.com/");
req.addHeader(HttpHeaders.IF_MATCH, "W/\"weak1\"");
req.addHeader(HttpHeaders.RANGE, "1");
req.addHeader(HttpHeaders.IF_RANGE, "W/\"weak2\""); // ETag doesn't match with If-Match ETag
// This will cause the precondition If-Match to fail because the ETags are different
final List<RequestProtocolError> requestProtocolErrors = impl.requestIsFatallyNonCompliant(req, false);
assertTrue(requestProtocolErrors.contains(RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR));
}
@Test
public void testRequestWithValidIfRangeDate() {
final HttpRequest req = new BasicHttpRequest("GET", "http://example.com/");
req.addHeader(HttpHeaders.RANGE, "bytes=0-499");
req.addHeader(HttpHeaders.LAST_MODIFIED, "Wed, 21 Oct 2023 07:28:00 GMT");
req.addHeader(HttpHeaders.IF_RANGE, "Wed, 21 Oct 2023 07:28:00 GMT");
assertTrue(impl.requestIsFatallyNonCompliant(req, false).isEmpty());
}
@Test
public void testRequestWithInvalidDateFormat() {
final HttpRequest req = new BasicHttpRequest("GET", "http://example.com/");
req.addHeader(HttpHeaders.RANGE, "bytes=0-499");
req.addHeader(HttpHeaders.LAST_MODIFIED, "Wed, 21 Oct 2023 07:28:00 GMT");
req.addHeader(HttpHeaders.IF_RANGE, "20/10/2023");
assertTrue(impl.requestIsFatallyNonCompliant(req, false).isEmpty());
}
@Test
public void testRequestWithMissingIfRangeDate() {
final HttpRequest req = new BasicHttpRequest("GET", "http://example.com/");
req.addHeader(HttpHeaders.RANGE, "bytes=0-499");
req.addHeader(HttpHeaders.LAST_MODIFIED, "Wed, 21 Oct 2023 07:28:00 GMT");
assertTrue(impl.requestIsFatallyNonCompliant(req, false).isEmpty());
}
@Test
public void testRequestWithWeakETagAndRangeAndDAte() {
// Setup request with GET method, Range header, If-Range header starting with "W/",
// and a Last-Modified date that doesn't match the If-Range date
final HttpRequest req = new BasicHttpRequest("GET", "http://example.com/");
req.addHeader(HttpHeaders.RANGE, "bytes=0-499");
req.addHeader(HttpHeaders.LAST_MODIFIED, "Fri, 20 Oct 2023 07:28:00 GMT");
req.addHeader(HttpHeaders.IF_RANGE, "Wed, 18 Oct 2023 07:28:00 GMT");
// Use your implementation to check the request
final List<RequestProtocolError> errors = impl.requestIsFatallyNonCompliant(req, false);
// Assert that the WEAK_ETAG_AND_RANGE_ERROR is in the list of errors
assertTrue(errors.contains(RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR));
}
@Test
public void testRequestWithWeekETagForPUTOrDELETEIfMatchWithStart() {
final HttpRequest req = new BasicHttpRequest("PUT", "http://example.com/");
req.setHeader(HttpHeaders.IF_MATCH, "*");
assertEquals(0, impl.requestIsFatallyNonCompliant(req, false).size());
}
@Test
public void testRequestOkETagForPUTOrDELETEIfMatch() {
final HttpRequest req = new BasicHttpRequest("PUT", "http://example.com/");
req.setHeader(HttpHeaders.IF_MATCH, "1234");
assertEquals(0, impl.requestIsFatallyNonCompliant(req, false).size());
}
}