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.hc.client5.http.impl.cache;
28  
29  import java.io.InputStream;
30  import java.time.Duration;
31  import java.time.Instant;
32  import java.util.HashMap;
33  import java.util.Iterator;
34  import java.util.Map;
35  import java.util.Random;
36  
37  import org.apache.hc.client5.http.cache.HttpCacheEntry;
38  import org.apache.hc.client5.http.utils.DateUtils;
39  import org.apache.hc.core5.http.ClassicHttpRequest;
40  import org.apache.hc.core5.http.ClassicHttpResponse;
41  import org.apache.hc.core5.http.Header;
42  import org.apache.hc.core5.http.HeaderElement;
43  import org.apache.hc.core5.http.HttpEntity;
44  import org.apache.hc.core5.http.HttpHeaders;
45  import org.apache.hc.core5.http.HttpMessage;
46  import org.apache.hc.core5.http.HttpRequest;
47  import org.apache.hc.core5.http.HttpResponse;
48  import org.apache.hc.core5.http.HttpStatus;
49  import org.apache.hc.core5.http.HttpVersion;
50  import org.apache.hc.core5.http.Method;
51  import org.apache.hc.core5.http.ProtocolVersion;
52  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
53  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
54  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
55  import org.apache.hc.core5.http.message.BasicHeader;
56  import org.apache.hc.core5.http.message.MessageSupport;
57  import org.apache.hc.core5.util.ByteArrayBuffer;
58  import org.apache.hc.core5.util.LangUtils;
59  import org.junit.jupiter.api.Assertions;
60  
61  public class HttpTestUtils {
62  
63      /*
64       * "The following HTTP/1.1 headers are hop-by-hop headers..."
65       *
66       * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
67       */
68      private static final String[] HOP_BY_HOP_HEADERS = { "Connection", "Keep-Alive", "Proxy-Authenticate",
69          "Proxy-Authorization", "TE", "Trailers", "Transfer-Encoding", "Upgrade" };
70  
71      /*
72       * "Multiple message-header fields with the same field-name MAY be present
73       * in a message if and only if the entire field-value for that header field
74       * is defined as a comma-separated list [i.e., #(values)]."
75       *
76       * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
77       */
78      private static final String[] MULTI_HEADERS = { "Accept", "Accept-Charset", "Accept-Encoding",
79          "Accept-Language", "Allow", "Cache-Control", "Connection", "Content-Encoding",
80          "Content-Language", "Expect", "Pragma", "Proxy-Authenticate", "TE", "Trailer",
81          "Transfer-Encoding", "Upgrade", "Via", HttpHeaders.WARNING, "WWW-Authenticate" };
82      private static final String[] SINGLE_HEADERS = { "Accept-Ranges", "Age", "Authorization",
83          "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type",
84          "Date", "ETag", "Expires", "From", "Host", "If-Match", "If-Modified-Since",
85          "If-None-Match", "If-Range", "If-Unmodified-Since", "Last-Modified", "Location",
86          "Max-Forwards", "Proxy-Authorization", "Range", "Referer", "Retry-After", "Server",
87          "User-Agent", "Vary" };
88  
89      /*
90       * Determines whether the given header name is considered a hop-by-hop
91       * header.
92       *
93       * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
94       */
95      public static boolean isHopByHopHeader(final String name) {
96          for (final String s : HOP_BY_HOP_HEADERS) {
97              if (s.equalsIgnoreCase(name)) {
98                  return true;
99              }
100         }
101         return false;
102     }
103 
104     /*
105      * Determines whether a given header name may only appear once in a message.
106      */
107     public static boolean isSingleHeader(final String name) {
108         for (final String s : SINGLE_HEADERS) {
109             if (s.equalsIgnoreCase(name)) {
110                 return true;
111             }
112         }
113         return false;
114     }
115     /*
116      * Assertions.asserts that two request or response bodies are byte-equivalent.
117      */
118     public static boolean equivalent(final HttpEntity e1, final HttpEntity e2) throws Exception {
119         final InputStream i1 = e1.getContent();
120         final InputStream i2 = e2.getContent();
121         if (i1 == null && i2 == null) {
122             return true;
123         }
124         if (i1 == null || i2 == null) {
125             return false; // avoid possible NPEs below
126         }
127         int b1 = -1;
128         while ((b1 = i1.read()) != -1) {
129             if (b1 != i2.read()) {
130                 return false;
131             }
132         }
133         return (-1 == i2.read());
134     }
135 
136     /*
137      * Retrieves the full header value by combining multiple headers and
138      * separating with commas, canonicalizing whitespace along the way.
139      *
140      * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
141      */
142     public static String getCanonicalHeaderValue(final HttpMessage r, final String name) {
143         if (isSingleHeader(name)) {
144             final Header h = r.getFirstHeader(name);
145             return (h != null) ? h.getValue() : null;
146         }
147         final StringBuilder buf = new StringBuilder();
148         boolean first = true;
149         for (final Header h : r.getHeaders(name)) {
150             if (!first) {
151                 buf.append(", ");
152             }
153             buf.append(h.getValue().trim());
154             first = false;
155         }
156         return buf.toString();
157     }
158 
159     /*
160      * Assertions.asserts that all the headers appearing in r1 also appear in r2
161      * with the same canonical header values.
162      */
163     public static boolean isEndToEndHeaderSubset(final HttpMessage r1, final HttpMessage r2) {
164         for (final Header h : r1.getHeaders()) {
165             if (!isHopByHopHeader(h.getName())) {
166                 final String r1val = getCanonicalHeaderValue(r1, h.getName());
167                 final String r2val = getCanonicalHeaderValue(r2, h.getName());
168                 if (!r1val.equals(r2val)) {
169                     return false;
170                 }
171             }
172         }
173         return true;
174     }
175 
176     /*
177      * Assertions.asserts that message {@code r2} represents exactly the same
178      * message as {@code r1}, except for hop-by-hop headers. "When a cache
179      * is semantically transparent, the client receives exactly the same
180      * response (except for hop-by-hop headers) that it would have received had
181      * its request been handled directly by the origin server."
182      *
183      * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html#sec1.3
184      */
185     public static boolean semanticallyTransparent(
186             final ClassicHttpResponse r1, final ClassicHttpResponse r2) throws Exception {
187         final boolean entitiesEquivalent = equivalent(r1.getEntity(), r2.getEntity());
188         if (!entitiesEquivalent) {
189             return false;
190         }
191         final boolean statusLinesEquivalent = LangUtils.equals(r1.getReasonPhrase(), r2.getReasonPhrase())
192                 && r1.getCode() == r2.getCode();
193         if (!statusLinesEquivalent) {
194             return false;
195         }
196         return isEndToEndHeaderSubset(r1, r2);
197     }
198 
199     /* Assertions.asserts that protocol versions equivalent. */
200     public static boolean equivalent(final ProtocolVersion v1, final ProtocolVersion v2) {
201         return LangUtils.equals(v1 != null ? v1 : HttpVersion.DEFAULT, v2 != null ? v2 : HttpVersion.DEFAULT );
202     }
203 
204     /* Assertions.asserts that two requests are morally equivalent. */
205     public static boolean equivalent(final HttpRequest r1, final HttpRequest r2) {
206         return equivalent(r1.getVersion(), r2.getVersion()) &&
207                 LangUtils.equals(r1.getMethod(), r2.getMethod()) &&
208                 LangUtils.equals(r1.getRequestUri(), r2.getRequestUri()) &&
209                 isEndToEndHeaderSubset(r1, r2);
210     }
211 
212     /* Assertions.asserts that two requests are morally equivalent. */
213     public static boolean equivalent(final HttpResponse r1, final HttpResponse r2) {
214         return equivalent(r1.getVersion(), r2.getVersion()) &&
215                 r1.getCode() == r2.getCode() &&
216                 LangUtils.equals(r1.getReasonPhrase(), r2.getReasonPhrase()) &&
217                 isEndToEndHeaderSubset(r1, r2);
218     }
219 
220     public static byte[] getRandomBytes(final int nbytes) {
221         final byte[] bytes = new byte[nbytes];
222         new Random().nextBytes(bytes);
223         return bytes;
224     }
225 
226     public static ByteArrayBuffer getRandomBuffer(final int nbytes) {
227         final ByteArrayBuffer buf = new ByteArrayBuffer(nbytes);
228         buf.setLength(nbytes);
229         new Random().nextBytes(buf.array());
230         return buf;
231     }
232 
233     /** Generates a response body with random content.
234      *  @param nbytes length of the desired response body
235      *  @return an {@link HttpEntity}
236      */
237     public static HttpEntity makeBody(final int nbytes) {
238         return new ByteArrayEntity(getRandomBytes(nbytes), null);
239     }
240 
241     public static HttpCacheEntry makeCacheEntry(final Instant requestDate, final Instant responseDate) {
242         final Duration diff = Duration.between(requestDate, responseDate);
243         final Instant when = requestDate.plusMillis(diff.toMillis() / 2);
244         return makeCacheEntry(requestDate, responseDate, getStockHeaders(when));
245     }
246 
247     public static Header[] getStockHeaders(final Instant when) {
248         return new Header[] {
249                 new BasicHeader("Date", DateUtils.formatStandardDate(when)),
250                 new BasicHeader("Server", "MockServer/1.0")
251         };
252     }
253 
254     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
255                                                 final Instant responseDate, final Header... headers) {
256         final byte[] bytes = getRandomBytes(128);
257         return makeCacheEntry(requestDate, responseDate, headers, bytes);
258     }
259 
260     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
261                                                 final Instant responseDate, final Header[] headers, final byte[] bytes) {
262         return makeCacheEntry(requestDate, responseDate, headers, bytes, null);
263     }
264 
265     public static HttpCacheEntry makeCacheEntry(final Map<String,String> variantMap) {
266         final Instant now = Instant.now();
267         return makeCacheEntry(now, now, getStockHeaders(now),
268                 getRandomBytes(128), variantMap);
269     }
270     @SuppressWarnings("deprecation")
271     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
272             final Instant responseDate, final Header[] headers, final byte[] bytes,
273             final Map<String,String> variantMap) {
274         return new HttpCacheEntry(DateUtils.toDate(requestDate), DateUtils.toDate(responseDate),
275                 HttpStatus.SC_OK, headers, new HeapResource(bytes), variantMap);
276     }
277 
278     public static HttpCacheEntry makeCacheEntry(final Header[] headers, final byte[] bytes) {
279         final Instant now = Instant.now();
280         return makeCacheEntry(now, now, headers, bytes);
281     }
282 
283     public static HttpCacheEntry makeCacheEntry(final byte[] bytes) {
284         final Instant now = Instant.now();
285         return makeCacheEntry(getStockHeaders(now), bytes);
286     }
287 
288     public static HttpCacheEntry makeCacheEntry(final Header... headers) {
289         return makeCacheEntry(headers, getRandomBytes(128));
290     }
291 
292     public static HttpCacheEntry makeCacheEntry() {
293         final Instant now = Instant.now();
294         return makeCacheEntry(now, now);
295     }
296 
297     public static ClassicHttpResponse make200Response() {
298         final ClassicHttpResponse out = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
299         out.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
300         out.setHeader("Server", "MockOrigin/1.0");
301         out.setHeader("Content-Length", "128");
302         out.setEntity(makeBody(128));
303         return out;
304     }
305 
306     public static final ClassicHttpResponse make200Response(final Instant date, final String cacheControl) {
307         final ClassicHttpResponse response = HttpTestUtils.make200Response();
308         response.setHeader("Date", DateUtils.formatStandardDate(date));
309         response.setHeader("Cache-Control",cacheControl);
310         response.setHeader("Etag","\"etag\"");
311         return response;
312     }
313 
314     public static ClassicHttpResponse make304Response() {
315         return new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not modified");
316     }
317 
318     public static final void assert110WarningFound(final HttpResponse response) {
319         boolean found110Warning = false;
320         final Iterator<HeaderElement> it = MessageSupport.iterate(response, HttpHeaders.WARNING);
321         while (it.hasNext()) {
322             final HeaderElement elt = it.next();
323             final String[] parts = elt.getName().split("\\s");
324             if ("110".equals(parts[0])) {
325                 found110Warning = true;
326                 break;
327             }
328         }
329         Assertions.assertTrue(found110Warning);
330     }
331 
332     public static ClassicHttpRequest makeDefaultRequest() {
333         return new BasicClassicHttpRequest(Method.GET.toString(), "/");
334     }
335 
336     public static ClassicHttpRequest makeDefaultHEADRequest() {
337         return new BasicClassicHttpRequest(Method.HEAD.toString(), "/");
338     }
339 
340     public static ClassicHttpResponse make500Response() {
341         return new BasicClassicHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
342     }
343 
344     public static Map<String, String> makeDefaultVariantMap(final String key, final String value) {
345         final Map<String, String> variants = new HashMap<>();
346         variants.put(key, value);
347 
348         return variants;
349     }
350 
351 
352     public static HttpCacheEntry makeCacheEntryWithNoRequestMethodOrEntity(final Header... headers) {
353         final Instant now = Instant.now();
354         return new HttpCacheEntry(now, now, HttpStatus.SC_OK, headers, null, null);
355     }
356 
357     public static HttpCacheEntry makeCacheEntryWithNoRequestMethod(final Header... headers) {
358         final Instant now = Instant.now();
359         return new HttpCacheEntry(now, now, HttpStatus.SC_OK, headers, new HeapResource(getRandomBytes(128)), null);
360     }
361 
362     public static HttpCacheEntry make204CacheEntryWithNoRequestMethod(final Header... headers) {
363         final Instant now = Instant.now();
364         return new HttpCacheEntry(now, now, HttpStatus.SC_NO_CONTENT, headers, null, null);
365     }
366 
367     public static HttpCacheEntry makeHeadCacheEntry(final Header... headers) {
368         final Instant now = Instant.now();
369         return new HttpCacheEntry(now, now, HttpStatus.SC_OK, headers, null, null);
370     }
371 
372     public static HttpCacheEntry makeHeadCacheEntryWithNoRequestMethod(final Header... headers) {
373         final Instant now = Instant.now();
374         return new HttpCacheEntry(now, now, HttpStatus.SC_OK, headers, null, null);
375     }
376 }