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.io.IOException;
30  import java.util.ArrayList;
31  import java.util.Date;
32  import java.util.List;
33  
34  import org.apache.http.Header;
35  import org.apache.http.HeaderElement;
36  import org.apache.http.HttpEntity;
37  import org.apache.http.HttpEntityEnclosingRequest;
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.annotation.Contract;
43  import org.apache.http.annotation.ThreadingBehavior;
44  import org.apache.http.client.ClientProtocolException;
45  import org.apache.http.client.cache.HeaderConstants;
46  import org.apache.http.client.methods.HttpRequestWrapper;
47  import org.apache.http.client.utils.DateUtils;
48  import org.apache.http.message.BasicHeader;
49  import org.apache.http.protocol.HTTP;
50  
51  /**
52   * @since 4.1
53   */
54  @Contract(threading = ThreadingBehavior.IMMUTABLE)
55  class ResponseProtocolCompliance {
56  
57      private static final String UNEXPECTED_100_CONTINUE = "The incoming request did not contain a "
58                      + "100-continue header, but the response was a Status 100, continue.";
59      private static final String UNEXPECTED_PARTIAL_CONTENT = "partial content was returned for a request that did not ask for it";
60  
61      /**
62       * When we get a response from a down stream server (Origin Server)
63       * we attempt to see if it is HTTP 1.1 Compliant and if not, attempt to
64       * make it so.
65       *
66       * @param request The {@link HttpRequest} that generated an origin hit and response
67       * @param response The {@link HttpResponse} from the origin server
68       * @throws IOException Bad things happened
69       */
70      public void ensureProtocolCompliance(final HttpRequestWrapper request, final HttpResponse response)
71              throws IOException {
72          if (backendResponseMustNotHaveBody(request, response)) {
73              consumeBody(response);
74              response.setEntity(null);
75          }
76  
77          requestDidNotExpect100ContinueButResponseIsOne(request, response);
78  
79          transferEncodingIsNotReturnedTo1_0Client(request, response);
80  
81          ensurePartialContentIsNotSentToAClientThatDidNotRequestIt(request, response);
82  
83          ensure200ForOPTIONSRequestWithNoBodyHasContentLengthZero(request, response);
84  
85          ensure206ContainsDateHeader(response);
86  
87          ensure304DoesNotContainExtraEntityHeaders(response);
88  
89          identityIsNotUsedInContentEncoding(response);
90  
91          warningsWithNonMatchingWarnDatesAreRemoved(response);
92      }
93  
94      private void consumeBody(final HttpResponse response) throws IOException {
95          final HttpEntity body = response.getEntity();
96          if (body != null) {
97              IOUtils.consume(body);
98          }
99      }
100 
101     private void warningsWithNonMatchingWarnDatesAreRemoved(
102             final HttpResponse response) {
103         final Date responseDate = DateUtils.parseDate(response.getFirstHeader(HTTP.DATE_HEADER).getValue());
104         if (responseDate == null) {
105             return;
106         }
107 
108         final Header[] warningHeaders = response.getHeaders(HeaderConstants.WARNING);
109 
110         if (warningHeaders == null || warningHeaders.length == 0) {
111             return;
112         }
113 
114         final List<Header> newWarningHeaders = new ArrayList<Header>();
115         boolean modified = false;
116         for(final Header h : warningHeaders) {
117             for(final WarningValue wv : WarningValue.getWarningValues(h)) {
118                 final Date warnDate = wv.getWarnDate();
119                 if (warnDate == null || warnDate.equals(responseDate)) {
120                     newWarningHeaders.add(new BasicHeader(HeaderConstants.WARNING,wv.toString()));
121                 } else {
122                     modified = true;
123                 }
124             }
125         }
126         if (modified) {
127             response.removeHeaders(HeaderConstants.WARNING);
128             for(final Header h : newWarningHeaders) {
129                 response.addHeader(h);
130             }
131         }
132     }
133 
134     private void identityIsNotUsedInContentEncoding(final HttpResponse response) {
135         final Header[] hdrs = response.getHeaders(HTTP.CONTENT_ENCODING);
136         if (hdrs == null || hdrs.length == 0) {
137             return;
138         }
139         final List<Header> newHeaders = new ArrayList<Header>();
140         boolean modified = false;
141         for (final Header h : hdrs) {
142             final StringBuilder buf = new StringBuilder();
143             boolean first = true;
144             for (final HeaderElement elt : h.getElements()) {
145                 if ("identity".equalsIgnoreCase(elt.getName())) {
146                     modified = true;
147                 } else {
148                     if (!first) {
149                         buf.append(",");
150                     }
151                     buf.append(elt.toString());
152                     first = false;
153                 }
154             }
155             final String newHeaderValue = buf.toString();
156             if (!"".equals(newHeaderValue)) {
157                 newHeaders.add(new BasicHeader(HTTP.CONTENT_ENCODING, newHeaderValue));
158             }
159         }
160         if (!modified) {
161             return;
162         }
163         response.removeHeaders(HTTP.CONTENT_ENCODING);
164         for (final Header h : newHeaders) {
165             response.addHeader(h);
166         }
167     }
168 
169     private void ensure206ContainsDateHeader(final HttpResponse response) {
170         if (response.getFirstHeader(HTTP.DATE_HEADER) == null) {
171             response.addHeader(HTTP.DATE_HEADER, DateUtils.formatDate(new Date()));
172         }
173 
174     }
175 
176     private void ensurePartialContentIsNotSentToAClientThatDidNotRequestIt(final HttpRequest request,
177             final HttpResponse response) throws IOException {
178         if (request.getFirstHeader(HeaderConstants.RANGE) != null
179                 || response.getStatusLine().getStatusCode() != HttpStatus.SC_PARTIAL_CONTENT) {
180             return;
181         }
182 
183         consumeBody(response);
184         throw new ClientProtocolException(UNEXPECTED_PARTIAL_CONTENT);
185     }
186 
187     private void ensure200ForOPTIONSRequestWithNoBodyHasContentLengthZero(final HttpRequest request,
188             final HttpResponse response) {
189         if (!request.getRequestLine().getMethod().equalsIgnoreCase(HeaderConstants.OPTIONS_METHOD)) {
190             return;
191         }
192 
193         if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
194             return;
195         }
196 
197         if (response.getFirstHeader(HTTP.CONTENT_LEN) == null) {
198             response.addHeader(HTTP.CONTENT_LEN, "0");
199         }
200     }
201 
202     private void ensure304DoesNotContainExtraEntityHeaders(final HttpResponse response) {
203         final String[] disallowedEntityHeaders = { HeaderConstants.ALLOW, HTTP.CONTENT_ENCODING,
204                 "Content-Language", HTTP.CONTENT_LEN, "Content-MD5",
205                 "Content-Range", HTTP.CONTENT_TYPE, HeaderConstants.LAST_MODIFIED
206         };
207         if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
208             for(final String hdr : disallowedEntityHeaders) {
209                 response.removeHeaders(hdr);
210             }
211         }
212     }
213 
214     private boolean backendResponseMustNotHaveBody(final HttpRequest request, final HttpResponse backendResponse) {
215         return HeaderConstants.HEAD_METHOD.equals(request.getRequestLine().getMethod())
216                 || backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT
217                 || backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_RESET_CONTENT
218                 || backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED;
219     }
220 
221     private void requestDidNotExpect100ContinueButResponseIsOne(final HttpRequestWrapper request,
222             final HttpResponse response) throws IOException {
223         if (response.getStatusLine().getStatusCode() != HttpStatus.SC_CONTINUE) {
224             return;
225         }
226 
227         final HttpRequest originalRequest = request.getOriginal();
228         if (originalRequest instanceof HttpEntityEnclosingRequest) {
229             if (((HttpEntityEnclosingRequest)originalRequest).expectContinue()) {
230                 return;
231             }
232         }
233         consumeBody(response);
234         throw new ClientProtocolException(UNEXPECTED_100_CONTINUE);
235     }
236 
237     private void transferEncodingIsNotReturnedTo1_0Client(final HttpRequestWrapper request,
238             final HttpResponse response) {
239         final HttpRequest originalRequest = request.getOriginal();
240         if (originalRequest.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) >= 0) {
241             return;
242         }
243 
244         removeResponseTransferEncoding(response);
245     }
246 
247     private void removeResponseTransferEncoding(final HttpResponse response) {
248         response.removeHeaders("TE");
249         response.removeHeaders(HTTP.TRANSFER_ENCODING);
250     }
251 
252 }