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 }