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