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