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