1 /* 2 * ==================================================================== 3 * Licensed to the Apache Software Foundation (ASF) under one 4 * or more contributor license agreements. See the NOTICE file 5 * distributed with this work for additional information 6 * regarding copyright ownership. The ASF licenses this file 7 * to you under the Apache License, Version 2.0 (the 8 * "License"); you may not use this file except in compliance 9 * with the License. You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, 14 * software distributed under the License is distributed on an 15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 * KIND, either express or implied. See the License for the 17 * specific language governing permissions and limitations 18 * under the License. 19 * ==================================================================== 20 * 21 * This software consists of voluntary contributions made by many 22 * individuals on behalf of the Apache Software Foundation. For more 23 * information on the Apache Software Foundation, please see 24 * <http://www.apache.org/>. 25 * 26 */ 27 package ch.boye.httpclientandroidlib.impl.client.cache; 28 29 import java.util.ArrayList; 30 import java.util.Arrays; 31 import java.util.List; 32 33 import ch.boye.httpclientandroidlib.Header; 34 import ch.boye.httpclientandroidlib.HeaderElement; 35 import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest; 36 import ch.boye.httpclientandroidlib.HttpRequest; 37 import ch.boye.httpclientandroidlib.HttpResponse; 38 import ch.boye.httpclientandroidlib.HttpStatus; 39 import ch.boye.httpclientandroidlib.HttpVersion; 40 import ch.boye.httpclientandroidlib.ProtocolVersion; 41 import ch.boye.httpclientandroidlib.annotation.Immutable; 42 import ch.boye.httpclientandroidlib.client.ClientProtocolException; 43 import ch.boye.httpclientandroidlib.client.cache.HeaderConstants; 44 import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper; 45 import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity; 46 import ch.boye.httpclientandroidlib.entity.ContentType; 47 import ch.boye.httpclientandroidlib.message.BasicHeader; 48 import ch.boye.httpclientandroidlib.message.BasicHttpResponse; 49 import ch.boye.httpclientandroidlib.message.BasicStatusLine; 50 import ch.boye.httpclientandroidlib.protocol.HTTP; 51 52 /** 53 * @since 4.1 54 */ 55 @Immutable 56 class RequestProtocolCompliance { 57 private final boolean weakETagOnPutDeleteAllowed; 58 RequestProtocolCompliance()59 public RequestProtocolCompliance() { 60 super(); 61 this.weakETagOnPutDeleteAllowed = false; 62 } 63 RequestProtocolCompliance(final boolean weakETagOnPutDeleteAllowed)64 public RequestProtocolCompliance(final boolean weakETagOnPutDeleteAllowed) { 65 super(); 66 this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed; 67 } 68 69 private static final List<String> disallowedWithNoCache = 70 Arrays.asList(HeaderConstants.CACHE_CONTROL_MIN_FRESH, HeaderConstants.CACHE_CONTROL_MAX_STALE, HeaderConstants.CACHE_CONTROL_MAX_AGE); 71 72 /** 73 * Test to see if the {@link HttpRequest} is HTTP1.1 compliant or not 74 * and if not, we can not continue. 75 * 76 * @param request the HttpRequest Object 77 * @return list of {@link RequestProtocolError} 78 */ requestIsFatallyNonCompliant(final HttpRequest request)79 public List<RequestProtocolError> requestIsFatallyNonCompliant(final HttpRequest request) { 80 final List<RequestProtocolError> theErrors = new ArrayList<RequestProtocolError>(); 81 82 RequestProtocolError anError = requestHasWeakETagAndRange(request); 83 if (anError != null) { 84 theErrors.add(anError); 85 } 86 87 if (!weakETagOnPutDeleteAllowed) { 88 anError = requestHasWeekETagForPUTOrDELETEIfMatch(request); 89 if (anError != null) { 90 theErrors.add(anError); 91 } 92 } 93 94 anError = requestContainsNoCacheDirectiveWithFieldName(request); 95 if (anError != null) { 96 theErrors.add(anError); 97 } 98 99 return theErrors; 100 } 101 102 /** 103 * If the {@link HttpRequest} is non-compliant but 'fixable' we go ahead and 104 * fix the request here. 105 * 106 * @param request the request to check for compliance 107 * @throws ClientProtocolException when we have trouble making the request compliant 108 */ makeRequestCompliant(final HttpRequestWrapper request)109 public void makeRequestCompliant(final HttpRequestWrapper request) 110 throws ClientProtocolException { 111 112 if (requestMustNotHaveEntity(request)) { 113 ((HttpEntityEnclosingRequest) request).setEntity(null); 114 } 115 116 verifyRequestWithExpectContinueFlagHas100continueHeader(request); 117 verifyOPTIONSRequestWithBodyHasContentType(request); 118 decrementOPTIONSMaxForwardsIfGreaterThen0(request); 119 stripOtherFreshnessDirectivesWithNoCache(request); 120 121 if (requestVersionIsTooLow(request) 122 || requestMinorVersionIsTooHighMajorVersionsMatch(request)) { 123 request.setProtocolVersion(HttpVersion.HTTP_1_1); 124 } 125 } 126 stripOtherFreshnessDirectivesWithNoCache(final HttpRequest request)127 private void stripOtherFreshnessDirectivesWithNoCache(final HttpRequest request) { 128 final List<HeaderElement> outElts = new ArrayList<HeaderElement>(); 129 boolean shouldStrip = false; 130 for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) { 131 for(final HeaderElement elt : h.getElements()) { 132 if (!disallowedWithNoCache.contains(elt.getName())) { 133 outElts.add(elt); 134 } 135 if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) { 136 shouldStrip = true; 137 } 138 } 139 } 140 if (!shouldStrip) { 141 return; 142 } 143 request.removeHeaders(HeaderConstants.CACHE_CONTROL); 144 request.setHeader(HeaderConstants.CACHE_CONTROL, buildHeaderFromElements(outElts)); 145 } 146 buildHeaderFromElements(final List<HeaderElement> outElts)147 private String buildHeaderFromElements(final List<HeaderElement> outElts) { 148 final StringBuilder newHdr = new StringBuilder(""); 149 boolean first = true; 150 for(final HeaderElement elt : outElts) { 151 if (!first) { 152 newHdr.append(","); 153 } else { 154 first = false; 155 } 156 newHdr.append(elt.toString()); 157 } 158 return newHdr.toString(); 159 } 160 requestMustNotHaveEntity(final HttpRequest request)161 private boolean requestMustNotHaveEntity(final HttpRequest request) { 162 return HeaderConstants.TRACE_METHOD.equals(request.getRequestLine().getMethod()) 163 && request instanceof HttpEntityEnclosingRequest; 164 } 165 decrementOPTIONSMaxForwardsIfGreaterThen0(final HttpRequest request)166 private void decrementOPTIONSMaxForwardsIfGreaterThen0(final HttpRequest request) { 167 if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine().getMethod())) { 168 return; 169 } 170 171 final Header maxForwards = request.getFirstHeader(HeaderConstants.MAX_FORWARDS); 172 if (maxForwards == null) { 173 return; 174 } 175 176 request.removeHeaders(HeaderConstants.MAX_FORWARDS); 177 final int currentMaxForwards = Integer.parseInt(maxForwards.getValue()); 178 179 request.setHeader(HeaderConstants.MAX_FORWARDS, Integer.toString(currentMaxForwards - 1)); 180 } 181 verifyOPTIONSRequestWithBodyHasContentType(final HttpRequest request)182 private void verifyOPTIONSRequestWithBodyHasContentType(final HttpRequest request) { 183 if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine().getMethod())) { 184 return; 185 } 186 187 if (!(request instanceof HttpEntityEnclosingRequest)) { 188 return; 189 } 190 191 addContentTypeHeaderIfMissing((HttpEntityEnclosingRequest) request); 192 } 193 addContentTypeHeaderIfMissing(final HttpEntityEnclosingRequest request)194 private void addContentTypeHeaderIfMissing(final HttpEntityEnclosingRequest request) { 195 if (request.getEntity().getContentType() == null) { 196 ((AbstractHttpEntity) request.getEntity()).setContentType( 197 ContentType.APPLICATION_OCTET_STREAM.getMimeType()); 198 } 199 } 200 verifyRequestWithExpectContinueFlagHas100continueHeader(final HttpRequest request)201 private void verifyRequestWithExpectContinueFlagHas100continueHeader(final HttpRequest request) { 202 if (request instanceof HttpEntityEnclosingRequest) { 203 204 if (((HttpEntityEnclosingRequest) request).expectContinue() 205 && ((HttpEntityEnclosingRequest) request).getEntity() != null) { 206 add100ContinueHeaderIfMissing(request); 207 } else { 208 remove100ContinueHeaderIfExists(request); 209 } 210 } else { 211 remove100ContinueHeaderIfExists(request); 212 } 213 } 214 remove100ContinueHeaderIfExists(final HttpRequest request)215 private void remove100ContinueHeaderIfExists(final HttpRequest request) { 216 boolean hasHeader = false; 217 218 final Header[] expectHeaders = request.getHeaders(HTTP.EXPECT_DIRECTIVE); 219 List<HeaderElement> expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>(); 220 221 for (final Header h : expectHeaders) { 222 for (final HeaderElement elt : h.getElements()) { 223 if (!(HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName()))) { 224 expectElementsThatAreNot100Continue.add(elt); 225 } else { 226 hasHeader = true; 227 } 228 } 229 230 if (hasHeader) { 231 request.removeHeader(h); 232 for (final HeaderElement elt : expectElementsThatAreNot100Continue) { 233 final BasicHeader newHeader = new BasicHeader(HTTP.EXPECT_DIRECTIVE, elt.getName()); 234 request.addHeader(newHeader); 235 } 236 return; 237 } else { 238 expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>(); 239 } 240 } 241 } 242 add100ContinueHeaderIfMissing(final HttpRequest request)243 private void add100ContinueHeaderIfMissing(final HttpRequest request) { 244 boolean hasHeader = false; 245 246 for (final Header h : request.getHeaders(HTTP.EXPECT_DIRECTIVE)) { 247 for (final HeaderElement elt : h.getElements()) { 248 if (HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName())) { 249 hasHeader = true; 250 } 251 } 252 } 253 254 if (!hasHeader) { 255 request.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE); 256 } 257 } 258 requestMinorVersionIsTooHighMajorVersionsMatch(final HttpRequest request)259 protected boolean requestMinorVersionIsTooHighMajorVersionsMatch(final HttpRequest request) { 260 final ProtocolVersion requestProtocol = request.getProtocolVersion(); 261 if (requestProtocol.getMajor() != HttpVersion.HTTP_1_1.getMajor()) { 262 return false; 263 } 264 265 if (requestProtocol.getMinor() > HttpVersion.HTTP_1_1.getMinor()) { 266 return true; 267 } 268 269 return false; 270 } 271 requestVersionIsTooLow(final HttpRequest request)272 protected boolean requestVersionIsTooLow(final HttpRequest request) { 273 return request.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) < 0; 274 } 275 276 /** 277 * Extract error information about the {@link HttpRequest} telling the 'caller' 278 * that a problem occured. 279 * 280 * @param errorCheck What type of error should I get 281 * @return The {@link HttpResponse} that is the error generated 282 */ getErrorForRequest(final RequestProtocolError errorCheck)283 public HttpResponse getErrorForRequest(final RequestProtocolError errorCheck) { 284 switch (errorCheck) { 285 case BODY_BUT_NO_LENGTH_ERROR: 286 return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, 287 HttpStatus.SC_LENGTH_REQUIRED, "")); 288 289 case WEAK_ETAG_AND_RANGE_ERROR: 290 return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, 291 HttpStatus.SC_BAD_REQUEST, "Weak eTag not compatible with byte range")); 292 293 case WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR: 294 return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, 295 HttpStatus.SC_BAD_REQUEST, 296 "Weak eTag not compatible with PUT or DELETE requests")); 297 298 case NO_CACHE_DIRECTIVE_WITH_FIELD_NAME: 299 return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, 300 HttpStatus.SC_BAD_REQUEST, 301 "No-Cache directive MUST NOT include a field name")); 302 303 default: 304 throw new IllegalStateException( 305 "The request was compliant, therefore no error can be generated for it."); 306 307 } 308 } 309 requestHasWeakETagAndRange(final HttpRequest request)310 private RequestProtocolError requestHasWeakETagAndRange(final HttpRequest request) { 311 // TODO: Should these be looking at all the headers marked as Range? 312 final String method = request.getRequestLine().getMethod(); 313 if (!(HeaderConstants.GET_METHOD.equals(method))) { 314 return null; 315 } 316 317 final Header range = request.getFirstHeader(HeaderConstants.RANGE); 318 if (range == null) { 319 return null; 320 } 321 322 final Header ifRange = request.getFirstHeader(HeaderConstants.IF_RANGE); 323 if (ifRange == null) { 324 return null; 325 } 326 327 final String val = ifRange.getValue(); 328 if (val.startsWith("W/")) { 329 return RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR; 330 } 331 332 return null; 333 } 334 requestHasWeekETagForPUTOrDELETEIfMatch(final HttpRequest request)335 private RequestProtocolError requestHasWeekETagForPUTOrDELETEIfMatch(final HttpRequest request) { 336 // TODO: Should these be looking at all the headers marked as If-Match/If-None-Match? 337 338 final String method = request.getRequestLine().getMethod(); 339 if (!(HeaderConstants.PUT_METHOD.equals(method) || HeaderConstants.DELETE_METHOD 340 .equals(method))) { 341 return null; 342 } 343 344 final Header ifMatch = request.getFirstHeader(HeaderConstants.IF_MATCH); 345 if (ifMatch != null) { 346 final String val = ifMatch.getValue(); 347 if (val.startsWith("W/")) { 348 return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR; 349 } 350 } else { 351 final Header ifNoneMatch = request.getFirstHeader(HeaderConstants.IF_NONE_MATCH); 352 if (ifNoneMatch == null) { 353 return null; 354 } 355 356 final String val2 = ifNoneMatch.getValue(); 357 if (val2.startsWith("W/")) { 358 return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR; 359 } 360 } 361 362 return null; 363 } 364 requestContainsNoCacheDirectiveWithFieldName(final HttpRequest request)365 private RequestProtocolError requestContainsNoCacheDirectiveWithFieldName(final HttpRequest request) { 366 for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) { 367 for(final HeaderElement elt : h.getElements()) { 368 if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equalsIgnoreCase(elt.getName()) 369 && elt.getValue() != null) { 370 return RequestProtocolError.NO_CACHE_DIRECTIVE_WITH_FIELD_NAME; 371 } 372 } 373 } 374 return null; 375 } 376 } 377