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  
28  
29  package org.apache.hc.client5.http.protocol;
30  
31  
32  import java.io.IOException;
33  import java.time.Instant;
34  
35  import org.apache.hc.client5.http.utils.DateUtils;
36  import org.apache.hc.client5.http.validator.ETag;
37  import org.apache.hc.client5.http.validator.ValidatorType;
38  import org.apache.hc.core5.annotation.Contract;
39  import org.apache.hc.core5.annotation.ThreadingBehavior;
40  import org.apache.hc.core5.http.EntityDetails;
41  import org.apache.hc.core5.http.Header;
42  import org.apache.hc.core5.http.HttpException;
43  import org.apache.hc.core5.http.HttpHeaders;
44  import org.apache.hc.core5.http.HttpRequest;
45  import org.apache.hc.core5.http.HttpRequestInterceptor;
46  import org.apache.hc.core5.http.ProtocolException;
47  import org.apache.hc.core5.http.protocol.HttpContext;
48  import org.apache.hc.core5.util.Args;
49  
50  /**
51   * <p>
52   * The {@code RequestIfRange} interceptor ensures that the request adheres to the RFC guidelines for the 'If-Range' header.
53   * The "If-Range" header field is used in conjunction with the Range header to conditionally request parts of a representation.
54   * If the validator given in the "If-Range" header matches the current validator for the representation, then the server should respond with the specified range of the document.
55   * If they do not match, the server should return the entire representation.
56   * </p>
57   *
58   * <p>
59   * Key points:
60   * </p>
61   * <ul>
62   *     <li>A client MUST NOT generate an If-Range header field in a request that does not contain a Range header field.</li>
63   *     <li>An origin server MUST ignore an If-Range header field received in a request for a target resource that does not support Range requests.</li>
64   *     <li>A client MUST NOT generate an If-Range header field containing an entity tag that is marked as weak.</li>
65   *     <li>A client MUST NOT generate an If-Range header field containing an HTTP-date unless the client has no entity tag for the corresponding representation and the date is a strong validator.</li>
66   *     <li>A server that receives an If-Range header field on a Range request MUST evaluate the condition before performing the method.</li>
67   * </ul>
68   *
69   * @since 5.4
70   */
71  @Contract(threading = ThreadingBehavior.IMMUTABLE)
72  public class RequestIfRange implements HttpRequestInterceptor {
73  
74      /**
75       * Default instance of {@link RequestIfRange}.
76       *
77       * @since 5.4
78       */
79      public static final RequestIfRange INSTANCE = new RequestIfRange();
80  
81      public RequestIfRange() {
82          super();
83      }
84  
85      /**
86       * Processes the given request to ensure it adheres to the RFC guidelines for the 'If-Range' header.
87       *
88       * @param request The HTTP request to be processed.
89       * @param entity  The entity details of the request.
90       * @param context The HTTP context.
91       * @throws HttpException If the request does not adhere to the RFC guidelines.
92       * @throws IOException   If an I/O error occurs.
93       */
94      @Override
95      public void process(final HttpRequest request, final EntityDetails entity, final HttpContext context)
96              throws HttpException, IOException {
97          Args.notNull(request, "HTTP request");
98  
99          final Header ifRangeHeader = request.getFirstHeader(HttpHeaders.IF_RANGE);
100 
101         // If there's no If-Range header, just return
102         if (ifRangeHeader == null) {
103             return;
104         }
105 
106         // If there's an If-Range header but no Range header, throw an exception
107         if (!request.containsHeader(HttpHeaders.RANGE)) {
108             throw new ProtocolException("Request with 'If-Range' header must also contain a 'Range' header.");
109         }
110 
111         final ETag eTag = ETag.get(request);
112 
113         // If there's a weak ETag in the If-Range header, throw an exception
114         if (eTag != null && eTag.getType() == ValidatorType.WEAK) {
115             throw new ProtocolException("'If-Range' header must not contain a weak entity tag.");
116         }
117 
118         final Instant dateInstant = DateUtils.parseStandardDate(request, HttpHeaders.DATE);
119 
120         if (dateInstant == null) {
121             return;
122         }
123 
124         final Instant lastModifiedInstant = DateUtils.parseStandardDate(request, HttpHeaders.LAST_MODIFIED);
125 
126         if (lastModifiedInstant == null) {
127             // If there's no Last-Modified header, we exit early because we can't deduce that it is strong.
128             return;
129         }
130 
131         // If the difference between the Last-Modified and Date headers is less than 1 second, throw an exception
132         if (lastModifiedInstant.plusSeconds(1).isAfter(dateInstant) && eTag != null) {
133             throw new ProtocolException("'If-Range' header with a Date must be a strong validator.");
134         }
135     }
136 
137 }